Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chantools scbforceclose: extract close tx from SCB and sign it #95

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ env:
# go needs absolute directories, using the $HOME variable doesn't work here.
GOCACHE: /home/runner/work/go/pkg/build
GOPATH: /home/runner/work/go
GO_VERSION: 1.22.3
GO_VERSION: 1.22.6

jobs:
########################
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ $ sudo mv chantools-*/chantools /usr/local/bin/

If there isn't a pre-built binary for your operating system or architecture
available or you want to build `chantools` from source for another reason, you
need to make sure you have `go 1.22.3` (or later) and `make` installed and can
need to make sure you have `go 1.22.6` (or later) and `make` installed and can
then run the following commands:

```bash
Expand Down
4 changes: 2 additions & 2 deletions cmd/chantools/dropchannelgraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func newChanAnnouncement(localPubKey, remotePubKey *btcec.PublicKey,
// The unconditional section of the announcement is the ShortChannelID
// itself which compactly encodes the location of the funding output
// within the blockchain.
chanAnn := &lnwire.ChannelAnnouncement{
chanAnn := &lnwire.ChannelAnnouncement1{
ShortChannelID: shortChanID,
Features: lnwire.NewRawFeatureVector(),
ChainHash: chainHash,
Expand Down Expand Up @@ -248,7 +248,7 @@ func newChanAnnouncement(localPubKey, remotePubKey *btcec.PublicKey,

// We announce the channel with the default values. Some of
// these values can later be changed by crafting a new ChannelUpdate.
chanUpdateAnn := &lnwire.ChannelUpdate{
chanUpdateAnn := &lnwire.ChannelUpdate1{
ShortChannelID: shortChanID,
ChainHash: chainHash,
Timestamp: uint32(time.Now().Unix()),
Expand Down
27 changes: 15 additions & 12 deletions cmd/chantools/forceclose.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ import (
"github.com/spf13/cobra"
)

const forceCloseWarning = `
If you are certain that a node is offline for good (AFTER you've tried SCB!)
and a channel is still open, you can use this method to force-close your
latest state that you have in your channel.db.

**!!! WARNING !!! DANGER !!! WARNING !!!**

If you do this and the state that you publish is *not* the latest state, then
the remote node *could* punish you by taking the whole channel amount *if* they
come online before you can sweep the funds from the time locked (144 - 2000
blocks) transaction *or* they have a watch tower looking out for them.

**This should absolutely be the last resort and you have been warned!**`

type forceCloseCommand struct {
APIURL string
ChannelDB string
Expand All @@ -35,18 +49,7 @@ func newForceCloseCommand() *cobra.Command {
Use: "forceclose",
Short: "Force-close the last state that is in the channel.db " +
"provided",
Long: `If you are certain that a node is offline for good (AFTER
you've tried SCB!) and a channel is still open, you can use this method to
force-close your latest state that you have in your channel.db.

**!!! WARNING !!! DANGER !!! WARNING !!!**

If you do this and the state that you publish is *not* the latest state, then
the remote node *could* punish you by taking the whole channel amount *if* they
come online before you can sweep the funds from the time locked (144 - 2000
blocks) transaction *or* they have a watch tower looking out for them.

**This should absolutely be the last resort and you have been warned!**`,
Long: forceCloseWarning,
Example: `chantools forceclose \
--fromsummary results/summary-xxxx-yyyy.json
--channeldb ~/.lnd/data/graph/mainnet/channel.db \
Expand Down
5 changes: 4 additions & 1 deletion cmd/chantools/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ func main() {
newFilterBackupCommand(),
newFixOldBackupCommand(),
newForceCloseCommand(),
newScbForceCloseCommand(),
newGenImportScriptCommand(),
newMigrateDBCommand(),
newPullAnchorCommand(),
Expand Down Expand Up @@ -310,7 +311,9 @@ func setupLogging() {
addSubLogger("CHDB", channeldb.UseLogger)
addSubLogger("BCKP", chanbackup.UseLogger)
addSubLogger("PEER", peer.UseLogger)
err := logWriter.InitLogRotator("./results/chantools.log", 10, 3)
err := logWriter.InitLogRotator(
"./results/chantools.log", build.Gzip, 10, 3,
)
if err != nil {
panic(err)
}
Expand Down
226 changes: 226 additions & 0 deletions cmd/chantools/scbforceclose.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package main

import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"os"
"strings"

"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightninglabs/chantools/scbforceclose"
"github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/input"
"github.com/spf13/cobra"
)

type scbForceCloseCommand struct {
APIURL string
Publish bool

// channel.backup.
SingleBackup string
SingleFile string
MultiBackup string
MultiFile string

rootKey *rootKey
cmd *cobra.Command
}

func newScbForceCloseCommand() *cobra.Command {
cc := &scbForceCloseCommand{}
cc.cmd = &cobra.Command{
Use: "scbforceclose",
Short: "Force-close the last state that is in the SCB " +
"provided",
Long: forceCloseWarning,
Example: `chantools scbforceclose --multi_file channel.backup`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
"be esplora compatible)",
)

cc.cmd.Flags().StringVar(
&cc.SingleBackup, "single_backup", "", "a hex encoded single "+
"channel backup obtained from exportchanbackup for "+
"force-closing channels",
)
cc.cmd.Flags().StringVar(
&cc.MultiBackup, "multi_backup", "", "a hex encoded "+
"multi-channel backup obtained from exportchanbackup "+
"for force-closing channels",
)
cc.cmd.Flags().StringVar(
&cc.SingleFile, "single_file", "", "the path to a "+
"single-channel backup file",
)
cc.cmd.Flags().StringVar(
&cc.MultiFile, "multi_file", "", "the path to a "+
"single-channel backup file (channel.backup)",
)

cc.cmd.Flags().BoolVar(
&cc.Publish, "publish", false, "publish force-closing TX to "+
"the chain API instead of just printing the TX",
)

cc.rootKey = newRootKey(cc.cmd, "decrypting the backup and signing tx")

return cc.cmd
}

func (c *scbForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
extendedKey, err := c.rootKey.read()
if err != nil {
return fmt.Errorf("error reading root key: %w", err)
}

api := &btc.ExplorerAPI{BaseURL: c.APIURL}

keyRing := &lnd.HDKeyRing{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}

signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
signer.MusigSessionManager = input.NewMusigSessionManager(
signer.FetchPrivateKey,
)

var backups []chanbackup.Single
if c.SingleBackup != "" || c.SingleFile != "" {
if c.SingleBackup != "" && c.SingleFile != "" {
return errors.New("must not pass --single_backup and " +
"--single_file together")
}
var singleBackupBytes []byte
if c.SingleBackup != "" {
singleBackupBytes, err = hex.DecodeString(
c.SingleBackup,
)
} else if c.SingleFile != "" {
singleBackupBytes, err = os.ReadFile(c.SingleFile)
}
if err != nil {
return fmt.Errorf("failed to get single backup: %w",
err)
}
var s chanbackup.Single
r := bytes.NewReader(singleBackupBytes)
if err := s.UnpackFromReader(r, keyRing); err != nil {
return fmt.Errorf("failed to unpack single backup: %w",
err)
}
backups = append(backups, s)
}
if c.MultiBackup != "" || c.MultiFile != "" {
if len(backups) != 0 {
return errors.New("must not pass single and multi " +
"backups together")
}
if c.MultiBackup != "" && c.MultiFile != "" {
return errors.New("must not pass --multi_backup and " +
"--multi_file together")
}
var multiBackupBytes []byte
if c.MultiBackup != "" {
multiBackupBytes, err = hex.DecodeString(c.MultiBackup)
} else if c.MultiFile != "" {
multiBackupBytes, err = os.ReadFile(c.MultiFile)
}
if err != nil {
return fmt.Errorf("failed to get multi backup: %w", err)
}
var m chanbackup.Multi
r := bytes.NewReader(multiBackupBytes)
if err := m.UnpackFromReader(r, keyRing); err != nil {
return fmt.Errorf("failed to unpack multi backup: %w",
err)
}
backups = append(backups, m.StaticBackups...)
}

backupsWithInputs := make([]chanbackup.Single, 0, len(backups))
for _, s := range backups {
if s.CloseTxInputs.IsSome() {
backupsWithInputs = append(backupsWithInputs, s)
}
}

fmt.Println()
fmt.Printf("Found %d channel backups, %d of them have close tx.\n",
len(backups), len(backupsWithInputs))

if len(backupsWithInputs) == 0 {
fmt.Println("No channel backups that can be used for force " +
"close.")
return nil
}

fmt.Println()
fmt.Println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
fmt.Println(strings.TrimSpace(forceCloseWarning))
fmt.Println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
fmt.Println()

fmt.Printf("Type YES to proceed: ")
var userInput string
if _, err := fmt.Scan(&userInput); err != nil {
return errors.New("failed to read user input")
}
if strings.TrimSpace(userInput) != "YES" {
return errors.New("canceled by user, must type uppercase 'YES'")
}

if c.Publish {
fmt.Println("Signed transactions will be broadcasted " +
"automatically.")
fmt.Printf("Type YES again to proceed: ")
if _, err := fmt.Scan(&userInput); err != nil {
return errors.New("failed to read user input")
}
if strings.TrimSpace(userInput) != "YES" {
return errors.New("canceled by user, must type " +
"uppercase 'YES'")
}
}

for _, s := range backupsWithInputs {
signedTx, err := scbforceclose.SignCloseTx(
s, keyRing, signer, signer,
)
if err != nil {
return fmt.Errorf("signCloseTx failed for %s: %w",
s.FundingOutpoint, err)
}
var buf bytes.Buffer
if err := signedTx.Serialize(&buf); err != nil {
return fmt.Errorf("failed to serialize signed %s: %w",
s.FundingOutpoint, err)
}
txHex := hex.EncodeToString(buf.Bytes())
fmt.Println("Channel point:", s.FundingOutpoint)
fmt.Println("Raw transaction hex:", txHex)
fmt.Println()

// Publish TX.
if c.Publish {
response, err := api.PublishTx(txHex)
if err != nil {
return err
}
log.Infof("Published TX %s, response: %s",
signedTx.TxHash(), response)
}
}

return nil
}
9 changes: 8 additions & 1 deletion cmd/chantools/zombierecovery_makeoffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import (
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/wallet"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet"
Expand Down Expand Up @@ -650,7 +652,12 @@ func matchScript(address string, key1, key2 *btcec.PublicKey,
pkScript, nil

case *btcutil.AddressTaproot:
pkScript, _, err := input.GenTaprootFundingScript(key1, key2, 0)
// FIXME: fill tapscriptRoot.
var tapscriptRoot fn.Option[chainhash.Hash]

pkScript, _, err := input.GenTaprootFundingScript(
key1, key2, 0, tapscriptRoot,
)
if err != nil {
return false, nil, nil, err
}
Expand Down
Loading
Loading