diff --git a/contractcourt/breach_arbitrator.go b/contractcourt/breach_arbitrator.go index dc690e85c..a8154d0e6 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 { @@ -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/contractcourt/briefcase.go b/contractcourt/briefcase.go index acc49be44..cd4fd90cc 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 } diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index f25ca7e0a..64307dd02 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/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index 9ffa79943..6bda4e398 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 87e55d5cc..53fa89355 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 c75b89822..2466544c9 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 3b07828d4..4c9d2b200 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 1c5620fc6..e7ab42169 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. diff --git a/contractcourt/taproot_briefcase.go b/contractcourt/taproot_briefcase.go index 0a2beeff7..4b703dd29 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 a7d52d963..441aebf1d 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 000000000..86bb07ed7 --- /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/docs/release-notes/release-notes-0.18.4.md b/docs/release-notes/release-notes-0.18.4.md index f6a2f3967..f7796706d 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 c3048c78c..aef92daba 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 a non-anchor channel but @@ -161,11 +157,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 diff --git a/go.mod b/go.mod index 586cc3299..6e2bd9f77 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 b64a5c43d..86c1c8a21 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= diff --git a/input/input.go b/input/input.go index 6693e9fa8..6835f82c8 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 695525955..c2af637ed 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 { diff --git a/lnwallet/aux_leaf_store.go b/lnwallet/aux_leaf_store.go index 4558c2f81..c457a9250 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/aux_resolutions.go b/lnwallet/aux_resolutions.go index e3b04fc5a..382232640 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 diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 33360228d..e7ec60772 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() @@ -2202,6 +2203,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 +2283,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, @@ -3152,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() @@ -4453,7 +4456,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 @@ -4736,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() @@ -6704,7 +6709,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], @@ -6723,7 +6728,7 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, leafStore, func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] { return s.FetchLeavesFromCommit( NewAuxChanState(chanState), remoteCommit, - *keyRing, + *keyRing, lntypes.Remote, ) }, ).Unpack() @@ -6748,8 +6753,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", @@ -6839,6 +6844,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, @@ -6956,6 +6962,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 @@ -7005,6 +7016,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 @@ -7029,8 +7045,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(), @@ -7061,6 +7079,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. @@ -7098,11 +7118,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 } @@ -7253,31 +7305,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 } @@ -7293,8 +7392,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(), @@ -7326,6 +7427,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() { @@ -7361,10 +7464,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 } @@ -7510,30 +7647,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 } @@ -7570,7 +7753,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 @@ -7605,7 +7789,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 "+ @@ -7619,7 +7803,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 "+ @@ -7765,6 +7950,7 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, return s.FetchLeavesFromCommit( NewAuxChanState(chanState), chanState.LocalCommitment, *keyRing, + lntypes.Local, ) }, ).Unpack() @@ -7871,7 +8057,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, @@ -7904,7 +8091,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/lnwallet/commitment.go b/lnwallet/commitment.go index 6d61729a4..36ff75ede 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 d71b292a7..6623d8014 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{}) } 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, + ) + }) +} diff --git a/sweep/fee_bumper.go b/sweep/fee_bumper.go index fd3dfba9e..5ea4d8e4b 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 acece3143..f2fff84b0 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 c623ca3c0..34202b145 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 } diff --git a/sweep/tx_input_set.go b/sweep/tx_input_set.go index 3a95fff2f..ce144a8eb 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.