From 80eb1b1bddd6f768ed0f3c5fca37029fee7d7ed1 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 17 Oct 2024 18:15:44 -0700 Subject: [PATCH 01/21] tapchannelmsg: expand fields of ContractResolution for 2nd level sweeps In this commit, we expand the fields of the `ContractResolution` struct to account for second level sweeps. When we have an HTLC that needs to go to the second level, we'll need to create 2 vPkts: one for a direct spend from the commitment txns with the second level txn, and the other for the spend from the sweeping transaction. We can construct that transaction early, but can only sign for it once we know the sweeping transaction. Therefore, we need to keep some extra data for a given resolution, namely the tapTweak and the control block. --- tapchannel/aux_sweeper.go | 6 +- tapchannelmsg/records.go | 185 +++++++++++++++++++++++++++++++--- tapchannelmsg/records_test.go | 56 +++++++--- 3 files changed, 219 insertions(+), 28 deletions(-) diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index 40522e846..67abe564c 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -1226,7 +1226,9 @@ func (a *AuxSweeper) resolveContract( // it into a resolution blob to return. return lfn.AndThen( sPkts, func(vPkts []*tappsbt.VPacket) lfn.Result[tlv.Blob] { - res := cmsg.NewContractResolution(vPkts) + res := cmsg.NewContractResolution( + vPkts, nil, lfn.None[cmsg.TapscriptSigDesc](), + ) var b bytes.Buffer if err := res.Encode(&b); err != nil { @@ -1264,7 +1266,7 @@ func extractInputVPackets(inputs []input.Input) lfn.Result[[]*tappsbt.VPacket] { return nil, err } - return res.VPkts(), nil + return res.Vpkts1(), nil }, ) if err != nil { diff --git a/tapchannelmsg/records.go b/tapchannelmsg/records.go index 0b750711d..1409d9189 100644 --- a/tapchannelmsg/records.go +++ b/tapchannelmsg/records.go @@ -1994,30 +1994,158 @@ func dVpktList(r io.Reader, val interface{}, buf *[8]byte, _ uint64) error { return tlv.NewTypeForEncodingErr(val, "*VpktList") } +// TapscriptSigDesc contains the information needed to re-sign for a given set +// of vPkts. For normal tapscript outputs, this is the taptweak and also the +// serialized control block. These are needed for second level HTLC outputs, as +// we can't sign the vPkts until we know the sweeping transaction. +type TapscriptSigDesc struct { + TapTweak tlv.RecordT[tlv.TlvType0, []byte] + + CtrlBlock tlv.RecordT[tlv.TlvType1, []byte] +} + +// NewTapscriptSigDesc creates a new tapscriptSigDesc with the given tap tweak +// and ctrlBlock. +func NewTapscriptSigDesc(tapTweak, ctrlBlock []byte) TapscriptSigDesc { + return TapscriptSigDesc{ + TapTweak: tlv.NewPrimitiveRecord[tlv.TlvType0](tapTweak), + CtrlBlock: tlv.NewPrimitiveRecord[tlv.TlvType1](ctrlBlock), + } +} + +// Encode attempts to encode the target tapscriptSigDesc into the passed +// io.Writer. +func (t *TapscriptSigDesc) Encode(w io.Writer) error { + tlvStream, err := tlv.NewStream( + t.TapTweak.Record(), t.CtrlBlock.Record(), + ) + if err != nil { + return err + } + + return tlvStream.Encode(w) +} + +// Decode attempts to decode the target tapscriptSigDesc from the passed +// io.Reader. +func (t *TapscriptSigDesc) Decode(r io.Reader) error { + tlvStream, err := tlv.NewStream( + t.TapTweak.Record(), t.CtrlBlock.Record(), + ) + if err != nil { + return err + } + + return tlvStream.Decode(r) +} + +// eTapscriptSigDesc is an encoder for tapscriptSigDesc. +func eTapscriptSigDesc(w io.Writer, val interface{}, _ *[8]byte) error { + if v, ok := val.(*TapscriptSigDesc); ok { + return v.Encode(w) + } + + return tlv.NewTypeForEncodingErr(val, "*tapscriptSigDesc") +} + +// dTapscriptSigDesc is a decoder for tapscriptSigDesc. +func dTapscriptSigDesc(r io.Reader, val interface{}, + _ *[8]byte, _ uint64) error { + + if typ, ok := val.(*TapscriptSigDesc); ok { + return typ.Decode(r) + } + + return tlv.NewTypeForEncodingErr(val, "*tapscriptSigDesc") +} + +// Record returns a tlv.Record that represents the tapscriptSigDesc. +func (t *TapscriptSigDesc) Record() tlv.Record { + size := func() uint64 { + var ( + buf bytes.Buffer + scratch [8]byte + ) + err := eTapscriptSigDesc(&buf, t, &scratch) + if err != nil { + panic(err) + } + + return uint64(buf.Len()) + } + + return tlv.MakeDynamicRecord( + 0, t, size, eTapscriptSigDesc, dTapscriptSigDesc, + ) +} + // ContractResolution houses all the information we need to resolve a contract // on chain. This includes a series of pre-populated and pre-signed vPackets. // The internal key, and other on-chain anchor information may be missing from // these packets. type ContractResolution struct { - // SweepVpkts is a list of pre-signed vPackets that can be anchored - // into an output in a transaction where the refrnced previous inputs - // are spent to sweep an asset. - SweepVpkts tlv.RecordT[tlv.TlvType0, VpktList] + // firstLevelSweepVpkts is a list of pre-signed vPackets that can be + // anchored into an output in a transaction where the referenced + // previous inputs are spent to sweep an asset. + firstLevelSweepVpkts tlv.RecordT[tlv.TlvType0, VpktList] + + // secondLevelSweepVpkts is a list of pre-signed vPackets that can be + // anchored into an output in a transaction where the referenced + // previous inputs are spent to sweep an asset. + secondLevelSweepVpkts tlv.OptionalRecordT[tlv.TlvType1, VpktList] + + // secondLevelSigDescs is a list of tapscriptSigDescs that contain the + // information we need to sign for each second level vPkt once the + // sweeping transaction is known. + secondLevelSigDescs tlv.OptionalRecordT[tlv.TlvType2, TapscriptSigDesc] } // NewContractResolution creates a new ContractResolution with the given list // of vpkts. -func NewContractResolution(pkts []*tappsbt.VPacket) ContractResolution { - return ContractResolution{ - SweepVpkts: tlv.NewRecordT[tlv.TlvType0](NewVpktList(pkts)), +func NewContractResolution(firstLevelPkts, secondLevelPkts []*tappsbt.VPacket, + secondLevelSweepDesc lfn.Option[TapscriptSigDesc]) ContractResolution { + + c := ContractResolution{ + firstLevelSweepVpkts: tlv.NewRecordT[tlv.TlvType0]( + NewVpktList(firstLevelPkts), + ), + } + + if len(secondLevelPkts) != 0 { + c.secondLevelSweepVpkts = tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType1]( + NewVpktList(secondLevelPkts), + ), + ) } + + secondLevelSweepDesc.WhenSome(func(sigDesc TapscriptSigDesc) { + c.secondLevelSigDescs = tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType2](sigDesc), + ) + }) + + return c } // Records returns the records that make up the ContractResolution. func (c *ContractResolution) Records() []tlv.Record { - return []tlv.Record{ - c.SweepVpkts.Record(), + records := []tlv.Record{ + c.firstLevelSweepVpkts.Record(), } + + c.secondLevelSweepVpkts.WhenSome( + func(r tlv.RecordT[tlv.TlvType1, VpktList]) { + records = append(records, r.Record()) + }, + ) + c.secondLevelSigDescs.WhenSome( + func(r tlv.RecordT[tlv.TlvType2, TapscriptSigDesc]) { + records = append(records, r.Record()) + }, + ) + + return records } // Encode serializes the ContractResolution to the given io.Writer. @@ -2032,15 +2160,44 @@ func (c *ContractResolution) Encode(w io.Writer) error { // Decode deserializes the ContractResolution from the given io.Reader. func (c *ContractResolution) Decode(r io.Reader) error { - tlvStream, err := tlv.NewStream(c.Records()...) + sweepZero := c.secondLevelSweepVpkts.Zero() + sigZero := c.secondLevelSigDescs.Zero() + + tlvStream, err := tlv.NewStream( + c.firstLevelSweepVpkts.Record(), + sweepZero.Record(), + sigZero.Record(), + ) if err != nil { return err } - return tlvStream.Decode(r) + tlvs, err := tlvStream.DecodeWithParsedTypes(r) + if err != nil { + return err + } + + if _, ok := tlvs[sweepZero.TlvType()]; ok { + c.secondLevelSweepVpkts = tlv.SomeRecordT(sweepZero) + } + if _, ok := tlvs[sigZero.TlvType()]; ok { + c.secondLevelSigDescs = tlv.SomeRecordT(sigZero) + } + + return nil +} + +// Vpkts1 returns the set of first level Vpkts. +func (c *ContractResolution) Vpkts1() []*tappsbt.VPacket { + return c.firstLevelSweepVpkts.Val.Pkts } -// VPkts returns the list of vPkts in the ContractResolution. -func (c *ContractResolution) VPkts() []*tappsbt.VPacket { - return c.SweepVpkts.Val.Pkts +// Vpkts2 returns the set of first level Vpkts. +func (c *ContractResolution) Vpkts2() []*tappsbt.VPacket { + var vPkts []*tappsbt.VPacket + c.secondLevelSweepVpkts.WhenSomeV(func(v VpktList) { + vPkts = v.Pkts + }) + + return vPkts } diff --git a/tapchannelmsg/records_test.go b/tapchannelmsg/records_test.go index e08145aff..cdd647315 100644 --- a/tapchannelmsg/records_test.go +++ b/tapchannelmsg/records_test.go @@ -20,6 +20,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" + "pgregory.net/rapid" ) const ( @@ -485,20 +486,51 @@ func TestAuxShutdownMsg(t *testing.T) { func TestContractResolution(t *testing.T) { t.Parallel() - const numPackets = 10 + sigDescGen := rapid.Custom(func(t *rapid.T) TapscriptSigDesc { + byteSliceGen := rapid.SliceOfN(rapid.Byte(), 1, 256) - testPkts := make([]*tappsbt.VPacket, numPackets) - for i := 0; i < numPackets; i++ { - testPkts[i] = tappsbt.RandPacket(t, true, false) - } - - testRes := NewContractResolution(testPkts) + return NewTapscriptSigDesc( + byteSliceGen.Draw(t, "taptweak"), + byteSliceGen.Draw(t, "ctrlBlock"), + ) + }) + + rapid.Check(t, func(r *rapid.T) { + numPackets := rapid.IntRange(1, 10).Draw(r, "numPackets") + + testPkts1 := make([]*tappsbt.VPacket, numPackets) + for i := 0; i < numPackets; i++ { + testPkts1[i] = tappsbt.RandPacket(t, true, false) + } + + var testPkts2 []*tappsbt.VPacket + + if rapid.Bool().Draw(r, "secondPacketSet") { + testPkts2 = make([]*tappsbt.VPacket, numPackets) + for i := 0; i < numPackets; i++ { + testPkts2[i] = tappsbt.RandPacket( + t, true, false, + ) + } + } + + var sigDesc lfn.Option[TapscriptSigDesc] + if rapid.Bool().Draw(r, "sigDescSet") { + sigDesc = lfn.Some( + sigDescGen.Draw(r, "sigDesc"), + ) + } + + testRes := NewContractResolution( + testPkts1, testPkts2, sigDesc, + ) - var b bytes.Buffer - require.NoError(t, testRes.Encode(&b)) + var b bytes.Buffer + require.NoError(t, testRes.Encode(&b)) - var newRes ContractResolution - require.NoError(t, newRes.Decode(&b)) + var newRes ContractResolution + require.NoError(t, newRes.Decode(&b)) - require.Equal(t, testRes, newRes) + require.Equal(t, testRes, newRes) + }) } From 4883a7203642e595999e71c4492e6141a5e61319 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 17 Oct 2024 18:16:07 -0700 Subject: [PATCH 02/21] tapchannelmsg: add FilterByHtlcIndex helper method to HtlcAssetOutput --- tapchannelmsg/records.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tapchannelmsg/records.go b/tapchannelmsg/records.go index 1409d9189..f63c5ac35 100644 --- a/tapchannelmsg/records.go +++ b/tapchannelmsg/records.go @@ -1333,6 +1333,20 @@ func NewHtlcAssetOutput( } } +// FilterByHtlcIndex returns a slice of AssetOutputs that are associated with +// the given htlc index. +func (h *HtlcAssetOutput) FilterByHtlcIndex(id input.HtlcIndex) []*AssetOutput { + if h.HtlcOutputs == nil { + return nil + } + + if outputs, ok := h.HtlcOutputs[id]; ok { + return outputs.Outputs + } + + return nil +} + // Record creates a Record out of a HtlcAssetOutput using the // eHtlcAssetOutput and dHtlcAssetOutput functions. // From 9368a753e4268beb0979a8c8df097e678db36a0e Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 17 Oct 2024 18:16:54 -0700 Subject: [PATCH 03/21] tapchannel: extract allocation creation from CreateSecondLevelHtlcPackets This'll be useful later when we need to generate the vPkt for second level spends. --- tapchannel/commitment.go | 77 ++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/tapchannel/commitment.go b/tapchannel/commitment.go index 0a5faf98b..eeb8a571d 100644 --- a/tapchannel/commitment.go +++ b/tapchannel/commitment.go @@ -1220,45 +1220,46 @@ func collectOutputs(a *Allocation, return outputs, nil } -// CreateSecondLevelHtlcPackets creates the virtual packets for the second level -// HTLC transaction. -func CreateSecondLevelHtlcPackets(chanState lnwallet.AuxChanState, - commitTx *wire.MsgTx, htlcAmt btcutil.Amount, - keys lnwallet.CommitmentKeyRing, chainParams *address.ChainParams, - htlcOutputs []*cmsg.AssetOutput) ([]*tappsbt.VPacket, []*Allocation, - error) { +// createSecondLevelHtlcAllocations creates the allocations for the second level +// HTLCs. This will be used to generate the vPkts that corresponds to the second +// level HTLC sweep. +func createSecondLevelHtlcAllocations(chanType channeldb.ChannelType, + initiator bool, htlcOutputs []*cmsg.AssetOutput, htlcAmt btcutil.Amount, + commitCsvDelay uint32, keys lnwallet.CommitmentKeyRing, + outputIndex fn.Option[uint32], +) ([]*Allocation, error) { + + // TODO(roasbeef): thaw height not implemented for taproot chans rn + // (lease expiry) - var leaseExpiry uint32 - if chanState.ChanType.HasLeaseExpiration() { - leaseExpiry = chanState.ThawHeight - } - - // Next, we'll generate the script used as the output for all second - // level HTLC which forces a covenant w.r.t what can be done with all - // HTLC outputs. scriptInfo, err := lnwallet.SecondLevelHtlcScript( - chanState.ChanType, chanState.IsInitiator, keys.RevocationKey, - keys.ToLocalKey, uint32(chanState.LocalChanCfg.CsvDelay), - leaseExpiry, lfn.None[txscript.TapLeaf](), + chanType, initiator, keys.RevocationKey, + keys.ToLocalKey, commitCsvDelay, + 0, lfn.None[txscript.TapLeaf](), ) if err != nil { - return nil, nil, fmt.Errorf("error creating second level htlc "+ - "script: %w", err) + return nil, fmt.Errorf("error creating second level "+ + "htlc script: %w", err) } sibling, htlcTree, err := LeavesFromTapscriptScriptTree(scriptInfo) if err != nil { - return nil, nil, fmt.Errorf("error creating second level HTLC "+ + return nil, fmt.Errorf("error creating second level HTLC "+ "script sibling: %w", err) } allocations := []*Allocation{{ - Type: SecondLevelHtlcAllocation, + Type: SecondLevelHtlcAllocation, + // If we're making the second-level transaction just to sign, + // then we'll have an output index of zero. Otherwise, we'll + // want to use the output index as appears in the final + // commitment transaction. + OutputIndex: outputIndex.UnwrapOr(0), Amount: cmsg.OutputSum(htlcOutputs), AssetVersion: asset.V1, BtcAmount: htlcAmt, Sequence: lnwallet.HtlcSecondLevelInputSequence( - chanState.ChanType, + chanType, ), InternalKey: htlcTree.InternalKey, NonAssetLeaves: sibling, @@ -1268,13 +1269,31 @@ func CreateSecondLevelHtlcPackets(chanState lnwallet.AuxChanState, ), }} - // The proofs in the asset outputs don't have the full commitment - // transaction, so we need to add it now to make them complete. + return allocations, nil +} + +// CreateSecondLevelHtlcPackets creates the virtual packets for the second level +// HTLC. +func CreateSecondLevelHtlcPackets(chanState lnwallet.AuxChanState, + commitTx *wire.MsgTx, htlcAmt btcutil.Amount, + keys lnwallet.CommitmentKeyRing, chainParams *address.ChainParams, + htlcOutputs []*cmsg.AssetOutput) ([]*tappsbt.VPacket, []*Allocation, + error) { + + allocations, err := createSecondLevelHtlcAllocations( + chanState.ChanType, chanState.IsInitiator, + htlcOutputs, htlcAmt, + uint32(chanState.LocalChanCfg.CsvDelay), keys, + fn.None[uint32](), + ) + if err != nil { + return nil, nil, err + } + inputProofs := fn.Map( htlcOutputs, func(o *cmsg.AssetOutput) *proof.Proof { p := o.Proof.Val p.AnchorTx = *commitTx - return &p }, ) @@ -1284,14 +1303,12 @@ func CreateSecondLevelHtlcPackets(chanState lnwallet.AuxChanState, return nil, nil, fmt.Errorf("error distributing coins: %w", err) } - // Prepare the output assets for each virtual packet, then create the - // output commitments. ctx := context.Background() for idx := range vPackets { err := tapsend.PrepareOutputAssets(ctx, vPackets[idx]) if err != nil { - return nil, nil, fmt.Errorf("unable to prepare output "+ - "assets: %w", err) + return nil, nil, fmt.Errorf("unable to prepare "+ + "output assets: %w", err) } } From 309f8d4bf7103aed1103b25de40e94bff1b565da Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 17 Oct 2024 18:19:07 -0700 Subject: [PATCH 04/21] tapchannel: add 2nd level information to tapscriptSweepDesc In this commit, we add two fields to the tapscriptSweepDesc struct related to 2nd level HTLCs. First, we add the second level sig index, which is needed to be able to know where to insert our signature after we generate it. We also add an optional lnwallet.AuxSigDesc, which contains the remote party's signature, and also the sighash needed for it and a sign desc to generate it with. --- tapchannel/aux_sweeper.go | 89 ++++++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 34 deletions(-) diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index 67abe564c..bf9168926 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -382,16 +382,27 @@ func (a *AuxSweeper) createAndSignSweepVpackets( // tapscriptSweepDesc is a helper struct that contains the tapscript tree and // the control block needed to generate a valid spend. -// -// TODO(roasbeef): only needs the merkle root? type tapscriptSweepDesc struct { + auxSigInfo lfn.Option[lnwallet.AuxSigDesc] + scriptTree input.TapscriptDescriptor ctrlBlockBytes []byte - relativeDelay fn.Option[uint64] + relativeDelay lfn.Option[uint64] + + absoluteDelay lfn.Option[uint64] + + secondLevelSigIndex lfn.Option[uint32] +} + +// tapscriptSweepDescs contains the sweep decs for the first and second level. +// Most outputs only go to the first level, but HTLCs on our local commitment +// transaction go to the second level. +type tapscriptSweepDescs struct { + firstLevel tapscriptSweepDesc - absoluteDelay fn.Option[uint64] //nolint:unused + secondLevel lfn.Option[tapscriptSweepDesc] } // commitNoDelaySweepDesc creates a sweep desc for a commitment output that @@ -399,9 +410,9 @@ type tapscriptSweepDesc struct { // non-delay output, so we don't need to worry about the CSV delay when // sweeping it. func commitNoDelaySweepDesc(keyRing *lnwallet.CommitmentKeyRing, - csvDelay uint32) lfn.Result[tapscriptSweepDesc] { + csvDelay uint32) lfn.Result[tapscriptSweepDescs] { - type returnType = tapscriptSweepDesc + type returnType = tapscriptSweepDescs // We'll make the script tree for the to remote script (we're remote as // this is their commitment transaction). We don't have an auxLeaf here @@ -410,8 +421,8 @@ func commitNoDelaySweepDesc(keyRing *lnwallet.CommitmentKeyRing, keyRing.ToRemoteKey, input.NoneTapLeaf(), ) if err != nil { - return lfn.Errf[returnType]("unable to make remote script "+ - "tree: %w", err) + return lfn.Errf[returnType]("unable to make remote "+ + "script tree: %w", err) } // Now that we have the script tree, we'll make the control block @@ -420,19 +431,19 @@ func commitNoDelaySweepDesc(keyRing *lnwallet.CommitmentKeyRing, input.ScriptPathSuccess, ) if err != nil { - return lfn.Errf[returnType]("unable to make ctrl block: %w", - err) } ctrlBlockBytes, err := ctrlBlock.ToBytes() if err != nil { - return lfn.Errf[returnType]("unable to encode ctrl block: %w", - err) + return lfn.Errf[returnType]("unable to encode ctrl "+ + "block: %w", err) } - return lfn.Ok(tapscriptSweepDesc{ - scriptTree: toRemoteScriptTree, - relativeDelay: fn.Some(uint64(csvDelay)), - ctrlBlockBytes: ctrlBlockBytes, + return lfn.Ok(tapscriptSweepDescs{ + firstLevel: tapscriptSweepDesc{ + scriptTree: toRemoteScriptTree, + relativeDelay: lfn.Some(uint64(csvDelay)), + ctrlBlockBytes: ctrlBlockBytes, + }, }) } @@ -440,9 +451,9 @@ func commitNoDelaySweepDesc(keyRing *lnwallet.CommitmentKeyRing, // resides on our local commitment transaction. This output is a delay output, // so we need to mind the CSV delay when sweeping it. func commitDelaySweepDesc(keyRing *lnwallet.CommitmentKeyRing, - csvDelay uint32) lfn.Result[tapscriptSweepDesc] { + csvDelay uint32) lfn.Result[tapscriptSweepDescs] { - type returnType = tapscriptSweepDesc + type returnType = tapscriptSweepDescs // We'll make the script tree for the to remote script (we're remote as // this is their commitment transaction). We don't have an auxLeaf here @@ -461,17 +472,18 @@ func commitDelaySweepDesc(keyRing *lnwallet.CommitmentKeyRing, input.ScriptPathSuccess, ) if err != nil { - return lfn.Err[returnType](err) } ctrlBlockBytes, err := ctrlBlock.ToBytes() if err != nil { return lfn.Err[returnType](err) } - return lfn.Ok(tapscriptSweepDesc{ - scriptTree: toLocalScriptTree, - relativeDelay: fn.Some(uint64(csvDelay)), - ctrlBlockBytes: ctrlBlockBytes, + return lfn.Ok(tapscriptSweepDescs{ + firstLevel: tapscriptSweepDesc{ + scriptTree: toLocalScriptTree, + relativeDelay: lfn.Some(uint64(csvDelay)), + ctrlBlockBytes: ctrlBlockBytes, + }, }) } @@ -479,9 +491,9 @@ func commitDelaySweepDesc(keyRing *lnwallet.CommitmentKeyRing, // the local output on the remote party's commitment transaction. We can seep // this in the case of a revoked commitment. func commitRevokeSweepDesc(keyRing *lnwallet.CommitmentKeyRing, - csvDelay uint32) lfn.Result[tapscriptSweepDesc] { + csvDelay uint32) lfn.Result[tapscriptSweepDescs] { - type returnType = tapscriptSweepDesc + type returnType = tapscriptSweepDescs // To sweep their revoked output, we'll make the script tree for the // local tree of their commitment transaction, which is actually their @@ -504,12 +516,14 @@ func commitRevokeSweepDesc(keyRing *lnwallet.CommitmentKeyRing, } ctrlBlockBytes, err := ctrlBlock.ToBytes() if err != nil { - return lfn.Err[returnType](err) + return lfn.Err[tapscriptSweepDescs](err) } - return lfn.Ok(tapscriptSweepDesc{ - scriptTree: toLocalScriptTree, - ctrlBlockBytes: ctrlBlockBytes, + return lfn.Ok(tapscriptSweepDescs{ + firstLevel: tapscriptSweepDesc{ + scriptTree: toLocalScriptTree, + ctrlBlockBytes: ctrlBlockBytes, + }, }) } @@ -1159,7 +1173,7 @@ func (a *AuxSweeper) resolveContract( } var ( - sweepDesc lfn.Result[tapscriptSweepDesc] + sweepDesc lfn.Result[tapscriptSweepDescs] assetOutputs []*cmsg.AssetOutput ) @@ -1192,13 +1206,13 @@ func (a *AuxSweeper) resolveContract( // The remote party has breached the channel. We'll sweep the revoked // key that we learned in the past. case input.TaprootCommitmentRevoke: - // In this case, we'll be sweeping the remote party's asset // outputs, as they broadcast a revoked commitment. For the + // In this case, we'll be sweeping the remote party's asset // remote party, this is actually their local output. assetOutputs = commitState.LocalAssets.Val.Outputs - // As we have multiple outputs to sweep above, we'll also have - // two sweep descs. + // Next, we'll make a sweep desk capable of sweeping the remote + // party's local output. sweepDesc = commitRevokeSweepDesc(req.KeyRing, req.CsvDelay) default: @@ -1216,10 +1230,17 @@ func (a *AuxSweeper) resolveContract( log.Infof("Sweeping %v asset outputs: %v", len(assetOutputs), limitSpewer.Sdump(assetOutputs)) + firstLevelSweepDesc := lfn.AndThen( + sweepDesc, + func(sweepDesc tapscriptSweepDescs) lfn.Result[tapscriptSweepDesc] { //nolint:lll + return lfn.Ok(sweepDesc.firstLevel) + }, + ) + // With the sweep desc constructed above, we'll create vPackets for // each of the local assets, then sign them all. sPkts := a.createAndSignSweepVpackets( - assetOutputs, req.SignDesc, sweepDesc, + assetOutputs, req.SignDesc, firstLevelSweepDesc, ) // With the vPackets fully generated and signed above, we'll serialize From fc924590324ed71b20908ed5770052e5d863588b Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 17 Oct 2024 18:22:16 -0700 Subject: [PATCH 05/21] tapchannel: add sweep desc gen for remote HTLC sweeps In this commit, we add sweep desc generation for remote HTLCs sweeps. This is needed when the remote party force closes, and we can spend the HTLC directly from their commitment transaction. --- tapchannel/aux_sweeper.go | 353 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index bf9168926..5f319b5b7 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "math" "net/url" "sync" "sync/atomic" @@ -11,6 +12,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/davecgh/go-spew/spew" "github.com/lightninglabs/taproot-assets/address" @@ -27,6 +29,7 @@ import ( lfn "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/sweep" @@ -431,6 +434,7 @@ func commitNoDelaySweepDesc(keyRing *lnwallet.CommitmentKeyRing, input.ScriptPathSuccess, ) if err != nil { + return lfn.Err[returnType](err) } ctrlBlockBytes, err := ctrlBlock.ToBytes() if err != nil { @@ -472,6 +476,7 @@ func commitDelaySweepDesc(keyRing *lnwallet.CommitmentKeyRing, input.ScriptPathSuccess, ) if err != nil { + return lfn.Err[returnType](err) } ctrlBlockBytes, err := ctrlBlock.ToBytes() if err != nil { @@ -527,6 +532,275 @@ func commitRevokeSweepDesc(keyRing *lnwallet.CommitmentKeyRing, }) } +// remoteHtlcTimeoutSweepDesc creates a sweep desc for an HTLC output that is +// close to timing out on the remote party's commitment transaction. +func remoteHtlcTimeoutSweepDesc(keyRing *lnwallet.CommitmentKeyRing, + payHash []byte, csvDelay uint32, htlcExpiry uint32, +) lfn.Result[tapscriptSweepDescs] { + + // We're sweeping a timed out HTLC, which means that we'll need to + // create the receiver's HTLC script tree (from the remote party's PoV). + htlcScriptTree, err := input.ReceiverHTLCScriptTaproot( + htlcExpiry, keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, + keyRing.RevocationKey, payHash, lntypes.Remote, + input.NoneTapLeaf(), + ) + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + + // Now that we have the script tree, we'll make the control block needed + // to spend it, but taking the revoked path. + ctrlBlock, err := htlcScriptTree.CtrlBlockForPath( + input.ScriptPathTimeout, + ) + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + ctrlBlockBytes, err := ctrlBlock.ToBytes() + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + + return lfn.Ok(tapscriptSweepDescs{ + firstLevel: tapscriptSweepDesc{ + relativeDelay: lfn.Some(uint64(csvDelay)), + absoluteDelay: lfn.Some(uint64(htlcExpiry)), + scriptTree: htlcScriptTree, + ctrlBlockBytes: ctrlBlockBytes, + }, + }) +} + +// remoteHtlcSuccessSweepDesc creates a sweep desc for an HTLC output present on +// the remote party's commitment transaction that we can sweep with the +// preimage. +func remoteHtlcSuccessSweepDesc(keyRing *lnwallet.CommitmentKeyRing, + payHash []byte, csvDelay uint32) lfn.Result[tapscriptSweepDescs] { + + // We're planning on sweeping an HTLC that we know the preimage to, + // which the remote party sent, so we'll construct the sender version of + // the HTLC script tree (from their PoV, they're the sender). + htlcScriptTree, err := input.SenderHTLCScriptTaproot( + keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, + keyRing.RevocationKey, payHash, lntypes.Remote, + input.NoneTapLeaf(), + ) + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + + // Now that we have the script tree, we'll make the control block needed + // to spend it, but taking the revoked path. + ctrlBlock, err := htlcScriptTree.CtrlBlockForPath( + input.ScriptPathSuccess, + ) + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + ctrlBlockBytes, err := ctrlBlock.ToBytes() + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + + return lfn.Ok(tapscriptSweepDescs{ + firstLevel: tapscriptSweepDesc{ + relativeDelay: lfn.Some(uint64(csvDelay)), + ctrlBlockBytes: ctrlBlockBytes, + scriptTree: htlcScriptTree, + }, + }) +} + +// localHtlcTimeoutSweepDesc creates a sweep desc for an HTLC output that is +// present on our local commitment transaction. These are second level HTLCs, so +// we'll need to perform two stages of sweeps. +func localHtlcTimeoutSweepDesc(req lnwallet.ResolutionReq, +) lfn.Result[tapscriptSweepDescs] { + + isIncoming := false + + payHash, err := req.PayHash.UnwrapOrErr( + fmt.Errorf("no pay hash"), + ) + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + htlcExpiry, err := req.CltvDelay.UnwrapOrErr( + fmt.Errorf("no htlc expiry"), + ) + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + + // We'll need to complete the control block to spend the second-level + // HTLC, so first we'll make the script tree for the HTLC. + htlcScriptTree, err := lnwallet.GenTaprootHtlcScript( + isIncoming, lntypes.Local, htlcExpiry, + payHash, req.KeyRing, lfn.None[txscript.TapLeaf](), + ) + if err != nil { + return lfn.Errf[tapscriptSweepDescs]("error creating "+ + "HTLC script: %w", err) + } + + // Now that we have the script tree, we'll make the control block needed + // to spend it, but taking the timeout path. + ctrlBlock, err := htlcScriptTree.CtrlBlockForPath( + input.ScriptPathTimeout, + ) + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + ctrlBlockBytes, err := ctrlBlock.ToBytes() + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + + // For the second level transaction, the witness looks like this: + // + // + // + // We're the sender, so we'll need to insert their sig at the very + // front. + sigIndex := lfn.Some(uint32(0)) + + // As this is an HTLC on our local commitment transaction, we'll also + // need to generate a sweep desc for second level HTLC. + secondLevelScriptTree, err := input.TaprootSecondLevelScriptTree( + req.KeyRing.RevocationKey, req.KeyRing.ToLocalKey, + req.CommitCsvDelay, lfn.None[txscript.TapLeaf](), + ) + if err != nil { + return lfn.Errf[tapscriptSweepDescs]("error "+ + "creating second level htlc script: %w", err) + } + secondLevelCtrBlock, err := secondLevelScriptTree.CtrlBlockForPath( + input.ScriptPathSuccess, + ) + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + secondLevelCtrlBlockBytes, err := secondLevelCtrBlock.ToBytes() + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + + secondLevelDesc := tapscriptSweepDesc{ + scriptTree: secondLevelScriptTree, + relativeDelay: lfn.Some(uint64(req.CommitCsvDelay)), + ctrlBlockBytes: secondLevelCtrlBlockBytes, + } + + return lfn.Ok(tapscriptSweepDescs{ + firstLevel: tapscriptSweepDesc{ + scriptTree: htlcScriptTree, + ctrlBlockBytes: ctrlBlockBytes, + relativeDelay: lfn.Some(uint64(req.CsvDelay)), + absoluteDelay: lfn.Some(uint64(htlcExpiry)), + auxSigInfo: req.AuxSigDesc, + secondLevelSigIndex: sigIndex, + }, + secondLevel: lfn.Some(secondLevelDesc), + }) +} + +// localHtlcSucessSweepDesc creates a sweep desc for an HTLC output that is +// present on our local commitment transaction that we can sweep with a +// preimage. These sweeps take two stages, so we'll add that extra information. +func localHtlcSucessSweepDesc(req lnwallet.ResolutionReq, +) lfn.Result[tapscriptSweepDescs] { + + isIncoming := true + + payHash, err := req.PayHash.UnwrapOrErr( + fmt.Errorf("no pay hash"), + ) + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + htlcExpiry, err := req.CltvDelay.UnwrapOrErr( + fmt.Errorf("no htlc expiry"), + ) + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + + // We'll need to complete the control block to spend the second-level + // HTLC, so first we'll make the script tree for the HTLC. + htlcScriptTree, err := lnwallet.GenTaprootHtlcScript( + isIncoming, lntypes.Local, htlcExpiry, + payHash, req.KeyRing, lfn.None[txscript.TapLeaf](), + ) + if err != nil { + return lfn.Errf[tapscriptSweepDescs]("error creating "+ + "HTLC script: %w", err) + } + + // Now that we have the script tree, we'll make the control block needed + // to spend it, but taking the success path. + ctrlBlock, err := htlcScriptTree.CtrlBlockForPath( + input.ScriptPathSuccess, + ) + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + ctrlBlockBytes, err := ctrlBlock.ToBytes() + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + + // For the second level transaction, the witness looks like this: + // + // * + // + // + // In this case, we're the receiver. After we sign the witness will look + // like this: . + // + // So we'll need to insert the remote party's signature at the very + // front. + sigIndex := lfn.Some(uint32(0)) + + // As this is an HTLC on our local commitment transaction, we'll also + // need to generate a sweep desc for second level HTLC. + secondLevelScriptTree, err := input.TaprootSecondLevelScriptTree( + req.KeyRing.RevocationKey, req.KeyRing.ToLocalKey, + req.CommitCsvDelay, lfn.None[txscript.TapLeaf](), + ) + if err != nil { + return lfn.Errf[tapscriptSweepDescs]("error "+ + "creating second level htlc script: %w", err) + } + secondLevelCtrBlock, err := secondLevelScriptTree.CtrlBlockForPath( + input.ScriptPathSuccess, + ) + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + secondLevelCtrlBlockBytes, err := secondLevelCtrBlock.ToBytes() + if err != nil { + return lfn.Err[tapscriptSweepDescs](err) + } + + secondLevelDesc := tapscriptSweepDesc{ + scriptTree: secondLevelScriptTree, + relativeDelay: lfn.Some(uint64(req.CommitCsvDelay)), + ctrlBlockBytes: secondLevelCtrlBlockBytes, + } + + return lfn.Ok(tapscriptSweepDescs{ + firstLevel: tapscriptSweepDesc{ + scriptTree: htlcScriptTree, + ctrlBlockBytes: ctrlBlockBytes, + relativeDelay: lfn.Some(uint64(req.CsvDelay)), + auxSigInfo: req.AuxSigDesc, + secondLevelSigIndex: sigIndex, + }, + secondLevel: lfn.Some(secondLevelDesc), + }) +} + // assetOutputToVPacket converts an asset outputs to the corresponding vPackets // that can be used to complete the proof needed to import a commitment // transaction. This new vPacket is added to the specified map. @@ -1115,6 +1389,9 @@ func (a *AuxSweeper) importCommitTx(req lnwallet.ResolutionReq, ) } +// errNoPayHash is an error returned when no payment hash is provided. +var errNoPayHash = fmt.Errorf("no payment hash provided") + // resolveContract takes in a resolution request and resolves it by creating a // serialized resolution blob that contains the virtual packets needed to sweep // the funds from the contract. @@ -1215,6 +1492,82 @@ func (a *AuxSweeper) resolveContract( // party's local output. sweepDesc = commitRevokeSweepDesc(req.KeyRing, req.CsvDelay) + // The remote party broadcasted a commitment transaction which held an + // HTLC that we can timeout eventually. + case input.TaprootHtlcOfferedRemoteTimeout: + // In this case, we're interested in sweeping the incoming + // assets for the remote party, which are actually the HTLCs we + // sent outgoing. We only care about this particular HTLC, so + // we'll filter out the rest. + htlcOutputs := commitState.OutgoingHtlcAssets.Val + assetOutputs = htlcOutputs.FilterByHtlcIndex( + req.HtlcID.UnwrapOr(math.MaxUint64), + ) + + payHash, err := req.PayHash.UnwrapOrErr(errNoPayHash) + if err != nil { + return lfn.Err[tlv.Blob](err) + } + + // Now that we know which output we'll be sweeping, we'll make a + // sweep desc for the timeout txn. + sweepDesc = remoteHtlcTimeoutSweepDesc( + req.KeyRing, payHash[:], req.CsvDelay, + req.CltvDelay.UnwrapOr(0), + ) + + // The remote party broadcasted a commitment transaction which held an + // outgoing HTLC that we may claim with a preimage. + case input.TaprootHtlcAcceptedRemoteSuccess: + // In this case, it's an outgoing HTLC from the PoV of the + // remote party, which is incoming for us. We'll only sweep this + // HTLC, so we'll filter out the rest. + htlcOutputs := commitState.IncomingHtlcAssets.Val + assetOutputs = htlcOutputs.FilterByHtlcIndex( + req.HtlcID.UnwrapOr(math.MaxUint64), + ) + + payHash, err := req.PayHash.UnwrapOrErr(errNoPayHash) + if err != nil { + return lfn.Err[tlv.Blob](err) + } + + // Now that we know which output we'll be sweeping, we'll make a + // sweep desc for the timeout txn. + sweepDesc = remoteHtlcSuccessSweepDesc( + req.KeyRing, payHash[:], req.CsvDelay, + ) + + // In this case, we broadcast a commitment transaction which held an + // HTLC that we may need to time out in the future. This is the + // second-level case, so we'll actually be creating+signing two sets of + // vPkts later (1st + 2nd level). + case input.TaprootHtlcLocalOfferedTimeout: + // Like the other HTLC cases, there's only a single output we + // care about here. + htlcOutputs := commitState.OutgoingHtlcAssets.Val + assetOutputs = htlcOutputs.FilterByHtlcIndex( + req.HtlcID.UnwrapOr(math.MaxUint64), + ) + + // With the output and pay desc located, we'll now create the + // sweep desc. + sweepDesc = localHtlcTimeoutSweepDesc(req) + + // In this case, we've broadcast a commitment, with an incoming HTLC + // that we can sweep. We'll annotate the sweepDesc with the information + // needed to sweep both this output, as well as the second level + // output it creates. + case input.TaprootHtlcAcceptedLocalSuccess: + htlcOutputs := commitState.IncomingHtlcAssets.Val + assetOutputs = htlcOutputs.FilterByHtlcIndex( + req.HtlcID.UnwrapOr(math.MaxUint64), + ) + + // With the output and pay desc located, we'll now create the + // sweep desc. + sweepDesc = localHtlcSucessSweepDesc(req) + default: return lfn.Errf[returnType]("unknown resolution type: %v", req.Type) From 258eabfd684f248e685c8683dea1941ea3dca69a Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 17 Oct 2024 18:25:29 -0700 Subject: [PATCH 06/21] tapchannel: add blobWithWitnessInfo struct In this commit, we add `blobWithWitnessInfo` which couples the raw resolution TLV blob with some additional information such as the input that carried the blob, and also preimage information. Preimage information is needed to know where to insert the preimage in the witness once we learn about it on chain. --- tapchannel/aux_sweeper.go | 99 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index 5f319b5b7..0c4668da3 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -1614,6 +1614,105 @@ func (a *AuxSweeper) resolveContract( ) } +// preimageDesc is a helper struct that contains the preimage and the witness +// index that the preimage should be placed within the witness stack. This is +// useful as in an earlier step, we've already pre-signed the witness, but will +// learn of the preimage later. +type preimageDesc struct { + // preimage is the preimage that we'll use to update the witness stack. + preimage lntypes.Preimage + + // witnessIndex is the index within the witness stack that the preimage + // should be placed at. + witnessIndex int +} + +// blobWithWitnessInfo is a helper struct that contains a resolution blob, along +// with optional preimage information. If the preimage information is present, +// then we'll use this to update the witness stack of the final vPacket before +// we anchor it into the sweep output. +type blobWithWitnessInfo struct { + // resolutionBlob is the serialized resolution blob that contains the + // vPackets. + resolutionBlob tlv.Blob + + // input is the sweep input that we created this blob using. + input input.Input + + // preimageInfo is an optional field that contains the preimage and info + // w.r.t where to place it in the witness stack. + preimageInfo lfn.Option[preimageDesc] + + // secondLevel indicates if this is a second level sweep. + secondLevel bool +} + +// newBlobWithWitnessInfo creates a new blobWithWitnessInfo struct from a passed +// input.Input, which stores the resolution blob and other information. +func newBlobWithWitnessInfo(i input.Input) blobWithWitnessInfo { + // If this is a success input, then we'll need to extract the preimage + // from the inner struct, so we can update the witness stack. + var ( + preimageInfo lfn.Option[preimageDesc] + secondLevel bool + ) + switch i.WitnessType() { + // This is the case when we're sweeping the HTLC output on our local + // commitment transaction via a second level HTLC. + // + // The final witness stack is: + // * + // + // + // So we'll place the preimage at index 2. + case input.TaprootHtlcAcceptedLocalSuccess: + preimage := i.Preimage() + + preimageInfo = lfn.MapOption( + func(p lntypes.Preimage) preimageDesc { + return preimageDesc{ + preimage: p, + witnessIndex: 2, + } + }, + )(preimage) + + // This is the case when we're sweeping the HTLC output we received on + // the remote party's version of the commitment transaction. + // + // The final witness stack is: + // + // + // So we'll place the preimage at index 1. + case input.TaprootHtlcAcceptedRemoteSuccess: + preimage := i.Preimage() + + preimageInfo = lfn.MapOption( + func(p lntypes.Preimage) preimageDesc { + return preimageDesc{ + preimage: p, + witnessIndex: 1, + } + }, + )(preimage) + + // For second level sweeps, we don't need to note anything about a + // preimage, but will note that this is a second level output. + case input.TaprootHtlcOfferedTimeoutSecondLevel: + fallthrough + case input.TaprootHtlcAcceptedSuccessSecondLevel: + secondLevel = true + } + + // We already know this has a blob from the filter in an earlier step. + return blobWithWitnessInfo{ + resolutionBlob: i.ResolutionBlob().UnwrapOr(nil), + input: i, + preimageInfo: preimageInfo, + secondLevel: secondLevel, + } +} + // extractInputVPackets extracts the vPackets from the inputs passed in. If // none of the inputs have any resolution blobs. Then an empty slice will be // returned. From 332f49a3ef1b0a7f8e9043bcfb19c321ae8c8126 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 17 Oct 2024 18:28:26 -0700 Subject: [PATCH 07/21] tapchannel: update createSweepVpackets to be second level aware In this commit, we update `createSweepVpackets` to be second level aware. This means that if we detect an auxSigInfo, then we know the vPkt we need to create is actually just the second level vPkt. We also don't need to generate a new script key either. Finally, we'll properly set the absolute delay if the sweep desc indicates as such. --- tapchannel/aux_sweeper.go | 92 ++++++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 31 deletions(-) diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index 0c4668da3..a5cb75ba2 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -179,6 +179,7 @@ func (a *AuxSweeper) Stop() error { // set of asset inputs into the backing wallet. func (a *AuxSweeper) createSweepVpackets(sweepInputs []*cmsg.AssetOutput, tapscriptDesc lfn.Result[tapscriptSweepDesc], + resReq lnwallet.ResolutionReq, ) lfn.Result[[]*tappsbt.VPacket] { type returnType = []*tappsbt.VPacket @@ -192,39 +193,59 @@ func (a *AuxSweeper) createSweepVpackets(sweepInputs []*cmsg.AssetOutput, return lfn.Err[returnType](err) } - // For each out we want to sweep, we'll construct an allocation that - // we'll use to deliver the funds back to the wallet. - ctx := context.Background() allocs := make([]*Allocation, 0, len(sweepInputs)) - for _, localAsset := range sweepInputs { - // For each output, we'll need to create a new script key to - // use for the sweep transaction. - scriptKey, err := a.cfg.AddrBook.NextScriptKey( - ctx, asset.TaprootAssetsKeyFamily, + ctx := context.Background() + + // If this is a second level HTLC sweep, then we already have + // the output information locked in, as this was a pre-signed + // transaction. + if sweepDesc.auxSigInfo.IsSome() { + alloc, err := createSecondLevelHtlcAllocations( + resReq.ChanType, resReq.Initiator, sweepInputs, + resReq.HtlcAmt, resReq.CommitCsvDelay, *resReq.KeyRing, + fn.Some(resReq.ContractPoint.Index), ) if err != nil { return lfn.Err[returnType](err) } - // With the script key created, we can make a new allocation - // that will be used to sweep the funds back to our wallet. - // - // We leave out the internal key here, as we'll make it later - // once we actually have the other set of inputs we need to - // sweep. - allocs = append(allocs, &Allocation{ - Type: CommitAllocationToLocal, - // We don't need to worry about sorting, as we'll - // always be the first output index in the transaction. - OutputIndex: 0, - Amount: localAsset.Amount.Val, - AssetVersion: asset.V1, - BtcAmount: tapsend.DummyAmtSats, - ScriptKey: scriptKey, - SortTaprootKeyBytes: schnorr.SerializePubKey( - scriptKey.PubKey, - ), - }) + allocs = append(allocs, alloc...) + } else { + // Otherwise, for each out we want to sweep, we'll construct an + // allocation that we'll use to deliver the funds back to the + // wallet. + for _, localAsset := range sweepInputs { + // For each output, we'll need to create a new script + // key to use for the sweep transaction. + scriptKey, err := a.cfg.AddrBook.NextScriptKey( + ctx, asset.TaprootAssetsKeyFamily, + ) + if err != nil { + return lfn.Err[[]*tappsbt.VPacket](err) + } + + // With the script key created, we can make a new + // allocation that will be used to sweep the funds back + // to our wallet. + // + // We leave out the internal key here, as we'll make it + // later once we actually have the other set of inputs + // we need to sweep. + allocs = append(allocs, &Allocation{ + Type: CommitAllocationToLocal, + // We don't need to worry about sorting, as + // we'll always be the first output index in the + // transaction. + OutputIndex: 0, + Amount: localAsset.Amount.Val, + AssetVersion: asset.V1, + BtcAmount: tapsend.DummyAmtSats, + ScriptKey: scriptKey, + SortTaprootKeyBytes: schnorr.SerializePubKey( + scriptKey.PubKey, + ), + }) + } } log.Infof("Created %v allocations for commit tx sweep: %v", @@ -268,6 +289,14 @@ func (a *AuxSweeper) createSweepVpackets(sweepInputs []*cmsg.AssetOutput, } }) + // Similarly, if we have an absolute delay, we'll set it for all + // the vOuts in this packet. + sweepDesc.absoluteDelay.WhenSome(func(expiry uint64) { + for _, vOut := range vPackets[idx].Outputs { + vOut.LockTime = expiry + } + }) + err := tapsend.PrepareOutputAssets(ctx, vPackets[idx]) if err != nil { return lfn.Errf[returnType]("unable to prepare output "+ @@ -355,7 +384,7 @@ func (a *AuxSweeper) signSweepVpackets(vPackets []*tappsbt.VPacket, // createAndSignSweepVpackets creates vPackets that sweep the funds from the // channel to the wallet, and then signs them as well. func (a *AuxSweeper) createAndSignSweepVpackets( - sweepInputs []*cmsg.AssetOutput, signDesc input.SignDescriptor, + sweepInputs []*cmsg.AssetOutput, resReq lnwallet.ResolutionReq, sweepDesc lfn.Result[tapscriptSweepDesc], ) lfn.Result[[]*tappsbt.VPacket] { @@ -369,7 +398,7 @@ func (a *AuxSweeper) createAndSignSweepVpackets( signPkts := func(vPkts []*tappsbt.VPacket, desc tapscriptSweepDesc) lfn.Result[[]*tappsbt.VPacket] { - err := a.signSweepVpackets(vPkts, signDesc, desc) + err := a.signSweepVpackets(vPkts, resReq.SignDesc, desc) if err != nil { return lfn.Err[returnType](err) } @@ -378,7 +407,8 @@ func (a *AuxSweeper) createAndSignSweepVpackets( } return lfn.AndThen2( - a.createSweepVpackets(sweepInputs, sweepDesc), sweepDesc, + a.createSweepVpackets(sweepInputs, sweepDesc, resReq), + sweepDesc, signPkts, ) } @@ -1593,7 +1623,7 @@ func (a *AuxSweeper) resolveContract( // With the sweep desc constructed above, we'll create vPackets for // each of the local assets, then sign them all. sPkts := a.createAndSignSweepVpackets( - assetOutputs, req.SignDesc, firstLevelSweepDesc, + assetOutputs, req, firstLevelSweepDesc, ) // With the vPackets fully generated and signed above, we'll serialize From be2b853f741e2ac60fe4eb3608856a354f0ef864 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 17 Oct 2024 18:29:47 -0700 Subject: [PATCH 08/21] tapchannel: update signSweepVpackets to be 2nd level HTLC aware In this commit, we update `signSweepVpackets` to be 2nd level HTLC aware. If we have an auxSigDesc, then that means we're spending a 2nd level HTLC txn. To spend this properly, we'll need to insert the remote party's signature at the proper location, as it's a 2-of-2 multi-sig. --- tapchannel/aux_sweeper.go | 61 +++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index a5cb75ba2..043aea2fb 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -6,6 +6,7 @@ import ( "fmt" "math" "net/url" + "slices" "sync" "sync/atomic" @@ -325,12 +326,14 @@ func (a *AuxSweeper) createSweepVpackets(sweepInputs []*cmsg.AssetOutput, // signSweepVpackets attempts to sign the vPackets specified using the passed // sign desc and script tree. func (a *AuxSweeper) signSweepVpackets(vPackets []*tappsbt.VPacket, - signDesc input.SignDescriptor, tapscriptDesc tapscriptSweepDesc) error { + signDesc input.SignDescriptor, tapTweak, ctrlBlock []byte, + auxSigDesc lfn.Option[lnwallet.AuxSigDesc], + secondLevelSigIndex lfn.Option[uint32]) error { // Before we sign below, we also need to generate the tapscript With // the vPackets prepared, we can now sign the output asset we'll create // at a later step. - for _, vPacket := range vPackets { + for vPktIndex, vPacket := range vPackets { if len(vPacket.Inputs) != 1 { return fmt.Errorf("expected single input, got %v", len(vPacket.Inputs)) @@ -346,12 +349,11 @@ func (a *AuxSweeper) signSweepVpackets(vPackets []*tappsbt.VPacket, // signature. signingKey, leafToSign := applySignDescToVIn( signDesc, vIn, &a.cfg.ChainParams, - tapscriptDesc.scriptTree.TapTweak(), + tapTweak, ) // In this case, the witness isn't special, so we'll set the // control block now for it. - ctrlBlock := tapscriptDesc.ctrlBlockBytes vIn.TaprootLeafScript[0].ControlBlock = ctrlBlock log.Debugf("signing vPacket for input=%v", @@ -376,6 +378,52 @@ func (a *AuxSweeper) signSweepVpackets(vPackets []*tappsbt.VPacket, return fmt.Errorf("error signing virtual packet, " + "got no sig") } + + // At this point, the witness looks like: + // . This is a second level transaction, so we have + // another signature that we need to add to the witness this + // additional signature for the multi-sig. + err = lfn.MapOptionZ( + auxSigDesc, + func(aux lnwallet.AuxSigDesc) error { + assetSigs, err := cmsg.DecodeAssetSigListRecord( + aux.AuxSig, + ) + if err != nil { + return fmt.Errorf("error "+ + "decoding asset sig list "+ + "record: %w", err) + } + auxSig := assetSigs.Sigs[vPktIndex] + + // With the sig obtained, we'll now insert the + // signature at the specified index. + //nolint:lll + sigIndex, err := secondLevelSigIndex.UnwrapOrErr( + fmt.Errorf("no sig index"), + ) + if err != nil { + return err + } + + auxSigBytes := append( + auxSig.Sig.Val.RawBytes(), + byte(auxSig.SigHashType.Val), + ) + + newAsset := vPacket.Outputs[0].Asset + + //nolint:lll + prevWitness := newAsset.PrevWitnesses[0].TxWitness + prevWitness = slices.Insert( + prevWitness, int(sigIndex), auxSigBytes, + ) + return newAsset.UpdateTxWitness(0, prevWitness) + }, + ) + if err != nil { + return err + } } return nil @@ -398,7 +446,10 @@ func (a *AuxSweeper) createAndSignSweepVpackets( signPkts := func(vPkts []*tappsbt.VPacket, desc tapscriptSweepDesc) lfn.Result[[]*tappsbt.VPacket] { - err := a.signSweepVpackets(vPkts, resReq.SignDesc, desc) + err := a.signSweepVpackets( + vPkts, resReq.SignDesc, nil, nil, + lfn.None[lnwallet.AuxSigDesc](), lfn.None[uint32](), + ) if err != nil { return lfn.Err[returnType](err) } From ca27aaca16fe6049d5d2cc52a16b4623cac96059 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 17 Oct 2024 18:31:30 -0700 Subject: [PATCH 09/21] tapchannel: update createAndSignSweepVpackets to be 2nd level aware In this commit, we make sure to pass in the correct signDesc when we go to sign for a second level txn. For a 2nd level txn, we'll actually use the signDesc that's needed to generate the 2-of-2 multi-sig, instead of the one that we'd normally use to sweep. --- tapchannel/aux_sweeper.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index 043aea2fb..15dc326ab 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -446,9 +446,19 @@ func (a *AuxSweeper) createAndSignSweepVpackets( signPkts := func(vPkts []*tappsbt.VPacket, desc tapscriptSweepDesc) lfn.Result[[]*tappsbt.VPacket] { + // If this is a second level output, then we'll use the + // specified aux sign desc, otherwise, we'll use the + // normal one. + signDesc := lfn.MapOption( + func(aux lnwallet.AuxSigDesc) input.SignDescriptor { + return aux.SignDetails.SignDesc + }, + )(desc.auxSigInfo).UnwrapOr(resReq.SignDesc) + err := a.signSweepVpackets( - vPkts, resReq.SignDesc, nil, nil, - lfn.None[lnwallet.AuxSigDesc](), lfn.None[uint32](), + vPkts, signDesc, desc.scriptTree.TapTweak(), + desc.ctrlBlockBytes, desc.auxSigInfo, + desc.secondLevelSigIndex, ) if err != nil { return lfn.Err[returnType](err) From d84c84f960cc96261030790577902f60f6060d63 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 17 Oct 2024 18:32:37 -0700 Subject: [PATCH 10/21] tapchannel: create 2nd lvl vPkts in resolveContract With all the prior commits in place, we can now create the new contract resolution that includes the 1st and 2nd level packets, and the information that we'll need to re-sign the second level packets later. --- tapchannel/aux_sweeper.go | 112 ++++++++++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 24 deletions(-) diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index 15dc326ab..b7bcfc23d 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -1541,8 +1541,9 @@ func (a *AuxSweeper) resolveContract( } var ( - sweepDesc lfn.Result[tapscriptSweepDescs] - assetOutputs []*cmsg.AssetOutput + sweepDesc lfn.Result[tapscriptSweepDescs] + assetOutputs []*cmsg.AssetOutput + needsSecondLevel bool ) switch req.Type { @@ -1645,6 +1646,8 @@ func (a *AuxSweeper) resolveContract( // sweep desc. sweepDesc = localHtlcTimeoutSweepDesc(req) + needsSecondLevel = true + // In this case, we've broadcast a commitment, with an incoming HTLC // that we can sweep. We'll annotate the sweepDesc with the information // needed to sweep both this output, as well as the second level @@ -1659,6 +1662,8 @@ func (a *AuxSweeper) resolveContract( // sweep desc. sweepDesc = localHtlcSucessSweepDesc(req) + needsSecondLevel = true + default: return lfn.Errf[returnType]("unknown resolution type: %v", req.Type) @@ -1674,35 +1679,94 @@ func (a *AuxSweeper) resolveContract( log.Infof("Sweeping %v asset outputs: %v", len(assetOutputs), limitSpewer.Sdump(assetOutputs)) - firstLevelSweepDesc := lfn.AndThen( - sweepDesc, - func(sweepDesc tapscriptSweepDescs) lfn.Result[tapscriptSweepDesc] { //nolint:lll - return lfn.Ok(sweepDesc.firstLevel) - }, - ) + tapSweepDesc, err := sweepDesc.Unpack() + if err != nil { + return lfn.Err[tlv.Blob](err) + } + + // With the sweep desc constructed above, we'll create vPackets for each + // of the local assets, then sign them all. + firstLevelPkts, err := a.createAndSignSweepVpackets( + assetOutputs, req, lfn.Ok(tapSweepDesc.firstLevel), + ).Unpack() + if err != nil { + return lfn.Err[tlv.Blob](err) + } - // With the sweep desc constructed above, we'll create vPackets for - // each of the local assets, then sign them all. - sPkts := a.createAndSignSweepVpackets( - assetOutputs, req, firstLevelSweepDesc, + var ( + secondLevelPkts []*tappsbt.VPacket + secondLevelSigDesc lfn.Option[cmsg.TapscriptSigDesc] ) - // With the vPackets fully generated and signed above, we'll serialize - // it into a resolution blob to return. - return lfn.AndThen( - sPkts, func(vPkts []*tappsbt.VPacket) lfn.Result[tlv.Blob] { - res := cmsg.NewContractResolution( - vPkts, nil, lfn.None[cmsg.TapscriptSigDesc](), - ) + // We'll only need a set of second level packets if we're sweeping a set + // of HTLC outputs on the local party's commitment transaction. + if needsSecondLevel { + log.Infof("Creating+signing 2nd level vPkts") + + // We'll make a place holder for the second level output based + // on the assetID+value tuple. + secondLevelInputs := []*cmsg.AssetOutput{cmsg.NewAssetOutput( + assetOutputs[0].AssetID.Val, + assetOutputs[0].Amount.Val, assetOutputs[0].Proof.Val, + )} + + // Unlike the first level packets, we can't yet sign the second + // level packets yet, as we don't know what the sweeping + // transaction will look like. So we'll just create them. + secondLevelPkts, err = lfn.MapOption( + //nolint:lll + func(desc tapscriptSweepDesc) lfn.Result[[]*tappsbt.VPacket] { + return a.createSweepVpackets( + secondLevelInputs, lfn.Ok(desc), req, + ) + }, + )(tapSweepDesc.secondLevel).UnwrapOr( + lfn.Ok[[]*tappsbt.VPacket](nil), + ).Unpack() + if err != nil { + return lfn.Errf[tlv.Blob]("unable to make "+ + "second level pkts: %w", err) + } + + // We'll update some of the details of the 2nd level pkt based + // on the first lvl packet created above (as we don't yet have + // the full proof for the first lvl packet above). + for pktIdx, vPkt := range secondLevelPkts { + prevAsset := firstLevelPkts[pktIdx].Outputs[0].Asset + + for inputIdx, vIn := range vPkt.Inputs { + //nolint:lll + prevScriptKey := prevAsset.ScriptKey + vIn.PrevID.ScriptKey = asset.ToSerialized( + prevScriptKey.PubKey, + ) - var b bytes.Buffer - if err := res.Encode(&b); err != nil { - return lfn.Err[returnType](err) + vPkt.SetInputAsset(inputIdx, prevAsset) } + } - return lfn.Ok(b.Bytes()) - }, + // With the vPackets fully generated and signed above, we'll + // serialize it into a resolution blob to return. + secondLevelSigDesc = lfn.MapOption( + func(d tapscriptSweepDesc) cmsg.TapscriptSigDesc { + return cmsg.NewTapscriptSigDesc( + d.scriptTree.TapTweak(), + d.ctrlBlockBytes, + ) + }, + )(tapSweepDesc.secondLevel) + } + + res := cmsg.NewContractResolution( + firstLevelPkts, secondLevelPkts, secondLevelSigDesc, ) + + var b bytes.Buffer + if err := res.Encode(&b); err != nil { + return lfn.Err[tlv.Blob](err) + } + + return lfn.Ok(b.Bytes()) } // preimageDesc is a helper struct that contains the preimage and the witness From 86d03b8c3eb63aebf544f58b60e24acca30a62d2 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 17 Oct 2024 18:34:47 -0700 Subject: [PATCH 11/21] tapchannel: update sweepContracts for HTLC sweeps In this commit, we add some intermediate functions and types needed to properly handle HTLC sweeps. We now use the preimage info added in a prior commit to make sure that once we learn of the preimage (after vPkt creation, but before publish), we'll properly insert it in the correct location. In sweepContracts, when we go to create the new anchor change output, we'll make sure to omit any second level transactions, as they already have a destination location specified. Direct spends are spends directly from the commitment transaction. If we don't have any direct spends, then we don't need to make a change addr, as second level spends are already bound to an anchor output. --- tapchannel/aux_sweeper.go | 258 ++++++++++++++++++++++++++++++++------ tapchannelmsg/records.go | 5 + 2 files changed, 226 insertions(+), 37 deletions(-) diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index b7bcfc23d..460384554 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -429,6 +429,91 @@ func (a *AuxSweeper) signSweepVpackets(vPackets []*tappsbt.VPacket, return nil } +// vPktsWithInput couples a vPkt along with the input that contained it. +type vPktsWithInput struct { + // btcInput is the Bitcoin that the vPkt will the spending from (on the + // TAP layer). + btcInput input.Input + + // vPkts is the set of vPacket that will be used to spend the input. + vPkts []*tappsbt.VPacket + + // tapSigDesc houses the information we'll need to re-sign the vPackets + // above. Note that this is only set if this is a second level packet. + tapSigDesc lfn.Option[cmsg.TapscriptSigDesc] +} + +// isPresigned returns true if the vPktsWithInput is presigned. This will be the +// for an HTLC spent directly from our local commitment transaction. +func (v vPktsWithInput) isPresigned() bool { + witType := v.btcInput.WitnessType() + switch witType { + case input.TaprootHtlcAcceptedLocalSuccess: + return true + case input.TaprootHtlcLocalOfferedTimeout: + return true + default: + return false + } +} + +// sweepVpkts contains the set of vPkts needed for sweeping an output. Most +// outputs will only have the first level specified. The second level is needed +// for HTLC outputs on our local commitment transaction. +type sweepVpkts struct { + // firstLevel houses vPackets that are used to sweep outputs directly + // from the commitment transaction. + firstLevel []vPktsWithInput + + // secondLevel is used to sweep outputs that are created by second level + // HTLC transactions. + secondLevel []vPktsWithInput +} + +// firstLevelPkts returns a slice of the first level pkts. +func (s sweepVpkts) firstLevelPkts() []*tappsbt.VPacket { + return fn.FlatMap( + s.firstLevel, func(v vPktsWithInput) []*tappsbt.VPacket { + return v.vPkts + }, + ) +} + +// secondLevelPkts returns a slice of the second level pkts. +func (s sweepVpkts) secondLevelPkts() []*tappsbt.VPacket { + return fn.FlatMap( + s.secondLevel, func(v vPktsWithInput) []*tappsbt.VPacket { + return v.vPkts + }, + ) +} + +// allPkts returns a slice of both the first and second level pkts. +func (s sweepVpkts) allPkts() []*tappsbt.VPacket { + return append(s.firstLevelPkts(), s.secondLevelPkts()...) +} + +// allVpktsWithInput returns a slice of all vPktsWithInput. +func (s sweepVpkts) allVpktsWithInput() []vPktsWithInput { + return append(s.firstLevel, s.secondLevel...) +} + +// directSpendPkts returns the slice of all vPkts that are a direct spend from +// the commitment transaction. This excludes vPkts that are the pre-signed 2nd +// level transaction variant. +func (s sweepVpkts) directSpendPkts() []*tappsbt.VPacket { + directSpends := lfn.Filter(func(vi vPktsWithInput) bool { + return !vi.isPresigned() + }, s.allVpktsWithInput()) + directPkts := fn.FlatMap( + directSpends, func(v vPktsWithInput) []*tappsbt.VPacket { + return v.vPkts + }, + ) + + return directPkts +} + // createAndSignSweepVpackets creates vPackets that sweep the funds from the // channel to the wallet, and then signs them as well. func (a *AuxSweeper) createAndSignSweepVpackets( @@ -640,6 +725,8 @@ func remoteHtlcTimeoutSweepDesc(keyRing *lnwallet.CommitmentKeyRing, return lfn.Err[tapscriptSweepDescs](err) } + // TODO(roasbeef): use GenTaprootHtlcScript instead? + // Now that we have the script tree, we'll make the control block needed // to spend it, but taking the revoked path. ctrlBlock, err := htlcScriptTree.CtrlBlockForPath( @@ -1222,8 +1309,8 @@ func importOutputProofs(scid lnwire.ShortChannelID, spew.Sdump(inputProofLocator)) // Before we combine the proofs below, we'll be sure to update - // the transition proof to include the proper block+merkle - // proof information. + // the transition proof to include the proper block+merkle proof + // information. blockHash, err := chainBridge.GetBlockHash( ctxb, int64(scid.BlockHeight), ) @@ -1813,6 +1900,7 @@ func newBlobWithWitnessInfo(i input.Input) blobWithWitnessInfo { ) switch i.WitnessType() { // This is the case when we're sweeping the HTLC output on our local + // commitment transaction via a second level HTLC. // // The final witness stack is: @@ -1868,40 +1956,106 @@ func newBlobWithWitnessInfo(i input.Input) blobWithWitnessInfo { } } +// prepVpkts decodes the set of vPkts, supplementing them as needed to ensure +// all inputs can be swept properly. +func prepVpkts(bRes blobWithWitnessInfo, + secondLevel bool) (*vPktsWithInput, error) { + + var res cmsg.ContractResolution + err := res.Decode(bytes.NewReader(bRes.resolutionBlob)) + if err != nil { + return nil, err + } + + // For each vPacket, if we have a preimage to insert, then we'll we'll + // update the witness to insert the preimage at the correct index. + var tapSigDesc lfn.Option[cmsg.TapscriptSigDesc] + pkts := res.Vpkts1() + if secondLevel { + pkts = res.Vpkts2() + tapSigDesc = res.SigDescs() + } + + err = lfn.MapOptionZ(bRes.preimageInfo, func(p preimageDesc) error { + for _, pkt := range pkts { + newAsset := pkt.Outputs[0].Asset + + prevWitness := newAsset.PrevWitnesses[0].TxWitness + prevWitness = slices.Insert( + prevWitness, p.witnessIndex, + p.preimage[:], + ) + err := newAsset.UpdateTxWitness(0, prevWitness) + if err != nil { + return err + } + } + + return nil + }) + if err != nil { + return nil, err + } + + return &vPktsWithInput{ + vPkts: pkts, + btcInput: bRes.input, + tapSigDesc: tapSigDesc, + }, nil +} + // extractInputVPackets extracts the vPackets from the inputs passed in. If // none of the inputs have any resolution blobs. Then an empty slice will be // returned. -func extractInputVPackets(inputs []input.Input) lfn.Result[[]*tappsbt.VPacket] { - type returnType = []*tappsbt.VPacket - - // Otherwise, we'll extract the set of resolution blobs from the inputs +func extractInputVPackets(inputs []input.Input) lfn.Result[sweepVpkts] { + // First, we'll extract the set of resolution blobs from the inputs // passed in. relevantInputs := fn.Filter(inputs, func(i input.Input) bool { return i.ResolutionBlob().IsSome() }) - resolutionBlobs := fn.Map(relevantInputs, func(i input.Input) tlv.Blob { - // We already know this has a blob from the filter above. - return i.ResolutionBlob().UnwrapOr(nil) - }) - - // With our set of resolution inputs extracted, we'll now decode them - // in the vPackets we'll use to generate the output to addr. - vPkts, err := fn.FlatMapErr( - resolutionBlobs, - func(b tlv.Blob) ([]*tappsbt.VPacket, error) { - var res cmsg.ContractResolution - if err := res.Decode(bytes.NewReader(b)); err != nil { - return nil, err - } + resolutionInfo := fn.Map( + relevantInputs, newBlobWithWitnessInfo, + ) - return res.Vpkts1(), nil + firstLevelSweeps := lfn.Filter( + func(info blobWithWitnessInfo) bool { + return !info.secondLevel }, + resolutionInfo, ) - if err != nil { - return lfn.Err[returnType](err) + secondLevelSweeps := lfn.Filter( + func(info blobWithWitnessInfo) bool { + return info.secondLevel + }, + resolutionInfo, + ) + + // With our set of resolution inputs extracted, we'll now decode them in + // the vPackets we'll use to generate the output to addr. + var vPkts1 []vPktsWithInput + for _, bRes := range firstLevelSweeps { + vpkt, err := prepVpkts(bRes, false) + if err != nil { + return lfn.Err[sweepVpkts](err) + } + + vPkts1 = append(vPkts1, *vpkt) } - return lfn.Ok(vPkts) + var vPkts2 []vPktsWithInput + for _, bRes := range secondLevelSweeps { + vpkt, err := prepVpkts(bRes, true) + if err != nil { + return lfn.Err[sweepVpkts](err) + } + + vPkts2 = append(vPkts2, *vpkt) + } + + return lfn.Ok(sweepVpkts{ + firstLevel: vPkts1, + secondLevel: vPkts2, + }) } // sweepContracts takes a set of inputs, and the change address we'd use to @@ -1925,13 +2079,25 @@ func (a *AuxSweeper) sweepContracts(inputs []input.Input, // Now that we know we have a relevant input set, extract all the // vPackets from the inputs. - vPkts, err := extractInputVPackets(inputs).Unpack() + sPkts, err := extractInputVPackets(inputs).Unpack() if err != nil { return lfn.Err[returnType](err) } log.Infof("Generating anchor output for vpkts=%v", - limitSpewer.Sdump(vPkts)) + limitSpewer.Sdump(sPkts)) + + // If this is a sweep from the local commitment transaction. Then we'll + // have both the first and second level sweeps. However for the first + // sweep, it's a broadcast of a pre-signed transaction, so we don't need + // an anchor output for those. + directPkts := sPkts.directSpendPkts() + + // If there're no direct level vPkts, then we can just return a nil + // error as we don't have a real sweep output to create. + if len(directPkts) == 0 { + return lfn.Err[sweep.SweepOutput](nil) + } // At this point, now that we're about to generate a new output, we'll // need an internal key, so we can update all the vPkts. @@ -1944,17 +2110,34 @@ func (a *AuxSweeper) sweepContracts(inputs []input.Input, if err != nil { return lfn.Err[returnType](err) } - for idx := range vPkts { - for _, vOut := range vPkts[idx].Outputs { + for idx := range directPkts { + for _, vOut := range directPkts[idx].Outputs { vOut.SetAnchorInternalKey( internalKey, a.cfg.ChainParams.HDCoinType, ) } } + // For any second level outputs we're sweeping, we'll need to sign for + // it, as now we know the txid of the sweeping transaction. We'll do + // this again when we register for the final broadcast, we we need to + // sign the right prevIDs. + for _, sweepSet := range sPkts.secondLevel { + for _, vPkt := range sweepSet.vPkts { + prevOut := sweepSet.btcInput.OutPoint() + for _, vIn := range vPkt.Inputs { + vIn.PrevID.OutPoint = prevOut + } + for _, vOut := range vPkt.Outputs { + //nolint:lll + vOut.Asset.PrevWitnesses[0].PrevID.OutPoint = prevOut + } + } + } + // Now that we have our set of resolutions, we'll make a new commitment // out of all the vPackets contained. - outCommitments, err := tapsend.CreateOutputCommitments(vPkts) + outCommitments, err := tapsend.CreateOutputCommitments(directPkts) if err != nil { return lfn.Errf[returnType]("unable to create "+ "output commitments: %w", err) @@ -2034,7 +2217,7 @@ func (a *AuxSweeper) registerAndBroadcastSweep(req *sweep.BumpRequest, // If we don't have any vPackets that had our resolution data in them, // then we can exit early. - if len(vPkts) == 0 { + if len(vPkts.firstLevel) == 0 && len(vPkts.secondLevel) == 0 { log.Infof("Sweep request had no vPkts, exiting") return nil } @@ -2057,8 +2240,8 @@ func (a *AuxSweeper) registerAndBroadcastSweep(req *sweep.BumpRequest, // We'll also use the passed in context to set the anchor key again for // all the vOuts. - for idx := range vPkts { - for _, vOut := range vPkts[idx].Outputs { + for idx := range vPkts.firstLevelPkts() { + for _, vOut := range vPkts.firstLevelPkts()[idx].Outputs { vOut.SetAnchorInternalKey( internalKey, a.cfg.ChainParams.HDCoinType, ) @@ -2066,7 +2249,7 @@ func (a *AuxSweeper) registerAndBroadcastSweep(req *sweep.BumpRequest, } // Now that we have our vPkts, we'll re-create the output commitments. - outCommitments, err := tapsend.CreateOutputCommitments(vPkts) + outCommitments, err := tapsend.CreateOutputCommitments(vPkts.allPkts()) if err != nil { return fmt.Errorf("unable to create output "+ "commitments: %w", err) @@ -2088,15 +2271,16 @@ func (a *AuxSweeper) registerAndBroadcastSweep(req *sweep.BumpRequest, // // TODO(roasbeef): base off allocations? then can serialize, then // re-use the logic - for idx := range vPkts { - vPkt := vPkts[idx] + allVpkts := vPkts.allPkts() + for idx := range allVpkts { + vPkt := allVpkts[idx] for outIdx := range vPkt.Outputs { exclusionCreator := sweepExclusionProofGen( changeInternalKey, ) proofSuffix, err := tapsend.CreateProofSuffixCustom( - sweepTx, vPkt, outCommitments, outIdx, vPkts, + sweepTx, vPkt, outCommitments, outIdx, allVpkts, exclusionCreator, ) if err != nil { @@ -2116,7 +2300,7 @@ func (a *AuxSweeper) registerAndBroadcastSweep(req *sweep.BumpRequest, // We pass false for the last arg as we already updated our suffix // proofs here. return shipChannelTxn( - a.cfg.TxSender, sweepTx, outCommitments, vPkts, int64(fee), + a.cfg.TxSender, sweepTx, outCommitments, allVpkts, int64(fee), ) } diff --git a/tapchannelmsg/records.go b/tapchannelmsg/records.go index f63c5ac35..12964693d 100644 --- a/tapchannelmsg/records.go +++ b/tapchannelmsg/records.go @@ -2201,6 +2201,11 @@ func (c *ContractResolution) Decode(r io.Reader) error { return nil } +// SigDescs returns the list of tapscriptSigDescs. +func (c *ContractResolution) SigDescs() lfn.Option[TapscriptSigDesc] { + return c.secondLevelSigDescs.ValOpt() +} + // Vpkts1 returns the set of first level Vpkts. func (c *ContractResolution) Vpkts1() []*tappsbt.VPacket { return c.firstLevelSweepVpkts.Val.Pkts From fc7b957db8261c960bf1fa25d2e9a141191c1a1c Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 17 Oct 2024 18:36:44 -0700 Subject: [PATCH 12/21] tapchannel: update registerAndBroadcastSweep for HTLC sweeps In this commit, we update `registerAndBroadcastSweep` to be able to handle HTLC sweeps. First, we only need to set the anchor for first level sweeps. We weren't able to sign the 2nd level sweeps earlier as we didn't know the txid of the transaction that swept them. Now that we're about to broadcast, we know the sweeping transaction so we can set the prevID properly, then sign the sweeping transaction. If the sweeper is broadcasting _just_ a second level txn with an asset, then we won't have an extra change output. This transaction also already has its internal key bound. --- tapchannel/aux_sweeper.go | 89 +++++++++++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 17 deletions(-) diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index 460384554..34669b061 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -1754,6 +1754,7 @@ func (a *AuxSweeper) resolveContract( default: return lfn.Errf[returnType]("unknown resolution type: %v", req.Type) + // TODO(roasbeef): need to do HTLC revocation casesj:w } // The input proofs above were made originally using the fake commit tx @@ -2222,34 +2223,88 @@ func (a *AuxSweeper) registerAndBroadcastSweep(req *sweep.BumpRequest, return nil } - ourSweepOutput, err := req.ExtraTxOut.UnwrapOrErr( - fmt.Errorf("extra tx out not populated"), + // If this is a transaction that's only sweeping HTLC outputs via a + // pre-signed transaction, then we won't actually have an extra sweep + // output. + err = lfn.MapOptionZ( + req.ExtraTxOut, + func(extraTxOut sweep.SweepOutput) error { + ourSweepOutput, err := req.ExtraTxOut.UnwrapOrErr( + fmt.Errorf("extra tx out not populated"), + ) + if err != nil { + return err + } + iKey, err := ourSweepOutput.InternalKey.UnwrapOrErr( + fmt.Errorf("internal key not populated"), + ) + if err != nil { + return err + } + + // We'll also use the passed in context to set the + // anchor key again for all the vOuts, but only for + // first level vPkts, as second level packets already + // commit to the internal key of the vOut. + vPkts := vPkts.directSpendPkts() + for idx := range vPkts { + for _, vOut := range vPkts[idx].Outputs { + vOut.SetAnchorInternalKey( + iKey, + a.cfg.ChainParams.HDCoinType, + ) + } + } + + return nil + }, ) if err != nil { return err } - internalKey, err := ourSweepOutput.InternalKey.UnwrapOrErr( - fmt.Errorf("internal key not populated"), - ) - if err != nil { - return err + + // For any second level outputs we're sweeping, we'll need to sign for + // it, as now we know the txid of the sweeping transaction. + for _, sweepSet := range vPkts.secondLevel { + for _, vPkt := range sweepSet.vPkts { + prevOut := sweepSet.btcInput.OutPoint() + for _, vIn := range vPkt.Inputs { + vIn.PrevID.OutPoint = prevOut + } + + for _, vOut := range vPkt.Outputs { + //nolint:lll + vOut.Asset.PrevWitnesses[0].PrevID.OutPoint = prevOut + } + } } - log.Infof("Using %x for internal key: ", - internalKey.PubKey.SerializeCompressed()) + // If we have second level vPkts, then we'll need to sign them here, as + // now we know the input we're spending which was set above. + for _, sweepSet := range vPkts.secondLevel { + tapSigDesc, err := sweepSet.tapSigDesc.UnwrapOrErr( + fmt.Errorf("tap sig desc not populated"), + ) + if err != nil { + return err + } - // We'll also use the passed in context to set the anchor key again for - // all the vOuts. - for idx := range vPkts.firstLevelPkts() { - for _, vOut := range vPkts.firstLevelPkts()[idx].Outputs { - vOut.SetAnchorInternalKey( - internalKey, a.cfg.ChainParams.HDCoinType, - ) + err = a.signSweepVpackets( + sweepSet.vPkts, *sweepSet.btcInput.SignDesc(), + tapSigDesc.TapTweak.Val, tapSigDesc.CtrlBlock.Val, + lfn.None[lnwallet.AuxSigDesc](), + lfn.None[uint32](), + ) + if err != nil { + return fmt.Errorf("unable to sign second level "+ + "vPkts: %w", err) } } // Now that we have our vPkts, we'll re-create the output commitments. - outCommitments, err := tapsend.CreateOutputCommitments(vPkts.allPkts()) + outCommitments, err := tapsend.CreateOutputCommitments( + vPkts.allPkts(), + ) if err != nil { return fmt.Errorf("unable to create output "+ "commitments: %w", err) From 44512e56b892b518599310d25df98a77490a903e Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 8 Nov 2024 17:52:24 -0800 Subject: [PATCH 13/21] tapdb: log anchor point in LogAnchorTxConfirm --- tapdb/assets_store.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tapdb/assets_store.go b/tapdb/assets_store.go index d5ee469ef..1cd1314a9 100644 --- a/tapdb/assets_store.go +++ b/tapdb/assets_store.go @@ -2992,9 +2992,11 @@ func (a *AssetStore) LogAnchorTxConfirm(ctx context.Context, ) if err != nil { return fmt.Errorf("unable to set asset spent: "+ - "%w, script_key=%v, asset_id=%v", err, + "%w, script_key=%v, asset_id=%v, "+ + "anchor_point=%v", err, spew.Sdump(inputs[idx].ScriptKey), - spew.Sdump(inputs[idx].AssetID)) + spew.Sdump(inputs[idx].AssetID), + spew.Sdump(inputs[idx].AnchorPoint)) } } From 98c44b76ff1ea650bd8c92b3f9857ace171d5a4f Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 8 Nov 2024 17:56:37 -0800 Subject: [PATCH 14/21] tapchannel: properly set RelativeLockTime in DistributeCoins If we don't do this here, then we'll sign a second level success transaction that doesn't have the sequence set properly. This will then propagate all the way up to anchor output, leading to an incorrect inclusion proof later. --- tapchannel/allocation.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tapchannel/allocation.go b/tapchannel/allocation.go index 76257fb55..a9edd3572 100644 --- a/tapchannel/allocation.go +++ b/tapchannel/allocation.go @@ -454,6 +454,9 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, AnchorOutputTapscriptSibling: sibling, ScriptKey: a.ScriptKey, ProofDeliveryAddress: deliveryAddr, + RelativeLockTime: uint64( + a.Sequence, + ), } p.packet.Outputs = append(p.packet.Outputs, vOut) From f2a46f70b087d8d0f4b447c316060768cfa29352 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 8 Nov 2024 18:00:28 -0800 Subject: [PATCH 15/21] tapchannel: properly thread thru lock time for 2nd level HTLCs In this commit, we fix an existing logic gap related to 2nd level HTLCs. Before if we were signing a timeout for the remote party, neither of us would properly apply the lock time to the vPkt. In this commit, we fix it by first gaining a new `whoseCommit` param in `FetchLeavesFromCommit`. We then use that to decide when to use a non zero CLTV timeout. --- server.go | 2 +- tapchannel/aux_leaf_creator.go | 25 ++++++++++++++++++++++--- tapchannel/aux_leaf_signer.go | 25 +++++++++++++++++++++---- tapchannel/aux_sweeper.go | 7 ++++++- tapchannel/commitment.go | 27 ++++++++++++++++++++------- 5 files changed, 70 insertions(+), 16 deletions(-) diff --git a/server.go b/server.go index 9c42c9518..f8abac9ce 100644 --- a/server.go +++ b/server.go @@ -770,7 +770,7 @@ func (s *Server) FetchLeavesFromCommit(chanState lnwl.AuxChanState, // The aux leaf creator is fully stateless, and we don't need to wait // for the server to be started before being able to use it. return tapchannel.FetchLeavesFromCommit( - s.chainParams, chanState, com, keys, + s.chainParams, chanState, com, keys, whoseCommit, ) } diff --git a/tapchannel/aux_leaf_creator.go b/tapchannel/aux_leaf_creator.go index 6d3338552..44cc43848 100644 --- a/tapchannel/aux_leaf_creator.go +++ b/tapchannel/aux_leaf_creator.go @@ -8,10 +8,12 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taproot-assets/address" + "github.com/lightninglabs/taproot-assets/fn" cmsg "github.com/lightninglabs/taproot-assets/tapchannelmsg" "github.com/lightningnetwork/lnd/channeldb" lfn "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lntypes" lnwl "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/tlv" ) @@ -78,7 +80,8 @@ func FetchLeavesFromView(chainParams *address.ChainParams, // to the passed aux blob, and an existing channel commitment. func FetchLeavesFromCommit(chainParams *address.ChainParams, chanState lnwl.AuxChanState, com channeldb.ChannelCommitment, - keys lnwl.CommitmentKeyRing) lfn.Result[lnwl.CommitDiffAuxResult] { + keys lnwl.CommitmentKeyRing, + whoseCommit lntypes.ChannelParty) lfn.Result[lnwl.CommitDiffAuxResult] { type returnType = lnwl.CommitDiffAuxResult @@ -115,9 +118,17 @@ func FetchLeavesFromCommit(chainParams *address.ChainParams, continue } + // If this is an incoming HTLC (to us), but on the + // remote party's commitment transaction, then they'll + // need to go to the second level to time it out. + var cltvTimeout fn.Option[uint32] + if whoseCommit == lntypes.Remote { + cltvTimeout = fn.Some(htlc.RefundTimeout) + } + leaf, err := CreateSecondLevelHtlcTx( chanState, com.CommitTx, htlc.Amt.ToSatoshis(), - keys, chainParams, htlcOutputs, + keys, chainParams, htlcOutputs, cltvTimeout, ) if err != nil { return lfn.Err[returnType](fmt.Errorf("unable "+ @@ -147,9 +158,17 @@ func FetchLeavesFromCommit(chainParams *address.ChainParams, continue } + // If this is an outgoing commit on our local + // commitment, then we'll need to go to the second level + // to time out it out. + var cltvTimeout fn.Option[uint32] + if whoseCommit == lntypes.Local { + cltvTimeout = fn.Some(htlc.RefundTimeout) + } + leaf, err := CreateSecondLevelHtlcTx( chanState, com.CommitTx, htlc.Amt.ToSatoshis(), - keys, chainParams, htlcOutputs, + keys, chainParams, htlcOutputs, cltvTimeout, ) if err != nil { return lfn.Err[returnType](fmt.Errorf("unable "+ diff --git a/tapchannel/aux_leaf_signer.go b/tapchannel/aux_leaf_signer.go index f7d73078e..da0742409 100644 --- a/tapchannel/aux_leaf_signer.go +++ b/tapchannel/aux_leaf_signer.go @@ -358,9 +358,17 @@ func verifyHtlcSignature(chainParams *address.ChainParams, keyRing lnwallet.CommitmentKeyRing, sigs []*cmsg.AssetSig, htlcOutputs []*cmsg.AssetOutput, baseJob lnwallet.BaseAuxJob) error { + // If we're validating a signature for an outgoing HTLC, then it's an + // outgoing HTLC for the remote party, so we'll need to sign it with the + // proper lock time. + var htlcTimeout fn.Option[uint32] + if !baseJob.Incoming { + htlcTimeout = fn.Some(baseJob.HTLC.Timeout) + } + vPackets, err := htlcSecondLevelPacketsFromCommit( chainParams, chanState, commitTx, baseJob.KeyRing, htlcOutputs, - baseJob, + baseJob, htlcTimeout, ) if err != nil { return fmt.Errorf("error generating second level packets: %w", @@ -494,9 +502,17 @@ func (s *AuxLeafSigner) generateHtlcSignature(chanState lnwallet.AuxChanState, signDesc input.SignDescriptor, baseJob lnwallet.BaseAuxJob) (lnwallet.AuxSigJobResp, error) { + // If we're generating a signature for an incoming HTLC, then it's an + // outgoing HTLC for the remote party, so we'll need to sign it with the + // proper lock time. + var htlcTimeout fn.Option[uint32] + if baseJob.Incoming { + htlcTimeout = fn.Some(baseJob.HTLC.Timeout) + } + vPackets, err := htlcSecondLevelPacketsFromCommit( s.cfg.ChainParams, chanState, commitTx, baseJob.KeyRing, - htlcOutputs, baseJob, + htlcOutputs, baseJob, htlcTimeout, ) if err != nil { return lnwallet.AuxSigJobResp{}, fmt.Errorf("error generating "+ @@ -584,11 +600,12 @@ func (s *AuxLeafSigner) generateHtlcSignature(chanState lnwallet.AuxChanState, func htlcSecondLevelPacketsFromCommit(chainParams *address.ChainParams, chanState lnwallet.AuxChanState, commitTx *wire.MsgTx, keyRing lnwallet.CommitmentKeyRing, htlcOutputs []*cmsg.AssetOutput, - baseJob lnwallet.BaseAuxJob) ([]*tappsbt.VPacket, error) { + baseJob lnwallet.BaseAuxJob, + htlcTimeout fn.Option[uint32]) ([]*tappsbt.VPacket, error) { packets, _, err := CreateSecondLevelHtlcPackets( chanState, commitTx, baseJob.HTLC.Amount.ToSatoshis(), - keyRing, chainParams, htlcOutputs, + keyRing, chainParams, htlcOutputs, htlcTimeout, ) if err != nil { return nil, fmt.Errorf("error creating second level HTLC "+ diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index 34669b061..c165230bb 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -201,10 +201,15 @@ func (a *AuxSweeper) createSweepVpackets(sweepInputs []*cmsg.AssetOutput, // the output information locked in, as this was a pre-signed // transaction. if sweepDesc.auxSigInfo.IsSome() { + var cltvTimeout fn.Option[uint32] + sweepDesc.absoluteDelay.WhenSome(func(delay uint64) { + cltvTimeout = fn.Some(uint32(delay)) + }) + alloc, err := createSecondLevelHtlcAllocations( resReq.ChanType, resReq.Initiator, sweepInputs, resReq.HtlcAmt, resReq.CommitCsvDelay, *resReq.KeyRing, - fn.Some(resReq.ContractPoint.Index), + fn.Some(resReq.ContractPoint.Index), cltvTimeout, ) if err != nil { return lfn.Err[returnType](err) diff --git a/tapchannel/commitment.go b/tapchannel/commitment.go index eeb8a571d..59a4d72a9 100644 --- a/tapchannel/commitment.go +++ b/tapchannel/commitment.go @@ -1226,7 +1226,7 @@ func collectOutputs(a *Allocation, func createSecondLevelHtlcAllocations(chanType channeldb.ChannelType, initiator bool, htlcOutputs []*cmsg.AssetOutput, htlcAmt btcutil.Amount, commitCsvDelay uint32, keys lnwallet.CommitmentKeyRing, - outputIndex fn.Option[uint32], + outputIndex fn.Option[uint32], htlcTimeout fn.Option[uint32], ) ([]*Allocation, error) { // TODO(roasbeef): thaw height not implemented for taproot chans rn @@ -1267,6 +1267,8 @@ func createSecondLevelHtlcAllocations(chanType channeldb.ChannelType, SortTaprootKeyBytes: schnorr.SerializePubKey( htlcTree.TaprootKey, ), + // TODO(roasbeef): don't need it here? + CLTV: htlcTimeout.UnwrapOr(0), }} return allocations, nil @@ -1277,14 +1279,13 @@ func createSecondLevelHtlcAllocations(chanType channeldb.ChannelType, func CreateSecondLevelHtlcPackets(chanState lnwallet.AuxChanState, commitTx *wire.MsgTx, htlcAmt btcutil.Amount, keys lnwallet.CommitmentKeyRing, chainParams *address.ChainParams, - htlcOutputs []*cmsg.AssetOutput) ([]*tappsbt.VPacket, []*Allocation, - error) { + htlcOutputs []*cmsg.AssetOutput, htlcTimeout fn.Option[uint32], +) ([]*tappsbt.VPacket, []*Allocation, error) { allocations, err := createSecondLevelHtlcAllocations( chanState.ChanType, chanState.IsInitiator, - htlcOutputs, htlcAmt, - uint32(chanState.LocalChanCfg.CsvDelay), keys, - fn.None[uint32](), + htlcOutputs, htlcAmt, uint32(chanState.LocalChanCfg.CsvDelay), + keys, fn.None[uint32](), htlcTimeout, ) if err != nil { return nil, nil, err @@ -1303,6 +1304,15 @@ func CreateSecondLevelHtlcPackets(chanState lnwallet.AuxChanState, return nil, nil, fmt.Errorf("error distributing coins: %w", err) } + // If the HTLC timeout was present, then we'll also manually add it as a + // param to the vOut here, as it's just used for sorting with + // allocations. + for _, vPkt := range vPackets { + for _, o := range vPkt.Outputs { + o.LockTime = uint64(htlcTimeout.UnwrapOr(0)) + } + } + ctx := context.Background() for idx := range vPackets { err := tapsend.PrepareOutputAssets(ctx, vPackets[idx]) @@ -1320,12 +1330,14 @@ func CreateSecondLevelHtlcPackets(chanState lnwallet.AuxChanState, func CreateSecondLevelHtlcTx(chanState lnwallet.AuxChanState, commitTx *wire.MsgTx, htlcAmt btcutil.Amount, keys lnwallet.CommitmentKeyRing, chainParams *address.ChainParams, - htlcOutputs []*cmsg.AssetOutput) (input.AuxTapLeaf, error) { + htlcOutputs []*cmsg.AssetOutput, htlcTimeout fn.Option[uint32], +) (input.AuxTapLeaf, error) { none := input.NoneTapLeaf() vPackets, allocations, err := CreateSecondLevelHtlcPackets( chanState, commitTx, htlcAmt, keys, chainParams, htlcOutputs, + htlcTimeout, ) if err != nil { return none, fmt.Errorf("error creating second level HTLC "+ @@ -1353,6 +1365,7 @@ func CreateSecondLevelHtlcTx(chanState lnwallet.AuxChanState, if err != nil { return none, fmt.Errorf("error creating aux leaf: %w", err) } + return lfn.Some(auxLeaf), nil } From c072d3f258832471148b27d2a5d062502b7123fc Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 8 Nov 2024 18:03:10 -0800 Subject: [PATCH 16/21] tapchannel: ensure the root commit asset has a valid witness In this commit, we fix a very subtle issue related to split witnesses and root assets. Before this commit, when we went to sign a second level txn, which is a split, we would create a virtual txn, which will commit to the _entire_ witness of the root asset in the vIn. Note that this bypasses the normal segwit encoding logic, as that currently doesn't apply to the root asset encoded in the split commit proof. In the future, we may want to expand the segwit encoding semantics to root assets after careful consideration. In this commit, we fix a bug that would cause an invalid 2nd level sig by ensuring that before we create the output commitments, we update the root asset witness in each split, to the funding witness script, which is just OP_TRUE. --- tapchannel/commitment.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tapchannel/commitment.go b/tapchannel/commitment.go index 59a4d72a9..a7c3b117a 100644 --- a/tapchannel/commitment.go +++ b/tapchannel/commitment.go @@ -528,6 +528,15 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, err) } + // The root asset of the split commitment will still commit to the full + // witness value. Therefore, we need to update the root asset witness to + // what it would be at broadcast time. + fundingWitness, err := fundingSpendWitness().Unpack() + if err != nil { + return nil, nil, fmt.Errorf("unable to make funding "+ + "witness: %w", err) + } + // Prepare the output assets for each virtual packet, then create the // output commitments. ctx := context.Background() @@ -537,6 +546,23 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, return nil, nil, fmt.Errorf("unable to prepare output "+ "assets: %w", err) } + + // With the packets prepared, we'll swap in the correct witness + // for each of them. + for outIdx := range vPackets[idx].Outputs { + outAsset := vPackets[idx].Outputs[outIdx].Asset + + // There is always only a single input, as we're + // sweeping a single contract w/ each vPkt. + const inputIndex = 0 + err := outAsset.UpdateTxWitness( + inputIndex, fundingWitness, + ) + if err != nil { + return nil, nil, fmt.Errorf("error updating "+ + "witness: %w", err) + } + } } outCommitments, err := tapsend.CreateOutputCommitments(vPackets) From f44cdd120aa9d35a1aecd2b9480c8e72d9db5442 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 8 Nov 2024 18:05:28 -0800 Subject: [PATCH 17/21] tapchannel: add an extra budget for each input w/ a blob In this commit, we fix an issue that would cause lnd to not broadcast sweeps of HTLCs due to their small value. Before we didn't add enough budget to actually convince lnd to init the fee function and broadcast the sweep. In the future, we should do a more careful calculation here based on the current BTC value of the asset, to make an economical decision. For now, we just increase the amt based on the amt of inputs we have. This gives lnd a fee budget to use for bumping. --- tapchannel/aux_sweeper.go | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index c165230bb..53eeecc80 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -38,6 +38,14 @@ import ( "golang.org/x/exp/maps" ) +const ( + // sweeperBudgetMultiplier is the multiplier used to determine the + // budget expressed in sats, report to lnd's sweeper. As we have small + // outputs on chain, we'll need an increased budget (the amount we + // should spend on fees) to make sure the outputs are always swept. + sweeperBudgetMultiplier = 20 +) + // resolutionReq carries a request to resolve a contract output along with a // response channel of the result. type resolutionReq struct { @@ -2445,13 +2453,22 @@ func (a *AuxSweeper) DeriveSweepAddr(inputs []input.Input, func (a *AuxSweeper) ExtraBudgetForInputs( inputs []input.Input) lfn.Result[btcutil.Amount] { - hasResolutionBlob := fn.Any(inputs, func(i input.Input) bool { + inputsWithBlobs := fn.Filter(inputs, func(i input.Input) bool { return i.ResolutionBlob().IsSome() }) var extraBudget btcutil.Amount - if hasResolutionBlob { - extraBudget = tapsend.DummyAmtSats + if len(inputsWithBlobs) != 0 { + // In this case, just 1k sats (tapsend.DummyAmtSats) may not be + // enough budget to pay for sweeping. So instead, we'll use a + // multiple of this to ensure that any time we care about an + // output, we're pretty much always able to sweep it. + // + // TODO(roasbeef): return the sats equiv budget of the asset + // amount + extraBudget = tapsend.DummyAmtSats * btcutil.Amount( + sweeperBudgetMultiplier*len(inputsWithBlobs), + ) } return lfn.Ok(extraBudget) From f9f7b55d1216d5364ea545e38682d984c30647ce Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 8 Nov 2024 18:08:03 -0800 Subject: [PATCH 18/21] tapchannel: utilize new outpointToTxIndex map in NotifyBroadcast In this commit, we update the logic of `notifyBroadcast` to use the new `outpointToTxIndex` map. We'll use this to make sure that the vOut has the correct anchor output index, as the sweeper applies a sorting routine before broadcast. Otherwise, we'll have invalid inclusion proofs. --- server.go | 2 +- tapchannel/aux_sweeper.go | 42 +++++++++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/server.go b/server.go index f8abac9ce..cf7165a6d 100644 --- a/server.go +++ b/server.go @@ -1169,5 +1169,5 @@ func (s *Server) NotifyBroadcast(req *sweep.BumpRequest, return err } - return s.cfg.AuxSweeper.NotifyBroadcast(req, tx, fee) + return s.cfg.AuxSweeper.NotifyBroadcast(req, tx, fee, outpointToTxIndex) } diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index 53eeecc80..4a5bd3394 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -84,6 +84,11 @@ type broadcastReq struct { // fee is the fee that was used for the transaction. fee btcutil.Amount + // outpointToTxIndex maps a spent outpoint to the tx index on the sweep + // transaction of the corresponding output. This is only needed to make + // sure we make proofs properly for the pre-signed HTLC transactions. + outpointToTxIndex map[wire.OutPoint]int + // resp is the error result of the broadcast. resp chan error } @@ -2215,7 +2220,8 @@ func sweepExclusionProofGen(sweepInternalKey keychain.KeyDescriptor, // registerAndBroadcastSweep finalizes a sweep attempt by generating a // transition proof for it, then registering the sweep with the porter. func (a *AuxSweeper) registerAndBroadcastSweep(req *sweep.BumpRequest, - sweepTx *wire.MsgTx, fee btcutil.Amount) error { + sweepTx *wire.MsgTx, fee btcutil.Amount, + outpointToTxIndex map[wire.OutPoint]int) error { // TODO(roasbeef): need to handle replacement -- will porter just // upsert in place? @@ -2292,6 +2298,27 @@ func (a *AuxSweeper) registerAndBroadcastSweep(req *sweep.BumpRequest, } } + // For pre-signed HTLC txns we'll need to make sure we update the output + // index in the vPkt. As the ordering is only determined at broadcast + // time. + if outpointToTxIndex != nil { + for _, sweepPkt := range vPkts.allVpktsWithInput() { + op := sweepPkt.btcInput.OutPoint() + finalOutputIndex, ok := outpointToTxIndex[op] + if !ok { + continue + } + + for _, vPkt := range sweepPkt.vPkts { + for _, vOut := range vPkt.Outputs { + vOut.AnchorOutputIndex = uint32( + finalOutputIndex, + ) + } + } + } + } + // If we have second level vPkts, then we'll need to sign them here, as // now we know the input we're spending which was set above. for _, sweepSet := range vPkts.secondLevel { @@ -2390,6 +2417,7 @@ func (a *AuxSweeper) contractResolver() { case req := <-a.broadcastReqs: req.resp <- a.registerAndBroadcastSweep( req.req, req.tx, req.fee, + req.outpointToTxIndex, ) case <-a.quit: @@ -2477,13 +2505,15 @@ func (a *AuxSweeper) ExtraBudgetForInputs( // NotifyBroadcast is used to notify external callers of the broadcast of a // sweep transaction, generated by the passed BumpRequest. func (a *AuxSweeper) NotifyBroadcast(req *sweep.BumpRequest, - tx *wire.MsgTx, fee btcutil.Amount) error { + tx *wire.MsgTx, fee btcutil.Amount, + outpointToTxIndex map[wire.OutPoint]int) error { auxReq := &broadcastReq{ - req: req, - tx: tx, - fee: fee, - resp: make(chan error, 1), + req: req, + tx: tx, + fee: fee, + outpointToTxIndex: outpointToTxIndex, + resp: make(chan error, 1), } if !fn.SendOrQuit(a.broadcastReqs, auxReq, a.quit) { From 74efdefa05f2fdcc8323ab044961588ce22e1908 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 8 Nov 2024 18:16:31 -0800 Subject: [PATCH 19/21] tapchannel: import script keys of all HTLC outputs, as known If we don't import these script keys, spends will fail later as we didn't credit the balance, so we can't spend them. --- tapchannel/aux_sweeper.go | 108 +++++++++++++++++++++++++++----------- 1 file changed, 76 insertions(+), 32 deletions(-) diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index 4a5bd3394..87cd0f39b 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -1249,6 +1249,42 @@ func (a *AuxSweeper) importCommitScriptKeys(req lnwallet.ResolutionReq) error { return nil } +// importOutputScriptKey imports the output script key that this scriptDesc can +// spend into the local addr book. +func (a *AuxSweeper) importOutputScriptKeys(desc tapscriptSweepDescs) error { + ctxb := context.Background() + + importScriptKey := func(desc tapscriptSweepDesc) error { + scriptTree := desc.scriptTree.Tree() + + outputKey := asset.NewScriptKey(scriptTree.TaprootKey).PubKey + scriptKey := asset.ScriptKey{ + PubKey: outputKey, + TweakedScriptKey: &asset.TweakedScriptKey{ + RawKey: keychain.KeyDescriptor{ + PubKey: scriptTree.InternalKey, + }, + Tweak: scriptTree.TapscriptRoot, + }, + } + + log.Debugf("Importing script_keys=%v", spew.Sdump(scriptKey)) + + return a.cfg.AddrBook.InsertScriptKey(ctxb, scriptKey, true) + } + + if err := importScriptKey(desc.firstLevel); err != nil { + return err + } + + return lfn.MapOptionZ( + desc.secondLevel, + func(secondary tapscriptSweepDesc) error { + return importScriptKey(secondary) + }, + ) +} + // importOutputProofs imports the output proofs into the pending asset funding // into our local database. This preps us to be able to detect force closes. func importOutputProofs(scid lnwire.ShortChannelID, @@ -1620,31 +1656,6 @@ func (a *AuxSweeper) resolveContract( return lfn.Err[returnType](err) } - // To be able to construct all the proofs we need to spend later, we'll - // make sure that this commitment transaction exists in our database. - // If not, then we'll complete the proof, register the script keys, and - // ship the pre-signed commitment transaction. - ctx := context.Background() - commitParcel, err := a.cfg.TxSender.QueryParcels( - ctx, fn.Some(req.CommitTx.TxHash()), false, - ) - if err != nil { - return lfn.Err[returnType](err) - } - if len(commitParcel) == 0 { - log.Infof("First time seeing commit_txid=%v, importing", - req.CommitTx.TxHash()) - - err := a.importCommitTx(req, commitState, fundingInfo) - if err != nil { - return lfn.Errf[returnType]("unable to import "+ - "commitment txn: %w", err) - } - } else { - log.Infof("Commitment commit_txid=%v already imported, "+ - "skipping", req.CommitTx.TxHash()) - } - var ( sweepDesc lfn.Result[tapscriptSweepDescs] assetOutputs []*cmsg.AssetOutput @@ -1775,6 +1786,44 @@ func (a *AuxSweeper) resolveContract( // TODO(roasbeef): need to do HTLC revocation casesj:w } + tapSweepDesc, err := sweepDesc.Unpack() + if err != nil { + return lfn.Err[tlv.Blob](err) + } + + // Now that we know what output we're sweeping, before we proceed, we'll + // import the relevant script key to disk. This way, we'll properly + // recognize spends of it. + if err := a.importOutputScriptKeys(tapSweepDesc); err != nil { + return lfn.Errf[tlv.Blob]("unable to import output script "+ + "key: %w", err) + } + + // To be able to construct all the proofs we need to spend later, we'll + // make sure that this commitment transaction exists in our database. If + // not, then we'll complete the proof, register the script keys, and + // ship the pre-signed commitment transaction. + ctx := context.Background() + commitParcel, err := a.cfg.TxSender.QueryParcels( + ctx, fn.Some(req.CommitTx.TxHash()), false, + ) + if err != nil { + return lfn.Err[returnType](err) + } + if len(commitParcel) == 0 { + log.Infof("First time seeing commit_txid=%v, importing", + req.CommitTx.TxHash()) + + err := a.importCommitTx(req, commitState, fundingInfo) + if err != nil { + return lfn.Errf[returnType]("unable to import "+ + "commitment txn: %w", err) + } + } else { + log.Infof("Commitment commit_txid=%v already imported, "+ + "skipping", req.CommitTx.TxHash()) + } + // The input proofs above were made originally using the fake commit tx // as an anchor. We now know the real commit tx, so we'll swap that in // to ensure the outpoints used below are correct. @@ -1782,13 +1831,8 @@ func (a *AuxSweeper) resolveContract( assetOut.Proof.Val.AnchorTx = *req.CommitTx } - log.Infof("Sweeping %v asset outputs: %v", len(assetOutputs), - limitSpewer.Sdump(assetOutputs)) - - tapSweepDesc, err := sweepDesc.Unpack() - if err != nil { - return lfn.Err[tlv.Blob](err) - } + log.Infof("Sweeping %v asset outputs (second_level=%v): %v", + len(assetOutputs), needsSecondLevel, spew.Sdump(assetOutputs)) // With the sweep desc constructed above, we'll create vPackets for each // of the local assets, then sign them all. From d6cbc5551319b672c33805cae5d9226211e08991 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 15 Nov 2024 14:24:16 +0100 Subject: [PATCH 20/21] tapchannel: use limit spewer only This fixes an issue of an infinite loop when attempting to spew a proof structure. --- tapchannel/aux_closer.go | 6 +++--- tapchannel/aux_funding_controller.go | 20 +++++++++++--------- tapchannel/aux_invoice_manager.go | 4 ++-- tapchannel/aux_leaf_signer.go | 3 +-- tapchannel/aux_sweeper.go | 21 ++++++++++++--------- 5 files changed, 29 insertions(+), 25 deletions(-) diff --git a/tapchannel/aux_closer.go b/tapchannel/aux_closer.go index cfa32ae53..eaff1ece3 100644 --- a/tapchannel/aux_closer.go +++ b/tapchannel/aux_closer.go @@ -10,7 +10,6 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" - "github.com/davecgh/go-spew/spew" "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/fn" @@ -250,7 +249,8 @@ func (a *AuxChanCloser) AuxCloseOutputs( } log.Tracef("Decoded local_shutdown=%v, remote_shutdown=%v", - spew.Sdump(localShutdown), spew.Sdump(remoteShutdown)) + limitSpewer.Sdump(localShutdown), + limitSpewer.Sdump(remoteShutdown)) // To start with, we'll now create the allocations for the asset // outputs. We track the amount that'll go to the anchor assets, so we @@ -570,7 +570,7 @@ func (a *AuxChanCloser) ShutdownBlob( return none, err } - log.Infof("Constructed shutdown record: %v", spew.Sdump(records)) + log.Infof("Constructed shutdown record: %v", limitSpewer.Sdump(records)) return lfn.Some[lnwire.CustomRecords](records), nil } diff --git a/tapchannel/aux_funding_controller.go b/tapchannel/aux_funding_controller.go index 94d154dab..f2c27d60e 100644 --- a/tapchannel/aux_funding_controller.go +++ b/tapchannel/aux_funding_controller.go @@ -19,7 +19,6 @@ import ( "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" - "github.com/davecgh/go-spew/spew" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" @@ -588,7 +587,7 @@ func (p *pendingAssetFunding) unlockAssetInputs(ctx context.Context, coinSelect tapfreighter.CoinSelector) error { log.Debugf("unlocking asset inputs: %v", - spew.Sdump(p.lockedAssetInputs)) + limitSpewer.Sdump(p.lockedAssetInputs)) err := coinSelect.ReleaseCoins(ctx, p.lockedAssetInputs...) if err != nil { @@ -1002,7 +1001,8 @@ func (f *FundingController) anchorVPackets(fundedPkt *tapsend.FundedPsbt, func (f *FundingController) signAndFinalizePsbt(ctx context.Context, pkt *psbt.Packet) (*wire.MsgTx, error) { - log.Debugf("Signing and finalizing PSBT w/ lnd: %v", spew.Sdump(pkt)) + log.Debugf("Signing and finalizing PSBT w/ lnd: %v", + limitSpewer.Sdump(pkt)) // By default, the wallet won't try to finalize output it sees are watch // only (like the asset input), so we'll have it sign ourselves first. @@ -1011,7 +1011,7 @@ func (f *FundingController) signAndFinalizePsbt(ctx context.Context, return nil, fmt.Errorf("unable to sign PSBT: %w", err) } - log.Debugf("Signed PSBT: %v", spew.Sdump(signedPkt)) + log.Debugf("Signed PSBT: %v", limitSpewer.Sdump(signedPkt)) finalizedPkt, err := f.cfg.ChainWallet.SignAndFinalizePsbt( ctx, signedPkt, @@ -1020,7 +1020,7 @@ func (f *FundingController) signAndFinalizePsbt(ctx context.Context, return nil, fmt.Errorf("unable to finalize PSBT: %w", err) } - log.Debugf("Finalized PSBT: %v", spew.Sdump(signedPkt)) + log.Debugf("Finalized PSBT: %v", limitSpewer.Sdump(signedPkt)) // Extra the tx manually, then perform some manual sanity checks to // make sure things are ready for broadcast. @@ -1123,7 +1123,8 @@ func (f *FundingController) completeChannelFunding(ctx context.Context, // with lnd that we arrived at the proper TxOut. fundingPsbt.UnsignedTx.TxOut[0].Value = int64(fundingReq.ChanAmt) - log.Debugf("Funding PSBT pre funding: %s", spew.Sdump(fundingPsbt)) + log.Debugf("Funding PSBT pre funding: %s", + limitSpewer.Sdump(fundingPsbt)) // With the PSBT template created, we'll now ask lnd to fund the PSBT. // This'll add yet another output (lnd's change output) to the @@ -1135,7 +1136,8 @@ func (f *FundingController) completeChannelFunding(ctx context.Context, return nil, fmt.Errorf("unable to fund PSBT: %w", err) } - log.Infof("Funding PSBT post funding: %s", spew.Sdump(finalFundedPsbt)) + log.Infof("Funding PSBT post funding: %s", + limitSpewer.Sdump(finalFundedPsbt)) // If we fail at any step in the process, we want to make sure we // unlock the inputs, so we'll add them to funding state now. @@ -1178,7 +1180,7 @@ func (f *FundingController) completeChannelFunding(ctx context.Context, } log.Debugf("Submitting finalized PSBT to lnd for verification: %s", - spew.Sdump(finalFundedPsbt.Pkt)) + limitSpewer.Sdump(finalFundedPsbt.Pkt)) // At this point, we're nearly done, we'll now present the final PSBT // to lnd to verification. If this passes, then we're clear to @@ -1737,7 +1739,7 @@ func (f *FundingController) chanFunder() { } log.Infof("Returning funding desc: %v", - spew.Sdump(fundingDesc)) + limitSpewer.Sdump(fundingDesc)) req.resp <- lfn.Some(*fundingDesc) diff --git a/tapchannel/aux_invoice_manager.go b/tapchannel/aux_invoice_manager.go index 7fcad8c9a..51b212c59 100644 --- a/tapchannel/aux_invoice_manager.go +++ b/tapchannel/aux_invoice_manager.go @@ -5,7 +5,6 @@ import ( "fmt" "sync" - "github.com/davecgh/go-spew/spew" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/fn" @@ -254,7 +253,8 @@ func (s *AuxInvoiceManager) priceFromQuote(rfqID rfqmsg.ID) ( acceptedSellQuotes := s.cfg.RfqManager.LocalAcceptedSellQuotes() log.Tracef("Currently available quotes: buy %v, sell %v", - spew.Sdump(acceptedBuyQuotes), spew.Sdump(acceptedSellQuotes)) + limitSpewer.Sdump(acceptedBuyQuotes), + limitSpewer.Sdump(acceptedSellQuotes)) buyQuote, isBuy := acceptedBuyQuotes[rfqID.Scid()] sellQuote, isSell := acceptedSellQuotes[rfqID.Scid()] diff --git a/tapchannel/aux_leaf_signer.go b/tapchannel/aux_leaf_signer.go index da0742409..f728b1ec4 100644 --- a/tapchannel/aux_leaf_signer.go +++ b/tapchannel/aux_leaf_signer.go @@ -9,7 +9,6 @@ import ( "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" - "github.com/davecgh/go-spew/spew" "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/commitment" @@ -649,7 +648,7 @@ func (v *schnorrSigValidator) ValidateWitnesses(newAsset *asset.Asset, if !ok { return fmt.Errorf("%w: no prev asset for "+ "input_prev_id=%v", vm.ErrNoInputs, - spew.Sdump(witness.PrevID)) + limitSpewer.Sdump(witness.PrevID)) } var ( diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index 87cd0f39b..89be2c08f 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -15,7 +15,6 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" - "github.com/davecgh/go-spew/spew" "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/fn" @@ -375,7 +374,7 @@ func (a *AuxSweeper) signSweepVpackets(vPackets []*tappsbt.VPacket, vIn.TaprootLeafScript[0].ControlBlock = ctrlBlock log.Debugf("signing vPacket for input=%v", - spew.Sdump(vIn.PrevID)) + limitSpewer.Sdump(vIn.PrevID)) // With everything set, we can now sign the new leaf we'll // sweep into. @@ -1235,7 +1234,7 @@ func (a *AuxSweeper) importCommitScriptKeys(req lnwallet.ResolutionReq) error { return fmt.Errorf("unknown close type: %v", req.CloseType) } - log.Debugf("Importing script_keys=%v", spew.Sdump(keysToImport)) + log.Debugf("Importing script_keys=%v", limitSpewer.Sdump(keysToImport)) ctxb := context.Background() for _, key := range keysToImport { @@ -1268,7 +1267,8 @@ func (a *AuxSweeper) importOutputScriptKeys(desc tapscriptSweepDescs) error { }, } - log.Debugf("Importing script_keys=%v", spew.Sdump(scriptKey)) + log.Debugf("Importing script_keys=%v", + limitSpewer.Sdump(scriptKey)) return a.cfg.AddrBook.InsertScriptKey(ctxb, scriptKey, true) } @@ -1327,7 +1327,7 @@ func importOutputProofs(scid lnwire.ShortChannelID, } log.Infof("Fetching funding input proof, locator=%v", - spew.Sdump(inputProofLocator)) + limitSpewer.Sdump(inputProofLocator)) // First, we'll make a courier to use in fetching the proofs we // need. @@ -1360,7 +1360,7 @@ func importOutputProofs(scid lnwire.ShortChannelID, } log.Infof("All proofs fetched, importing locator=%v", - spew.Sdump(inputProofLocator)) + limitSpewer.Sdump(inputProofLocator)) // Before we combine the proofs below, we'll be sure to update // the transition proof to include the proper block+merkle proof @@ -1832,7 +1832,8 @@ func (a *AuxSweeper) resolveContract( } log.Infof("Sweeping %v asset outputs (second_level=%v): %v", - len(assetOutputs), needsSecondLevel, spew.Sdump(assetOutputs)) + len(assetOutputs), needsSecondLevel, + limitSpewer.Sdump(assetOutputs)) // With the sweep desc constructed above, we'll create vPackets for each // of the local assets, then sign them all. @@ -2270,7 +2271,8 @@ func (a *AuxSweeper) registerAndBroadcastSweep(req *sweep.BumpRequest, // TODO(roasbeef): need to handle replacement -- will porter just // upsert in place? - log.Infof("Register broadcast of sweep_tx=%v", spew.Sdump(sweepTx)) + log.Infof("Register broadcast of sweep_tx=%v", + limitSpewer.Sdump(sweepTx)) // In order to properly register the sweep, we'll need to first extra a // unified set of vPackets from the specified inputs. @@ -2431,7 +2433,8 @@ func (a *AuxSweeper) registerAndBroadcastSweep(req *sweep.BumpRequest, } } - log.Infof("Proofs generated for sweep_tx=%v", spew.Sdump(sweepTx)) + log.Infof("Proofs generated for sweep_tx=%v", + limitSpewer.Sdump(sweepTx)) // With the output commitments re-created, we have all we need to log // and ship the transaction. From f12575cdccd608a69e573ad858b66f2b04b1db7d Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 15 Nov 2024 17:24:29 -0800 Subject: [PATCH 21/21] server: log utxo index with NotifyBroadcast --- server.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server.go b/server.go index cf7165a6d..7daa8e4d7 100644 --- a/server.go +++ b/server.go @@ -1162,8 +1162,9 @@ func (s *Server) NotifyBroadcast(req *sweep.BumpRequest, tx *wire.MsgTx, fee btcutil.Amount, outpointToTxIndex map[wire.OutPoint]int) error { - srvrLog.Tracef("NotifyBroadcast called, req=%v, tx=%v, fee=%v", - spew.Sdump(req), spew.Sdump(tx), fee) + srvrLog.Tracef("NotifyBroadcast called, req=%v, tx=%v, fee=%v, "+ + "out_index=%v", spew.Sdump(req), spew.Sdump(tx), fee, + spew.Sdump(outpointToTxIndex)) if err := s.waitForReady(); err != nil { return err