@@ -8,11 +8,14 @@ import (
88 "os"
99 "strings"
1010
11+ "github.com/btcsuite/btcd/wire"
1112 "github.com/lightninglabs/chantools/btc"
1213 "github.com/lightninglabs/chantools/lnd"
1314 "github.com/lightninglabs/chantools/scbforceclose"
1415 "github.com/lightningnetwork/lnd/chanbackup"
16+ "github.com/lightningnetwork/lnd/channeldb"
1517 "github.com/lightningnetwork/lnd/input"
18+ "github.com/lightningnetwork/lnd/lnwallet"
1619 "github.com/spf13/cobra"
1720)
1821
@@ -209,7 +212,18 @@ func (c *scbForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
209212 txHex := hex .EncodeToString (buf .Bytes ())
210213 fmt .Println ("Channel point:" , s .FundingOutpoint )
211214 fmt .Println ("Raw transaction hex:" , txHex )
212- fmt .Println ()
215+
216+ // Classify outputs: identify to_remote using known templates,
217+ // anchors (330 sat), and log the rest as to_local/htlc without
218+ // deriving per-commitment. The remote key is not tweaked in
219+ // the backup (except for very old channels which we don't
220+ // support anyway).
221+ class , err := classifyOutputs (s , signedTx )
222+ if err == nil {
223+ printOutputClassification (class , signedTx )
224+ } else {
225+ fmt .Printf ("failed to classify outputs: %v\n " , err )
226+ }
213227
214228 // Publish TX.
215229 if c .Publish {
@@ -224,3 +238,163 @@ func (c *scbForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
224238
225239 return nil
226240}
241+
242+ // classifyAndLogOutputs attempts to identify the to_remote output by comparing
243+ // against known script templates (p2wkh, delayed p2wsh, lease), marks 330-sat
244+ // anchors, and logs remaining outputs as to_local/htlc without deriving
245+ // per-commitment data.
246+ func printOutputClassification (class outputClassification , tx * wire.MsgTx ) {
247+ if class .ToRemoteIdx >= 0 {
248+ log .Infof ("to_remote: idx=%d amount=%d sat" , class .ToRemoteIdx ,
249+ class .ToRemoteAmt )
250+
251+ if len (class .ToRemotePkScript ) != 0 {
252+ log .Infof ("to_remote PkScript (hex): %x" ,
253+ class .ToRemotePkScript )
254+ }
255+ } else {
256+ log .Infof ("to_remote: not identified" )
257+ }
258+
259+ for _ , idx := range class .AnchorIdxs {
260+ log .Infof ("possible anchor: idx=%d amount=%d sat" , idx ,
261+ tx .TxOut [idx ].Value )
262+ }
263+
264+ for _ , idx := range class .OtherIdxs {
265+ log .Infof ("possible to_local/htlc: idx=%d amount=%d sat" , idx ,
266+ tx .TxOut [idx ].Value )
267+ }
268+ }
269+
270+ // outputClassification is the result of classifying the outputs of a channel
271+ // force close transaction.
272+ type outputClassification struct {
273+ // ToRemoteIdx is the index of the to_remote output.
274+ ToRemoteIdx int
275+
276+ // ToRemoteAmt is the amount of the to_remote output.
277+ ToRemoteAmt int64
278+
279+ // ToRemotePkScript is the PkScript of the to_remote output.
280+ ToRemotePkScript []byte
281+
282+ // AnchorIdxs is the indices of the anchor outputs on the commitment
283+ // transaction.
284+ AnchorIdxs []int
285+
286+ // OtherIdxs is the indices of the other outputs on the commitment
287+ // transaction.
288+ OtherIdxs []int
289+ }
290+
291+ // classifyOutputs attempts to identify the to_remote output and classify the
292+ // other outputs into anchors and to_local/htlc.
293+ func classifyOutputs (s chanbackup.Single , tx * wire.MsgTx ) (outputClassification ,
294+ error ) {
295+
296+ // Best-effort get the remote key used for to_remote.
297+ remoteDesc := s .RemoteChanCfg .PaymentBasePoint
298+ remoteKey := remoteDesc .PubKey
299+
300+ // Compute the expected to_remote pkScript.
301+ var toRemotePkScript []byte
302+ if remoteKey != nil {
303+ chanType , err := chanTypeFromBackupVersion (s .Version )
304+ if err != nil {
305+ return outputClassification {}, fmt .Errorf ("failed to " +
306+ "get channel type: %w" , err )
307+ }
308+ desc , _ , err := lnwallet .CommitScriptToRemote (
309+ chanType , s .IsInitiator , remoteKey ,
310+ s .LeaseExpiry ,
311+ input .NoneTapLeaf (),
312+ )
313+ if err != nil {
314+ return outputClassification {}, fmt .Errorf ("failed to " +
315+ "get commit script to remote: %w" , err )
316+ }
317+ toRemotePkScript = desc .PkScript ()
318+ }
319+
320+ // anchorSats is anchor output value in sats.
321+ const anchorSats = 330
322+
323+ result := outputClassification {
324+ ToRemoteIdx : - 1 ,
325+ ToRemotePkScript : toRemotePkScript ,
326+ }
327+
328+ // First pass: find to_remote by script match.
329+ for idx , out := range tx .TxOut {
330+ if len (toRemotePkScript ) != 0 &&
331+ bytes .Equal (out .PkScript , toRemotePkScript ) {
332+
333+ result .ToRemoteIdx = idx
334+ result .ToRemoteAmt = out .Value
335+ break
336+ }
337+ }
338+
339+ // Second pass: classify anchors and the rest.
340+ for idx , out := range tx .TxOut {
341+ if idx == result .ToRemoteIdx {
342+ continue
343+ }
344+ if out .Value == anchorSats {
345+ result .AnchorIdxs = append (result .AnchorIdxs , idx )
346+ } else {
347+ result .OtherIdxs = append (result .OtherIdxs , idx )
348+ }
349+ }
350+
351+ return result , nil
352+ }
353+
354+ // chanTypeFromBackupVersion maps a backup SingleBackupVersion to an approximate
355+ // channeldb.ChannelType sufficient for deriving to_remote scripts.
356+ func chanTypeFromBackupVersion (v chanbackup.SingleBackupVersion ) (
357+ channeldb.ChannelType , error ) {
358+
359+ var chanType channeldb.ChannelType
360+ switch v {
361+ case chanbackup .DefaultSingleVersion :
362+ chanType = channeldb .SingleFunderBit
363+
364+ case chanbackup .TweaklessCommitVersion :
365+ chanType = channeldb .SingleFunderTweaklessBit
366+
367+ case chanbackup .AnchorsCommitVersion :
368+ chanType = channeldb .AnchorOutputsBit
369+ chanType |= channeldb .SingleFunderTweaklessBit
370+
371+ case chanbackup .AnchorsZeroFeeHtlcTxCommitVersion :
372+ chanType = channeldb .ZeroHtlcTxFeeBit
373+ chanType |= channeldb .AnchorOutputsBit
374+ chanType |= channeldb .SingleFunderTweaklessBit
375+
376+ case chanbackup .ScriptEnforcedLeaseVersion :
377+ chanType = channeldb .LeaseExpirationBit
378+ chanType |= channeldb .ZeroHtlcTxFeeBit
379+ chanType |= channeldb .AnchorOutputsBit
380+ chanType |= channeldb .SingleFunderTweaklessBit
381+
382+ case chanbackup .SimpleTaprootVersion :
383+ chanType = channeldb .ZeroHtlcTxFeeBit
384+ chanType |= channeldb .AnchorOutputsBit
385+ chanType |= channeldb .SingleFunderTweaklessBit
386+ chanType |= channeldb .SimpleTaprootFeatureBit
387+
388+ case chanbackup .TapscriptRootVersion :
389+ chanType = channeldb .ZeroHtlcTxFeeBit
390+ chanType |= channeldb .AnchorOutputsBit
391+ chanType |= channeldb .SingleFunderTweaklessBit
392+ chanType |= channeldb .SimpleTaprootFeatureBit
393+ chanType |= channeldb .TapscriptRootBit
394+
395+ default :
396+ return 0 , fmt .Errorf ("unknown Single version: %v" , v )
397+ }
398+
399+ return chanType , nil
400+ }
0 commit comments