diff --git a/README.md b/README.md index 64eef16..8c97d64 100644 --- a/README.md +++ b/README.md @@ -277,27 +277,34 @@ Usage: Available Commands: chanbackup Create a channel.backup file from a channel database + closepoolaccount Tries to close a Pool account that has expired compactdb Create a copy of a channel.db file in safe/read-only mode + deletepayments Remove all (failed) payments from a channel DB derivekey Derive a key with a specific derivation path dropchannelgraph Remove all graph related data from a channel DB dumpbackup Dump the content of a channel.backup file dumpchannels Dump all channel information from an lnd channel database + fakechanbackup Fake a channel backup file to attempt fund recovery filterbackup Filter an lnd channel.backup file and remove certain channels fixoldbackup Fixes an old channel.backup file that is affected by the lnd issue #3881 (unable to derive shachain root key) forceclose Force-close the last state that is in the channel.db provided genimportscript Generate a script containing the on-chain keys of an lnd wallet that can be imported into other software like bitcoind - help Help about any command migratedb Apply all recent lnd channel database migrations removechannel Remove a single channel from the given channel DB rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels rescuefunding Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the initiator of the channel needs to run + rescuetweakedkey Attempt to rescue funds locked in an address with a key that was affected by a specific bug in lnd showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed signrescuefunding Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the remote node (the non-initiator) of the channel needs to run summary Compile a summary about the current state of channels sweeptimelock Sweep the force-closed state after the time lock has expired sweeptimelockmanual Sweep the force-closed state of a single channel manually if only a channel backup file is available + sweepremoteclosed Go through all the addresses that could have funds of channels that were force-closed by the remote party. A public block explorer is queried for each address and if any balance is found, all funds are swept to a given address + triggerforceclose Connect to a peer and send a custom message to trigger a force close of the specified channel vanitygen Generate a seed with a custom lnd node identity public key that starts with the given prefix walletinfo Shows info about an lnd wallet.db file and optionally extracts the BIP32 HD root key + zombierecovery Try rescuing funds stuck in channels with zombie nodes + help Help about any command Flags: -h, --help help for chantools @@ -336,6 +343,7 @@ Quick access: + [sweepremoteclosed](doc/chantools_sweepremoteclosed.md) + [sweeptimelock](doc/chantools_sweeptimelock.md) + [sweeptimelockmanual](doc/chantools_sweeptimelockmanual.md) ++ [triggerforceclose](doc/chantools_triggerforceclose.md) + [vanitygen](doc/chantools_vanitygen.md) + [walletinfo](doc/chantools_walletinfo.md) + [zombierecovery](doc/chantools_zombierecovery.md) diff --git a/btc/explorer_api.go b/btc/explorer_api.go index b0fdbc9..e24357a 100644 --- a/btc/explorer_api.go +++ b/btc/explorer_api.go @@ -89,7 +89,9 @@ func (a *ExplorerAPI) Transaction(txid string) (*TX, error) { func (a *ExplorerAPI) Outpoint(addr string) (*TX, int, error) { var txs []*TX - err := fetchJSON(fmt.Sprintf("%s/address/%s/txs", a.BaseURL, addr), &txs) + err := fetchJSON( + fmt.Sprintf("%s/address/%s/txs", a.BaseURL, addr), &txs, + ) if err != nil { return nil, 0, err } @@ -104,6 +106,28 @@ func (a *ExplorerAPI) Outpoint(addr string) (*TX, int, error) { return nil, 0, fmt.Errorf("no tx found") } +func (a *ExplorerAPI) Spends(addr string) ([]*TX, error) { + var txs []*TX + err := fetchJSON( + fmt.Sprintf("%s/address/%s/txs", a.BaseURL, addr), &txs, + ) + if err != nil { + return nil, err + } + + var spends []*TX + for txIndex := range txs { + tx := txs[txIndex] + for _, vin := range tx.Vin { + if vin.Prevout.ScriptPubkeyAddr == addr { + spends = append(spends, tx) + } + } + } + + return spends, nil +} + func (a *ExplorerAPI) Unspent(addr string) ([]*Vout, error) { var ( stats = &AddressStats{} diff --git a/cmd/chantools/dropchannelgraph.go b/cmd/chantools/dropchannelgraph.go index d8743b0..7214bfb 100644 --- a/cmd/chantools/dropchannelgraph.go +++ b/cmd/chantools/dropchannelgraph.go @@ -242,7 +242,7 @@ func newChanAnnouncement(localPubKey, remotePubKey *btcec.PublicKey, // Our channel update message flags will signal that we support the // max_htlc field. - msgFlags := lnwire.ChanUpdateOptionMaxHtlc + msgFlags := lnwire.ChanUpdateRequiredMaxHtlc // We announce the channel with the default values. Some of // these values can later be changed by crafting a new ChannelUpdate. diff --git a/cmd/chantools/root.go b/cmd/chantools/root.go index 76831d2..a52e0ff 100644 --- a/cmd/chantools/root.go +++ b/cmd/chantools/root.go @@ -20,13 +20,14 @@ import ( "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/peer" "github.com/spf13/cobra" "golang.org/x/crypto/ssh/terminal" ) const ( defaultAPIURL = "https://blockstream.info/api" - version = "0.10.6" + version = "0.10.7" na = "n/a" Commit = "" @@ -104,6 +105,7 @@ func main() { newSweepTimeLockCommand(), newSweepTimeLockManualCommand(), newSweepRemoteClosedCommand(), + newTriggerForceCloseCommand(), newVanityGenCommand(), newWalletInfoCommand(), newZombieRecoveryCommand(), @@ -263,6 +265,7 @@ func setupLogging() { setSubLogger("CHAN", log) addSubLogger("CHDB", channeldb.UseLogger) addSubLogger("BCKP", chanbackup.UseLogger) + addSubLogger("PEER", peer.UseLogger) err := logWriter.InitLogRotator("./results/chantools.log", 10, 3) if err != nil { panic(err) diff --git a/cmd/chantools/triggerforceclose.go b/cmd/chantools/triggerforceclose.go new file mode 100644 index 0000000..780adbe --- /dev/null +++ b/cmd/chantools/triggerforceclose.go @@ -0,0 +1,202 @@ +package main + +import ( + "fmt" + "net" + "strconv" + "strings" + "time" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/connmgr" + "github.com/btcsuite/btcd/wire" + "github.com/guggero/chantools/btc" + "github.com/guggero/chantools/lnd" + "github.com/lightningnetwork/lnd/brontide" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lncfg" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tor" + "github.com/spf13/cobra" +) + +var ( + dialTimeout = time.Minute +) + +type triggerForceCloseCommand struct { + Peer string + ChannelPoint string + + APIURL string + + rootKey *rootKey + cmd *cobra.Command +} + +func newTriggerForceCloseCommand() *cobra.Command { + cc := &triggerForceCloseCommand{} + cc.cmd = &cobra.Command{ + Use: "triggerforceclose", + Short: "Connect to a peer and send a custom message to " + + "trigger a force close of the specified channel", + Example: `chantools triggerforceclose \ + --peer 03abce...@xx.yy.zz.aa:9735 \ + --channel_point abcdef01234...:x`, + RunE: cc.Execute, + } + cc.cmd.Flags().StringVar( + &cc.Peer, "peer", "", "remote peer address "+ + "(@[:])", + ) + cc.cmd.Flags().StringVar( + &cc.ChannelPoint, "channel_point", "", "funding transaction "+ + "outpoint of the channel to trigger the force close "+ + "of (:)", + ) + cc.cmd.Flags().StringVar( + &cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+ + "be esplora compatible)", + ) + cc.rootKey = newRootKey(cc.cmd, "deriving the identity key") + + return cc.cmd +} + +func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error { + extendedKey, err := c.rootKey.read() + if err != nil { + return fmt.Errorf("error reading root key: %w", err) + } + + identityPath := lnd.IdentityPath(chainParams) + child, pubKey, _, err := lnd.DeriveKey( + extendedKey, identityPath, chainParams, + ) + if err != nil { + return fmt.Errorf("could not derive identity key: %w", err) + } + identityPriv, err := child.ECPrivKey() + if err != nil { + return fmt.Errorf("could not get identity private key: %w", err) + } + identityECDH := &keychain.PrivKeyECDH{ + PrivKey: identityPriv, + } + + peerAddr, err := lncfg.ParseLNAddressString( + c.Peer, "9735", net.ResolveTCPAddr, + ) + if err != nil { + return fmt.Errorf("error parsing peer address: %w", err) + } + + outPoint, err := parseOutPoint(c.ChannelPoint) + if err != nil { + return fmt.Errorf("error parsing channel point: %w", err) + } + channelID := lnwire.NewChanIDFromOutPoint(outPoint) + + conn, err := noiseDial( + identityECDH, peerAddr, &tor.ClearNet{}, dialTimeout, + ) + if err != nil { + return fmt.Errorf("error dialing peer: %w", err) + } + + log.Infof("Attempting to connect to peer %x, dial timeout is %v", + pubKey.SerializeCompressed(), dialTimeout) + req := &connmgr.ConnReq{ + Addr: peerAddr, + Permanent: false, + } + p, err := lnd.ConnectPeer(conn, req, chainParams, identityECDH) + if err != nil { + return fmt.Errorf("error connecting to peer: %w", err) + } + + log.Infof("Connection established to peer %x", + pubKey.SerializeCompressed()) + + // We'll wait until the peer is active. + select { + case <-p.ActiveSignal(): + case <-p.QuitSignal(): + return fmt.Errorf("peer %x disconnected", + pubKey.SerializeCompressed()) + } + + // Channel ID (32 byte) + u16 for the data length (which will be 0). + data := make([]byte, 34) + copy(data[:32], channelID[:]) + + log.Infof("Sending channel error message to peer to trigger force "+ + "close of channel %v", c.ChannelPoint) + + _ = lnwire.SetCustomOverrides([]uint16{lnwire.MsgError}) + msg, err := lnwire.NewCustom(lnwire.MsgError, data) + if err != nil { + return err + } + + err = p.SendMessageLazy(true, msg) + if err != nil { + return fmt.Errorf("error sending message: %w", err) + } + + log.Infof("Message sent, waiting for force close transaction to " + + "appear in mempool") + + api := &btc.ExplorerAPI{BaseURL: c.APIURL} + channelAddress, err := api.Address(c.ChannelPoint) + if err != nil { + return fmt.Errorf("error getting channel address: %w", err) + } + + spends, err := api.Spends(channelAddress) + if err != nil { + return fmt.Errorf("error getting spends: %w", err) + } + 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 fmt.Errorf("error getting spends: %w", err) + } + } + + log.Infof("Found force close transaction %v", spends[0].TXID) + log.Infof("You can now use the sweepremoteclosed command to sweep " + + "the funds from the channel") + + return nil +} + +func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress, + netCfg tor.Net, timeout time.Duration) (*brontide.Conn, error) { + + return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial) +} + +func parseOutPoint(s string) (*wire.OutPoint, error) { + split := strings.Split(s, ":") + if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 { + return nil, fmt.Errorf("invalid channel point format: %v", s) + } + + index, err := strconv.ParseInt(split[1], 10, 64) + if err != nil { + return nil, fmt.Errorf("unable to decode output index: %v", err) + } + + txid, err := chainhash.NewHashFromStr(split[0]) + if err != nil { + return nil, fmt.Errorf("unable to parse hex string: %v", err) + } + + return &wire.OutPoint{ + Hash: *txid, + Index: uint32(index), + }, nil +} diff --git a/doc/chantools.md b/doc/chantools.md index 699b98d..7d1f9c1 100644 --- a/doc/chantools.md +++ b/doc/chantools.md @@ -42,6 +42,7 @@ Complete documentation is available at https://github.com/guggero/chantools/. * [chantools sweepremoteclosed](chantools_sweepremoteclosed.md) - Go through all the addresses that could have funds of channels that were force-closed by the remote party. A public block explorer is queried for each address and if any balance is found, all funds are swept to a given address * [chantools sweeptimelock](chantools_sweeptimelock.md) - Sweep the force-closed state after the time lock has expired * [chantools sweeptimelockmanual](chantools_sweeptimelockmanual.md) - Sweep the force-closed state of a single channel manually if only a channel backup file is available +* [chantools triggerforceclose](chantools_triggerforceclose.md) - Connect to a peer and send a custom message to trigger a force close of the specified channel * [chantools vanitygen](chantools_vanitygen.md) - Generate a seed with a custom lnd node identity public key that starts with the given prefix * [chantools walletinfo](chantools_walletinfo.md) - Shows info about an lnd wallet.db file and optionally extracts the BIP32 HD root key * [chantools zombierecovery](chantools_zombierecovery.md) - Try rescuing funds stuck in channels with zombie nodes diff --git a/doc/chantools_fakechanbackup.md b/doc/chantools_fakechanbackup.md index e72b873..18d064d 100644 --- a/doc/chantools_fakechanbackup.md +++ b/doc/chantools_fakechanbackup.md @@ -61,7 +61,7 @@ chantools fakechanbackup --from_channel_graph lncli_describegraph.json \ --channelpoint string funding transaction outpoint of the channel to rescue (:) as it is displayed on 1ml.com --from_channel_graph string the full LN channel graph in the JSON format that the 'lncli describegraph' returns -h, --help help for fakechanbackup - --multi_file string the fake channel backup file to create (default "results/fake-2022-09-11-19-20-32.backup") + --multi_file string the fake channel backup file to create (default "results/fake-2023-02-25-14-15-10.backup") --remote_node_addr string the remote node connection information in the format pubkey@host:port --rootkey string BIP32 HD root key of the wallet to use for encrypting the backup; leave empty to prompt for lnd 24 word aezeed --short_channel_id string the short channel ID in the format xx diff --git a/doc/chantools_triggerforceclose.md b/doc/chantools_triggerforceclose.md new file mode 100644 index 0000000..ed0ca63 --- /dev/null +++ b/doc/chantools_triggerforceclose.md @@ -0,0 +1,38 @@ +## chantools triggerforceclose + +Connect to a peer and send a custom message to trigger a force close of the specified channel + +``` +chantools triggerforceclose [flags] +``` + +### Examples + +``` +chantools triggerforceclose \ + --peer 03abce...@xx.yy.zz.aa:9735 \ + --channel_point abcdef01234...:x +``` + +### Options + +``` + --apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api") + --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag + --channel_point string funding transaction outpoint of the channel to trigger the force close of (:) + -h, --help help for triggerforceclose + --peer string remote peer address (@[:]) + --rootkey string BIP32 HD root key of the wallet to use for deriving the identity key; leave empty to prompt for lnd 24 word aezeed +``` + +### Options inherited from parent commands + +``` + -r, --regtest Indicates if regtest parameters should be used + -t, --testnet Indicates if testnet parameters should be used +``` + +### SEE ALSO + +* [chantools](chantools.md) - Chantools helps recover funds from lightning channels + diff --git a/lnd/brontide.go b/lnd/brontide.go new file mode 100644 index 0000000..f1b116c --- /dev/null +++ b/lnd/brontide.go @@ -0,0 +1,235 @@ +package lnd + +import ( + "fmt" + "os" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/connmgr" + "github.com/lightningnetwork/lnd/aliasmgr" + "github.com/lightningnetwork/lnd/brontide" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/discovery" + "github.com/lightningnetwork/lnd/feature" + "github.com/lightningnetwork/lnd/htlcswitch" + "github.com/lightningnetwork/lnd/htlcswitch/hodl" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lncfg" + "github.com/lightningnetwork/lnd/lnpeer" + "github.com/lightningnetwork/lnd/lntest/mock" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/netann" + "github.com/lightningnetwork/lnd/peer" + "github.com/lightningnetwork/lnd/pool" + "github.com/lightningnetwork/lnd/queue" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/ticker" +) + +const ( + defaultChannelCommitBatchSize = 10 + defaultCoopCloseTargetConfs = 6 +) + +var ( + chanEnableTimeout = 19 * time.Minute + defaultChannelCommitInterval = 50 * time.Millisecond + defaultPendingCommitInterval = 1 * time.Minute +) + +func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq, + netParams *chaincfg.Params, + identityECDH keychain.SingleKeyECDH) (*peer.Brontide, error) { + + featureMgr, err := feature.NewManager(feature.Config{}) + if err != nil { + return nil, err + } + + initFeatures := featureMgr.Get(feature.SetInit) + legacyFeatures := featureMgr.Get(feature.SetLegacyGlobal) + + addr := conn.RemoteAddr() + pubKey := conn.RemotePub() + peerAddr := &lnwire.NetAddress{ + IdentityKey: pubKey, + Address: addr, + ChainNet: netParams.Net, + } + errBuffer, err := queue.NewCircularBuffer(500) + if err != nil { + return nil, err + } + + pongBuf := make([]byte, lnwire.MaxPongBytes) + + writeBufferPool := pool.NewWriteBuffer( + pool.DefaultWriteBufferGCInterval, + pool.DefaultWriteBufferExpiryInterval, + ) + writePool := pool.NewWrite( + writeBufferPool, lncfg.DefaultWriteWorkers, + pool.DefaultWorkerTimeout, + ) + + readBufferPool := pool.NewReadBuffer( + pool.DefaultReadBufferGCInterval, + pool.DefaultReadBufferExpiryInterval, + ) + readPool := pool.NewRead( + readBufferPool, lncfg.DefaultWriteWorkers, + pool.DefaultWorkerTimeout, + ) + commitFee := chainfee.SatPerKVByte( + lnwallet.DefaultAnchorsCommitMaxFeeRateSatPerVByte * 1000, + ) + + if err := writePool.Start(); err != nil { + return nil, fmt.Errorf("unable to start write pool: %v", err) + } + if err := readPool.Start(); err != nil { + return nil, fmt.Errorf("unable to start read pool: %v", err) + } + + channelDB, err := channeldb.Open(os.TempDir()) + if err != nil { + return nil, err + } + + gossiper := discovery.New(discovery.Config{ + ChainHash: *netParams.GenesisHash, + Broadcast: func(skips map[route.Vertex]struct{}, + msg ...lnwire.Message) error { + + return nil + }, + NotifyWhenOnline: func(peerPubKey [33]byte, + peerChan chan<- lnpeer.Peer) { + + }, + NotifyWhenOffline: func(peerPubKey [33]byte) <-chan struct{} { + return make(chan struct{}) + }, + SelfNodeAnnouncement: func( + refresh bool) (lnwire.NodeAnnouncement, error) { + + return lnwire.NodeAnnouncement{}, nil + }, + ProofMatureDelta: 0, + TrickleDelay: time.Millisecond * 50, + RetransmitTicker: ticker.New(time.Minute * 30), + RebroadcastInterval: time.Hour * 24, + RotateTicker: ticker.New(discovery.DefaultSyncerRotationInterval), + HistoricalSyncTicker: ticker.New(discovery.DefaultHistoricalSyncInterval), + NumActiveSyncers: 0, + MinimumBatchSize: 10, + SubBatchDelay: discovery.DefaultSubBatchDelay, + IgnoreHistoricalFilters: true, + PinnedSyncers: make(map[route.Vertex]struct{}), + MaxChannelUpdateBurst: discovery.DefaultMaxChannelUpdateBurst, + ChannelUpdateInterval: discovery.DefaultChannelUpdateInterval, + IsAlias: aliasmgr.IsAlias, + SignAliasUpdate: func(u *lnwire.ChannelUpdate) (*ecdsa.Signature, error) { + return nil, fmt.Errorf("unimplemented") + }, + FindBaseByAlias: func(alias lnwire.ShortChannelID) (lnwire.ShortChannelID, error) { + return lnwire.ShortChannelID{}, fmt.Errorf("unimplemented") + }, + GetAlias: func(id lnwire.ChannelID) (lnwire.ShortChannelID, error) { + return lnwire.ShortChannelID{}, fmt.Errorf("unimplemented") + }, + FindChannel: func(node *btcec.PublicKey, + chanID lnwire.ChannelID) (*channeldb.OpenChannel, error) { + + return nil, fmt.Errorf("unimplemented") + }, + }, &keychain.KeyDescriptor{ + KeyLocator: keychain.KeyLocator{}, + PubKey: identityECDH.PubKey(), + }) + + pCfg := peer.Config{ + Conn: conn, + ConnReq: connReq, + Addr: peerAddr, + Inbound: false, + Features: initFeatures, + LegacyFeatures: legacyFeatures, + OutgoingCltvRejectDelta: lncfg.DefaultOutgoingCltvRejectDelta, + ChanActiveTimeout: chanEnableTimeout, + ErrorBuffer: errBuffer, + WritePool: writePool, + ReadPool: readPool, + ChannelDB: channelDB.ChannelStateDB(), + AuthGossiper: gossiper, + ChainNotifier: &mock.ChainNotifier{}, + DisconnectPeer: func(key *btcec.PublicKey) error { + fmt.Printf("Peer %x disconnected\n", + key.SerializeCompressed()) + return nil + }, + GenNodeAnnouncement: func(b bool, + modifier ...netann.NodeAnnModifier) ( + lnwire.NodeAnnouncement, error) { + + return lnwire.NodeAnnouncement{}, + fmt.Errorf("unimplemented") + }, + + PongBuf: pongBuf, + + PrunePersistentPeerConnection: func(bytes [33]byte) {}, + + FetchLastChanUpdate: func(id lnwire.ShortChannelID) ( + *lnwire.ChannelUpdate, error) { + + return nil, fmt.Errorf("unimplemented") + }, + + Hodl: &hodl.Config{}, + UnsafeReplay: false, + MaxOutgoingCltvExpiry: htlcswitch.DefaultMaxOutgoingCltvExpiry, + MaxChannelFeeAllocation: htlcswitch.DefaultMaxLinkFeeAllocation, + CoopCloseTargetConfs: defaultCoopCloseTargetConfs, + MaxAnchorsCommitFeeRate: commitFee.FeePerKWeight(), + ChannelCommitInterval: defaultChannelCommitInterval, + PendingCommitInterval: defaultPendingCommitInterval, + ChannelCommitBatchSize: defaultChannelCommitBatchSize, + HandleCustomMessage: func(peer [33]byte, + msg *lnwire.Custom) error { + + fmt.Printf("Received custom message from %x: %v\n", + peer[:], msg) + return nil + }, + GetAliases: func( + base lnwire.ShortChannelID) []lnwire.ShortChannelID { + + return nil + }, + RequestAlias: func() (lnwire.ShortChannelID, error) { + return lnwire.ShortChannelID{}, nil + }, + AddLocalAlias: func(alias, base lnwire.ShortChannelID, + gossip bool) error { + + return nil + }, + Quit: make(chan struct{}), + } + + copy(pCfg.PubKeyBytes[:], peerAddr.IdentityKey.SerializeCompressed()) + copy(pCfg.ServerPubKey[:], identityECDH.PubKey().SerializeCompressed()) + + p := peer.NewBrontide(pCfg) + if err := p.Start(); err != nil { + return nil, err + } + + return p, nil +}