mirror of
https://github.com/guggero/chantools.git
synced 2026-08-13 12:33:34 +02:00
Merge pull request #194 from lightninglabs/triggerforceclose-lnd-19
lnd: improve brontide mock, improve triggerforceclose, make zombierecovery makeoffer CLN compatible
This commit is contained in:
commit
f794333c0b
12 changed files with 404 additions and 118 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,6 +95,45 @@ func (s *Signer) FindMultisigKey(targetPubkey, peerPubKey *btcec.PublicKey,
|
|||
return nil, errors.New("no matching pubkeys found")
|
||||
}
|
||||
|
||||
func (s *Signer) AddPartialSignatureWithDesc(packet *psbt.Packet,
|
||||
signDesc *input.SignDescriptor) error {
|
||||
|
||||
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(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(&signDesc.KeyDesc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching private key for descriptor "+
|
||||
"%v: %w", signDesc.KeyDesc, err)
|
||||
}
|
||||
ourPubKey := ourPrivKey.PubKey()
|
||||
|
||||
// Great, we were able to create our sig, let's add it to the PSBT.
|
||||
updater, err := psbt.NewUpdater(packet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating PSBT updater: %w", err)
|
||||
}
|
||||
status, err := updater.Sign(
|
||||
signDesc.InputIndex, ourSig, ourPubKey.SerializeCompressed(),
|
||||
nil, signDesc.WitnessScript,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding signature to PSBT: %w", err)
|
||||
}
|
||||
if status != 0 {
|
||||
return fmt.Errorf("unexpected status for signature update, "+
|
||||
"got %d wanted 0", status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Signer) AddPartialSignature(packet *psbt.Packet,
|
||||
keyDesc keychain.KeyDescriptor, utxo *wire.TxOut, witnessScript []byte,
|
||||
inputIndex int) error {
|
||||
|
|
@ -112,40 +151,8 @@ func (s *Signer) AddPartialSignature(packet *psbt.Packet,
|
|||
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))
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching private key for descriptor "+
|
||||
"%v: %w", keyDesc, err)
|
||||
}
|
||||
ourPubKey := ourPrivKey.PubKey()
|
||||
|
||||
// Great, we were able to create our sig, let's add it to the PSBT.
|
||||
updater, err := psbt.NewUpdater(packet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating PSBT updater: %w", err)
|
||||
}
|
||||
status, err := updater.Sign(
|
||||
inputIndex, ourSig, ourPubKey.SerializeCompressed(), nil,
|
||||
witnessScript,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding signature to PSBT: %w", err)
|
||||
}
|
||||
if status != 0 {
|
||||
return fmt.Errorf("unexpected status for signature update, "+
|
||||
"got %d wanted 0", status)
|
||||
}
|
||||
|
||||
return nil
|
||||
return s.AddPartialSignatureWithDesc(packet, signDesc)
|
||||
}
|
||||
|
||||
var _ lnd.ChannelSigner = (*Signer)(nil)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -169,11 +169,24 @@ 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,
|
||||
|
|
@ -181,7 +194,8 @@ func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
|
|||
)
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -262,6 +276,8 @@ 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)
|
||||
|
|
@ -269,6 +285,12 @@ func closeChannel(identityPriv *btcec.PrivateKey, api *btc.ExplorerAPI,
|
|||
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)
|
||||
|
|
@ -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 != "" {
|
||||
|
|
@ -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
|
||||
|
|
@ -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"+
|
||||
|
|
@ -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",
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcwallet/wallet"
|
||||
"github.com/lightninglabs/chantools/cln"
|
||||
"github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/lightningnetwork/lnd/fn/v2"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
|
|
@ -40,6 +41,8 @@ type zombieRecoveryMakeOfferCommand struct {
|
|||
|
||||
MatchOnly bool
|
||||
|
||||
HsmSecret string
|
||||
|
||||
rootKey *rootKey
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
|
@ -80,6 +83,12 @@ a counter offer.`,
|
|||
&cc.MatchOnly, "matchonly", false, "only match the keys, "+
|
||||
"don't create an offer",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.HsmSecret, "hsm_secret", "", "the hex encoded HSM secret "+
|
||||
"to use for deriving the multisig keys for a CLN "+
|
||||
"node; obtain by running 'xxd -p -c32 "+
|
||||
"~/.lightning/bitcoin/hsm_secret'",
|
||||
)
|
||||
|
||||
cc.rootKey = newRootKey(cc.cmd, "signing the offer")
|
||||
|
||||
|
|
@ -89,11 +98,6 @@ a counter offer.`,
|
|||
func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
|
||||
_ []string) error {
|
||||
|
||||
extendedKey, err := c.rootKey.read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %w", err)
|
||||
}
|
||||
|
||||
if c.FeeRate == 0 {
|
||||
c.FeeRate = defaultFeeSatPerVByte
|
||||
}
|
||||
|
|
@ -183,20 +187,72 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
|
|||
}
|
||||
}
|
||||
|
||||
// Make sure one of the nodes is ours.
|
||||
_, pubKey, _, err := lnd.DeriveKey(
|
||||
extendedKey, lnd.IdentityPath(chainParams), chainParams,
|
||||
var (
|
||||
signer lnd.ChannelSigner
|
||||
ourNode *btcec.PublicKey
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deriving identity pubkey: %w", err)
|
||||
switch {
|
||||
case c.HsmSecret != "":
|
||||
secretBytes, err := hex.DecodeString(c.HsmSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding HSM secret: %w", err)
|
||||
}
|
||||
|
||||
var hsmSecret [32]byte
|
||||
copy(hsmSecret[:], secretBytes)
|
||||
|
||||
ourNode, _, err = cln.NodeKey(hsmSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deriving CLN node pubkey: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
signer = &cln.Signer{
|
||||
HsmSecret: hsmSecret,
|
||||
}
|
||||
|
||||
default:
|
||||
extendedKey, err := c.rootKey.read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %w", err)
|
||||
}
|
||||
|
||||
_, ourNode, _, err = lnd.DeriveKey(
|
||||
extendedKey, lnd.IdentityPath(chainParams), chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deriving identity pubkey: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
signer = &lnd.Signer{
|
||||
ExtendedKey: extendedKey,
|
||||
ChainParams: chainParams,
|
||||
}
|
||||
}
|
||||
|
||||
pubKeyStr := hex.EncodeToString(pubKey.SerializeCompressed())
|
||||
// Make sure one of the nodes is ours.
|
||||
pubKeyStr := hex.EncodeToString(ourNode.SerializeCompressed())
|
||||
if keys1.Node1.PubKey != pubKeyStr && keys1.Node2.PubKey != pubKeyStr {
|
||||
return fmt.Errorf("derived pubkey %s from seed but that key "+
|
||||
"was not found in the match files", pubKeyStr)
|
||||
}
|
||||
|
||||
// We need to have the peer pubkey ready, in case we're using a CLN
|
||||
// signer.
|
||||
peerPubKeyStr := keys1.Node1.PubKey
|
||||
if keys1.Node1.PubKey == pubKeyStr {
|
||||
peerPubKeyStr = keys1.Node2.PubKey
|
||||
}
|
||||
peerPubKeyBytes, err := hex.DecodeString(peerPubKeyStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding peer pubkey: %w", err)
|
||||
}
|
||||
peerPubKey, err := btcec.ParsePubKey(peerPubKeyBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing peer pubkey: %w", err)
|
||||
}
|
||||
|
||||
// Pick the correct list of keys. There are 4 possibilities, given 2
|
||||
// files with 2 node slots each.
|
||||
var (
|
||||
|
|
@ -344,6 +400,12 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
|
|||
PrevOutputFetcher: prevOutFetcher,
|
||||
}
|
||||
|
||||
// For CLN, we also need to set the peer's public key in the
|
||||
// key descriptor.
|
||||
if _, ok := signer.(*cln.Signer); ok {
|
||||
signDesc.KeyDesc.PubKey = peerPubKey
|
||||
}
|
||||
|
||||
switch a := channelAddr.(type) {
|
||||
case *btcutil.AddressWitnessScriptHash:
|
||||
estimator.AddWitnessInput(input.MultiSigWitnessSize)
|
||||
|
|
@ -359,9 +421,15 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
|
|||
signDesc.HashType = txscript.SigHashDefault
|
||||
signDesc.SignMethod = input.TaprootKeySpendSignMethod
|
||||
|
||||
lndSigner, ok := signer.(*lnd.Signer)
|
||||
if !ok {
|
||||
return errors.New("taproot channels not " +
|
||||
"supported for CLN")
|
||||
}
|
||||
|
||||
err := addMuSig2Data(
|
||||
extendedKey, &pIn, channel, theirChannels[idx],
|
||||
op, a.WitnessProgram(),
|
||||
lndSigner.ExtendedKey, &pIn, channel,
|
||||
theirChannels[idx], op, a.WitnessProgram(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding MuSig2 data: "+
|
||||
|
|
@ -487,18 +555,20 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
|
|||
|
||||
// Loop a second time through the inputs and sign each input. We now
|
||||
// have all the witness/non-witness data filled in the psbt package.
|
||||
signer := &lnd.Signer{
|
||||
ExtendedKey: extendedKey,
|
||||
ChainParams: chainParams,
|
||||
}
|
||||
for idx := range packet.UnsignedTx.TxIn {
|
||||
signDesc := signDescs[idx]
|
||||
|
||||
// If we're dealing with a taproot channel, we'll need to
|
||||
// create a MuSig2 partial signature.
|
||||
if signDesc.SignMethod == input.TaprootKeySpendSignMethod {
|
||||
lndSigner, ok := signer.(*lnd.Signer)
|
||||
if !ok {
|
||||
return errors.New("taproot channels not yet " +
|
||||
"supported for CLN")
|
||||
}
|
||||
|
||||
err := muSig2PartialSign(
|
||||
signer, &signDesc.KeyDesc, packet, idx,
|
||||
lndSigner, &signDesc.KeyDesc, packet, idx,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating MuSig2 "+
|
||||
|
|
@ -508,34 +578,11 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
|
|||
continue
|
||||
}
|
||||
|
||||
ourSigRaw, err := signer.SignOutputRaw(
|
||||
packet.UnsignedTx, signDesc,
|
||||
)
|
||||
err = signer.AddPartialSignatureWithDesc(packet, signDesc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error signing with our key: %w", err)
|
||||
}
|
||||
ourSig := append(ourSigRaw.Serialize(), byte(signDesc.HashType))
|
||||
|
||||
// Great, we were able to create our sig, let's add it to the
|
||||
// PSBT.
|
||||
updater, err := psbt.NewUpdater(packet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating PSBT updater: %w",
|
||||
return fmt.Errorf("error adding partial signature: %w",
|
||||
err)
|
||||
}
|
||||
status, err := updater.Sign(
|
||||
idx, ourSig,
|
||||
signDesc.KeyDesc.PubKey.SerializeCompressed(), nil,
|
||||
signDesc.WitnessScript,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding signature to PSBT: %w",
|
||||
err)
|
||||
}
|
||||
if status != 0 {
|
||||
return fmt.Errorf("unexpected status for signature "+
|
||||
"update, got %d wanted 0", status)
|
||||
}
|
||||
}
|
||||
|
||||
// Looks like we're done!
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/lightninglabs/chantools/btc"
|
||||
"github.com/lightninglabs/chantools/cln"
|
||||
"github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -22,6 +23,9 @@ type zombieRecoverySignOfferCommand struct {
|
|||
HsmSecret string
|
||||
RemotePeer string
|
||||
|
||||
APIURL string
|
||||
Publish bool
|
||||
|
||||
rootKey *rootKey
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
|
@ -54,6 +58,16 @@ peer to recover funds from one or more channels.`,
|
|||
"peer node identity key, only required when running "+
|
||||
"'signoffer' on the CLN side",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use for "+
|
||||
"publishing the final transaction (must be esplora "+
|
||||
"compatible)",
|
||||
)
|
||||
cc.cmd.Flags().BoolVar(
|
||||
&cc.Publish, "publish", false, "if set, the final PSBT "+
|
||||
"will be published to the network after signing, "+
|
||||
"otherwise it will just be printed to stdout",
|
||||
)
|
||||
|
||||
cc.rootKey = newRootKey(cc.cmd, "signing the offer")
|
||||
|
||||
|
|
@ -115,11 +129,13 @@ func (c *zombieRecoverySignOfferCommand) Execute(_ *cobra.Command,
|
|||
}
|
||||
}
|
||||
|
||||
return signOffer(packet, signer, remoteNode)
|
||||
return signOffer(
|
||||
packet, signer, remoteNode, newExplorerAPI(c.APIURL), c.Publish,
|
||||
)
|
||||
}
|
||||
|
||||
func signOffer(packet *psbt.Packet, signer lnd.ChannelSigner,
|
||||
peerPubKey *btcec.PublicKey) error {
|
||||
peerPubKey *btcec.PublicKey, api *btc.ExplorerAPI, publish bool) error {
|
||||
|
||||
// Now let's check that the packet has the expected proprietary key with
|
||||
// our pubkey that we need to sign with.
|
||||
|
|
@ -243,9 +259,19 @@ func signOffer(packet *psbt.Packet, signer lnd.ChannelSigner,
|
|||
return fmt.Errorf("unable to serialize final TX: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Success, we counter signed the PSBT and extracted the "+
|
||||
"final\ntransaction. Please publish this using any bitcoin "+
|
||||
"node:\n\n%x\n\n", buf.Bytes())
|
||||
// Publish TX.
|
||||
if publish {
|
||||
response, err := api.PublishTx(hex.EncodeToString(buf.Bytes()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("Published TX %s, response: %s",
|
||||
finalTx.TxHash().String(), response)
|
||||
} else {
|
||||
fmt.Printf("Success, we counter signed the PSBT and extracted "+
|
||||
"the final\ntransaction. Please publish this using "+
|
||||
"any bitcoin node:\n\n%x\n\n", buf.Bytes())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ type SummaryEntryFile struct {
|
|||
FundsClosedSpent uint64 `json:"funds_closed_channels_spent"`
|
||||
FundsForceClose uint64 `json:"funds_force_closed_maybe_ours"`
|
||||
FundsCoopClose uint64 `json:"funds_coop_closed_maybe_ours"`
|
||||
OpenChannelList []*SummaryEntry `json:"open_channel_list"`
|
||||
}
|
||||
|
||||
func ExtractSummaryFromDump(data string) ([]*SummaryEntry, error) {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ chantools zombierecovery makeoffer \
|
|||
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
|
||||
-h, --help help for makeoffer
|
||||
--hsm_secret string the hex encoded HSM secret to use for deriving the multisig keys for a CLN node; obtain by running 'xxd -p -c32 ~/.lightning/bitcoin/hsm_secret'
|
||||
--matchonly only match the keys, don't create an offer
|
||||
--node1_keys string the JSON file generated in theprevious step ('preparekeys') command of node 1
|
||||
--node2_keys string the JSON file generated in theprevious step ('preparekeys') command of node 2
|
||||
|
|
|
|||
|
|
@ -21,10 +21,12 @@ chantools zombierecovery signoffer \
|
|||
### Options
|
||||
|
||||
```
|
||||
--apiurl string API URL to use for publishing the final transaction (must be esplora compatible) (default "https://api.node-recovery.com")
|
||||
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||
-h, --help help for signoffer
|
||||
--hsm_secret string the hex encoded HSM secret to use for deriving the multisig keys for a CLN node; obtain by running 'xxd -p -c32 ~/.lightning/bitcoin/hsm_secret'
|
||||
--psbt string the base64 encoded PSBT that the other party sent as an offer to rescue funds
|
||||
--publish if set, the final PSBT will be published to the network after signing, otherwise it will just be printed to stdout
|
||||
--remote_peer string the hex encoded remote peer node identity key, only required when running 'signoffer' on the CLN side
|
||||
--rootkey string BIP32 HD root key of the wallet to use for signing the offer; leave empty to prompt for lnd 24 word aezeed
|
||||
--walletdb string read the seed/master root key to use for signing the offer from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
|
||||
|
|
|
|||
128
lnd/brontide.go
128
lnd/brontide.go
|
|
@ -3,6 +3,7 @@ package lnd
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
|
|
@ -10,11 +11,16 @@ import (
|
|||
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/connmgr"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/aliasmgr"
|
||||
"github.com/lightningnetwork/lnd/brontide"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/channelnotifier"
|
||||
"github.com/lightningnetwork/lnd/discovery"
|
||||
"github.com/lightningnetwork/lnd/feature"
|
||||
"github.com/lightningnetwork/lnd/fn/v2"
|
||||
"github.com/lightningnetwork/lnd/graph/db/models"
|
||||
"github.com/lightningnetwork/lnd/htlcswitch"
|
||||
"github.com/lightningnetwork/lnd/htlcswitch/hodl"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
|
|
@ -24,7 +30,9 @@ import (
|
|||
"github.com/lightningnetwork/lnd/lntest/mock"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chancloser"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/msgmux"
|
||||
"github.com/lightningnetwork/lnd/netann"
|
||||
"github.com/lightningnetwork/lnd/peer"
|
||||
"github.com/lightningnetwork/lnd/pool"
|
||||
|
|
@ -46,11 +54,12 @@ var (
|
|||
|
||||
func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
|
||||
netParams *chaincfg.Params,
|
||||
identityECDH keychain.SingleKeyECDH) (*peer.Brontide, error) {
|
||||
identityECDH keychain.SingleKeyECDH) (*peer.Brontide, *channeldb.DB,
|
||||
error) {
|
||||
|
||||
featureMgr, err := feature.NewManager(feature.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
initFeatures := featureMgr.Get(feature.SetInit)
|
||||
|
|
@ -65,7 +74,7 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
|
|||
}
|
||||
errBuffer, err := queue.NewCircularBuffer(500)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pongBuf := make([]byte, lnwire.MaxPongBytes)
|
||||
|
|
@ -92,27 +101,31 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
|
|||
)
|
||||
|
||||
if err := writePool.Start(); err != nil {
|
||||
return nil, fmt.Errorf("unable to start write pool: %w", err)
|
||||
return nil, nil, fmt.Errorf("unable to start write pool: %w",
|
||||
err)
|
||||
}
|
||||
if err := readPool.Start(); err != nil {
|
||||
return nil, fmt.Errorf("unable to start read pool: %w", err)
|
||||
return nil, nil, fmt.Errorf("unable to start read pool: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
randNum := rand.Int31()
|
||||
backend, err := kvdb.GetBoltBackend(&kvdb.BoltBackendConfig{
|
||||
DBPath: os.TempDir(),
|
||||
DBFileName: "channel.db",
|
||||
DBFileName: fmt.Sprintf("channel-%d.db", randNum),
|
||||
NoFreelistSync: true,
|
||||
AutoCompact: false,
|
||||
AutoCompactMinAge: kvdb.DefaultBoltAutoCompactMinAge,
|
||||
DBTimeout: kvdb.DefaultDBTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
channelDB, err := channeldb.CreateWithBackend(backend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
_ = backend.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
gossiper := discovery.New(discovery.Config{
|
||||
|
|
@ -175,11 +188,53 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
|
|||
PubKey: identityECDH.PubKey(),
|
||||
})
|
||||
|
||||
chanStatusMgr, err := netann.NewChanStatusManager(&netann.
|
||||
ChanStatusConfig{
|
||||
ChanStatusSampleInterval: 30 * time.Second,
|
||||
ChanDisableTimeout: 2 * time.Minute,
|
||||
DB: channelDB.ChannelStateDB(),
|
||||
IsChannelActive: func(lnwire.ChannelID) bool {
|
||||
return true
|
||||
},
|
||||
ApplyChannelUpdate: func(*lnwire.ChannelUpdate1,
|
||||
*wire.OutPoint, bool) error {
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
_ = channelDB.Close()
|
||||
return nil, nil, fmt.Errorf("unable to create channel status "+
|
||||
"manager: %w", err)
|
||||
}
|
||||
|
||||
channelNotifier := channelnotifier.New(channelDB.ChannelStateDB())
|
||||
interceptableSwitchNotifier := &mock.ChainNotifier{
|
||||
EpochChan: make(chan *chainntnfs.BlockEpoch, 1),
|
||||
}
|
||||
interceptableSwitchNotifier.EpochChan <- &chainntnfs.BlockEpoch{
|
||||
Height: 1,
|
||||
}
|
||||
interceptableSwitch, err := htlcswitch.NewInterceptableSwitch(
|
||||
&htlcswitch.InterceptableSwitchConfig{
|
||||
CltvRejectDelta: 13,
|
||||
CltvInterceptDelta: 16,
|
||||
Notifier: interceptableSwitchNotifier,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
_ = channelDB.Close()
|
||||
return nil, nil, fmt.Errorf("unable to create interceptable "+
|
||||
"switch: %w", err)
|
||||
}
|
||||
|
||||
pCfg := peer.Config{
|
||||
Conn: conn,
|
||||
ConnReq: connReq,
|
||||
Conn: conn,
|
||||
ConnReq: connReq,
|
||||
PubKeyBytes: [33]byte(
|
||||
identityECDH.PubKey().SerializeCompressed(),
|
||||
),
|
||||
Addr: peerAddr,
|
||||
Inbound: false,
|
||||
Features: initFeatures,
|
||||
LegacyFeatures: legacyFeatures,
|
||||
OutgoingCltvRejectDelta: lncfg.DefaultOutgoingCltvRejectDelta,
|
||||
|
|
@ -187,9 +242,30 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
|
|||
ErrorBuffer: errBuffer,
|
||||
WritePool: writePool,
|
||||
ReadPool: readPool,
|
||||
Switch: &mockMessageSwitch{},
|
||||
InterceptSwitch: interceptableSwitch,
|
||||
ChannelDB: channelDB.ChannelStateDB(),
|
||||
ChainArb: nil,
|
||||
AuthGossiper: gossiper,
|
||||
ChainNotifier: &mock.ChainNotifier{},
|
||||
ChanStatusMgr: chanStatusMgr,
|
||||
ChainIO: &mock.ChainIO{},
|
||||
FeeEstimator: nil,
|
||||
Signer: nil,
|
||||
SigPool: nil,
|
||||
Wallet: &lnwallet.LightningWallet{
|
||||
WalletController: &mock.WalletController{},
|
||||
},
|
||||
ChainNotifier: &mock.ChainNotifier{},
|
||||
BestBlockView: chainntnfs.NewBestBlockTracker(
|
||||
&mock.ChainNotifier{},
|
||||
),
|
||||
RoutingPolicy: models.ForwardingPolicy{},
|
||||
Sphinx: nil,
|
||||
WitnessBeacon: nil,
|
||||
Invoices: nil,
|
||||
ChannelNotifier: channelNotifier,
|
||||
HtlcNotifier: nil,
|
||||
TowerClient: nil,
|
||||
DisconnectPeer: func(key *btcec.PublicKey) error {
|
||||
fmt.Printf("Peer %x disconnected\n",
|
||||
key.SerializeCompressed())
|
||||
|
|
@ -201,23 +277,20 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
|
|||
return lnwire.NodeAnnouncement{},
|
||||
errors.New("unimplemented")
|
||||
},
|
||||
|
||||
PongBuf: pongBuf,
|
||||
|
||||
PrunePersistentPeerConnection: func(_ [33]byte) {},
|
||||
|
||||
FetchLastChanUpdate: func(_ lnwire.ShortChannelID) (
|
||||
*lnwire.ChannelUpdate1, error) {
|
||||
|
||||
return nil, errors.New("unimplemented")
|
||||
},
|
||||
|
||||
FundingManager: nil,
|
||||
Hodl: &hodl.Config{},
|
||||
UnsafeReplay: false,
|
||||
MaxOutgoingCltvExpiry: htlcswitch.DefaultMaxOutgoingCltvExpiry,
|
||||
MaxChannelFeeAllocation: htlcswitch.DefaultMaxLinkFeeAllocation,
|
||||
CoopCloseTargetConfs: defaultCoopCloseTargetConfs,
|
||||
MaxAnchorsCommitFeeRate: commitFee.FeePerKWeight(),
|
||||
CoopCloseTargetConfs: defaultCoopCloseTargetConfs,
|
||||
ServerPubKey: [33]byte{},
|
||||
ChannelCommitInterval: defaultChannelCommitInterval,
|
||||
PendingCommitInterval: defaultPendingCommitInterval,
|
||||
ChannelCommitBatchSize: defaultChannelCommitBatchSize,
|
||||
|
|
@ -241,7 +314,19 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
|
|||
|
||||
return nil
|
||||
},
|
||||
Quit: make(chan struct{}),
|
||||
AuxLeafStore: fn.None[lnwallet.AuxLeafStore](),
|
||||
AuxSigner: fn.None[lnwallet.AuxSigner](),
|
||||
AuxResolver: fn.None[lnwallet.AuxContractResolver](),
|
||||
AuxTrafficShaper: fn.None[htlcswitch.AuxTrafficShaper](),
|
||||
PongBuf: pongBuf,
|
||||
DisallowRouteBlinding: false,
|
||||
DisallowQuiescence: false,
|
||||
MaxFeeExposure: 0,
|
||||
MsgRouter: fn.None[msgmux.Router](),
|
||||
AuxChanCloser: fn.None[chancloser.AuxChanCloser](),
|
||||
ShouldFwdExpEndorsement: nil,
|
||||
NoDisconnectOnPongFailure: false,
|
||||
Quit: make(chan struct{}),
|
||||
}
|
||||
|
||||
copy(pCfg.PubKeyBytes[:], peerAddr.IdentityKey.SerializeCompressed())
|
||||
|
|
@ -249,8 +334,9 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
|
|||
|
||||
p := peer.NewBrontide(pCfg)
|
||||
if err := p.Start(); err != nil {
|
||||
return nil, err
|
||||
_ = channelDB.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
return p, channelDB, nil
|
||||
}
|
||||
|
|
|
|||
40
lnd/mock.go
Normal file
40
lnd/mock.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package lnd
|
||||
|
||||
import (
|
||||
"github.com/lightningnetwork/lnd/htlcswitch"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
// mockMessageSwitch is a mock implementation of the messageSwitch interface
|
||||
// used for testing without relying on a *htlcswitch.Switch in unit tests.
|
||||
type mockMessageSwitch struct {
|
||||
links []htlcswitch.ChannelUpdateHandler
|
||||
}
|
||||
|
||||
// BestHeight currently returns a dummy value.
|
||||
func (m *mockMessageSwitch) BestHeight() uint32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// CircuitModifier currently returns a dummy value.
|
||||
func (m *mockMessageSwitch) CircuitModifier() htlcswitch.CircuitModifier {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveLink currently does nothing.
|
||||
func (m *mockMessageSwitch) RemoveLink(lnwire.ChannelID) {}
|
||||
|
||||
// CreateAndAddLink currently returns a dummy value.
|
||||
func (m *mockMessageSwitch) CreateAndAddLink(htlcswitch.ChannelLinkConfig,
|
||||
*lnwallet.LightningChannel) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLinksByInterface returns the active links.
|
||||
func (m *mockMessageSwitch) GetLinksByInterface([33]byte) (
|
||||
[]htlcswitch.ChannelUpdateHandler, error) {
|
||||
|
||||
return m.links, nil
|
||||
}
|
||||
|
|
@ -35,6 +35,9 @@ type ChannelSigner interface {
|
|||
FindMultisigKey(targetPubkey, peerPubKey *btcec.PublicKey,
|
||||
maxNumKeys uint32) (*keychain.KeyDescriptor, error)
|
||||
|
||||
AddPartialSignatureWithDesc(packet *psbt.Packet,
|
||||
signDesc *input.SignDescriptor) error
|
||||
|
||||
AddPartialSignature(packet *psbt.Packet,
|
||||
keyDesc keychain.KeyDescriptor, utxo *wire.TxOut,
|
||||
witnessScript []byte, inputIndex int) error
|
||||
|
|
@ -223,11 +226,18 @@ func (s *Signer) AddPartialSignature(packet *psbt.Packet,
|
|||
packet.UnsignedTx, prevOutFetcher,
|
||||
),
|
||||
}
|
||||
|
||||
return s.AddPartialSignatureWithDesc(packet, signDesc)
|
||||
}
|
||||
|
||||
func (s *Signer) AddPartialSignatureWithDesc(packet *psbt.Packet,
|
||||
signDesc *input.SignDescriptor) error {
|
||||
|
||||
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))
|
||||
|
||||
// Great, we were able to create our sig, let's add it to the PSBT.
|
||||
updater, err := psbt.NewUpdater(packet)
|
||||
|
|
@ -235,8 +245,9 @@ func (s *Signer) AddPartialSignature(packet *psbt.Packet,
|
|||
return fmt.Errorf("error creating PSBT updater: %w", err)
|
||||
}
|
||||
status, err := updater.Sign(
|
||||
inputIndex, ourSig, keyDesc.PubKey.SerializeCompressed(), nil,
|
||||
witnessScript,
|
||||
signDesc.InputIndex, ourSig,
|
||||
signDesc.KeyDesc.PubKey.SerializeCompressed(), nil,
|
||||
signDesc.WitnessScript,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding signature to PSBT: %w", err)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue