@@ -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,165 @@ 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+
248+ if class .ToRemoteIdx >= 0 {
249+ log .Infof ("to_remote: idx=%d amount=%d sat" , class .ToRemoteIdx ,
250+ class .ToRemoteAmt )
251+
252+ if len (class .ToRemotePkScript ) != 0 {
253+ log .Infof ("to_remote PkScript (hex): %x" ,
254+ class .ToRemotePkScript )
255+ }
256+ } else {
257+ log .Infof ("to_remote: not identified" )
258+ }
259+
260+ for _ , idx := range class .AnchorIdxs {
261+ log .Infof ("possible anchor: idx=%d amount=%d sat" , idx ,
262+ tx .TxOut [idx ].Value )
263+ }
264+
265+ for _ , idx := range class .OtherIdxs {
266+ log .Infof ("possible to_local/htlc: idx=%d amount=%d sat" , idx ,
267+ tx .TxOut [idx ].Value )
268+ }
269+
270+ }
271+
272+ // outputClassification is the result of classifying the outputs of a channel
273+ // force close transaction.
274+ type outputClassification struct {
275+ // ToRemoteIdx is the index of the to_remote output.
276+ ToRemoteIdx int
277+
278+ // ToRemoteAmt is the amount of the to_remote output.
279+ ToRemoteAmt int64
280+
281+ // ToRemotePkScript is the PkScript of the to_remote output.
282+ ToRemotePkScript []byte
283+
284+ // AnchorIdxs is the indices of the anchor outputs on the commitment
285+ // transaction.
286+ AnchorIdxs []int
287+
288+ // OtherIdxs is the indices of the other outputs on the commitment
289+ // transaction.
290+ OtherIdxs []int
291+ }
292+
293+ // classifyOutputs attempts to identify the to_remote output and classify the
294+ // other outputs into anchors and to_local/htlc.
295+ func classifyOutputs (s chanbackup.Single , tx * wire.MsgTx ) (outputClassification ,
296+ error ) {
297+
298+ // Best-effort get the remote key used for to_remote.
299+ remoteDesc := s .RemoteChanCfg .PaymentBasePoint
300+ remoteKey := remoteDesc .PubKey
301+
302+ // Compute the expected to_remote pkScript.
303+ var toRemotePkScript []byte
304+ if remoteKey != nil {
305+ chanType , err := chanTypeFromBackupVersion (s .Version )
306+ if err != nil {
307+ return outputClassification {}, fmt .Errorf ("failed to " +
308+ "get channel type: %w" , err )
309+ }
310+ desc , _ , err := lnwallet .CommitScriptToRemote (
311+ chanType , s .IsInitiator , remoteKey ,
312+ s .LeaseExpiry ,
313+ input .NoneTapLeaf (),
314+ )
315+ if err != nil {
316+ return outputClassification {}, fmt .Errorf ("failed to " +
317+ "get commit script to remote: %w" , err )
318+ }
319+ toRemotePkScript = desc .PkScript ()
320+ }
321+
322+ // anchorSats is anchor output value in sats.
323+ const anchorSats = 330
324+
325+ result := outputClassification {
326+ ToRemoteIdx : - 1 ,
327+ ToRemotePkScript : toRemotePkScript ,
328+ }
329+
330+ // First pass: find to_remote by script match.
331+ for idx , out := range tx .TxOut {
332+ if len (toRemotePkScript ) != 0 &&
333+ bytes .Equal (out .PkScript , toRemotePkScript ) {
334+
335+ result .ToRemoteIdx = idx
336+ result .ToRemoteAmt = out .Value
337+ break
338+ }
339+ }
340+
341+ // Second pass: classify anchors and the rest.
342+ for idx , out := range tx .TxOut {
343+ if idx == result .ToRemoteIdx {
344+ continue
345+ }
346+ if out .Value == anchorSats {
347+ result .AnchorIdxs = append (result .AnchorIdxs , idx )
348+ } else {
349+ result .OtherIdxs = append (result .OtherIdxs , idx )
350+ }
351+ }
352+
353+ return result , nil
354+ }
355+
356+ // chanTypeFromBackupVersion maps a backup SingleBackupVersion to an approximate
357+ // channeldb.ChannelType sufficient for deriving to_remote scripts.
358+ func chanTypeFromBackupVersion (v chanbackup.SingleBackupVersion ) (
359+ channeldb.ChannelType , error ) {
360+
361+ var chanType channeldb.ChannelType
362+ switch v {
363+ case chanbackup .DefaultSingleVersion :
364+ chanType = channeldb .SingleFunderBit
365+
366+ case chanbackup .TweaklessCommitVersion :
367+ chanType = channeldb .SingleFunderTweaklessBit
368+
369+ case chanbackup .AnchorsCommitVersion :
370+ chanType = channeldb .AnchorOutputsBit
371+ chanType |= channeldb .SingleFunderTweaklessBit
372+
373+ case chanbackup .AnchorsZeroFeeHtlcTxCommitVersion :
374+ chanType = channeldb .ZeroHtlcTxFeeBit
375+ chanType |= channeldb .AnchorOutputsBit
376+ chanType |= channeldb .SingleFunderTweaklessBit
377+
378+ case chanbackup .ScriptEnforcedLeaseVersion :
379+ chanType = channeldb .LeaseExpirationBit
380+ chanType |= channeldb .ZeroHtlcTxFeeBit
381+ chanType |= channeldb .AnchorOutputsBit
382+ chanType |= channeldb .SingleFunderTweaklessBit
383+
384+ case chanbackup .SimpleTaprootVersion :
385+ chanType = channeldb .ZeroHtlcTxFeeBit
386+ chanType |= channeldb .AnchorOutputsBit
387+ chanType |= channeldb .SingleFunderTweaklessBit
388+ chanType |= channeldb .SimpleTaprootFeatureBit
389+
390+ case chanbackup .TapscriptRootVersion :
391+ chanType = channeldb .ZeroHtlcTxFeeBit
392+ chanType |= channeldb .AnchorOutputsBit
393+ chanType |= channeldb .SingleFunderTweaklessBit
394+ chanType |= channeldb .SimpleTaprootFeatureBit
395+ chanType |= channeldb .TapscriptRootBit
396+
397+ default :
398+ return 0 , fmt .Errorf ("unknown Single version: %v" , v )
399+ }
400+
401+ return chanType , nil
402+ }
0 commit comments