Skip to content

Commit 75030dc

Browse files
committed
triggerforceclose: add --all_public_channels option
1 parent eb1f07b commit 75030dc

File tree

2 files changed

+140
-21
lines changed

2 files changed

+140
-21
lines changed

cmd/chantools/triggerforceclose.go

Lines changed: 131 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package main
22

33
import (
44
"encoding/hex"
5+
"errors"
56
"fmt"
7+
"os"
68
"strconv"
79
"strings"
810
"time"
@@ -11,9 +13,12 @@ import (
1113
"github.com/btcsuite/btcd/chaincfg/chainhash"
1214
"github.com/btcsuite/btcd/connmgr"
1315
"github.com/btcsuite/btcd/wire"
16+
"github.com/hasura/go-graphql-client"
17+
"github.com/lightninglabs/chantools/btc"
1418
"github.com/lightninglabs/chantools/cln"
1519
"github.com/lightninglabs/chantools/lnd"
1620
"github.com/lightningnetwork/lnd/brontide"
21+
"github.com/lightningnetwork/lnd/fn/v2"
1722
"github.com/lightningnetwork/lnd/keychain"
1823
"github.com/lightningnetwork/lnd/lncfg"
1924
"github.com/lightningnetwork/lnd/lnwire"
@@ -32,7 +37,8 @@ type triggerForceCloseCommand struct {
3237
Peer string
3338
ChannelPoint string
3439

35-
APIURL string
40+
APIURL string
41+
AllPublicChannels bool
3642

3743
TorProxy string
3844

@@ -71,6 +77,11 @@ does not properly respond to a Data Loss Protection re-establish message).'`,
7177
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
7278
"be esplora compatible)",
7379
)
80+
cc.cmd.Flags().BoolVar(
81+
&cc.AllPublicChannels, "all_public_channels", false,
82+
"query all public channels from the Amboss API and attempt "+
83+
"to trigger a force close for each of them",
84+
)
7485
cc.cmd.Flags().StringVar(
7586
&cc.TorProxy, "torproxy", "", "SOCKS5 proxy to use for Tor "+
7687
"connections (to .onion addresses)",
@@ -125,50 +136,150 @@ func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
125136
}
126137
}
127138

139+
api := newExplorerAPI(c.APIURL)
140+
switch {
141+
case c.ChannelPoint != "" && c.Peer != "":
142+
_, err := closeChannel(
143+
identityPriv, api, c.ChannelPoint, c.Peer, c.TorProxy,
144+
)
145+
return err
146+
147+
case c.AllPublicChannels:
148+
client := graphql.NewClient(
149+
"https://api.amboss.space/graphql", nil,
150+
)
151+
ourNodeKey := hex.EncodeToString(
152+
identityPriv.PubKey().SerializeCompressed(),
153+
)
154+
155+
log.Infof("Fetching public channels for node %s", ourNodeKey)
156+
channels, err := fetchChannels(client, ourNodeKey)
157+
if err != nil {
158+
return fmt.Errorf("error fetching channels: %w", err)
159+
}
160+
161+
channels = fn.Filter(channels, func(c *gqChannel) bool {
162+
return c.ClosureInfo.ClosedHeight == 0
163+
})
164+
165+
log.Infof("Found %d public open channels, attempting to force "+
166+
"close each of them", len(channels))
167+
168+
var (
169+
pubKeys []string
170+
outputs []string
171+
)
172+
for _, openChan := range channels {
173+
addr := pickAddr(openChan.Node2Info.Node.Addresses)
174+
peerAddr := fmt.Sprintf("%s@%s", openChan.Node2, addr)
175+
log.Infof("Attempting to force close channel %s with "+
176+
"peer %s", openChan.ChanPoint, peerAddr)
177+
178+
outputAddrs, err := closeChannel(
179+
identityPriv, api, openChan.ChanPoint,
180+
peerAddr, c.TorProxy,
181+
)
182+
if err != nil {
183+
log.Errorf("Error closing channel %s, "+
184+
"skipping: %v", openChan.ChanPoint, err)
185+
continue
186+
}
187+
188+
pubKeys = append(pubKeys, openChan.Node2)
189+
outputs = append(outputs, outputAddrs...)
190+
}
191+
192+
peersBytes := []byte(strings.Join(pubKeys, "\n"))
193+
outputsBytes := []byte(strings.Join(outputs, "\n"))
194+
195+
fileName := fmt.Sprintf("results/forceclose-peers-%s.txt",
196+
time.Now().Format("2006-01-02"))
197+
log.Infof("Writing peers to %s", fileName)
198+
err = os.WriteFile(fileName, peersBytes, 0644)
199+
if err != nil {
200+
return fmt.Errorf("error writing peers to file: %w",
201+
err)
202+
}
203+
204+
fileName = fmt.Sprintf("results/forceclose-addresses-%s.txt",
205+
time.Now().Format("2006-01-02"))
206+
log.Infof("Writing addresses to %s", fileName)
207+
return os.WriteFile(fileName, outputsBytes, 0644)
208+
209+
default:
210+
return errors.New("either --channel_point and --peer or " +
211+
"--all_public_channels must be specified")
212+
}
213+
}
214+
215+
func pickAddr(addrs []*gqAddress) string {
216+
// If there's only one address, we'll just return that one.
217+
if len(addrs) == 1 {
218+
return addrs[0].Address
219+
}
220+
221+
// We'll pick the first address that is not a Tor address.
222+
for _, addr := range addrs {
223+
if !strings.HasSuffix(addr.Address, ".onion") {
224+
return addr.Address
225+
}
226+
}
227+
228+
// If all addresses are Tor addresses, we'll just return the first one.
229+
if len(addrs) > 0 {
230+
return addrs[0].Address
231+
}
232+
233+
return ""
234+
}
235+
236+
func closeChannel(identityPriv *btcec.PrivateKey, api *btc.ExplorerAPI,
237+
channelPoint, peer, torProxy string) ([]string, error) {
238+
128239
identityECDH := &keychain.PrivKeyECDH{
129240
PrivKey: identityPriv,
130241
}
131242

132-
outPoint, err := parseOutPoint(c.ChannelPoint)
243+
outPoint, err := parseOutPoint(channelPoint)
133244
if err != nil {
134-
return fmt.Errorf("error parsing channel point: %w", err)
245+
return nil, fmt.Errorf("error parsing channel point: %w", err)
135246
}
136247

137-
err = requestForceClose(
138-
c.Peer, c.TorProxy, identityPriv.PubKey(), *outPoint,
139-
identityECDH,
140-
)
248+
err = requestForceClose(peer, torProxy, *outPoint, identityECDH)
141249
if err != nil {
142-
return fmt.Errorf("error requesting force close: %w", err)
250+
return nil, fmt.Errorf("error requesting force close: %w", err)
143251
}
144252

145253
log.Infof("Message sent, waiting for force close transaction to " +
146254
"appear in mempool")
147255

148-
api := newExplorerAPI(c.APIURL)
149-
channelAddress, err := api.Address(c.ChannelPoint)
256+
channelAddress, err := api.Address(channelPoint)
150257
if err != nil {
151-
return fmt.Errorf("error getting channel address: %w", err)
258+
return nil, fmt.Errorf("error getting channel address: %w", err)
152259
}
153260

154261
spends, err := api.Spends(channelAddress)
155262
if err != nil {
156-
return fmt.Errorf("error getting spends: %w", err)
263+
return nil, fmt.Errorf("error getting spends: %w", err)
157264
}
158265
for len(spends) == 0 {
159266
log.Infof("No spends found yet, waiting 5 seconds...")
160267
time.Sleep(5 * time.Second)
161268
spends, err = api.Spends(channelAddress)
162269
if err != nil {
163-
return fmt.Errorf("error getting spends: %w", err)
270+
return nil, fmt.Errorf("error getting spends: %w", err)
164271
}
165272
}
166273

167274
log.Infof("Found force close transaction %v", spends[0].TXID)
168275
log.Infof("You can now use the sweepremoteclosed command to sweep " +
169276
"the funds from the channel")
170277

171-
return nil
278+
outputAddrs := fn.Map(spends[0].Vout, func(v *btc.Vout) string {
279+
return v.ScriptPubkeyAddr
280+
})
281+
282+
return outputAddrs, nil
172283
}
173284

174285
func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress,
@@ -177,8 +288,7 @@ func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress,
177288
return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial)
178289
}
179290

180-
func connectPeer(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
181-
identity keychain.SingleKeyECDH,
291+
func connectPeer(peerHost, torProxy string, identity keychain.SingleKeyECDH,
182292
dialTimeout time.Duration) (*peer.Brontide, error) {
183293

184294
var dialNet tor.Net = &tor.ClearNet{}
@@ -199,6 +309,8 @@ func connectPeer(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
199309
return nil, fmt.Errorf("error parsing peer address: %w", err)
200310
}
201311

312+
peerPubKey := peerAddr.IdentityKey
313+
202314
log.Debugf("Attempting to dial resolved peer address %v",
203315
peerAddr.String())
204316
conn, err := noiseDial(identity, peerAddr, dialNet, dialTimeout)
@@ -231,12 +343,10 @@ func connectPeer(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
231343
return p, nil
232344
}
233345

234-
func requestForceClose(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
235-
channelPoint wire.OutPoint, identity keychain.SingleKeyECDH) error {
346+
func requestForceClose(peerHost, torProxy string, channelPoint wire.OutPoint,
347+
identity keychain.SingleKeyECDH) error {
236348

237-
p, err := connectPeer(
238-
peerHost, torProxy, peerPubKey, identity, dialTimeout,
239-
)
349+
p, err := connectPeer(peerHost, torProxy, identity, dialTimeout)
240350
if err != nil {
241351
return fmt.Errorf("error connecting to peer: %w", err)
242352
}

cmd/chantools/zombierecovery_findmatches.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ P.S.: If you don't want to be notified about future matches, please let me know.
6262
`
6363
)
6464

65+
type gqAddress struct {
66+
Address string `graphql:"addr"`
67+
}
68+
6569
type gqChannel struct {
6670
ChanPoint string `graphql:"chan_point"`
6771
Capacity string `graphql:"capacity"`
@@ -70,6 +74,11 @@ type gqChannel struct {
7074
} `graphql:"closure_info"`
7175
Node1 string `graphql:"node1_pub"`
7276
Node2 string `graphql:"node2_pub"`
77+
Node2Info struct {
78+
Node struct {
79+
Addresses []*gqAddress `graphql:"addresses"`
80+
} `graphql:"node"`
81+
} `graphql:"node2_info"`
7382
ChannelID string `graphql:"long_channel_id"`
7483
}
7584

0 commit comments

Comments
 (0)