|  | 
|  | 1 | +package main | 
|  | 2 | + | 
|  | 3 | +import ( | 
|  | 4 | +	"fmt" | 
|  | 5 | +	"net" | 
|  | 6 | +	"strconv" | 
|  | 7 | +	"strings" | 
|  | 8 | +	"time" | 
|  | 9 | + | 
|  | 10 | +	"github.com/btcsuite/btcd/chaincfg/chainhash" | 
|  | 11 | +	"github.com/btcsuite/btcd/connmgr" | 
|  | 12 | +	"github.com/btcsuite/btcd/wire" | 
|  | 13 | +	"github.com/guggero/chantools/btc" | 
|  | 14 | +	"github.com/guggero/chantools/lnd" | 
|  | 15 | +	"github.com/lightningnetwork/lnd/brontide" | 
|  | 16 | +	"github.com/lightningnetwork/lnd/keychain" | 
|  | 17 | +	"github.com/lightningnetwork/lnd/lncfg" | 
|  | 18 | +	"github.com/lightningnetwork/lnd/lnwire" | 
|  | 19 | +	"github.com/lightningnetwork/lnd/tor" | 
|  | 20 | +	"github.com/spf13/cobra" | 
|  | 21 | +) | 
|  | 22 | + | 
|  | 23 | +var ( | 
|  | 24 | +	dialTimeout = time.Minute | 
|  | 25 | +) | 
|  | 26 | + | 
|  | 27 | +type triggerForceCloseCommand struct { | 
|  | 28 | +	Peer         string | 
|  | 29 | +	ChannelPoint string | 
|  | 30 | + | 
|  | 31 | +	APIURL string | 
|  | 32 | + | 
|  | 33 | +	rootKey *rootKey | 
|  | 34 | +	cmd     *cobra.Command | 
|  | 35 | +} | 
|  | 36 | + | 
|  | 37 | +func newTriggerForceCloseCommand() *cobra.Command { | 
|  | 38 | +	cc := &triggerForceCloseCommand{} | 
|  | 39 | +	cc.cmd = &cobra.Command{ | 
|  | 40 | +		Use: "triggerforceclose", | 
|  | 41 | +		Short: "Connect to a peer and send a custom message to " + | 
|  | 42 | +			"trigger a force close of the specified channel", | 
|  | 43 | +		Example: `chantools triggerforceclose \ | 
|  | 44 | + | 
|  | 45 | +	--channel_point abcdef01234...:x`, | 
|  | 46 | +		RunE: cc.Execute, | 
|  | 47 | +	} | 
|  | 48 | +	cc.cmd.Flags().StringVar( | 
|  | 49 | +		&cc.Peer, "peer", "", "remote peer address "+ | 
|  | 50 | +			"(<pubkey>@<host>[:<port>])", | 
|  | 51 | +	) | 
|  | 52 | +	cc.cmd.Flags().StringVar( | 
|  | 53 | +		&cc.ChannelPoint, "channel_point", "", "funding transaction "+ | 
|  | 54 | +			"outpoint of the channel to trigger the force close "+ | 
|  | 55 | +			"of (<txid>:<txindex>)", | 
|  | 56 | +	) | 
|  | 57 | +	cc.cmd.Flags().StringVar( | 
|  | 58 | +		&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+ | 
|  | 59 | +			"be esplora compatible)", | 
|  | 60 | +	) | 
|  | 61 | +	cc.rootKey = newRootKey(cc.cmd, "deriving the identity key") | 
|  | 62 | + | 
|  | 63 | +	return cc.cmd | 
|  | 64 | +} | 
|  | 65 | + | 
|  | 66 | +func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error { | 
|  | 67 | +	extendedKey, err := c.rootKey.read() | 
|  | 68 | +	if err != nil { | 
|  | 69 | +		return fmt.Errorf("error reading root key: %w", err) | 
|  | 70 | +	} | 
|  | 71 | + | 
|  | 72 | +	identityPath := lnd.IdentityPath(chainParams) | 
|  | 73 | +	child, pubKey, _, err := lnd.DeriveKey( | 
|  | 74 | +		extendedKey, identityPath, chainParams, | 
|  | 75 | +	) | 
|  | 76 | +	if err != nil { | 
|  | 77 | +		return fmt.Errorf("could not derive identity key: %w", err) | 
|  | 78 | +	} | 
|  | 79 | +	identityPriv, err := child.ECPrivKey() | 
|  | 80 | +	if err != nil { | 
|  | 81 | +		return fmt.Errorf("could not get identity private key: %w", err) | 
|  | 82 | +	} | 
|  | 83 | +	identityECDH := &keychain.PrivKeyECDH{ | 
|  | 84 | +		PrivKey: identityPriv, | 
|  | 85 | +	} | 
|  | 86 | + | 
|  | 87 | +	peerAddr, err := lncfg.ParseLNAddressString( | 
|  | 88 | +		c.Peer, "9735", net.ResolveTCPAddr, | 
|  | 89 | +	) | 
|  | 90 | +	if err != nil { | 
|  | 91 | +		return fmt.Errorf("error parsing peer address: %w", err) | 
|  | 92 | +	} | 
|  | 93 | + | 
|  | 94 | +	outPoint, err := parseOutPoint(c.ChannelPoint) | 
|  | 95 | +	if err != nil { | 
|  | 96 | +		return fmt.Errorf("error parsing channel point: %w", err) | 
|  | 97 | +	} | 
|  | 98 | +	channelID := lnwire.NewChanIDFromOutPoint(outPoint) | 
|  | 99 | + | 
|  | 100 | +	conn, err := noiseDial( | 
|  | 101 | +		identityECDH, peerAddr, &tor.ClearNet{}, dialTimeout, | 
|  | 102 | +	) | 
|  | 103 | +	if err != nil { | 
|  | 104 | +		return fmt.Errorf("error dialing peer: %w", err) | 
|  | 105 | +	} | 
|  | 106 | + | 
|  | 107 | +	log.Infof("Attempting to connect to peer %x, dial timeout is %v", | 
|  | 108 | +		pubKey.SerializeCompressed(), dialTimeout) | 
|  | 109 | +	req := &connmgr.ConnReq{ | 
|  | 110 | +		Addr:      peerAddr, | 
|  | 111 | +		Permanent: false, | 
|  | 112 | +	} | 
|  | 113 | +	p, err := lnd.ConnectPeer(conn, req, chainParams, identityECDH) | 
|  | 114 | +	if err != nil { | 
|  | 115 | +		return fmt.Errorf("error connecting to peer: %w", err) | 
|  | 116 | +	} | 
|  | 117 | + | 
|  | 118 | +	log.Infof("Connection established to peer %x", | 
|  | 119 | +		pubKey.SerializeCompressed()) | 
|  | 120 | + | 
|  | 121 | +	// We'll wait until the peer is active. | 
|  | 122 | +	select { | 
|  | 123 | +	case <-p.ActiveSignal(): | 
|  | 124 | +	case <-p.QuitSignal(): | 
|  | 125 | +		return fmt.Errorf("peer %x disconnected", | 
|  | 126 | +			pubKey.SerializeCompressed()) | 
|  | 127 | +	} | 
|  | 128 | + | 
|  | 129 | +	// Channel ID (32 byte) + u16 for the data length (which will be 0). | 
|  | 130 | +	data := make([]byte, 34) | 
|  | 131 | +	copy(data[:32], channelID[:]) | 
|  | 132 | + | 
|  | 133 | +	log.Infof("Sending channel error message to peer to trigger force "+ | 
|  | 134 | +		"close of channel %v", c.ChannelPoint) | 
|  | 135 | + | 
|  | 136 | +	_ = lnwire.SetCustomOverrides([]uint16{lnwire.MsgError}) | 
|  | 137 | +	msg, err := lnwire.NewCustom(lnwire.MsgError, data) | 
|  | 138 | +	if err != nil { | 
|  | 139 | +		return err | 
|  | 140 | +	} | 
|  | 141 | + | 
|  | 142 | +	err = p.SendMessageLazy(true, msg) | 
|  | 143 | +	if err != nil { | 
|  | 144 | +		return fmt.Errorf("error sending message: %w", err) | 
|  | 145 | +	} | 
|  | 146 | + | 
|  | 147 | +	log.Infof("Message sent, waiting for force close transaction to " + | 
|  | 148 | +		"appear in mempool") | 
|  | 149 | + | 
|  | 150 | +	api := &btc.ExplorerAPI{BaseURL: c.APIURL} | 
|  | 151 | +	channelAddress, err := api.Address(c.ChannelPoint) | 
|  | 152 | +	if err != nil { | 
|  | 153 | +		return fmt.Errorf("error getting channel address: %w", err) | 
|  | 154 | +	} | 
|  | 155 | + | 
|  | 156 | +	spends, err := api.Spends(channelAddress) | 
|  | 157 | +	if err != nil { | 
|  | 158 | +		return fmt.Errorf("error getting spends: %w", err) | 
|  | 159 | +	} | 
|  | 160 | +	for len(spends) == 0 { | 
|  | 161 | +		log.Infof("No spends found yet, waiting 5 seconds...") | 
|  | 162 | +		time.Sleep(5 * time.Second) | 
|  | 163 | +		spends, err = api.Spends(channelAddress) | 
|  | 164 | +		if err != nil { | 
|  | 165 | +			return fmt.Errorf("error getting spends: %w", err) | 
|  | 166 | +		} | 
|  | 167 | +	} | 
|  | 168 | + | 
|  | 169 | +	log.Infof("Found force close transaction %v", spends[0].TXID) | 
|  | 170 | +	log.Infof("You can now use the sweepremoteclosed command to sweep " + | 
|  | 171 | +		"the funds from the channel") | 
|  | 172 | + | 
|  | 173 | +	return nil | 
|  | 174 | +} | 
|  | 175 | + | 
|  | 176 | +func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress, | 
|  | 177 | +	netCfg tor.Net, timeout time.Duration) (*brontide.Conn, error) { | 
|  | 178 | + | 
|  | 179 | +	return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial) | 
|  | 180 | +} | 
|  | 181 | + | 
|  | 182 | +func parseOutPoint(s string) (*wire.OutPoint, error) { | 
|  | 183 | +	split := strings.Split(s, ":") | 
|  | 184 | +	if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 { | 
|  | 185 | +		return nil, fmt.Errorf("invalid channel point format: %v", s) | 
|  | 186 | +	} | 
|  | 187 | + | 
|  | 188 | +	index, err := strconv.ParseInt(split[1], 10, 64) | 
|  | 189 | +	if err != nil { | 
|  | 190 | +		return nil, fmt.Errorf("unable to decode output index: %v", err) | 
|  | 191 | +	} | 
|  | 192 | + | 
|  | 193 | +	txid, err := chainhash.NewHashFromStr(split[0]) | 
|  | 194 | +	if err != nil { | 
|  | 195 | +		return nil, fmt.Errorf("unable to parse hex string: %v", err) | 
|  | 196 | +	} | 
|  | 197 | + | 
|  | 198 | +	return &wire.OutPoint{ | 
|  | 199 | +		Hash:  *txid, | 
|  | 200 | +		Index: uint32(index), | 
|  | 201 | +	}, nil | 
|  | 202 | +} | 
0 commit comments