Work in progress for peering

This commit is contained in:
Calin Culianu 2020-01-07 21:42:18 +02:00
parent b0c42329b6
commit d5b772dfd1
No known key found for this signature in database
GPG key ID: 21810A542031C02C
14 changed files with 578 additions and 24 deletions

View file

@ -152,8 +152,10 @@ SOURCES += \
Mixins.cpp \
Mgr.cpp \
Options.cpp \
PeerMgr.cpp \
RecordFile.cpp \
RPC.cpp \
ServerMisc.cpp \
Servers.cpp \
SrvMgr.cpp \
Storage.cpp \
@ -178,8 +180,10 @@ HEADERS += \
Mgr.h \
Mixins.h \
Options.h \
PeerMgr.h \
RecordFile.h \
RPC.h \
ServerMisc.h \
Servers.h \
SrvMgr.h \
Storage.h \

View file

@ -76,7 +76,7 @@ public:
/// or if the file cannot be opened, the default banner text will be emitted to the client as a fallback.
QString bannerFile = "";
bool peerDiscovery = false, peerAnnounceSelf = false; ///< comes from config setting: 'peering' and 'announce' TODO
bool peerDiscovery = true, peerAnnounceSelf = true; ///< comes from config setting: 'peering' and 'announce' TODO -- for now hard-coded on for testing
std::optional<QString> hostName; ///< corresponds to hostname in server config
std::optional<quint16> publicTcp; ///< corresponds to public_tcp_port in server config -- if unspecified will default to the first TCP interface, if !has_value, it will not be announced

189
PeerMgr.cpp Normal file
View file

@ -0,0 +1,189 @@
#include "PeerMgr.h"
#include "Options.h"
#include "Servers.h"
#include "Storage.h"
#include "Util.h"
#include <QHostInfo>
#include <QSet>
PeerMgr::PeerMgr(const std::shared_ptr<Storage> &storage_ , const std::shared_ptr<const Options> &options_)
: IdMixin(newId()), storage(storage_), options(options_)
{
setObjectName("PeerMgr");
_thread.setObjectName(objectName());
}
PeerMgr::~PeerMgr() { stop(); /* noop if already stopped */ Debug() << __func__; }
void PeerMgr::startup()
{
if (storage->genesisHash().length() != HashLen)
throw InternalError("PeerMgr cannot be started until we have a valid genesis hash! FIXME!");
if (const auto chain = storage->getChain(); !QSet<QString>{"test", "main"}.contains(chain))
// can only do peering with testnet or mainnet after they have been defined (no regtest)
throw InternalError(QString("PeerMgr cannot be started for the given chain \"%1\"").arg(chain));
else if (chain == "test")
parseServersDotJson(":resources/servers_testnet.json");
else
parseServersDotJson(":resources/servers_testnet.json");
start();
}
void PeerMgr::parseServersDotJson(const QString &fnIn)
{
QVariantMap m = Util::Json::parseFile(fnIn).toMap();
const QString fn = Util::basename(fnIn); // use basename for error messages below, etc
if (m.isEmpty()) throw InternalError(QString("PeerMgr: %1 file parsed to an empty dict! FIXME!").arg(fn));
for (auto it = m.begin(); it != m.end(); ++it) {
PeerInfo info;
QVariantMap d = it.value().toMap();
info.hostName = it.key().trimmed().toLower();
// skip empties/malformed entries, or pruning entries -- thisdefensive programming.. ideally we include only good entries in servers.json
if (info.hostName.isEmpty() || d.isEmpty() || (!d.value("pruning").isNull() && d.value("pruning").toString() != "-")) {
Debug() << "Server \"" << info.hostName << "\" in " << fn << " has either no data or uses pruning, skipping";
continue;
}
bool ok;
unsigned val = d.value("s", 0).toUInt(&ok);
if (ok && val && val <= USHRT_MAX)
info.ssl = quint16(val);
val = d.value("t", 0).toUInt(&ok);
if (ok && val && val <= USHRT_MAX)
info.tcp = quint16(val);
info.protocolVersion = d.value("version", ServerMisc::MinProtocolVersion.toString()).toString();
if (!info.isMinimallyValid()) {
Debug() << "Bad server in " << fn << ": " << info.hostName;
continue;
} else if (info.protocolVersion < ServerMisc::MinProtocolVersion || info.protocolVersion > ServerMisc::MaxProtocolVersion) {
Debug() << "Server in " << fn << " has incompatible protocol version (" << info.protocolVersion.toString() << "), skipping";
continue;
}
seedPeers[info.hostName] = info;
}
if (seedPeers.isEmpty())
throw InternalError(QString("PeerMgr: No valid peers parsed from %1").arg(fn));
seedPeers.squeeze();
Debug() << objectName() << ": using " << seedPeers.size() << " peers from " << fn;
}
void PeerMgr::on_started()
{
Debug() << objectName() << ": started ok";
}
void PeerMgr::cleanup()
{
}
void PeerMgr::on_rpcAddPeer(const PeerInfoList &infos, const QHostAddress &source)
{
Debug() << __func__ << " source: " << source.toString();
// TODO: perhaps put all these in a queue and collapse dupes down -- as it stands clients can spam the same request
// over and over and cause us to waste time doing network lookups.
for (const auto & pi : infos) {
// For each peer in the list, do a DNS lookup and verify that the source address matches at least one
// of the resolved addresses. If that is the case, we can proceed with the peer add (addPeerVerifiedSource).
// Otherwise, we reject add_peer requests from random sources.
std::shared_ptr<std::optional<int>> lookupId = std::make_shared<decltype(lookupId)::element_type>();
*lookupId = QHostInfo::lookupHost(pi.hostName, this, [this, pi, source, lookupId](const QHostInfo &result){
lookupId->reset(); // signify we no longer need a cancellation .. calls reset on the std::optional (not on the shared_ptr)
if (result.error() != QHostInfo::NoError) {
Debug() << "add_peer: Host lookup error for " << pi.hostName << ": " << result.errorString();
return;
}
for (const auto & addr : result.addresses()) {
if (addr == source) {
Debug() << "add_peer: " << pi.hostName << " address (" << addr.toString() << ") matches source (" << source.toString() << "), processing further ...";
addPeerVerifiedSource(pi, addr);
return;
}
}
Debug() << "add_peer: Rejected because source (" << source.toString() << ") does not match resolved address ("
<< (result.addresses().isEmpty() ? QString() : result.addresses().front().toString()) << ")";
});
QTimer::singleShot(DNSTimeoutMS, this, [lookupId, hostName = pi.hostName]{
if (lookupId->has_value()) {
QHostInfo::abortHostLookup(lookupId->value());
Debug() << "add_peer: hostname lookup for " << hostName << " timed out after " << QString::number(DNSTimeoutMS/1e3, 'f', 1) << " secs";
}
});
}
}
void PeerMgr::addPeerVerifiedSource(const PeerInfo &piIn, const QHostAddress & addr)
{
PeerInfo pi(piIn);
pi.addr = addr;
Debug() << __func__ << " peer " << pi.hostName << " ipaddr: " << pi.addr.toString();
// TODO ...
}
void PeerMgr::allServersStarted()
{
// TODO ...
Debug() << __func__;
}
/* static */ QList<PeerInfo> PeerInfo::fromFeaturesMap(const QVariantMap &m)
{
QList<PeerInfo> ret;
if (!m.value("pruning").isNull())
throw BadFeaturesMap("Pruning not supported");
PeerInfo base;
base.subversion = m.value("server_version", "Unknown").toString().trimmed().left(80);
base.protocolMin = m.value("protocol_min").toString().trimmed().left(80);
base.protocolMax = m.value("protocol_max").toString().trimmed().left(80);
base.genesisHash = QByteArray::fromHex(m.value("genesis_hash").toString().trimmed().toUtf8()).left(HashLen+1);
if (base.genesisHash.length() != HashLen)
throw BadFeaturesMap("Bad genesis hash");
base.hashFunction = m.value("hash_function").toString().trimmed().toLower();
if (base.hashFunction != ServerMisc::HashFunction)
throw BadFeaturesMap("Bad/incompatible hash function");
if (!base.protocolMin.isValid() || !base.protocolMax.isValid() || base.protocolMin > base.protocolMax)
throw BadFeaturesMap("Bad protocol min/max");
if (base.protocolMin > ServerMisc::MaxProtocolVersion || base.protocolMax < ServerMisc::MinProtocolVersion)
throw BadFeaturesMap("Incompatible server protocol");
const auto hosts = m.value("hosts").toMap();
if (hosts.size() > 4)
// Disallow huge maps
throw BadFeaturesMap("Hosts map cannot have more than 4 hosts in it!");
// now, parse each host
for (auto it = hosts.begin(); it != hosts.end(); ++it) {
PeerInfo pi(base); // copy c'tor of base, but fill in host, tcp, and ssl
pi.hostName = it.key().trimmed().toLower().left(120); // we don't support super long hostnames as a paranoia defense
const auto m = it.value().toMap(); // <--- note to self: shadows outer scope 'm'
if (!m.value("tcp_port").isNull()) {
bool ok;
unsigned val = m.value("tcp_port", 0).toUInt(&ok);
if (!ok || !val || val > USHRT_MAX) throw BadFeaturesMap("Bad tcp_port");
pi.tcp = quint16(val);
}
if (!m.value("ssl_port").isNull()) {
bool ok;
unsigned val = m.value("ssl_port", 0).toUInt(&ok);
if (!ok || !val || val > USHRT_MAX) throw BadFeaturesMap("Bad ssl_port");
pi.ssl = quint16(val);
}
if (!pi.isMinimallyValid())
throw BadFeaturesMap(QString("Bad host: ") + pi.hostName);
ret.push_back(pi);
}
if (ret.isEmpty())
throw BadFeaturesMap("No hosts!");
return ret;
}

97
PeerMgr.h Normal file
View file

@ -0,0 +1,97 @@
//
// Fulcrum - A fast & nimble SPV Server for Bitcoin Cash
// Copyright (C) 2019-2020 Calin A. Culianu <calin.culianu@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program (see LICENSE.txt). If not, see
// <https://www.gnu.org/licenses/>.
//
#pragma once
#include "Mixins.h"
#include "Mgr.h"
#include "ServerMisc.h"
#include "Version.h"
#include <QHash>
#include <QHostAddress>
#include <QList>
struct Options;
class Storage;
struct PeerInfo;
using PeerInfoList = QList<PeerInfo>;
class PeerMgr : public Mgr, public IdMixin, public ThreadObjectMixin, public TimersByNameMixin
{
Q_OBJECT
public:
PeerMgr(const std::shared_ptr<Storage> & , const std::shared_ptr<const Options> &);
~PeerMgr() override;
void startup() noexcept(false) override; ///< may throw
void cleanup() override;
/// The hostname lookup timeout for add_peer hostname verification
static constexpr int DNSTimeoutMS = 3000;
public slots:
/// The various Server instances are connected to this slot (via their gotRpcAddPeer signals), connections made by SrvMgr.
void on_rpcAddPeer(const PeerInfoList &, const QHostAddress & source);
void allServersStarted(); ///< tells this instance all our services are up, so it may begin searching for peers and publishing our information
protected:
void on_started() override;
private:
const std::shared_ptr<const Storage> storage; ///< from SrvMgr, read-only, for getChain() and genesisHash()
const std::shared_ptr<const Options> options; ///< from SrvMgr that creates us.
QHash<QString, PeerInfo> seedPeers; ///< parsed PeerInfos from server.json or servers_testnet.json
void parseServersDotJson(const QString &) noexcept(false); ///< may throw
void addPeerVerifiedSource(const PeerInfo &, const QHostAddress &resolvedAddress);
};
/// Thrown by PeerInfo::fromFeaturesMap
struct BadFeaturesMap : public Exception { using Exception::Exception; };
struct PeerInfo
{
QString hostName;
QHostAddress addr; ///< may originally come from json or be empty -- is populated/reverified later after hostname lookup.
quint16 ssl = 0; ///< ssl port - 0 means undefined (no port)
quint16 tcp = 0; ///< tcp port - 0 means undefined (no port)
Version protocolVersion; ///< may originally come from json or be empty -- is populated/reverified later after connection to peer
QString subversion; ///< if we actually managed to connect to the server, its subversion string e.g. "Fulcrum 1.0", or may come initially from features dict
/// These get populated if we managed to connect to the server, or if the server's info came from a features dictionary
QByteArray genesisHash; ///< if known, may be empty, may come from features map
Version protocolMin,
protocolMax;
QString hashFunction = ServerMisc::HashFunction; /// may also come from features map
void clear() { *this = PeerInfo(); }
bool isTor() const { return hostName.toLower().endsWith(".onion"); }
/// minimal checking that hostname is not empty and that at least an ssl or a tcp port are defined.
bool isMinimallyValid() const { return !hostName.isEmpty() && (ssl || tcp) && hashFunction == ServerMisc::HashFunction; }
/// Pass it the "features" map as returned by server.features. Normally only one PeerInfo will be returned, but
/// there may be multiple in the case of .onion in the 'hosts' sub-map. All of the hosts returned have identical
/// members with the exception of .hostName, .ssl, and .tcp which may differ. Will never return an empty list,
/// instead, it will throw if no servers are contained in the map. Will also throw if other minimal checks fail.
static PeerInfoList fromFeaturesMap(const QVariantMap &m) noexcept(false); ///< NOTE: May throw BadFeaturesMap
};
Q_DECLARE_METATYPE(PeerInfo);
Q_DECLARE_METATYPE(PeerInfoList);

10
ServerMisc.cpp Normal file
View file

@ -0,0 +1,10 @@
#include "Common.h"
#include "ServerMisc.h"
namespace ServerMisc
{
const Version MinProtocolVersion(1,4,0);
const Version MaxProtocolVersion(1,4,2);
const QString AppVersion(VERSION);
const QString AppSubVersion = QString("%1 %2").arg(APPNAME).arg(VERSION);
}

35
ServerMisc.h Normal file
View file

@ -0,0 +1,35 @@
//
// Fulcrum - A fast & nimble SPV Server for Bitcoin Cash
// Copyright (C) 2019-2020 Calin A. Culianu <calin.culianu@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program (see LICENSE.txt). If not, see
// <https://www.gnu.org/licenses/>.
//
#pragma once
#include "Version.h"
#include <QString>
namespace ServerMisc
{
constexpr const char * const HashFunction = "sha256";
/// Used in various places to rejects old clients or incompatible peers. Currently 1.4 and 1.4.2 respectively.
extern const Version MinProtocolVersion, MaxProtocolVersion;
extern const QString AppVersion, ///< in string form suitable for sending in protocol or banner e.g. "1.0"
AppSubVersion; ///< e.g. "Fulcrum 1.0"
}

View file

@ -20,6 +20,8 @@
#include "BitcoinD.h"
#include "Merkle.h"
#include "PeerMgr.h"
#include "ServerMisc.h"
#include "Storage.h"
#include "SubsMgr.h"
@ -322,11 +324,6 @@ namespace {
}
/* static */ const Version Server::MinProtocolVersion(1,4,0);
const Version Server::MaxProtocolVersion(1,4,2);
/* static */ const QString Server::AppVersion(VERSION);
const QString Server::AppSubVersion = QString("%1 %2").arg(APPNAME).arg(VERSION);
Server::Server(const QHostAddress &a, quint16 p, const std::shared_ptr<const Options> & opts,
const std::shared_ptr<Storage> &s, const std::shared_ptr<BitcoinDMgr> &bdm)
: AbstractTcpServer(a, p), options(opts), storage(s), bitcoindmgr(bdm)
@ -622,16 +619,30 @@ void Server::generic_async_to_bitcoind(Client *c, const RPC::Message::Id & reqId
void Server::rpc_server_add_peer(Client *c, const RPC::Message &m)
{
// TODO: Implement this. This is a stub implementation always returning true.
const auto map = m.paramsList().front().toMap();
if (map.isEmpty())
throw RPCError(QString("%1 expected a non-empty dictionary argument").arg(m.method));
emit c->sendResult(m.id, true);
bool retval = true;
try {
const auto peerList = PeerInfo::fromFeaturesMap(map); // this may throw BadFeaturesMap
if (peerList.isEmpty() || peerList.front().genesisHash != storage->genesisHash())
throw BadFeaturesMap("Incompatible genesis hash");
const auto peerAddress = c->peerAddress();
Debug() << "add_peer tentatively accepted for host " << peerList.front().hostName << " (" << peerList.size() << ")" << " from " << peerAddress.toString();
emit gotRpcAddPeer(peerList, peerAddress);
} catch (const BadFeaturesMap & e) {
const auto hm = map.value("hosts").toMap();
const QString hostNamePart = !hm.isEmpty() ? QString(" (%1)").arg(hm.firstKey()) : QString();
Debug() << "Refusing add_peer" << hostNamePart << " for reason: " << e.what();
retval = false;
}
emit c->sendResult(m.id, retval);
}
void Server::rpc_server_banner(Client *c, const RPC::Message &m)
{
constexpr int MAX_BANNER_DATA = 16384;
static const QString bannerFallback = QString("Connected to a %1 server").arg(AppSubVersion);
static const QString bannerFallback = QString("Connected to a %1 server").arg(ServerMisc::AppSubVersion);
const QString bannerFile(options->bannerFile);
if (bannerFile.isEmpty() || !QFile::exists(bannerFile) || !QFileInfo(bannerFile).isReadable()) {
// fallback -- banner file invalid/not readable/not specified
@ -654,8 +665,8 @@ void Server::rpc_server_banner(Client *c, const RPC::Message &m)
} else {
// read banner file ok, perform variable substitutions
ret = QString::fromUtf8(bannerFileData)
.replace("$SERVER_VERSION", AppVersion)
.replace("$SERVER_SUBVERSION", AppSubVersion)
.replace("$SERVER_VERSION", ServerMisc::AppVersion)
.replace("$SERVER_SUBVERSION", ServerMisc::AppSubVersion)
.replace("$DONATION_ADDRESS", donationAddress)
.replace("$DAEMON_VERSION", daemonVersion.toString(true))
.replace("$DAEMON_SUBVERSION", subversion)
@ -675,10 +686,10 @@ void Server::rpc_server_features(Client *c, const RPC::Message &m)
QVariantMap r;
r["pruning"] = QVariant(); // null
r["genesis_hash"] = QString(Util::ToHexFast(storage->genesisHash()));
r["server_version"] = AppSubVersion;
r["protocol_min"] = MinProtocolVersion.toString();
r["protocol_max"] = MaxProtocolVersion.toString();
r["hash_function"] = "sha256";
r["server_version"] = ServerMisc::AppSubVersion;
r["protocol_min"] = ServerMisc::MinProtocolVersion.toString();
r["protocol_max"] = ServerMisc::MaxProtocolVersion.toString();
r["hash_function"] = ServerMisc::HashFunction;
QVariantMap hmap;
if (options->publicTcp.has_value())
@ -718,13 +729,13 @@ void Server::rpc_server_version(Client *c, const RPC::Message &m)
throw RPCError(QString("%1 already sent").arg(m.method));
Version ver = l[1].toString().left(kMaxServerVersionLen); // try and parse version, see Version.cpp, QString constructor.
if (!ver.isValid() || ver < MinProtocolVersion || ver > MaxProtocolVersion)
if (!ver.isValid() || ver < ServerMisc::MinProtocolVersion || ver > ServerMisc::MaxProtocolVersion)
throw RPCErrorWithDisconnect("Unsupported protocol version");
c->info.userAgent = l[0].toString().left(kMaxServerVersionLen);
c->info.protocolVersion = ver;
c->info.alreadySentVersion = true;
emit c->sendResult(m.id, QStringList({AppSubVersion, ver.toString()}));
emit c->sendResult(m.id, QStringList({ServerMisc::AppSubVersion, ver.toString()}));
}
/// returns the 'branch' and 'root' keys ready to be put in the results dictionary

View file

@ -21,6 +21,7 @@
#include "Common.h"
#include "Mixins.h"
#include "Options.h"
#include "PeerMgr.h"
#include "RPC.h"
#include "Util.h"
#include "Version.h"
@ -138,12 +139,6 @@ public:
// this must be called in the thread context of this thread
QVariantMap stats() const;
/// Used in various places to rejects old clients or incompatible peers. Currently 1.4 and 1.4.2 respectively.
static const Version MinProtocolVersion, MaxProtocolVersion;
static const QString AppVersion, ///< in string form suitable for sending in protocol or banner e.g. "1.0"
AppSubVersion; ///< e.g. "Fulcrum 1.0"
signals:
/// connected to SrvMgr clientConnected slot by SrvMgr class
void clientConnected(IdMixin::Id clientId, const QHostAddress & remoteAddress);
@ -153,6 +148,10 @@ signals:
/// Connected to SrvMgr parent's "newHeader" signal (which itself is connected to Controller's newHeader).
/// Used to notify clients that are subscribed to headers that a new header has arrived.
void newHeader(unsigned height, const QByteArray &header);
/// Inform PeerMgr of new add_peer request coming in
void gotRpcAddPeer(const PeerInfoList &, const QHostAddress &source);
public slots:
void onMessage(IdMixin::Id clientId, const RPC::Message &m);
void onErrorMessage(IdMixin::Id clientId, const RPC::Message &m);

View file

@ -19,6 +19,7 @@
#include "SrvMgr.h"
#include "BitcoinD.h"
#include "PeerMgr.h"
#include "Servers.h"
#include "Storage.h"
#include "Util.h"
@ -51,12 +52,19 @@ void SrvMgr::startup()
void SrvMgr::cleanup()
{
peermgr.reset(); // unique_ptr, kill peermgr (if any)
servers.clear(); // unique_ptrs auto-delete all servers
}
// throw Exception on error
void SrvMgr::startServers()
{
if (options->peerDiscovery) {
Log() << "SrvMgr: starting PeerMgr ...";
peermgr = std::make_unique<PeerMgr>(storage, options);
peermgr->startup();
} else peermgr.reset();
const auto num = options->interfaces.length() + options->sslInterfaces.length();
Log() << "SrvMgr: starting " << num << " " << Util::Pluralize("service", num) << " ...";
const auto firstSsl = options->interfaces.size();
@ -78,9 +86,16 @@ void SrvMgr::startServers()
// if srv receives this message, it will delete the client then we will get a signal back that it is now gone
connect(this, &SrvMgr::clientExceedsConnectionLimit, srv, qOverload<IdMixin::Id>(&Server::killClient));
if (peermgr) {
connect(srv, &Server::gotRpcAddPeer, peermgr.get(), &PeerMgr::on_rpcAddPeer);
}
srv->tryStart();
++i;
}
if (peermgr)
peermgr->allServersStarted();
}
void SrvMgr::clientConnected(IdMixin::Id cid, const QHostAddress &addr)

View file

@ -27,6 +27,7 @@
#include <memory>
class BitcoinDMgr;
class PeerMgr;
class Server;
class Storage;
@ -70,6 +71,7 @@ private:
std::shared_ptr<Storage> storage;
std::shared_ptr<BitcoinDMgr> bitcoindmgr;
std::list<std::unique_ptr<Server>> servers;
std::unique_ptr<PeerMgr> peermgr; ///< will be nullptr if options->peerDiscovery is false
QMultiHash<QHostAddress, IdMixin::Id> addrIdMap;
};

View file

@ -21,6 +21,7 @@
#include "BTC.h"
#include "Controller.h"
#include "Mixins.h"
#include "PeerMgr.h"
#include "RPC.h"
#include "SrvMgr.h"
@ -44,6 +45,9 @@ void App::register_MetaTypes()
qRegisterMetaType<QHostAddress>("QHostAddress");
qRegisterMetaType<PeerInfo>("PeerInfo");
qRegisterMetaType<PeerInfoList>("PeerInfoList");
registered = true;
}
}

View file

@ -1,3 +1,6 @@
<RCC>
<qresource prefix="/file"/>
<qresource prefix="/">
<file>resources/servers.json</file>
<file>resources/servers_testnet.json</file>
</qresource>
</RCC>

149
resources/servers.json Normal file
View file

@ -0,0 +1,149 @@
{
"bch.imaginary.cash": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4.1"
},
"bch0.kister.net": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4.1"
},
"bch.loping.net": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4.1"
},
"j2tjfxntnsqpojaamnndgmfrc6lh3thattnlpc2xx53h2ojoi7agccid.onion": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4.1",
"display": "bch.loping.net"
},
"bch.soul-dev.com": {
"pruning": "-",
"s": "50002",
"version": "1.4.1"
},
"bchx.disdev.org": {
"pruning": "-",
"s": "50002",
"version": "1.4.2"
},
"bitcoincash.quangld.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4.2"
},
"blackie.c3-soft.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4.1"
},
"crypto.mldlabs.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4.1"
},
"electron-cash.dragon.zone": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4.2"
},
"electron.jochen-hoenicke.de": {
"pruning": "-",
"s": "51002",
"t": "51001",
"version": "1.4.1"
},
"electroncash.dk": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4.2"
},
"electrum.imaginary.cash": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4.1"
},
"electrumx.hillsideinternet.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4.1"
},
"electrum-abc.criptolayer.net": {
"pruning": "-",
"s": "50012",
"version": "1.4.1"
},
"electrumx-cash.1209k.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4.2"
},
"greedyhog.ddns.net": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4.1"
},
"wallet.satoshiscoffeehouse.com": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4.1"
},
"7nshufncf3nmp7pa42oqhnj6whsjgo2eok4jveex62tczuhvqur5ciad.onion": {
"pruning": "-",
"t": "50001",
"version": "1.4.1",
"display": "electrum.imaginary.cash"
},
"kisternetg2pq7wx.onion": {
"pruning": "-",
"t": "50001",
"version": "1.4.1"
},
"electron.coinucopia.io": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4"
},
"bxdp2p6abpqt5etc.onion": {
"pruning": "-",
"t": "50001",
"version": "1.4"
},
"jh3jgcrwweh6yvmprtjnp72u2hqn34nlftlg3msrr4vmlapft4yvt2id.onion": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4"
},
"electroncash.de": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4"
},
"jktsologn7uprtwn7gsgmwuddj6rxsqmwc2vaug7jwcwzm2bxqnfpwad.onion": {
"pruning": "-",
"s": "50002",
"t": "50001",
"version": "1.4",
"display": "electroncash.de"
}
}

View file

@ -0,0 +1,36 @@
{
"bch0.kister.net": {
"s": "51002"
},
"blackie.c3-soft.com": {
"s": "60002"
},
"testnet.imaginary.cash": {
"s": "50002"
},
"testnet.bitcoincash.network": {
"s": "60002"
},
"ebf56u6xk2e2fjlqyuz4zjj2gsrrnavfronrerh7qcvzllknytdgmmyd.onion": {
"s": "50004",
"t": "50003"
},
"jktsologn7uprtwn7gsgmwuddj6rxsqmwc2vaug7jwcwzm2bxqnfpwad.onion": {
"s": "50004",
"t": "50003",
"display": "electroncash.de"
},
"electroncash.de": {
"s": "50004",
"t": "50003"
},
"tbch.loping.net": {
"s": "60002",
"t": "60001"
},
"scgjgc67226l65u52wyvulioxixv34p5dth73oj35ej7ham2zavdtxid.onion": {
"s": "60002",
"t": "60001",
"display": "tbch.loping.net"
}
}