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