From bf350c6ede89c0571b5e39ae6710eba59205ea0b Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 15 Oct 2024 19:06:15 -0700 Subject: [PATCH 01/12] lnwallet: expand attributes in ResolutionReq In this commit, we add some additional attributes to the ResolutionReq struct. These will be used to make sure that we can properly handle all the HTLC variants, on chain. The `AuxSigDesc` will be used to communicate if an HTLC needs to go to the second level or not. It contains the second-level sig information needed to finalize a broadcast to the second level. --- lnwallet/aux_resolutions.go | 38 ++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/lnwallet/aux_resolutions.go b/lnwallet/aux_resolutions.go index e3b04fc5aa9..382232640d7 100644 --- a/lnwallet/aux_resolutions.go +++ b/lnwallet/aux_resolutions.go @@ -3,6 +3,7 @@ package lnwallet import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwire" @@ -24,6 +25,19 @@ const ( Breach ) +// AuxSigDesc stores optional information related to 2nd level HTLCs for aux +// channels. +type AuxSigDesc struct { + // AuxSig is the second-level signature for the HTLC that we are trying + // to resolve. This is only present if this is a resolution request for + // an HTLC on our commitment transaction. + AuxSig []byte + + // SignDetails is the sign details for the second-level HTLC. This may + // be used to generate the second signature needed for broadcast. + SignDetails input.SignDetails +} + // ResolutionReq is used to ask an outside sub-system for additional // information needed to resolve a contract. type ResolutionReq struct { @@ -31,6 +45,9 @@ type ResolutionReq struct { // resolve. ChanPoint wire.OutPoint + // ChanType is the type of the channel that we are trying to resolve. + ChanType channeldb.ChannelType + // ShortChanID is the short channel ID of the channel that we are // trying to resolve. ShortChanID lnwire.ShortChannelID @@ -44,6 +61,13 @@ type ResolutionReq struct { // FundingBlob is an optional funding blob for the channel. FundingBlob fn.Option[tlv.Blob] + // HtlcID is the ID of the HTLC that we are trying to resolve. This is + // only set if this is a resolution request for an HTLC. + HtlcID fn.Option[input.HtlcIndex] + + // HtlcAmt is the amount of the HTLC that we are trying to resolve. + HtlcAmt btcutil.Amount + // Type is the type of the witness that we are trying to resolve. Type input.WitnessType @@ -69,14 +93,26 @@ type ResolutionReq struct { // CsvDelay is the CSV delay for the local output for this commitment. CsvDelay uint32 + // CommitCsvDelay is the CSV delay for the remote output for this + // commitment. + CommitCsvDelay uint32 + // BreachCsvDelay is the CSV delay for the remote output. This is only // set when the CloseType is Breach. This indicates the CSV delay to // use for the remote party's to_local delayed output, that is now // rightfully ours in a breach situation. BreachCsvDelay fn.Option[uint32] - // CltvDelay is the CLTV delay for the outpoint. + // CltvDelay is the CLTV delay for the outpoint/transaction. CltvDelay fn.Option[uint32] + + // PayHash is the payment hash for the HTLC that we are trying to + // resolve. This is optional as it only applies HTLC outputs. + PayHash fn.Option[[32]byte] + + // AuxSigDesc is an optional field that contains additional information + // needed to sweep second level HTLCs. + AuxSigDesc fn.Option[AuxSigDesc] } // AuxContractResolver is an interface that is used to resolve contracts that From 98b41c657639705f916883e827f0474e152a5e4e Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 15 Oct 2024 19:09:44 -0700 Subject: [PATCH 02/12] channel: always specify ChanType in ResolutionReq --- lnwallet/channel.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 33360228d50..b9739b017bd 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2202,6 +2202,7 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // resolution data for this output. resolveReq := ResolutionReq{ ChanPoint: chanState.FundingOutpoint, + ChanType: chanState.ChanType, ShortChanID: chanState.ShortChanID(), Initiator: chanState.IsInitiator, FundingBlob: chanState.CustomBlob, @@ -2281,6 +2282,7 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // resolution data for this output. resolveReq := ResolutionReq{ ChanPoint: chanState.FundingOutpoint, + ChanType: chanState.ChanType, ShortChanID: chanState.ShortChanID(), Initiator: chanState.IsInitiator, FundingBlob: chanState.CustomBlob, @@ -6839,6 +6841,7 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, // resolution data for this output. resolveReq := ResolutionReq{ ChanPoint: chanState.FundingOutpoint, + ChanType: chanState.ChanType, ShortChanID: chanState.ShortChanID(), Initiator: chanState.IsInitiator, CommitBlob: chanState.RemoteCommitment.CustomBlob, From 08d0aa23689b4a189283a55c6f802cc68cf66684 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 15 Oct 2024 19:10:38 -0700 Subject: [PATCH 03/12] channel: add ResolutionBlob to Incoming+Outgoing HtlcResolution Similar to the other blobs we have for the commitment output force close resolution, these blobs will be used to ensure that we have everything needed to sweep aux HTLCs. --- lnwallet/channel.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index b9739b017bd..260ed3e840f 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -6959,6 +6959,11 @@ type IncomingHtlcResolution struct { // necessary items required to spend the sole output of the above // transaction. SweepSignDesc input.SignDescriptor + + // ResolutionBlob is a blob used for aux channels that permits a + // spender of the output to properly resolve it in the case of a force + // close. + ResolutionBlob fn.Option[tlv.Blob] } // OutgoingHtlcResolution houses the information necessary to sweep any @@ -7008,6 +7013,11 @@ type OutgoingHtlcResolution struct { // necessary items required to spend the sole output of the above // transaction. SweepSignDesc input.SignDescriptor + + // ResolutionBlob is a blob used for aux channels that permits a + // spender of the output to properly resolve it in the case of a force + // close. + ResolutionBlob fn.Option[tlv.Blob] } // HtlcResolutions contains the items necessary to sweep HTLC's on chain From af60fa0c3a7cf99dc66d45be044e7f1040ec3d4a Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 15 Oct 2024 19:15:43 -0700 Subject: [PATCH 04/12] lnwallet: populate resolution blob for incoming+outgoing HTLC resolutions In this commit, we populate the resolution blobs for the incoming and outgoing HTLCs. We take care to populate the AuxSigDesc with the correct information, as we need to pass along the second-level aux signature and also sign desc along with it. --- lnwallet/channel.go | 277 +++++++++++++++++++++++++++++++++-------- macaroons/fuzz_test.go | 51 ++++++++ 2 files changed, 276 insertions(+), 52 deletions(-) create mode 100644 macaroons/fuzz_test.go diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 260ed3e840f..0f167bf6ac5 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -4455,7 +4455,9 @@ func (lc *LightningChannel) ProcessChanSyncMsg(ctx context.Context, // Next, we'll need to send over any updates we sent as part of // this new proposed commitment state. for _, logUpdate := range commitDiff.LogUpdates { - commitUpdates = append(commitUpdates, logUpdate.UpdateMsg) + commitUpdates = append( + commitUpdates, logUpdate.UpdateMsg, + ) } // If this is a taproot channel, then we need to regenerate the @@ -6706,7 +6708,7 @@ type UnilateralCloseSummary struct { // happen in case we have lost state) it should be set to an empty struct, in // which case we will attempt to sweep the non-HTLC output using the passed // commitPoint. -func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, +func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, //nolint:funlen signer input.Signer, commitSpend *chainntnfs.SpendDetail, remoteCommit channeldb.ChannelCommitment, commitPoint *btcec.PublicKey, leafStore fn.Option[AuxLeafStore], @@ -6750,8 +6752,8 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, chainfee.SatPerKWeight(remoteCommit.FeePerKw), commitType, signer, remoteCommit.Htlcs, keyRing, &chanState.LocalChanCfg, &chanState.RemoteChanCfg, commitSpend.SpendingTx, - chanState.ChanType, isRemoteInitiator, leaseExpiry, - auxResult.AuxLeaves, + chanState.ChanType, isRemoteInitiator, leaseExpiry, chanState, + auxResult.AuxLeaves, auxResolver, ) if err != nil { return nil, fmt.Errorf("unable to create htlc resolutions: %w", @@ -7042,8 +7044,10 @@ func newOutgoingHtlcResolution(signer input.Signer, htlc *channeldb.HTLC, keyRing *CommitmentKeyRing, feePerKw chainfee.SatPerKWeight, csvDelay, leaseExpiry uint32, whoseCommit lntypes.ChannelParty, isCommitFromInitiator bool, - chanType channeldb.ChannelType, - auxLeaves fn.Option[CommitAuxLeaves]) (*OutgoingHtlcResolution, error) { + chanType channeldb.ChannelType, chanState *channeldb.OpenChannel, + auxLeaves fn.Option[CommitAuxLeaves], + auxResolver fn.Option[AuxContractResolver], +) (*OutgoingHtlcResolution, error) { op := wire.OutPoint{ Hash: commitTx.TxHash(), @@ -7074,6 +7078,8 @@ func newOutgoingHtlcResolution(signer input.Signer, return nil, err } + htlcCsvDelay := HtlcSecondLevelInputSequence(chanType) + // If we're spending this HTLC output from the remote node's // commitment, then we won't need to go to the second level as our // outputs don't have a CSV delay. @@ -7111,11 +7117,43 @@ func newOutgoingHtlcResolution(signer input.Signer, } } + resReq := ResolutionReq{ + ChanPoint: chanState.FundingOutpoint, + ChanType: chanType, + ShortChanID: chanState.ShortChanID(), + Initiator: chanState.IsInitiator, + CommitBlob: chanState.RemoteCommitment.CustomBlob, + FundingBlob: chanState.CustomBlob, + Type: input.TaprootHtlcOfferedRemoteTimeout, + CloseType: RemoteForceClose, + CommitTx: commitTx, + ContractPoint: op, + SignDesc: signDesc, + KeyRing: keyRing, + CsvDelay: htlcCsvDelay, + CltvDelay: fn.Some(htlc.RefundTimeout), + CommitFee: chanState.RemoteCommitment.CommitFee, + HtlcID: fn.Some(htlc.HtlcIndex), + PayHash: fn.Some(htlc.RHash), + } + resolveRes := fn.MapOptionZ( + auxResolver, + func(a AuxContractResolver) fn.Result[tlv.Blob] { + return a.ResolveContract(resReq) + }, + ) + if err := resolveRes.Err(); err != nil { + return nil, fmt.Errorf("unable to aux resolve: %w", err) + } + + resolutionBlob := resolveRes.Option() + return &OutgoingHtlcResolution{ - Expiry: htlc.RefundTimeout, - ClaimOutpoint: op, - SweepSignDesc: signDesc, - CsvDelay: HtlcSecondLevelInputSequence(chanType), + Expiry: htlc.RefundTimeout, + ClaimOutpoint: op, + SweepSignDesc: signDesc, + CsvDelay: csvDelay, + ResolutionBlob: resolutionBlob, }, nil } @@ -7266,31 +7304,78 @@ func newOutgoingHtlcResolution(signer input.Signer, keyRing.CommitPoint, localChanCfg.DelayBasePoint.PubKey, ) + // In addition to the info in txSignDetails, we also need extra + // information to sweep the second level output after confirmation. + sweepSignDesc := input.SignDescriptor{ + KeyDesc: localChanCfg.DelayBasePoint, + SingleTweak: localDelayTweak, + WitnessScript: htlcSweepWitnessScript, + Output: &wire.TxOut{ + PkScript: htlcSweepScript.PkScript(), + Value: int64(secondLevelOutputAmt), + }, + HashType: sweepSigHash(chanType), + PrevOutputFetcher: txscript.NewCannedPrevOutputFetcher( + htlcSweepScript.PkScript(), + int64(secondLevelOutputAmt), + ), + SignMethod: signMethod, + ControlBlock: ctrlBlock, + } + + // This might be an aux channel, so we'll go ahead and attempt to + // generate the resolution blob for the channel so we can pass along to + // the sweeping sub-system. + resolveRes := fn.MapOptionZ( + auxResolver, func(a AuxContractResolver) fn.Result[tlv.Blob] { + resReq := ResolutionReq{ + ChanPoint: chanState.FundingOutpoint, + ChanType: chanType, + ShortChanID: chanState.ShortChanID(), + Initiator: chanState.IsInitiator, + CommitBlob: chanState.LocalCommitment.CustomBlob, //nolint:lll + FundingBlob: chanState.CustomBlob, + Type: input.TaprootHtlcLocalOfferedTimeout, //nolint:lll + CloseType: LocalForceClose, + CommitTx: commitTx, + ContractPoint: op, + SignDesc: sweepSignDesc, + KeyRing: keyRing, + CsvDelay: htlcCsvDelay, + HtlcAmt: btcutil.Amount(txOut.Value), + CommitCsvDelay: csvDelay, + CltvDelay: fn.Some(htlc.RefundTimeout), + CommitFee: chanState.LocalCommitment.CommitFee, //nolint:lll + HtlcID: fn.Some(htlc.HtlcIndex), + PayHash: fn.Some(htlc.RHash), + AuxSigDesc: fn.Some(AuxSigDesc{ + SignDetails: *txSignDetails, + AuxSig: func() []byte { + tlvType := htlcCustomSigType.TypeVal() //nolint:lll + return htlc.CustomRecords[uint64(tlvType)] //nolint:lll + }(), + }), + } + + return a.ResolveContract(resReq) + }, + ) + if err := resolveRes.Err(); err != nil { + return nil, fmt.Errorf("unable to aux resolve: %w", err) + } + resolutionBlob := resolveRes.Option() + return &OutgoingHtlcResolution{ Expiry: htlc.RefundTimeout, SignedTimeoutTx: timeoutTx, SignDetails: txSignDetails, CsvDelay: csvDelay, + ResolutionBlob: resolutionBlob, ClaimOutpoint: wire.OutPoint{ Hash: timeoutTx.TxHash(), Index: 0, }, - SweepSignDesc: input.SignDescriptor{ - KeyDesc: localChanCfg.DelayBasePoint, - SingleTweak: localDelayTweak, - WitnessScript: htlcSweepWitnessScript, - Output: &wire.TxOut{ - PkScript: htlcSweepScript.PkScript(), - Value: int64(secondLevelOutputAmt), - }, - HashType: sweepSigHash(chanType), - PrevOutputFetcher: txscript.NewCannedPrevOutputFetcher( - htlcSweepScript.PkScript(), - int64(secondLevelOutputAmt), - ), - SignMethod: signMethod, - ControlBlock: ctrlBlock, - }, + SweepSignDesc: sweepSignDesc, }, nil } @@ -7306,8 +7391,10 @@ func newIncomingHtlcResolution(signer input.Signer, htlc *channeldb.HTLC, keyRing *CommitmentKeyRing, feePerKw chainfee.SatPerKWeight, csvDelay, leaseExpiry uint32, whoseCommit lntypes.ChannelParty, isCommitFromInitiator bool, - chanType channeldb.ChannelType, - auxLeaves fn.Option[CommitAuxLeaves]) (*IncomingHtlcResolution, error) { + chanType channeldb.ChannelType, chanState *channeldb.OpenChannel, + auxLeaves fn.Option[CommitAuxLeaves], + auxResolver fn.Option[AuxContractResolver], +) (*IncomingHtlcResolution, error) { op := wire.OutPoint{ Hash: commitTx.TxHash(), @@ -7339,6 +7426,8 @@ func newIncomingHtlcResolution(signer input.Signer, return nil, err } + htlcCsvDelay := HtlcSecondLevelInputSequence(chanType) + // If we're spending this output from the remote node's commitment, // then we can skip the second layer and spend the output directly. if whoseCommit.IsRemote() { @@ -7374,10 +7463,44 @@ func newIncomingHtlcResolution(signer input.Signer, } } + resReq := ResolutionReq{ + ChanPoint: chanState.FundingOutpoint, + ChanType: chanType, + ShortChanID: chanState.ShortChanID(), + Initiator: chanState.IsInitiator, + CommitBlob: chanState.RemoteCommitment.CustomBlob, + Type: input.TaprootHtlcAcceptedRemoteSuccess, + FundingBlob: chanState.CustomBlob, + CloseType: RemoteForceClose, + CommitTx: commitTx, + ContractPoint: op, + SignDesc: signDesc, + KeyRing: keyRing, + HtlcID: fn.Some(htlc.HtlcIndex), + CsvDelay: htlcCsvDelay, + CltvDelay: fn.Some(htlc.RefundTimeout), + CommitFee: chanState.RemoteCommitment.CommitFee, + PayHash: fn.Some(htlc.RHash), + CommitCsvDelay: csvDelay, + HtlcAmt: htlc.Amt.ToSatoshis(), + } + resolveRes := fn.MapOptionZ( + auxResolver, + func(a AuxContractResolver) fn.Result[tlv.Blob] { + return a.ResolveContract(resReq) + }, + ) + if err := resolveRes.Err(); err != nil { + return nil, fmt.Errorf("unable to aux resolve: %w", err) + } + + resolutionBlob := resolveRes.Option() + return &IncomingHtlcResolution{ - ClaimOutpoint: op, - SweepSignDesc: signDesc, - CsvDelay: HtlcSecondLevelInputSequence(chanType), + ClaimOutpoint: op, + SweepSignDesc: signDesc, + CsvDelay: htlcCsvDelay, + ResolutionBlob: resolutionBlob, }, nil } @@ -7523,30 +7646,76 @@ func newIncomingHtlcResolution(signer input.Signer, localDelayTweak := input.SingleTweakBytes( keyRing.CommitPoint, localChanCfg.DelayBasePoint.PubKey, ) + + // In addition to the info in txSignDetails, we also need extra + // information to sweep the second level output after confirmation. + sweepSignDesc := input.SignDescriptor{ + KeyDesc: localChanCfg.DelayBasePoint, + SingleTweak: localDelayTweak, + WitnessScript: htlcSweepWitnessScript, + Output: &wire.TxOut{ + PkScript: htlcSweepScript.PkScript(), + Value: int64(secondLevelOutputAmt), + }, + HashType: sweepSigHash(chanType), + PrevOutputFetcher: txscript.NewCannedPrevOutputFetcher( + htlcSweepScript.PkScript(), + int64(secondLevelOutputAmt), + ), + SignMethod: signMethod, + ControlBlock: ctrlBlock, + } + + resolveRes := fn.MapOptionZ( + auxResolver, func(a AuxContractResolver) fn.Result[tlv.Blob] { + resReq := ResolutionReq{ + ChanPoint: chanState.FundingOutpoint, + ChanType: chanType, + ShortChanID: chanState.ShortChanID(), + Initiator: chanState.IsInitiator, + CommitBlob: chanState.LocalCommitment.CustomBlob, //nolint:lll + Type: input.TaprootHtlcAcceptedLocalSuccess, //nolint:lll + FundingBlob: chanState.CustomBlob, + CloseType: LocalForceClose, + CommitTx: commitTx, + ContractPoint: op, + SignDesc: sweepSignDesc, + KeyRing: keyRing, + HtlcID: fn.Some(htlc.HtlcIndex), + CsvDelay: htlcCsvDelay, + CommitFee: chanState.LocalCommitment.CommitFee, //nolint:lll + PayHash: fn.Some(htlc.RHash), + AuxSigDesc: fn.Some(AuxSigDesc{ + SignDetails: *txSignDetails, + AuxSig: func() []byte { + tlvType := htlcCustomSigType.TypeVal() //nolint:lll + return htlc.CustomRecords[uint64(tlvType)] //nolint:lll + }(), + }), + CommitCsvDelay: csvDelay, + HtlcAmt: btcutil.Amount(txOut.Value), + CltvDelay: fn.Some(htlc.RefundTimeout), + } + + return a.ResolveContract(resReq) + }, + ) + if err := resolveRes.Err(); err != nil { + return nil, fmt.Errorf("unable to aux resolve: %w", err) + } + + resolutionBlob := resolveRes.Option() + return &IncomingHtlcResolution{ SignedSuccessTx: successTx, SignDetails: txSignDetails, CsvDelay: csvDelay, + ResolutionBlob: resolutionBlob, ClaimOutpoint: wire.OutPoint{ Hash: successTx.TxHash(), Index: 0, }, - SweepSignDesc: input.SignDescriptor{ - KeyDesc: localChanCfg.DelayBasePoint, - SingleTweak: localDelayTweak, - WitnessScript: htlcSweepWitnessScript, - Output: &wire.TxOut{ - PkScript: htlcSweepScript.PkScript(), - Value: int64(secondLevelOutputAmt), - }, - HashType: sweepSigHash(chanType), - PrevOutputFetcher: txscript.NewCannedPrevOutputFetcher( - htlcSweepScript.PkScript(), - int64(secondLevelOutputAmt), - ), - SignMethod: signMethod, - ControlBlock: ctrlBlock, - }, + SweepSignDesc: sweepSignDesc, }, nil } @@ -7583,7 +7752,8 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, localChanCfg, remoteChanCfg *channeldb.ChannelConfig, commitTx *wire.MsgTx, chanType channeldb.ChannelType, isCommitFromInitiator bool, leaseExpiry uint32, - auxLeaves fn.Option[CommitAuxLeaves]) (*HtlcResolutions, error) { + chanState *channeldb.OpenChannel, auxLeaves fn.Option[CommitAuxLeaves], + auxResolver fn.Option[AuxContractResolver]) (*HtlcResolutions, error) { // TODO(roasbeef): don't need to swap csv delay? dustLimit := remoteChanCfg.DustLimit @@ -7618,7 +7788,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, signer, localChanCfg, commitTx, &htlc, keyRing, feePerKw, uint32(csvDelay), leaseExpiry, whoseCommit, isCommitFromInitiator, - chanType, auxLeaves, + chanType, chanState, auxLeaves, auxResolver, ) if err != nil { return nil, fmt.Errorf("incoming resolution "+ @@ -7632,7 +7802,8 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ohr, err := newOutgoingHtlcResolution( signer, localChanCfg, commitTx, &htlc, keyRing, feePerKw, uint32(csvDelay), leaseExpiry, whoseCommit, - isCommitFromInitiator, chanType, auxLeaves, + isCommitFromInitiator, chanType, chanState, auxLeaves, + auxResolver, ) if err != nil { return nil, fmt.Errorf("outgoing resolution "+ @@ -7884,7 +8055,8 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, func(a AuxContractResolver) fn.Result[tlv.Blob] { //nolint:lll return a.ResolveContract(ResolutionReq{ - ChanPoint: chanState.FundingOutpoint, + ChanPoint: chanState.FundingOutpoint, //nolint:lll + ChanType: chanState.ChanType, ShortChanID: chanState.ShortChanID(), Initiator: chanState.IsInitiator, CommitBlob: chanState.LocalCommitment.CustomBlob, @@ -7917,7 +8089,8 @@ 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, auxResult.AuxLeaves, + chanState.IsInitiator, leaseExpiry, chanState, + auxResult.AuxLeaves, auxResolver, ) if err != nil { return nil, fmt.Errorf("unable to gen htlc resolution: %w", err) diff --git a/macaroons/fuzz_test.go b/macaroons/fuzz_test.go new file mode 100644 index 00000000000..defae4143c9 --- /dev/null +++ b/macaroons/fuzz_test.go @@ -0,0 +1,51 @@ +package macaroons + +import ( + "context" + "testing" + + "gopkg.in/macaroon-bakery.v2/bakery" + "gopkg.in/macaroon.v2" +) + +func FuzzUnmarshalMacaroon(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + mac := &macaroon.Macaroon{} + _ = mac.UnmarshalBinary(data) + }) +} + +func FuzzAuthChecker(f *testing.F) { + rootKeyStore := bakery.NewMemRootKeyStore() + ctx := context.Background() + + f.Fuzz(func(t *testing.T, location, entity, action, method string, + rootKey, id []byte) { + + macService, err := NewService( + rootKeyStore, location, true, IPLockChecker, + ) + if err != nil { + return + } + + requiredPermissions := []bakery.Op{{ + Entity: entity, + Action: action, + }} + + mac, err := macaroon.New(rootKey, id, location, macaroon.V2) + if err != nil { + return + } + + macBytes, err := mac.MarshalBinary() + if err != nil { + return + } + + _ = macService.CheckMacAuth( + ctx, macBytes, requiredPermissions, method, + ) + }) +} From 1e7c5415ae231e570cbf97625996ddaac04b2524 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 15 Oct 2024 19:17:06 -0700 Subject: [PATCH 05/12] input: add new Preimage method to input.Input In this commit, we add a new method to obtain an option of a preimage to the input.Input struct. This is useful for callers that have an Input, and want to optionally obtain the preimage. --- contractcourt/breach_arbitrator.go | 5 +++++ input/input.go | 31 +++++++++++++++++++++++++++++- input/mocks.go | 11 +++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/contractcourt/breach_arbitrator.go b/contractcourt/breach_arbitrator.go index dc690e85c73..82b99774600 100644 --- a/contractcourt/breach_arbitrator.go +++ b/contractcourt/breach_arbitrator.go @@ -1160,6 +1160,11 @@ func (bo *breachedOutput) SignDesc() *input.SignDescriptor { return &bo.signDesc } +// Preimage returns the preimage that was used to create the breached output. +func (bo *breachedOutput) Preimage() fn.Option[lntypes.Preimage] { + return fn.None[lntypes.Preimage]() +} + // CraftInputScript computes a valid witness that allows us to spend from the // breached output. It does so by first generating and memoizing the witness // generation function, which parameterized primarily by the witness type and diff --git a/input/input.go b/input/input.go index 6693e9fa8a5..6835f82c8e4 100644 --- a/input/input.go +++ b/input/input.go @@ -69,6 +69,9 @@ type Input interface { // ResolutionBlob returns a special opaque blob to be used to // sweep/resolve this input. ResolutionBlob() fn.Option[tlv.Blob] + + // Preimage returns the preimage for the input if it is an HTLC input. + Preimage() fn.Option[lntypes.Preimage] } // TxInfo describes properties of a parent tx that are relevant for CPFP. @@ -285,6 +288,11 @@ func (bi *BaseInput) CraftInputScript(signer Signer, txn *wire.MsgTx, return witnessFunc(txn, hashCache, txinIdx) } +// Preimage returns the preimage for the input if it is an HTLC input. +func (bi *BaseInput) Preimage() fn.Option[lntypes.Preimage] { + return fn.None[lntypes.Preimage]() +} + // HtlcSucceedInput constitutes a sweep input that needs a pre-image. The input // is expected to reside on the commitment tx of the remote party and should // not be a second level tx output. @@ -357,7 +365,6 @@ func (h *HtlcSucceedInput) CraftInputScript(signer Signer, txn *wire.MsgTx, } desc.SignMethod = TaprootScriptSpendSignMethod - witness, err = SenderHTLCScriptTaprootRedeem( signer, &desc, txn, h.preimage, nil, nil, ) @@ -375,6 +382,15 @@ func (h *HtlcSucceedInput) CraftInputScript(signer Signer, txn *wire.MsgTx, }, nil } +// Preimage returns the preimage for the input if it is an HTLC input. +func (h *HtlcSucceedInput) Preimage() fn.Option[lntypes.Preimage] { + if len(h.preimage) == 0 { + return fn.None[lntypes.Preimage]() + } + + return fn.Some(lntypes.Preimage(h.preimage)) +} + // HtlcSecondLevelAnchorInput is an input type used to spend HTLC outputs // using a re-signed second level transaction, either via the timeout or success // paths. @@ -391,6 +407,8 @@ type HtlcSecondLevelAnchorInput struct { hashCache *txscript.TxSigHashes, prevOutputFetcher txscript.PrevOutputFetcher, txinIdx int) (wire.TxWitness, error) + + preimage []byte } // RequiredTxOut returns the tx out needed to be present on the sweep tx for @@ -427,6 +445,15 @@ func (i *HtlcSecondLevelAnchorInput) CraftInputScript(signer Signer, }, nil } +// Preimage returns the preimage for the input if it is an HTLC input. +func (i *HtlcSecondLevelAnchorInput) Preimage() fn.Option[lntypes.Preimage] { + if len(i.preimage) == 0 { + return fn.None[lntypes.Preimage]() + } + + return fn.Some(lntypes.Preimage(i.preimage)) +} + // MakeHtlcSecondLevelTimeoutAnchorInput creates an input allowing the sweeper // to spend the HTLC output on our commit using the second level timeout // transaction. @@ -545,6 +572,7 @@ func MakeHtlcSecondLevelSuccessAnchorInput(signedTx *wire.MsgTx, SignedTx: signedTx, inputKit: input.inputKit, createWitness: createWitness, + preimage: preimage[:], } } @@ -588,6 +616,7 @@ func MakeHtlcSecondLevelSuccessTaprootInput(signedTx *wire.MsgTx, inputKit: input.inputKit, SignedTx: signedTx, createWitness: createWitness, + preimage: preimage[:], } } diff --git a/input/mocks.go b/input/mocks.go index 695525955c6..c2af637eda9 100644 --- a/input/mocks.go +++ b/input/mocks.go @@ -140,6 +140,17 @@ func (m *MockInput) ResolutionBlob() fn.Option[tlv.Blob] { return info.(fn.Option[tlv.Blob]) } +func (m *MockInput) Preimage() fn.Option[lntypes.Preimage] { + args := m.Called() + + info := args.Get(0) + if info == nil { + return fn.None[lntypes.Preimage]() + } + + return info.(fn.Option[lntypes.Preimage]) +} + // MockWitnessType implements the `WitnessType` interface and is used by other // packages for mock testing. type MockWitnessType struct { From 9b8adf5f5c7720e09478517c2dd714fa589be0db Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 15 Oct 2024 19:18:23 -0700 Subject: [PATCH 06/12] contractcourt: add HtlcBlobs to taprootBriefcase In this commit, we add the set of HtlcBlobs to the taprootBriefcase struct. This new field will store all the resolution blobs for a given HTLC. We also add some new property based tests along the way for adequate test coverage. --- contractcourt/taproot_briefcase.go | 124 +++++++++++++++++- contractcourt/taproot_briefcase_test.go | 41 ++++++ ...BlobEncodeDecode-20240902140253-81338.fail | 78 +++++++++++ go.mod | 1 + go.sum | 2 + 5 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 contractcourt/testdata/rapid/TestHtlcAuxBlobEncodeDecode/TestHtlcAuxBlobEncodeDecode-20240902140253-81338.fail diff --git a/contractcourt/taproot_briefcase.go b/contractcourt/taproot_briefcase.go index 0a2beeff798..4b703dd2952 100644 --- a/contractcourt/taproot_briefcase.go +++ b/contractcourt/taproot_briefcase.go @@ -39,7 +39,9 @@ type taprootBriefcase struct { // used to sweep a remote party's breached output. BreachedCommitBlob tlv.OptionalRecordT[tlv.TlvType3, tlv.Blob] - // TODO(roasbeef): htlc blobs + // HtlcBlobs is an optikonal record that contains the opaque blobs for + // the set of active HTLCs on the commitment transaction. + HtlcBlobs tlv.OptionalRecordT[tlv.TlvType4, htlcAuxBlobs] } // TODO(roasbeef): morph into new tlv record @@ -70,6 +72,9 @@ func (t *taprootBriefcase) EncodeRecords() []tlv.Record { records = append(records, r.Record()) }, ) + t.HtlcBlobs.WhenSome(func(r tlv.RecordT[tlv.TlvType4, htlcAuxBlobs]) { + records = append(records, r.Record()) + }) return records } @@ -96,10 +101,11 @@ func (t *taprootBriefcase) Encode(w io.Writer) error { func (t *taprootBriefcase) Decode(r io.Reader) error { settledCommitBlob := t.SettledCommitBlob.Zero() breachedCommitBlob := t.BreachedCommitBlob.Zero() + htlcBlobs := t.HtlcBlobs.Zero() + records := append( - t.DecodeRecords(), - settledCommitBlob.Record(), - breachedCommitBlob.Record(), + t.DecodeRecords(), settledCommitBlob.Record(), + breachedCommitBlob.Record(), htlcBlobs.Record(), ) stream, err := tlv.NewStream(records...) if err != nil { @@ -117,6 +123,9 @@ func (t *taprootBriefcase) Decode(r io.Reader) error { if v, ok := typeMap[t.BreachedCommitBlob.TlvType()]; ok && v == nil { t.BreachedCommitBlob = tlv.SomeRecordT(breachedCommitBlob) } + if v, ok := typeMap[t.HtlcBlobs.TlvType()]; ok && v == nil { + t.HtlcBlobs = tlv.SomeRecordT(htlcBlobs) + } return nil } @@ -686,3 +695,110 @@ func (t *tapTweaks) Decode(r io.Reader) error { return stream.Decode(r) } + +// htlcAuxBlobs is a map of resolver IDs to their corresponding HTLC blobs. +// This is used to store the resolution blobs for HTLCs that are not yet +// resolved. +type htlcAuxBlobs map[resolverID]tlv.Blob + +// newAuxHtlcBlobs returns a new instance of the htlcAuxBlobs struct. +func newAuxHtlcBlobs() htlcAuxBlobs { + return make(htlcAuxBlobs) +} + +// Encode encodes the set of HTLC blobs into the target writer. +func (h *htlcAuxBlobs) Encode(w io.Writer) error { + var buf [8]byte + + numBlobs := uint64(len(*h)) + if err := tlv.WriteVarInt(w, numBlobs, &buf); err != nil { + return err + } + + for id, blob := range *h { + if _, err := w.Write(id[:]); err != nil { + return err + } + + if err := varBytesEncoder(w, &blob, &buf); err != nil { + return err + } + } + + return nil +} + +// Decode decodes the set of HTLC blobs from the target reader. +func (h *htlcAuxBlobs) Decode(r io.Reader) error { + var buf [8]byte + + numBlobs, err := tlv.ReadVarInt(r, &buf) + if err != nil { + return err + } + + for i := uint64(0); i < numBlobs; i++ { + var id resolverID + if _, err := io.ReadFull(r, id[:]); err != nil { + return err + } + + var blob tlv.Blob + if err := varBytesDecoder(r, &blob, &buf, 0); err != nil { + return err + } + + (*h)[id] = blob + } + + return nil +} + +// eHtlcAuxBlobsEncoder is a custom TLV encoder for the htlcAuxBlobs struct. +func htlcAuxBlobsEncoder(w io.Writer, val any, _ *[8]byte) error { + if t, ok := val.(*htlcAuxBlobs); ok { + return (*t).Encode(w) + } + + return tlv.NewTypeForEncodingErr(val, "htlcAuxBlobs") +} + +// dHtlcAuxBlobsDecoder is a custom TLV decoder for the htlcAuxBlobs struct. +func htlcAuxBlobsDecoder(r io.Reader, val any, _ *[8]byte, + l uint64) error { + + if typ, ok := val.(*htlcAuxBlobs); ok { + blobReader := io.LimitReader(r, int64(l)) + + htlcBlobs := newAuxHtlcBlobs() + err := htlcBlobs.Decode(blobReader) + if err != nil { + return err + } + + *typ = htlcBlobs + + return nil + } + + return tlv.NewTypeForDecodingErr(val, "htlcAuxBlobs", l, l) +} + +// Record returns a tlv.Record for the htlcAuxBlobs struct. +func (h *htlcAuxBlobs) Record() tlv.Record { + recordSize := func() uint64 { + var ( + b bytes.Buffer + buf [8]byte + ) + if err := htlcAuxBlobsEncoder(&b, h, &buf); err != nil { + panic(err) + } + + return uint64(len(b.Bytes())) + } + + return tlv.MakeDynamicRecord( + 0, h, recordSize, htlcAuxBlobsEncoder, htlcAuxBlobsDecoder, + ) +} diff --git a/contractcourt/taproot_briefcase_test.go b/contractcourt/taproot_briefcase_test.go index a7d52d9635d..441aebf1d77 100644 --- a/contractcourt/taproot_briefcase_test.go +++ b/contractcourt/taproot_briefcase_test.go @@ -7,6 +7,7 @@ import ( "github.com/lightningnetwork/lnd/tlv" "github.com/stretchr/testify/require" + "pgregory.net/rapid" ) func randResolverCtrlBlocks(t *testing.T) resolverCtrlBlocks { @@ -53,6 +54,25 @@ func randHtlcTweaks(t *testing.T) htlcTapTweaks { return tweaks } +func randHtlcAuxBlobs(t *testing.T) htlcAuxBlobs { + numBlobs := rand.Int() % 256 + blobs := make(htlcAuxBlobs, numBlobs) + + for i := 0; i < numBlobs; i++ { + var id resolverID + _, err := rand.Read(id[:]) + require.NoError(t, err) + + var blob [100]byte + _, err = rand.Read(blob[:]) + require.NoError(t, err) + + blobs[id] = blob[:] + } + + return blobs +} + // TestTaprootBriefcase tests the encode/decode methods of the taproot // briefcase extension. func TestTaprootBriefcase(t *testing.T) { @@ -93,6 +113,9 @@ func TestTaprootBriefcase(t *testing.T) { BreachedCommitBlob: tlv.SomeRecordT( tlv.NewPrimitiveRecord[tlv.TlvType3](commitBlob[:]), ), + HtlcBlobs: tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType4](randHtlcAuxBlobs(t)), + ), } var b bytes.Buffer @@ -103,3 +126,21 @@ func TestTaprootBriefcase(t *testing.T) { require.Equal(t, testCase, &decodedCase) } + +// TestHtlcAuxBlobEncodeDecode tests the encode/decode methods of the HTLC aux +// blobs. +func TestHtlcAuxBlobEncodeDecode(t *testing.T) { + t.Parallel() + + rapid.Check(t, func(t *rapid.T) { + htlcBlobs := rapid.Make[htlcAuxBlobs]().Draw(t, "htlcAuxBlobs") + + var b bytes.Buffer + require.NoError(t, htlcBlobs.Encode(&b)) + + decodedBlobs := newAuxHtlcBlobs() + require.NoError(t, decodedBlobs.Decode(&b)) + + require.Equal(t, htlcBlobs, decodedBlobs) + }) +} diff --git a/contractcourt/testdata/rapid/TestHtlcAuxBlobEncodeDecode/TestHtlcAuxBlobEncodeDecode-20240902140253-81338.fail b/contractcourt/testdata/rapid/TestHtlcAuxBlobEncodeDecode/TestHtlcAuxBlobEncodeDecode-20240902140253-81338.fail new file mode 100644 index 00000000000..86bb07ed75e --- /dev/null +++ b/contractcourt/testdata/rapid/TestHtlcAuxBlobEncodeDecode/TestHtlcAuxBlobEncodeDecode-20240902140253-81338.fail @@ -0,0 +1,78 @@ +# 2024/09/02 14:02:53.354676 [TestHtlcAuxBlobEncodeDecode] [rapid] draw htlcAuxBlobs: contractcourt.htlcAuxBlobs{contractcourt.resolverID{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}:[]uint8{}} +# +v0.4.8#15807814492030881602 +0x5555555555555 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 +0x0 \ No newline at end of file diff --git a/go.mod b/go.mod index 586cc329937..6e2bd9f77d1 100644 --- a/go.mod +++ b/go.mod @@ -64,6 +64,7 @@ require ( google.golang.org/protobuf v1.33.0 gopkg.in/macaroon-bakery.v2 v2.0.1 gopkg.in/macaroon.v2 v2.0.0 + pgregory.net/rapid v1.1.0 ) require ( diff --git a/go.sum b/go.sum index b64a5c43d1d..86c1c8a21ae 100644 --- a/go.sum +++ b/go.sum @@ -1076,6 +1076,8 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= +pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= From 7ef2683586b59f18283b3a5f127481a7d88f6afe Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 15 Oct 2024 19:19:22 -0700 Subject: [PATCH 07/12] contractcourt: update encode/decode for taproot aux data When we read/write the aux data, we need to make sure we always set the new fields for aux HTLCs. --- contractcourt/briefcase.go | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/contractcourt/briefcase.go b/contractcourt/briefcase.go index acc49be441f..cd4fd90ccbb 100644 --- a/contractcourt/briefcase.go +++ b/contractcourt/briefcase.go @@ -1571,6 +1571,7 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error { }) } + htlcBlobs := newAuxHtlcBlobs() for _, htlc := range c.HtlcResolutions.IncomingHTLCs { htlc := htlc @@ -1581,8 +1582,9 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error { continue } + var resID resolverID if htlc.SignedSuccessTx != nil { - resID := newResolverID( + resID = newResolverID( htlc.SignedSuccessTx.TxIn[0].PreviousOutPoint, ) //nolint:lll @@ -1598,10 +1600,14 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error { tapCase.CtrlBlocks.Val.IncomingHtlcCtrlBlocks[resID] = bridgeCtrlBlock } } else { - resID := newResolverID(htlc.ClaimOutpoint) + resID = newResolverID(htlc.ClaimOutpoint) //nolint:lll tapCase.CtrlBlocks.Val.IncomingHtlcCtrlBlocks[resID] = ctrlBlock } + + htlc.ResolutionBlob.WhenSome(func(b []byte) { + htlcBlobs[resID] = b + }) } for _, htlc := range c.HtlcResolutions.OutgoingHTLCs { htlc := htlc @@ -1613,8 +1619,9 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error { continue } + var resID resolverID if htlc.SignedTimeoutTx != nil { - resID := newResolverID( + resID = newResolverID( htlc.SignedTimeoutTx.TxIn[0].PreviousOutPoint, ) //nolint:lll @@ -1632,10 +1639,14 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error { tapCase.CtrlBlocks.Val.OutgoingHtlcCtrlBlocks[resID] = bridgeCtrlBlock } } else { - resID := newResolverID(htlc.ClaimOutpoint) + resID = newResolverID(htlc.ClaimOutpoint) //nolint:lll tapCase.CtrlBlocks.Val.OutgoingHtlcCtrlBlocks[resID] = ctrlBlock } + + htlc.ResolutionBlob.WhenSome(func(b []byte) { + htlcBlobs[resID] = b + }) } if c.AnchorResolution != nil { @@ -1643,6 +1654,12 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error { tapCase.TapTweaks.Val.AnchorTweak = anchorSignDesc.TapTweak } + if len(htlcBlobs) != 0 { + tapCase.HtlcBlobs = tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType4](htlcBlobs), + ) + } + return tapCase.Encode(w) } @@ -1661,6 +1678,8 @@ func decodeTapRootAuxData(r io.Reader, c *ContractResolutions) error { }) } + htlcBlobs := tapCase.HtlcBlobs.ValOpt().UnwrapOr(newAuxHtlcBlobs()) + for i := range c.HtlcResolutions.IncomingHTLCs { htlc := c.HtlcResolutions.IncomingHTLCs[i] @@ -1687,7 +1706,12 @@ func decodeTapRootAuxData(r io.Reader, c *ContractResolutions) error { htlc.SweepSignDesc.ControlBlock = ctrlBlock } + if htlcBlob, ok := htlcBlobs[resID]; ok { + htlc.ResolutionBlob = fn.Some(htlcBlob) + } + c.HtlcResolutions.IncomingHTLCs[i] = htlc + } for i := range c.HtlcResolutions.OutgoingHTLCs { htlc := c.HtlcResolutions.OutgoingHTLCs[i] @@ -1715,6 +1739,10 @@ func decodeTapRootAuxData(r io.Reader, c *ContractResolutions) error { htlc.SweepSignDesc.ControlBlock = ctrlBlock } + if htlcBlob, ok := htlcBlobs[resID]; ok { + htlc.ResolutionBlob = fn.Some(htlcBlob) + } + c.HtlcResolutions.OutgoingHTLCs[i] = htlc } From ab41f28a8fc67dd65fca27aca5ef6da042bb1a0a Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 15 Oct 2024 19:36:11 -0700 Subject: [PATCH 08/12] contractcourt: pass in new aux resolution blob to sweeper in resolvers With this commit, we update all the resolvers to pass in the new htlc resolution blobs. Along the way, we remove the old blocking guard on this resolution logic for HTLCs with blobs. --- .../htlc_incoming_contest_resolver.go | 11 ----------- contractcourt/htlc_lease_resolver.go | 12 +++++++++--- .../htlc_outgoing_contest_resolver.go | 11 ----------- contractcourt/htlc_success_resolver.go | 19 +++++++------------ contractcourt/htlc_timeout_resolver.go | 16 +++++----------- 5 files changed, 21 insertions(+), 48 deletions(-) diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index 9ffa799437e..6bda4e398b0 100644 --- a/contractcourt/htlc_incoming_contest_resolver.go +++ b/contractcourt/htlc_incoming_contest_resolver.go @@ -99,17 +99,6 @@ func (h *htlcIncomingContestResolver) Resolve( return nil, nil } - // If the HTLC has custom records, then for now we'll pause resolution. - // - // TODO(roasbeef): Implement resolving HTLCs with custom records - // (follow-up PR). - if len(h.htlc.CustomRecords) != 0 { - select { //nolint:gosimple - case <-h.quit: - return nil, errResolverShuttingDown - } - } - // First try to parse the payload. If that fails, we can stop resolution // now. payload, nextHopOnionBlob, err := h.decodePayload() diff --git a/contractcourt/htlc_lease_resolver.go b/contractcourt/htlc_lease_resolver.go index 87e55d5cc46..53fa8935534 100644 --- a/contractcourt/htlc_lease_resolver.go +++ b/contractcourt/htlc_lease_resolver.go @@ -6,7 +6,9 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/tlv" ) // htlcLeaseResolver is a struct that houses the lease specific HTLC resolution @@ -52,8 +54,8 @@ func (h *htlcLeaseResolver) deriveWaitHeight(csvDelay uint32, // send to the sweeper so the output can ultimately be swept. func (h *htlcLeaseResolver) makeSweepInput(op *wire.OutPoint, wType, cltvWtype input.StandardWitnessType, - signDesc *input.SignDescriptor, - csvDelay, broadcastHeight uint32, payHash [32]byte) *input.BaseInput { + signDesc *input.SignDescriptor, csvDelay, broadcastHeight uint32, + payHash [32]byte, resBlob fn.Option[tlv.Blob]) *input.BaseInput { if h.hasCLTV() { log.Infof("%T(%x): CSV and CLTV locks expired, offering "+ @@ -63,13 +65,17 @@ func (h *htlcLeaseResolver) makeSweepInput(op *wire.OutPoint, op, cltvWtype, signDesc, broadcastHeight, csvDelay, h.leaseExpiry, + input.WithResolutionBlob(resBlob), ) } log.Infof("%T(%x): CSV lock expired, offering second-layer output to "+ "sweeper: %v", h, payHash, op) - return input.NewCsvInput(op, wType, signDesc, broadcastHeight, csvDelay) + return input.NewCsvInput( + op, wType, signDesc, broadcastHeight, csvDelay, + input.WithResolutionBlob(resBlob), + ) } // SupplementState allows the user of a ContractResolver to supplement it with diff --git a/contractcourt/htlc_outgoing_contest_resolver.go b/contractcourt/htlc_outgoing_contest_resolver.go index c75b898222d..2466544c982 100644 --- a/contractcourt/htlc_outgoing_contest_resolver.go +++ b/contractcourt/htlc_outgoing_contest_resolver.go @@ -58,17 +58,6 @@ func (h *htlcOutgoingContestResolver) Resolve( return nil, nil } - // If the HTLC has custom records, then for now we'll pause resolution. - // - // TODO(roasbeef): Implement resolving HTLCs with custom records - // (follow-up PR). - if len(h.htlc.CustomRecords) != 0 { - select { //nolint:gosimple - case <-h.quit: - return nil, errResolverShuttingDown - } - } - // Otherwise, we'll watch for two external signals to decide if we'll // morph into another resolver, or fully resolve the contract. // diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 3b07828d484..4c9d2b200bb 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -123,17 +123,6 @@ func (h *htlcSuccessResolver) Resolve( return nil, nil } - // If the HTLC has custom records, then for now we'll pause resolution. - // - // TODO(roasbeef): Implement resolving HTLCs with custom records - // (follow-up PR). - if len(h.htlc.CustomRecords) != 0 { - select { //nolint:gosimple - case <-h.quit: - return nil, errResolverShuttingDown - } - } - // If we don't have a success transaction, then this means that this is // an output on the remote party's commitment transaction. if h.htlcResolution.SignedSuccessTx == nil { @@ -258,6 +247,9 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx(immediate bool) ( h.htlcResolution.SignedSuccessTx, h.htlcResolution.SignDetails, h.htlcResolution.Preimage, h.broadcastHeight, + input.WithResolutionBlob( + h.htlcResolution.ResolutionBlob, + ), ) } else { //nolint:lll @@ -414,7 +406,7 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx(immediate bool) ( input.LeaseHtlcAcceptedSuccessSecondLevel, &h.htlcResolution.SweepSignDesc, h.htlcResolution.CsvDelay, uint32(commitSpend.SpendingHeight), - h.htlc.RHash, + h.htlc.RHash, h.htlcResolution.ResolutionBlob, ) // Calculate the budget for this sweep. @@ -470,6 +462,9 @@ func (h *htlcSuccessResolver) resolveRemoteCommitOutput(immediate bool) ( h.htlcResolution.Preimage[:], h.broadcastHeight, h.htlcResolution.CsvDelay, + input.WithResolutionBlob( + h.htlcResolution.ResolutionBlob, + ), )) } else { inp = lnutils.Ptr(input.MakeHtlcSucceedInput( diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 1c5620fc607..e7ab4216917 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -426,17 +426,6 @@ func (h *htlcTimeoutResolver) Resolve( return nil, nil } - // If the HTLC has custom records, then for now we'll pause resolution. - // - // TODO(roasbeef): Implement resolving HTLCs with custom records - // (follow-up PR). - if len(h.htlc.CustomRecords) != 0 { - select { //nolint:gosimple - case <-h.quit: - return nil, errResolverShuttingDown - } - } - // Start by spending the HTLC output, either by broadcasting the // second-level timeout transaction, or directly if this is the remote // commitment. @@ -499,6 +488,9 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx(immediate bool) error { h.htlcResolution.SignedTimeoutTx, h.htlcResolution.SignDetails, h.broadcastHeight, + input.WithResolutionBlob( + h.htlcResolution.ResolutionBlob, + ), )) } else { inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutAnchorInput( @@ -592,6 +584,7 @@ func (h *htlcTimeoutResolver) sweepDirectHtlcOutput(immediate bool) error { &h.htlcResolution.ClaimOutpoint, htlcWitnessType, &h.htlcResolution.SweepSignDesc, h.broadcastHeight, h.htlcResolution.CsvDelay, h.htlcResolution.Expiry, + input.WithResolutionBlob(h.htlcResolution.ResolutionBlob), ) // Calculate the budget. @@ -846,6 +839,7 @@ func (h *htlcTimeoutResolver) handleCommitSpend( &h.htlcResolution.SweepSignDesc, h.htlcResolution.CsvDelay, uint32(commitSpend.SpendingHeight), h.htlc.RHash, + h.htlcResolution.ResolutionBlob, ) // Calculate the budget for this sweep. From ba16a74491ec1b20513b0e347c55d59af0f4451f Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 7 Nov 2024 18:28:08 -0800 Subject: [PATCH 09/12] sweep: expand NotifyBroadcast to include an outpoint index In this commit, we expand the `NotifyBroadcast` to include an outpoint index. This is useful as it indicates the index of a given required tx out input. --- contractcourt/breach_arbitrator.go | 2 +- sweep/fee_bumper.go | 30 ++++++++++++++++++++++-------- sweep/interface.go | 3 ++- sweep/mock_test.go | 2 +- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/contractcourt/breach_arbitrator.go b/contractcourt/breach_arbitrator.go index 82b99774600..a8154d0e618 100644 --- a/contractcourt/breach_arbitrator.go +++ b/contractcourt/breach_arbitrator.go @@ -752,7 +752,7 @@ justiceTxBroadcast: } return aux.NotifyBroadcast( - &bumpReq, finalTx.justiceTx, finalTx.fee, + &bumpReq, finalTx.justiceTx, finalTx.fee, nil, ) }) if err != nil { diff --git a/sweep/fee_bumper.go b/sweep/fee_bumper.go index fd3dfba9e19..5ea4d8e4bd6 100644 --- a/sweep/fee_bumper.go +++ b/sweep/fee_bumper.go @@ -603,7 +603,9 @@ func (t *TxPublisher) broadcast(requestID uint64) (*BumpResult, error) { // Before we go to broadcast, we'll notify the aux sweeper, if it's // present of this new broadcast attempt. err := fn.MapOptionZ(t.cfg.AuxSweeper, func(aux AuxSweeper) error { - return aux.NotifyBroadcast(record.req, tx, record.fee) + return aux.NotifyBroadcast( + record.req, tx, record.fee, record.outpointToTxIndex, + ) }) if err != nil { return nil, fmt.Errorf("unable to notify aux sweeper: %w", err) @@ -725,6 +727,9 @@ type monitorRecord struct { // fee is the fee paid by the tx. fee btcutil.Amount + + // outpointToTxIndex is a map of outpoint to tx index. + outpointToTxIndex map[wire.OutPoint]int } // Start starts the publisher by subscribing to block epoch updates and kicking @@ -1042,10 +1047,11 @@ func (t *TxPublisher) createAndPublishTx(requestID uint64, // The tx has been created without any errors, we now register a new // record by overwriting the same requestID. t.records.Store(requestID, &monitorRecord{ - tx: sweepCtx.tx, - req: r.req, - feeFunction: r.feeFunction, - fee: sweepCtx.fee, + tx: sweepCtx.tx, + req: r.req, + feeFunction: r.feeFunction, + fee: sweepCtx.fee, + outpointToTxIndex: sweepCtx.outpointToTxIndex, }) // Attempt to broadcast this new tx. @@ -1199,6 +1205,10 @@ type sweepTxCtx struct { fee btcutil.Amount extraTxOut fn.Option[SweepOutput] + + // outpointToTxIndex maps the outpoint of the inputs to their index in + // the sweep transaction. + outpointToTxIndex map[wire.OutPoint]int } // createSweepTx creates a sweeping tx based on the given inputs, change @@ -1229,6 +1239,7 @@ func (t *TxPublisher) createSweepTx(inputs []input.Input, // We start by adding all inputs that commit to an output. We do this // since the input and output index must stay the same for the // signatures to be valid. + outpointToTxIndex := make(map[wire.OutPoint]int) for _, o := range inputs { if o.RequiredTxOut() == nil { continue @@ -1240,6 +1251,8 @@ func (t *TxPublisher) createSweepTx(inputs []input.Input, Sequence: o.BlocksToMaturity(), }) sweepTx.AddTxOut(o.RequiredTxOut()) + + outpointToTxIndex[o.OutPoint()] = len(sweepTx.TxOut) - 1 } // Sum up the value contained in the remaining inputs, and add them to @@ -1331,9 +1344,10 @@ func (t *TxPublisher) createSweepTx(inputs []input.Input, )(changeOutputsOpt) return &sweepTxCtx{ - tx: sweepTx, - fee: txFee, - extraTxOut: fn.FlattenOption(extraTxOut), + tx: sweepTx, + fee: txFee, + extraTxOut: fn.FlattenOption(extraTxOut), + outpointToTxIndex: outpointToTxIndex, }, nil } diff --git a/sweep/interface.go b/sweep/interface.go index acece31430d..f2fff84b08f 100644 --- a/sweep/interface.go +++ b/sweep/interface.go @@ -93,5 +93,6 @@ type AuxSweeper interface { // NotifyBroadcast is used to notify external callers of the broadcast // of a sweep transaction, generated by the passed BumpRequest. NotifyBroadcast(req *BumpRequest, tx *wire.MsgTx, - totalFees btcutil.Amount) error + totalFees btcutil.Amount, + outpointToTxIndex map[wire.OutPoint]int) error } diff --git a/sweep/mock_test.go b/sweep/mock_test.go index c623ca3c0b4..34202b1453d 100644 --- a/sweep/mock_test.go +++ b/sweep/mock_test.go @@ -352,7 +352,7 @@ func (m *MockAuxSweeper) ExtraBudgetForInputs( // NotifyBroadcast is used to notify external callers of the broadcast // of a sweep transaction, generated by the passed BumpRequest. func (*MockAuxSweeper) NotifyBroadcast(_ *BumpRequest, _ *wire.MsgTx, - _ btcutil.Amount) error { + _ btcutil.Amount, _ map[wire.OutPoint]int) error { return nil } From 87b4991bb62799ebb254d555af22396b2b593bbf Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 7 Nov 2024 18:42:42 -0800 Subject: [PATCH 10/12] lnwallet: add whoseCommit to FetchLeavesFromCommit This is useful for additional context to know which commit the AuxLeafStore is fetching the leaves for. --- contractcourt/chain_watcher.go | 1 + lnwallet/aux_leaf_store.go | 3 ++- lnwallet/channel.go | 10 ++++++---- lnwallet/commitment.go | 2 +- lnwallet/mock.go | 5 +++-- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index f25ca7e0a75..64307dd0204 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -436,6 +436,7 @@ func (c *chainWatcher) handleUnknownLocalState( return s.FetchLeavesFromCommit( lnwallet.NewAuxChanState(c.cfg.chanState), c.cfg.chanState.LocalCommitment, *commitKeyRing, + lntypes.Local, ) }, ).Unpack() diff --git a/lnwallet/aux_leaf_store.go b/lnwallet/aux_leaf_store.go index 4558c2f81ce..c457a92509b 100644 --- a/lnwallet/aux_leaf_store.go +++ b/lnwallet/aux_leaf_store.go @@ -178,7 +178,8 @@ type AuxLeafStore interface { // commitment. FetchLeavesFromCommit(chanState AuxChanState, commit channeldb.ChannelCommitment, - keyRing CommitmentKeyRing) fn.Result[CommitDiffAuxResult] + keyRing CommitmentKeyRing, whoseCommit lntypes.ChannelParty, + ) fn.Result[CommitDiffAuxResult] // FetchLeavesFromRevocation attempts to fetch the auxiliary leaves // from a channel revocation that stores balance + blob information. diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 0f167bf6ac5..e7ec607727c 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -666,6 +666,7 @@ func (lc *LightningChannel) diskCommitToMemCommit( return s.FetchLeavesFromCommit( NewAuxChanState(lc.channelState), *diskCommit, *commitKeys.GetForParty(whoseCommit), + whoseCommit, ) }, ).Unpack() @@ -1834,7 +1835,7 @@ func (lc *LightningChannel) restorePendingLocalUpdates( func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] { return s.FetchLeavesFromCommit( NewAuxChanState(lc.channelState), pendingCommit, - *pendingRemoteKeys, + *pendingRemoteKeys, lntypes.Remote, ) }, ).Unpack() @@ -3154,7 +3155,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, leafStore, func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] { return s.FetchLeavesFromCommit( NewAuxChanState(chanState), *diskCommit, - *keyRing, + *keyRing, lntypes.Remote, ) }, ).Unpack() @@ -4740,7 +4741,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, leafStore, func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] { return s.FetchLeavesFromCommit( NewAuxChanState(chanState), *diskCommit, - *keyRing, + *keyRing, lntypes.Local, ) }, ).Unpack() @@ -6727,7 +6728,7 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, //nolint:funlen leafStore, func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] { return s.FetchLeavesFromCommit( NewAuxChanState(chanState), remoteCommit, - *keyRing, + *keyRing, lntypes.Remote, ) }, ).Unpack() @@ -7949,6 +7950,7 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, return s.FetchLeavesFromCommit( NewAuxChanState(chanState), chanState.LocalCommitment, *keyRing, + lntypes.Local, ) }, ).Unpack() diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 6d61729a41c..36ff75edeb4 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -1307,7 +1307,7 @@ func findOutputIndexesFromRemote(revocationPreimage *chainhash.Hash, leafStore, func(a AuxLeafStore) fn.Result[CommitDiffAuxResult] { return a.FetchLeavesFromCommit( NewAuxChanState(chanState), chanCommit, - *keyRing, + *keyRing, lntypes.Remote, ) }, ).Unpack() diff --git a/lnwallet/mock.go b/lnwallet/mock.go index d71b292a7c3..6623d8014f7 100644 --- a/lnwallet/mock.go +++ b/lnwallet/mock.go @@ -19,6 +19,7 @@ import ( "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/tlv" "github.com/stretchr/testify/mock" @@ -420,8 +421,8 @@ func (*MockAuxLeafStore) FetchLeavesFromView( // correspond to the passed aux blob, and an existing channel // commitment. func (*MockAuxLeafStore) FetchLeavesFromCommit(_ AuxChanState, - _ channeldb.ChannelCommitment, - _ CommitmentKeyRing) fn.Result[CommitDiffAuxResult] { + _ channeldb.ChannelCommitment, _ CommitmentKeyRing, + _ lntypes.ChannelParty) fn.Result[CommitDiffAuxResult] { return fn.Ok(CommitDiffAuxResult{}) } From 414894348aa9e2e98c6eaa17f9257edd23e6e10f Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 7 Nov 2024 18:54:33 -0800 Subject: [PATCH 11/12] sweep: update BudgetInputSet.Budget() to factor in extra budget In this commit, we update the `Budget()` call to factor in the `extraBudget` value. Otherwise, when we go to intialize the fee function, we won't factor in the extra budget, and will determine that we can't broadcast/bump. --- sweep/tx_input_set.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sweep/tx_input_set.go b/sweep/tx_input_set.go index 3a95fff2fb8..ce144a8eb3c 100644 --- a/sweep/tx_input_set.go +++ b/sweep/tx_input_set.go @@ -373,7 +373,9 @@ func (b *BudgetInputSet) Budget() btcutil.Amount { budget += input.params.Budget } - return budget + // We'll also tack on the extra budget which will eventually be + // accounted for by the wallet txns when we're broadcasting. + return budget + b.extraBudget } // DeadlineHeight returns the deadline height of the set. From 9a1adbeeaabac4fc5cfdb4e2930f6181e4fc1af5 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 15 Nov 2024 09:50:01 +0100 Subject: [PATCH 12/12] docs: move 0.18.4 items, add full list of custom chan PRs --- docs/release-notes/release-notes-0.18.4.md | 20 ++++++++++++++++++-- docs/release-notes/release-notes-0.19.0.md | 10 +--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/docs/release-notes/release-notes-0.18.4.md b/docs/release-notes/release-notes-0.18.4.md index f6a2f3967b3..f7796706d18 100644 --- a/docs/release-notes/release-notes-0.18.4.md +++ b/docs/release-notes/release-notes-0.18.4.md @@ -18,11 +18,23 @@ - [Tooling and Documentation](#tooling-and-documentation) # Bug Fixes + +* [Fix a bug](https://github.com/lightningnetwork/lnd/pull/9134) that would + cause a nil pointer dereference during the probing of a payment request that + does not contain a payment address. + # New Features The main channel state machine and database now allow for processing and storing -custom Taproot script leaves, [allowing the implementation of custom channel -types](https://github.com/lightningnetwork/lnd/pull/8960). +custom Taproot script leaves, allowing the implementation of custom channel +types in a series of changes: + * https://github.com/lightningnetwork/lnd/pull/9025 + * https://github.com/lightningnetwork/lnd/pull/9030 + * https://github.com/lightningnetwork/lnd/pull/9049 + * https://github.com/lightningnetwork/lnd/pull/9072 + * https://github.com/lightningnetwork/lnd/pull/9095 + * https://github.com/lightningnetwork/lnd/pull/8960 + * https://github.com/lightningnetwork/lnd/pull/9194 ## Functional Enhancements @@ -82,6 +94,10 @@ types](https://github.com/lightningnetwork/lnd/pull/8960). ## Breaking Changes ## Performance Improvements +* [A new method](https://github.com/lightningnetwork/lnd/pull/9195) + `AssertTxnsNotInMempool` has been added to `lntest` package to allow batch + exclusion check in itest. + # Technical and Architectural Updates ## BOLT Spec Updates diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index 04092157c41..42d1aaaaa9a 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -22,10 +22,6 @@ * [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/8857) to correctly propagate mission control and debug level config values to the main LND config struct so that the GetDebugInfo response is accurate. - -* [Fix a bug](https://github.com/lightningnetwork/lnd/pull/9134) that would - cause a nil pointer dereference during the probing of a payment request that - does not contain a payment address. * [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9033) where we would not signal an error when trying to bump an non-anchor channel but @@ -157,11 +153,7 @@ The underlying functionality between those two options remain the same. ## Breaking Changes ## Performance Improvements -* Log rotation can now use ZSTD - -* [A new method](https://github.com/lightningnetwork/lnd/pull/9195) - `AssertTxnsNotInMempool` has been added to `lntest` package to allow batch - exclusion check in itest. +* Log rotation can now use ZSTD # Technical and Architectural Updates ## BOLT Spec Updates