diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 260ed3e84..0f167bf6a 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 000000000..defae4143 --- /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, + ) + }) +}