Skip to content

lnd: improve brontide mock, improve triggerforceclose, make zombierecovery makeoffer CLN compatible #194

New issue

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

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

Already on GitHub? # to your account

Merged
merged 9 commits into from
Jun 18, 2025
3 changes: 3 additions & 0 deletions btc/summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ func SummarizeChannels(api *ExplorerAPI, channels []*dataformat.SummaryEntry,
} else {
summaryFile.OpenChannels++
summaryFile.FundsOpenChannels += channel.LocalBalance
summaryFile.OpenChannelList = append(
summaryFile.OpenChannelList, channel,
)
channel.ClosingTX = nil
channel.HasPotential = true
}
Expand Down
49 changes: 28 additions & 21 deletions cln/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,36 +95,22 @@ func (s *Signer) FindMultisigKey(targetPubkey, peerPubKey *btcec.PublicKey,
return nil, errors.New("no matching pubkeys found")
}

func (s *Signer) AddPartialSignature(packet *psbt.Packet,
keyDesc keychain.KeyDescriptor, utxo *wire.TxOut, witnessScript []byte,
inputIndex int) error {
func (s *Signer) AddPartialSignatureWithDesc(packet *psbt.Packet,
signDesc *input.SignDescriptor) error {

// Now we add our partial signature.
prevOutFetcher := wallet.PsbtPrevOutputFetcher(packet)
signDesc := &input.SignDescriptor{
KeyDesc: keyDesc,
WitnessScript: witnessScript,
Output: utxo,
InputIndex: inputIndex,
HashType: txscript.SigHashAll,
PrevOutputFetcher: prevOutFetcher,
SigHashes: txscript.NewTxSigHashes(
packet.UnsignedTx, prevOutFetcher,
),
}
ourSigRaw, err := s.SignOutputRaw(packet.UnsignedTx, signDesc)
if err != nil {
return fmt.Errorf("error signing with our key: %w", err)
}
ourSig := append(ourSigRaw.Serialize(), byte(txscript.SigHashAll))
ourSig := append(ourSigRaw.Serialize(), byte(signDesc.HashType))

// Because of the way we derive keys in CLN, the public key in the key
// descriptor is the peer's public key, not our own. So we need to
// derive our own public key from the private key.
ourPrivKey, err := s.FetchPrivateKey(&keyDesc)
ourPrivKey, err := s.FetchPrivateKey(&signDesc.KeyDesc)
if err != nil {
return fmt.Errorf("error fetching private key for descriptor "+
"%v: %w", keyDesc, err)
"%v: %w", signDesc.KeyDesc, err)
}
ourPubKey := ourPrivKey.PubKey()

Expand All @@ -134,8 +120,8 @@ func (s *Signer) AddPartialSignature(packet *psbt.Packet,
return fmt.Errorf("error creating PSBT updater: %w", err)
}
status, err := updater.Sign(
inputIndex, ourSig, ourPubKey.SerializeCompressed(), nil,
witnessScript,
signDesc.InputIndex, ourSig, ourPubKey.SerializeCompressed(),
nil, signDesc.WitnessScript,
)
if err != nil {
return fmt.Errorf("error adding signature to PSBT: %w", err)
Expand All @@ -148,4 +134,25 @@ func (s *Signer) AddPartialSignature(packet *psbt.Packet,
return nil
}

func (s *Signer) AddPartialSignature(packet *psbt.Packet,
keyDesc keychain.KeyDescriptor, utxo *wire.TxOut, witnessScript []byte,
inputIndex int) error {

// Now we add our partial signature.
prevOutFetcher := wallet.PsbtPrevOutputFetcher(packet)
signDesc := &input.SignDescriptor{
KeyDesc: keyDesc,
WitnessScript: witnessScript,
Output: utxo,
InputIndex: inputIndex,
HashType: txscript.SigHashAll,
PrevOutputFetcher: prevOutFetcher,
SigHashes: txscript.NewTxSigHashes(
packet.UnsignedTx, prevOutFetcher,
),
}

return s.AddPartialSignatureWithDesc(packet, signDesc)
}

var _ lnd.ChannelSigner = (*Signer)(nil)
13 changes: 12 additions & 1 deletion cmd/chantools/sweepremoteclosed.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,10 @@ func findTargetsCln(hsmSecret [32]byte, pubKeys []*btcec.PublicKey,
targets []*targetAddr
api = newExplorerAPI(apiURL)
)
for _, pubKey := range pubKeys {
for idx, pubKey := range pubKeys {
log.Infof("Trying to find targets for pubkey %x (%d of %d)",
pubKey.SerializeCompressed(), idx+1, len(pubKeys))

for index := range recoveryWindow {
desc := &keychain.KeyDescriptor{
PubKey: pubKey,
Expand All @@ -370,6 +373,14 @@ func findTargetsCln(hsmSecret [32]byte, pubKeys []*btcec.PublicKey,
"for addresses with funds: %w", err)
}
targets = append(targets, foundTargets...)

if idx > 0 && idx%200 == 0 {
log.Infof("Tried %d addresses for pubkey "+
"%x (%d of %d), found %d targets so "+
"far", index+1,
pubKey.SerializeCompressed(), idx+1,
len(pubKeys), len(targets))
}
}
}

Expand Down
75 changes: 63 additions & 12 deletions cmd/chantools/triggerforceclose.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,19 +169,33 @@ func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
pubKeys []string
outputs []string
)
for _, openChan := range channels {
for idx, openChan := range channels {
addr := pickAddr(openChan.Node2Info.Node.Addresses)
peerAddr := fmt.Sprintf("%s@%s", openChan.Node2, addr)

if c.TorProxy == "" &&
strings.Contains(addr, ".onion") {

log.Infof("Skipping channel %s with peer %s "+
"because it is a Tor address and no "+
"Tor proxy is configured",
openChan.ChanPoint, peerAddr)
continue
}

log.Infof("Attempting to force close channel %s with "+
"peer %s", openChan.ChanPoint, peerAddr)
"peer %s (channel %d of %d)",
openChan.ChanPoint, peerAddr, idx+1,
len(channels))

outputAddrs, err := closeChannel(
identityPriv, api, openChan.ChanPoint,
peerAddr, c.TorProxy,
)
if err != nil {
log.Errorf("Error closing channel %s, "+
"skipping: %v", openChan.ChanPoint, err)
"skipping and trying next one. "+
"Reason: %v", openChan.ChanPoint, err)
continue
}

Expand Down Expand Up @@ -220,7 +234,7 @@ func pickAddr(addrs []*gqAddress) string {

// We'll pick the first address that is not a Tor address.
for _, addr := range addrs {
if !strings.HasSuffix(addr.Address, ".onion") {
if !strings.Contains(addr.Address, ".onion") {
return addr.Address
}
}
Expand Down Expand Up @@ -262,13 +276,21 @@ func closeChannel(identityPriv *btcec.PrivateKey, api *btc.ExplorerAPI,
if err != nil {
return nil, fmt.Errorf("error getting spends: %w", err)
}

counter := 0
for len(spends) == 0 {
log.Infof("No spends found yet, waiting 5 seconds...")
time.Sleep(5 * time.Second)
spends, err = api.Spends(channelAddress)
if err != nil {
return nil, fmt.Errorf("error getting spends: %w", err)
}

counter++
if counter >= 12 {
return nil, errors.New("no spends found after 60 " +
"seconds, aborting re-try loop")
}
}

log.Infof("Found force close transaction %v", spends[0].TXID)
Expand All @@ -289,7 +311,11 @@ func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress,
}

func connectPeer(peerHost, torProxy string, identity keychain.SingleKeyECDH,
dialTimeout time.Duration) (*peer.Brontide, error) {
dialTimeout time.Duration) (*peer.Brontide, func() error, error) {

cleanup := func() error {
return nil
}

var dialNet tor.Net = &tor.ClearNet{}
if torProxy != "" {
Expand All @@ -306,7 +332,8 @@ func connectPeer(peerHost, torProxy string, identity keychain.SingleKeyECDH,
peerHost, "9735", dialNet.ResolveTCPAddr,
)
if err != nil {
return nil, fmt.Errorf("error parsing peer address: %w", err)
return nil, cleanup, fmt.Errorf("error parsing peer address: "+
"%w", err)
}

peerPubKey := peerAddr.IdentityKey
Expand All @@ -315,7 +342,11 @@ func connectPeer(peerHost, torProxy string, identity keychain.SingleKeyECDH,
peerAddr.String())
conn, err := noiseDial(identity, peerAddr, dialNet, dialTimeout)
if err != nil {
return nil, fmt.Errorf("error dialing peer: %w", err)
return nil, cleanup, fmt.Errorf("error dialing peer: %w", err)
}

cleanup = func() error {
return conn.Close()
}

log.Infof("Attempting to establish p2p connection to peer %x, dial"+
Expand All @@ -324,9 +355,20 @@ func connectPeer(peerHost, torProxy string, identity keychain.SingleKeyECDH,
Addr: peerAddr,
Permanent: false,
}
p, err := lnd.ConnectPeer(conn, req, chainParams, identity)
p, channelDB, err := lnd.ConnectPeer(conn, req, chainParams, identity)
if err != nil {
return nil, fmt.Errorf("error connecting to peer: %w", err)
return nil, cleanup, fmt.Errorf("error connecting to peer: %w",
err)
}

cleanup = func() error {
p.Disconnect(errors.New("done with peer"))
if channelDB != nil {
if err := channelDB.Close(); err != nil {
log.Errorf("Error closing channel DB: %v", err)
}
}
return conn.Close()
}

log.Infof("Connection established to peer %x",
Expand All @@ -336,17 +378,23 @@ func connectPeer(peerHost, torProxy string, identity keychain.SingleKeyECDH,
select {
case <-p.ActiveSignal():
case <-p.QuitSignal():
return nil, fmt.Errorf("peer %x disconnected",
return nil, cleanup, fmt.Errorf("peer %x disconnected",
peerPubKey.SerializeCompressed())
}

return p, nil
return p, cleanup, nil
}

func requestForceClose(peerHost, torProxy string, channelPoint wire.OutPoint,
identity keychain.SingleKeyECDH) error {

p, err := connectPeer(peerHost, torProxy, identity, dialTimeout)
p, cleanup, err := connectPeer(
peerHost, torProxy, identity, dialTimeout,
)
defer func() {
_ = cleanup()
}()

if err != nil {
return fmt.Errorf("error connecting to peer: %w", err)
}
Expand Down Expand Up @@ -383,6 +431,9 @@ func requestForceClose(peerHost, torProxy string, channelPoint wire.OutPoint,
return fmt.Errorf("error sending message: %w", err)
}

// Wait a few seconds to give the peer time to process the message.
time.Sleep(5 * time.Second)

return nil
}

Expand Down
Loading