mirror of
https://github.com/cculianu/Fulcrum.git
synced 2026-08-13 12:33:27 +02:00
1366 lines
63 KiB
C++
1366 lines
63 KiB
C++
//
|
|
// 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/>.
|
|
//
|
|
#include "Servers.h"
|
|
|
|
#include "BitcoinD.h"
|
|
#include "Merkle.h"
|
|
#include "PeerMgr.h"
|
|
#include "ServerMisc.h"
|
|
#include "Storage.h"
|
|
#include "SubsMgr.h"
|
|
|
|
#include <QByteArray>
|
|
#include <QCoreApplication>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QtNetwork>
|
|
#include <QString>
|
|
#include <QTextCodec>
|
|
#include <QTextStream>
|
|
#include <QTimer>
|
|
|
|
#include <cstdlib>
|
|
#include <list>
|
|
#include <utility>
|
|
|
|
TcpServerError::~TcpServerError() {} // for vtable
|
|
|
|
AbstractTcpServer::AbstractTcpServer(const QHostAddress &a, quint16 p)
|
|
: QTcpServer(nullptr), IdMixin(newId()), addr(a), port(p)
|
|
{
|
|
assert(qobj()); // Runtime check that derived class followed the rules outlined at the top of Mixins.h
|
|
|
|
_thread.setObjectName(prettyName());
|
|
setObjectName(prettyName());
|
|
}
|
|
|
|
AbstractTcpServer::~AbstractTcpServer()
|
|
{
|
|
Debug() << __FUNCTION__;
|
|
stop();
|
|
}
|
|
|
|
QString AbstractTcpServer::hostPort() const
|
|
{
|
|
return QString("%1:%2").arg(addr.toString()).arg(port);
|
|
}
|
|
|
|
QString AbstractTcpServer::prettyName() const
|
|
{
|
|
return QString("Srv %1").arg(hostPort());
|
|
}
|
|
|
|
void AbstractTcpServer::tryStart(ulong timeout_ms)
|
|
{
|
|
if (!_thread.isRunning()) {
|
|
ThreadObjectMixin::start(); // call super
|
|
Log() << "Starting listener service for " << prettyName() << " ...";
|
|
if (auto result = chan.get<QString>(timeout_ms); result != "ok") {
|
|
result = result.isEmpty() ? "Startup timed out!" : result;
|
|
throw TcpServerError(result);
|
|
}
|
|
Log() << "Service started, listening for connections on " << hostPort();
|
|
} else {
|
|
throw TcpServerError(prettyName() + " already started");
|
|
}
|
|
}
|
|
|
|
void AbstractTcpServer::on_started()
|
|
{
|
|
QString result = "ok";
|
|
conns.push_back(connect(this, SIGNAL(newConnection()), this,SLOT(pvt_on_newConnection())));
|
|
conns.push_back(connect(this, &QTcpServer::acceptError, this, [this](QAbstractSocket::SocketError e){ on_acceptError(e);}));
|
|
if (!listen(addr, port)) {
|
|
result = errorString();
|
|
result = result.isEmpty() ? "Error binding/listening for connections" : QString("Could not bind to %1: %2").arg(hostPort()).arg(result);
|
|
Debug() << __FUNCTION__ << " listen failed";
|
|
} else {
|
|
Debug() << "started ok";
|
|
}
|
|
chan.put(result);
|
|
}
|
|
|
|
void AbstractTcpServer::on_finished()
|
|
{
|
|
close(); /// stop listening
|
|
chan.put("finished");
|
|
Debug() << objectName() << " finished.";
|
|
ThreadObjectMixin::on_finished();
|
|
}
|
|
|
|
void AbstractTcpServer::on_acceptError(QAbstractSocket::SocketError e)
|
|
{
|
|
Error() << objectName() << "; error acceptError, code: " << int(e);
|
|
}
|
|
|
|
/*static*/
|
|
QString AbstractTcpServer::prettySock(QAbstractSocket *sock)
|
|
{
|
|
return QString("%1:%2")
|
|
.arg(sock ? sock->peerAddress().toString() : "(null)")
|
|
.arg(sock ? sock->peerPort() : 0);
|
|
}
|
|
|
|
void AbstractTcpServer::pvt_on_newConnection()
|
|
{
|
|
QTcpSocket *sock = nextPendingConnection();
|
|
if (sock) {
|
|
Debug() << "Got connection from: " << prettySock(sock);
|
|
on_newConnection(sock);
|
|
} else {
|
|
Warning() << __FUNCTION__ << ": nextPendingConnection returned a nullptr! Called at the wrong time? FIXME!";
|
|
}
|
|
}
|
|
|
|
|
|
SimpleHttpServer::SimpleHttpServer(const QHostAddress &listenAddr, quint16 listenPort, qint64 maxBuffer, qint64 timeLimit)
|
|
: AbstractTcpServer(listenAddr, listenPort), MAX_BUFFER(maxBuffer > 0 ? maxBuffer : DEFAULT_MAX_BUFFER),
|
|
TIME_LIMIT(timeLimit)
|
|
{
|
|
// re-set name for debug/logging
|
|
_thread.setObjectName(prettyName());
|
|
setObjectName(prettyName());
|
|
}
|
|
|
|
QString SimpleHttpServer::prettyName() const
|
|
{
|
|
return QString("Http%1").arg(AbstractTcpServer::prettyName());
|
|
}
|
|
|
|
void SimpleHttpServer::on_newConnection(QTcpSocket *sock)
|
|
{
|
|
sock->setReadBufferSize(MAX_BUFFER);
|
|
const QString sockName(prettySock(sock));
|
|
connect(sock, &QAbstractSocket::disconnected, this, [sock,sockName] {
|
|
Debug() << sockName << " disconnected";
|
|
sock->deleteLater();
|
|
});
|
|
connect(sock, &QObject::destroyed, this, [sockName](QObject *){
|
|
Debug() << sockName << " destroyed";
|
|
});
|
|
connect(sock, &QAbstractSocket::readyRead, this, [sock,sockName,this] {
|
|
try {
|
|
while(sock->canReadLine()) {
|
|
auto line = QString(sock->readLine()).trimmed();
|
|
//Debug() << sockName << " Got line: " << line;
|
|
if (QString loc = sock->property("req-loc").toString(); loc.isEmpty()) {
|
|
auto toks = line.split(" ");
|
|
if (toks.length() != 3 || (toks[0] != "GET" && toks[1] != "POST") || toks[2] != "HTTP/1.1")
|
|
throw Exception(QString("Invalid request: %1").arg(line));
|
|
Trace() << sockName << " " << line;
|
|
sock->setProperty("req-loc", toks[1]);
|
|
sock->setProperty("req-meth", toks[0]);
|
|
sock->setProperty("req-ver", toks[2]);
|
|
} else if (auto loc = sock->property("req-loc").toString(),
|
|
meth = sock->property("req-meth").toString(),
|
|
ver = sock->property("req-ver").toString();
|
|
line.isEmpty() && !loc.isEmpty() && !meth.isEmpty() && !ver.isEmpty()) {
|
|
// got line by itself, prepare response
|
|
Request req;
|
|
auto & response = req.response;
|
|
req.httpVersion = ver;
|
|
req.method = meth == "GET" ? Method::GET : Method::POST;
|
|
auto vmap = sock->property("req-header").toMap();
|
|
for (auto it = vmap.begin(); it != vmap.end(); ++it)
|
|
// save header
|
|
req.header[it.key()] = it.value().toString();
|
|
if (auto i = loc.indexOf('?'); i > -1) {
|
|
req.queryString = loc.mid(i+1);
|
|
req.endPoint = loc.left(i);
|
|
} else
|
|
req.endPoint = loc;
|
|
if (auto it = endPoints.find(req.endPoint); it != endPoints.end() || (it=endPoints.find("*")) != endPoints.end()) {
|
|
it.value()(req); // call lambda
|
|
} else {
|
|
// could not find any enpoints that match, set up a 404 response
|
|
response.status = 404;
|
|
response.statusText = "Unknown resource";
|
|
response.data = err404Msg.toUtf8();
|
|
}
|
|
// setup header
|
|
QByteArray responseHeader;
|
|
{
|
|
QTextStream ss(&responseHeader, QIODevice::WriteOnly);
|
|
ss.setCodec(QTextCodec::codecForName("UTF-8"));
|
|
ss << "HTTP/1.1 " << response.status << " " << req.response.statusText.trimmed() << "\r\n";
|
|
ss << "Content-Type: " << response.contentType.trimmed() << "\r\n";
|
|
ss << "Content-Length: " << response.data.length() << "\r\n";
|
|
ss << response.headerExtra;
|
|
ss << "\r\n";
|
|
}
|
|
const qint64 respTotalLen = responseHeader.length() + response.data.length();
|
|
sock->setProperty("resp-len", respTotalLen);
|
|
// write out header
|
|
sock->write(responseHeader);
|
|
if (response.data.length())
|
|
// write out response data
|
|
sock->write(response.data);
|
|
} else {
|
|
// save params
|
|
auto vmap = sock->property("req-header").toMap();
|
|
auto toks = line.split(": ");
|
|
if (toks.length() >= 2) {
|
|
auto name = toks.front(); toks.pop_front();
|
|
auto value = toks.join(": ");
|
|
vmap[name] = value;
|
|
sock->setProperty("req-header", vmap);
|
|
} else
|
|
throw Exception("garbage data, closing connection");
|
|
}
|
|
}
|
|
|
|
if (sock->bytesAvailable() > MAX_BUFFER)
|
|
throw Exception("too much data, closing connection");
|
|
|
|
} catch (const std::exception &e) {
|
|
Warning() << "Client: " << sockName << "; " << e.what();
|
|
sock->abort();
|
|
sock->deleteLater();
|
|
}
|
|
});
|
|
connect(sock, &QAbstractSocket::bytesWritten, this, [sock, sockName](qint64 bytes) {
|
|
qint64 nWrit = sock->property("resp-written").toLongLong() + bytes;
|
|
sock->setProperty("resp-written", nWrit);
|
|
auto var = sock->property("resp-len");
|
|
if (const auto n2write = var.toLongLong(); !var.isNull() && nWrit >= n2write) {
|
|
// graceful disconnect
|
|
Debug() << sockName << " wrote " << nWrit << "/" << n2write << " bytes, disconnecting";
|
|
sock->disconnectFromHost();
|
|
} else {
|
|
Trace() << sockName << " wrote: " << bytes << " bytes";
|
|
}
|
|
});
|
|
if (TIME_LIMIT > 0) {
|
|
QTimer::singleShot(TIME_LIMIT, sock, [sock, sockName, this]{
|
|
Debug() << sockName << " killing connection after " << (TIME_LIMIT/1e3) << " seconds";
|
|
sock->abort();
|
|
sock->deleteLater();
|
|
});
|
|
}
|
|
}
|
|
|
|
void SimpleHttpServer::addEndpoint(const QString &endPoint, const Lambda &callback)
|
|
{
|
|
if (!endPoint.startsWith("/") && endPoint != "*")
|
|
Warning() << __FUNCTION__ << " endPoint " << endPoint << " does not start with '/' -- it will never be reached! FIXME!";
|
|
endPoints[endPoint] = callback;
|
|
}
|
|
|
|
|
|
// ---- Classes Server & Client ----
|
|
|
|
// class Server constants
|
|
namespace {
|
|
// TODO: maybe move these to a more global place? For now here is fine.
|
|
namespace Constants {
|
|
constexpr int kMaxServerVersionLen = 80, ///< the maximum server version length we accept to prevent memory exhaustion attacks
|
|
kMaxTxHex = 2*1024*1024, ///< >1MB raw tx max (over 1 MiB, 1 traditional PoT MB should be enough).
|
|
kMaxErrorCount = 10; ///< The maximum number of errors we tolerate from a Client before disconnecting them.
|
|
|
|
// types in a Message.params object that we accept as booleans
|
|
const std::set<QVariant::Type> acceptableBoolVariantTypes = {
|
|
QVariant::Type::Bool, QVariant::Type::Int, QVariant::Type::UInt, QVariant::Type::LongLong,
|
|
QVariant::Type::ULongLong, QVariant::Type::Double,
|
|
};
|
|
|
|
}
|
|
using namespace Constants;
|
|
|
|
std::pair<bool, bool> parseBoolSemiLooselyButNotTooLoosely(const QVariant &v) {
|
|
std::pair<bool, bool> ret{false, false};
|
|
if (acceptableBoolVariantTypes.count(v.type()))
|
|
ret = {v.toBool(), true};
|
|
return ret;
|
|
}
|
|
|
|
QString formatBitcoinDErrorResponseToLookLikeDumbElectrumXPythonRepr(const RPC::Message &errResponse){
|
|
constexpr auto escapeSingleQuote = [](const QString &s) -> QString {
|
|
const QByteArray b = s.toUtf8();
|
|
QByteArray ret;
|
|
ret.reserve(int(b.length()*1.5));
|
|
bool inesc = false;
|
|
for (int i = 0; i < b.size(); ++i) {
|
|
const char c = b[i];
|
|
if (c == '\\')
|
|
inesc = !inesc;
|
|
else if (c == '\'' && !inesc)
|
|
ret.push_back('\\');
|
|
else
|
|
inesc = false;
|
|
ret.push_back(c);
|
|
}
|
|
return QString::fromUtf8(ret);
|
|
};
|
|
return QString("daemon error: DaemonError({'code': %1, 'message': '%2'})")
|
|
.arg(errResponse.errorCode()).arg(escapeSingleQuote(errResponse.errorMessage()));
|
|
}
|
|
|
|
/// used internally by RPC methods. Given a hashHex, ensure it's 32 bytes (or DataLen) of hash data and nothing else.
|
|
/// Returns DataLen (default=32) bytes of valid hex decoded data or an empty QByteArray on failure.
|
|
QByteArray validateHashHex(const QString & hashHex, const int DataLen = HashLen) {
|
|
QByteArray ret = hashHex.trimmed().left(DataLen*2).toUtf8();
|
|
// ugh, QByteArray returns dummy bytes at the end if it can't fully parse. So we have to check the original
|
|
// hash byte length as well.
|
|
if (ret.length() != DataLen*2 || (ret = QByteArray::fromHex(ret)).length() != DataLen)
|
|
ret.clear();
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
|
|
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)
|
|
{
|
|
// re-set name for debug/logging
|
|
_thread.setObjectName(prettyName());
|
|
setObjectName(prettyName());
|
|
StaticData::init(); // only does something first time it's called, otherwise a no-op
|
|
}
|
|
|
|
Server::~Server() { stop(); } // paranoia about pure virtual, and vtable consistency, etc
|
|
|
|
QString Server::prettyName() const
|
|
{
|
|
return QString("Tcp%1").arg(AbstractTcpServer::prettyName());
|
|
}
|
|
|
|
// this must be called in the thread context of this thread
|
|
QVariantMap Server::stats() const
|
|
{
|
|
QVariantMap ret;
|
|
ret["numClients"] = clientsById.count();
|
|
QVariantList clientList;
|
|
for (const auto & client : clientsById) {
|
|
// note we call this thread-unsafe function stats() here because client lives in our thread. but if that design
|
|
// changes, update this to call client->statsSafe(100) instead
|
|
auto map = client->stats().toMap();
|
|
auto name = map.take("name").toString();
|
|
map["version"] = QVariantList({client->info.uaVersion().toString(), client->info.protocolVersion.toString()});
|
|
map["userAgent"] = client->info.userAgent;
|
|
map["errCt"] = client->info.errCt;
|
|
map["nRequestsRcv"] = client->info.nRequestsRcv;
|
|
map["isSubscribedToHeaders"] = client->isSubscribedToHeaders;
|
|
map["nSubscriptions"] = client->nShSubs.load();
|
|
// the below don't really make much sense for this class (they are always 0 or empty)
|
|
map.remove("nDisconnects");
|
|
map.remove("nSocketErrors");
|
|
map.remove("lastSocketError");
|
|
map.remove("nUnansweredRequests");
|
|
map.remove("nRequestsSent");
|
|
clientList.append(QVariantMap({{name, map}}));
|
|
}
|
|
ret["clients"] = clientList;
|
|
return ret;
|
|
}
|
|
|
|
void Server::on_started()
|
|
{
|
|
AbstractTcpServer::on_started();
|
|
|
|
// refresh bitcionD version info, server version & subversion, whenever it comes back (in case user upgraded bitcoind from under our feet!)
|
|
conns += connect(bitcoindmgr.get(), &BitcoinDMgr::gotFirstGoodConnection, this, &Server::refreshBitcoinDNetworkInfo);
|
|
// try and refresh once now as soon as we come alive (usually we don't come alive until bitcoind connections are established so this is ok)
|
|
refreshBitcoinDNetworkInfo();
|
|
}
|
|
|
|
void Server::on_newConnection(QTcpSocket *sock) { newClient(sock); }
|
|
|
|
Client *
|
|
Server::newClient(QTcpSocket *sock)
|
|
{
|
|
const auto clientId = newId();
|
|
auto ret = clientsById[clientId] = new Client(rpcMethods(), clientId, this, sock, options->maxBuffer);
|
|
const auto addr = ret->peerAddress();
|
|
// if deleted, we need to purge it from map
|
|
auto on_destroyed = [clientId, addr, this](QObject *o) {
|
|
// this whole call is here so that delete client->sock ends up auto-removing the map entry
|
|
// as a convenience.
|
|
Debug() << "Client nested 'on_destroyed' called";
|
|
auto client = clientsById.take(clientId);
|
|
if (client) {
|
|
if (client != o) {
|
|
Error() << " client != passed-in pointer to on_destroy in " << __FILE__ << " line " << __LINE__ << ". FIXME!";
|
|
}
|
|
Debug("client id %ld purged from map", long(clientId));
|
|
}
|
|
// tell SrvMgr this client is gone so it can decrement its clients-per-ip count.
|
|
emit clientDisconnected(clientId, addr);
|
|
};
|
|
connect(ret, &QObject::destroyed, this, on_destroyed);
|
|
connect(ret, &AbstractConnection::lostConnection, this, [this, clientId](AbstractConnection *cl){
|
|
if (auto client = dynamic_cast<Client *>(cl) ; client) {
|
|
Debug() << client->prettyName() << " lost connection";
|
|
killClient(client);
|
|
} else {
|
|
Error() << "Internal error: lostConnection callback received null client! (expected client id: " << clientId << ")";
|
|
}
|
|
});
|
|
connect(ret, &RPC::ConnectionBase::gotMessage, this, &Server::onMessage);
|
|
connect(ret, &RPC::ConnectionBase::gotErrorMessage, this, &Server::onErrorMessage);
|
|
connect(ret, &RPC::ConnectionBase::peerError, this, &Server::onPeerError);
|
|
|
|
// tell SrvMgr about this client so it can keep track of clients-per-ip and other statistics
|
|
emit clientConnected(clientId, addr);
|
|
|
|
return ret;
|
|
}
|
|
|
|
void Server::killClient(Client *client)
|
|
{
|
|
if (!client)
|
|
return;
|
|
Debug() << __FUNCTION__ << " (id: " << client->id << ")";
|
|
clientsById.remove(client->id); // ensure gone from map asap so future lookups fail
|
|
client->do_disconnect();
|
|
}
|
|
// public slot
|
|
void Server::killClient(IdMixin::Id clientId)
|
|
{
|
|
killClient(clientsById.take(clientId));
|
|
}
|
|
// public slot
|
|
void Server::killClientsByAddress(const QHostAddress &address)
|
|
{
|
|
std::list<Client *> clientsMatched;
|
|
int ctr = 0;
|
|
for (auto it = clientsById.begin(); it != clientsById.end(); ++it) {
|
|
if (auto client = it.value(); client->peerAddress() == address)
|
|
clientsMatched.push_back(client); // save to list so as to not mutate hashtable while iterating...
|
|
}
|
|
for (auto & client : clientsMatched) {
|
|
killClient(client);
|
|
++ctr;
|
|
}
|
|
if (ctr)
|
|
Debug() << "Killed " << ctr << Util::Pluralize(" client", ctr) << " matching address: " << address.toString();
|
|
}
|
|
|
|
void Server::onMessage(IdMixin::Id clientId, const RPC::Message &m)
|
|
{
|
|
Trace() << "onMessage: " << clientId << " json: " << m.toJsonString();
|
|
if (Client *c = getClient(clientId); c) {
|
|
auto member = StaticData::dispatchTable.value(m.method);
|
|
if (!member)
|
|
Error() << "Unknown method: \"" << m.method << "\". This shouldn't happen. FIXME! Json: " << m.toJsonString();
|
|
else {
|
|
// indicate a good request, accepted request
|
|
++c->info.nRequestsRcv;
|
|
try {
|
|
// call ptr to member -- note member is free to throw if it wants to send an error immediately
|
|
(this->*member)(c, m);
|
|
} catch (const RPCError & e) {
|
|
emit c->sendError(e.disconnect, e.code, e.what(), m.id);
|
|
} catch (const std::exception & e) {
|
|
emit c->sendError(false, RPC::ErrorCodes::Code_InternalError,
|
|
QString("internal error: %1").arg(e.what()), m.id);
|
|
} catch (...) {
|
|
emit c->sendError(false, RPC::ErrorCodes::Code_InternalError, "internal error: unknown", m.id);
|
|
}
|
|
}
|
|
} else {
|
|
Debug() << "Unknown client: " << clientId;
|
|
}
|
|
}
|
|
void Server::onErrorMessage(IdMixin::Id clientId, const RPC::Message &m)
|
|
{
|
|
Trace() << "onErrorMessage: " << clientId << " json: " << m.toJsonString();
|
|
if (Client *c = getClient(clientId); c) {
|
|
// we never expect client to send us errors. Always return invalid request, disconnect client.
|
|
emit c->sendError(true, RPC::Code_InvalidRequest, "Not a valid request object");
|
|
}
|
|
}
|
|
void Server::onPeerError(IdMixin::Id clientId, const QString &what)
|
|
{
|
|
Debug() << "onPeerError, client " << clientId << " error: " << what;
|
|
if (Client *c = getClient(clientId); c) {
|
|
if (++c->info.errCt - c->info.nRequestsRcv >= kMaxErrorCount) {
|
|
Warning() << "Excessive errors (" << kMaxErrorCount << ") for: " << c->prettyName() << ", disconnecting";
|
|
killClient(c);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Server::refreshBitcoinDNetworkInfo()
|
|
{
|
|
bitcoindmgr->submitRequest(this, newId(), "getnetworkinfo", QVariantList(),
|
|
// success
|
|
[this](const RPC::Message & reply) {
|
|
const QVariantMap networkInfo = reply.result().toMap();
|
|
bool ok = false;
|
|
const auto val = networkInfo.value("version", 0).toUInt(&ok);
|
|
if (ok) {
|
|
// e.g. 0.20.6 comes in like this from bitcoind (as an unsigned int): 200600
|
|
bitcoinDInfo.version = Version(val, Version::BitcoinD);
|
|
//Debug() << "Refreshed version info from bitcoind";
|
|
} else {
|
|
Warning() << "Failed to get version info from bitcoind";
|
|
}
|
|
bitcoinDInfo.relayFee = networkInfo.value("relayfee", 0.0).toDouble();
|
|
bitcoinDInfo.subversion = networkInfo.value("subversion", "").toString();
|
|
});
|
|
}
|
|
|
|
// --- RPC METHODS ---
|
|
namespace {
|
|
Util::ThreadPool::FailFunc defaultTPFailFunc(Client *c, const RPC::Message::Id &id) {
|
|
return [c, id](const QString &what) {
|
|
emit c->sendError(false, RPC::Code_InternalError, QString("internal error: %1").arg(what), id);
|
|
};
|
|
}
|
|
BitcoinDMgr::FailF defaultBDFailFunc(Client *c, const RPC::Message::Id &id) {
|
|
return [c, id](const RPC::Message::Id &, const QString &what) {
|
|
emit c->sendError(false, RPC::Code_InternalError, QString("internal error: %1").arg(what), id);
|
|
};
|
|
}
|
|
}
|
|
|
|
Server::RPCError::~RPCError() {}
|
|
Server::RPCErrorWithDisconnect::~RPCErrorWithDisconnect() {}
|
|
|
|
void Server::generic_do_async(Client *c, const RPC::Message::Id &reqId, const std::function<QVariant ()> &work)
|
|
{
|
|
if (LIKELY(work)) {
|
|
struct ResErr {
|
|
QVariant results;
|
|
bool error = false, doDisconnect = false;
|
|
QString errMsg;
|
|
int errCode = 0;
|
|
};
|
|
|
|
auto reserr = std::make_shared<ResErr>(); ///< shared with lambda for both work and completion. this is how they communicate.
|
|
|
|
Util::ThreadPool::SubmitWork(
|
|
c, // <--- all work done in client context, so if client is deleted, completion not called
|
|
// runs in worker thread, must not access anything other than reserr and work
|
|
[reserr,work]{
|
|
try {
|
|
QVariant result = work();
|
|
reserr->results.swap( result ); // constant-time copy
|
|
} catch (const RPCError & e) {
|
|
reserr->error = true;
|
|
reserr->doDisconnect = e.disconnect;
|
|
reserr->errMsg = e.what();
|
|
reserr->errCode = e.code;
|
|
}
|
|
},
|
|
// completion: runs in client thread (only called if client not already deleted)
|
|
[c, reqId, reserr] {
|
|
if (reserr->error) {
|
|
emit c->sendError(reserr->doDisconnect, reserr->errCode, reserr->errMsg, reqId);
|
|
return;
|
|
}
|
|
// no error, send results to client
|
|
emit c->sendResult(reqId, reserr->results);
|
|
},
|
|
// default fail function just sends json rpc error "internal error: <message>"
|
|
defaultTPFailFunc(c, reqId)
|
|
);
|
|
} else
|
|
Error() << "INTERNAL ERROR: work must be valid! FIXME!";
|
|
}
|
|
|
|
void Server::generic_async_to_bitcoind(Client *c, const RPC::Message::Id & reqId, const QString &method,
|
|
const QVariantList & params,
|
|
const BitcoinDSuccessFunc & successFunc,
|
|
const BitcoinDErrorFunc & errorFunc)
|
|
{
|
|
if (UNLIKELY(QThread::currentThread() != c->thread())) {
|
|
// Paranoia, in case I or a future programmer forgets this rule.
|
|
Warning() << __FUNCTION__ << " is meant to be called from the Client thread only. The current thread is not the"
|
|
<< " Client thread. This may cause problems if the Client is deleted while submitting the request. FIXME!";
|
|
}
|
|
bitcoindmgr->submitRequest(c, newId(), method, params,
|
|
// success
|
|
[c, reqId, successFunc](const RPC::Message & reply) {
|
|
try {
|
|
const QVariant result = successFunc ? successFunc(reply) : reply.result(); // if no successFunc specified, use default which just copies the result to the client.
|
|
emit c->sendResult(reqId, result);
|
|
} catch (const RPCError &e) {
|
|
emit c->sendError(e.disconnect, e.code, e.what(), reqId);
|
|
} catch (const std::exception &e) {
|
|
emit c->sendError(false, RPC::ErrorCodes::Code_InternalError, e.what(), reqId);
|
|
}
|
|
},
|
|
// error
|
|
[c, reqId, errorFunc](const RPC::Message & errorReply) {
|
|
try {
|
|
if (errorFunc)
|
|
errorFunc(errorReply); // this should throw RPCError
|
|
throw RPCError(errorReply.errorMessage(), RPC::Code_App_DaemonError);
|
|
} catch (const RPCError &e) {
|
|
emit c->sendError(e.disconnect, e.code, e.what(), reqId);
|
|
} catch (const std::exception &e) {
|
|
emit c->sendError(false, RPC::ErrorCodes::Code_InternalError, QString("internal error: %1").arg(e.what()),
|
|
reqId);
|
|
}
|
|
},
|
|
// use default function on failure, sends json rpc error "internal error: <message>"
|
|
defaultBDFailFunc(c, reqId)
|
|
);
|
|
}
|
|
|
|
void Server::rpc_server_add_peer(Client *c, const RPC::Message &m)
|
|
{
|
|
const auto map = m.paramsList().front().toMap();
|
|
if (map.isEmpty())
|
|
throw RPCError(QString("%1 expected a non-empty dictionary argument").arg(m.method));
|
|
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(ServerMisc::AppSubVersion);
|
|
const QString bannerFile(options->bannerFile);
|
|
if (bannerFile.isEmpty() || !QFile::exists(bannerFile) || !QFileInfo(bannerFile).isReadable()) {
|
|
// fallback -- banner file invalid/not readable/not specified
|
|
emit c->sendResult(m.id, bannerFallback);
|
|
} else {
|
|
// banner file specified, now let's open it up in a worker thread to keep the server responsive and return immediately
|
|
generic_do_async(c, m.id,
|
|
[bannerFile = options->bannerFile,
|
|
donationAddress = options->donationAddress,
|
|
daemonVersion = bitcoinDInfo.version,
|
|
subversion = bitcoinDInfo.subversion] {
|
|
QVariant ret;
|
|
QFile bf(bannerFile);
|
|
if (QByteArray bannerFileData;
|
|
!bf.open(QIODevice::ReadOnly) || ((bannerFileData = bf.read(MAX_BANNER_DATA)).isEmpty()
|
|
&& bf.error() != QFile::FileError::NoError))
|
|
{
|
|
// error reading banner file, just send the fallback
|
|
ret = bannerFallback;
|
|
} else {
|
|
// read banner file ok, perform variable substitutions
|
|
ret = QString::fromUtf8(bannerFileData)
|
|
.replace("$SERVER_VERSION", ServerMisc::AppVersion)
|
|
.replace("$SERVER_SUBVERSION", ServerMisc::AppSubVersion)
|
|
.replace("$DONATION_ADDRESS", donationAddress)
|
|
.replace("$DAEMON_VERSION", daemonVersion.toString(true))
|
|
.replace("$DAEMON_SUBVERSION", subversion)
|
|
.left(MAX_BANNER_DATA); ///< Note this ".left()" is really not in bytes but unicode codepoints, that's fine, the limit is a soft limit
|
|
}
|
|
// send result to client (either the fallback or the real deal)
|
|
return ret;
|
|
});
|
|
}
|
|
}
|
|
void Server::rpc_server_donation_address(Client *c, const RPC::Message &m)
|
|
{
|
|
emit c->sendResult(m.id, options->donationAddress);
|
|
}
|
|
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"] = 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())
|
|
hmap["tcp_port"] = unsigned(options->publicTcp.value());
|
|
if (options->publicSsl.has_value())
|
|
hmap["ssl_port"] = unsigned(options->publicSsl.value());
|
|
if (hmap.isEmpty()) {
|
|
// This should not normally happen but it can if user specified public_tcp_port=0 and public_ssl_port=0.
|
|
// In that case we have to report SOMETHING here as per electrumx protocol specs, so we just use the local port
|
|
// that the client is connected to right now. TODO: verify if we can get away with an empty dictionary here
|
|
// instead?
|
|
const QString key = c->isSsl() ? "ssl_port" : "tcp_port";
|
|
hmap[key] = unsigned(c->localPort());
|
|
}
|
|
const QString hostName = options->hostName.value_or(c->localAddress().toString());
|
|
|
|
r["hosts"] = QVariantMap{
|
|
{ hostName, hmap },
|
|
};
|
|
emit c->sendResult(m.id, r);
|
|
}
|
|
void Server::rpc_server_peers_subscribe(Client *c, const RPC::Message &m)
|
|
{
|
|
// TODO: Implement. See: https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server-peers-subscribe
|
|
emit c->sendResult(m.id, QVariantList());
|
|
}
|
|
void Server::rpc_server_ping(Client *c, const RPC::Message &m)
|
|
{
|
|
emit c->sendResult(m.id);
|
|
}
|
|
void Server::rpc_server_version(Client *c, const RPC::Message &m)
|
|
{
|
|
QVariantList l = m.paramsList();
|
|
assert(l.size() == 2);
|
|
|
|
if (c->info.alreadySentVersion)
|
|
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 < 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({ServerMisc::AppSubVersion, ver.toString()}));
|
|
}
|
|
|
|
/// returns the 'branch' and 'root' keys ready to be put in the results dictionary
|
|
auto Server::getHeadersBranchAndRoot(unsigned height, unsigned cp_height) -> HeadersBranchAndRootPair
|
|
{
|
|
HeadersBranchAndRootPair ret;
|
|
if (height <= cp_height) {
|
|
auto pair = storage->headerBranchAndRoot(height, cp_height);
|
|
auto & [branch, root] = pair;
|
|
std::reverse(root.begin(), root.end());
|
|
ret.second = QString(Util::ToHexFast(root)); // we cast to QString to prevent JSON null for empty string ""
|
|
QVariantList & branchList = ret.first;
|
|
branchList.reserve(int(branch.size()));
|
|
for (auto & item : branch) {
|
|
std::reverse(item.begin(), item.end());
|
|
branchList.push_back(Util::ToHexFast(item));
|
|
if (branchList.back().isNull())
|
|
// this should never happen, but it pays to be paranoid
|
|
throw InternalError("bad data retrieved from merkle cache");
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
|
|
void Server::rpc_blockchain_block_header(Client *c, const RPC::Message &m)
|
|
{
|
|
QVariantList l = m.paramsList();
|
|
assert(!l.isEmpty());
|
|
bool ok;
|
|
const unsigned height = l.front().toUInt(&ok);
|
|
if (!ok || height >= Storage::MAX_HEADERS)
|
|
throw RPCError("Invalid height");
|
|
ok = true;
|
|
const unsigned cp_height = l.size() > 1 ? l.back().toUInt(&ok) : 0;
|
|
if (!ok || cp_height >= Storage::MAX_HEADERS)
|
|
throw RPCError("Invalid cp_height");
|
|
if (cp_height) {
|
|
const auto tip = storage->latestTip().first;
|
|
if (tip < 0) throw InternalError("chain height is negative");
|
|
if ( ! (height <= cp_height && cp_height <= unsigned(tip)) )
|
|
throw RPCError(QString("header height %1 must be <= cp_height %2 which must be <= chain height %3")
|
|
.arg(height).arg(cp_height).arg(tip));
|
|
}
|
|
generic_do_async(c, m.id, [height, cp_height, this] {
|
|
QString err;
|
|
const auto optHdr = storage->headerForHeight(height, &err); // may return nothing (but will set err) if height is now beyond chain height due to reorg
|
|
if (QByteArray hdr; err.isEmpty() && optHdr.has_value() && !(hdr = optHdr.value()).isEmpty()) {
|
|
const auto hexHdr = Util::ToHexFast(hdr); // hexHdr definitely not empty, no need to cast to QString (avoids a copy!)
|
|
QVariant ret;
|
|
if (!cp_height)
|
|
ret = hexHdr;
|
|
else {
|
|
const auto [branch, root] = getHeadersBranchAndRoot(height, cp_height); // may throw if chain mutated and height or cp_height are no longer legal
|
|
ret = QVariantMap{
|
|
{ "header" , hexHdr },
|
|
{ "branch", branch },
|
|
{ "root", root },
|
|
};
|
|
}
|
|
return ret;
|
|
} else
|
|
throw RPCError(err.isEmpty() ? "Unknown error" : err);
|
|
});
|
|
}
|
|
|
|
void Server::rpc_blockchain_block_headers(Client *c, const RPC::Message &m)
|
|
{
|
|
QVariantList l = m.paramsList();
|
|
assert(l.size() >= 2);
|
|
bool ok;
|
|
const unsigned height = l.front().toUInt(&ok);
|
|
if (!ok || height >= Storage::MAX_HEADERS)
|
|
throw RPCError("Invalid height");
|
|
unsigned count = l[1].toUInt(&ok);
|
|
if (!ok || count >= Storage::MAX_HEADERS)
|
|
throw RPCError("Invalid count");
|
|
const auto tip = storage->latestTip().first;
|
|
if (tip < 0) throw InternalError("chain height is negative");
|
|
static constexpr unsigned MAX_COUNT = 2016; ///< TODO: make this cofigurable. this is the current electrumx limit, for now.
|
|
count = std::min(std::min(unsigned(tip+1) - height, count), MAX_COUNT);
|
|
ok = true;
|
|
const unsigned cp_height = l.size() > 2 ? l.back().toUInt(&ok) : 0;
|
|
if (!ok || cp_height >= Storage::MAX_HEADERS)
|
|
throw RPCError("Invalid cp_height");
|
|
if (count && cp_height) {
|
|
if ( ! (height + (count - 1) <= cp_height && cp_height <= unsigned(tip)) )
|
|
throw RPCError(QString("header height + (count - 1) %1 must be <= cp_height %2 which must be <= chain height %3")
|
|
.arg(height + (count - 1)).arg(cp_height).arg(tip));
|
|
}
|
|
generic_do_async(c, m.id, [height, count, cp_height, this] {
|
|
// EX doesn't seem to return error here if invalid height/no results, so we will do same.
|
|
const auto hdrs = storage->headersFromHeight(height, std::min(count, MAX_COUNT));
|
|
const size_t nHdrs = hdrs.size(), hdrSz = size_t(BTC::GetBlockHeaderSize()), hdrHexSz = hdrSz*2;
|
|
QByteArray hexHeaders(int(nHdrs * hdrHexSz), Qt::Uninitialized);
|
|
for (size_t i = 0, offset = 0; i < nHdrs; ++i, offset += hdrHexSz) {
|
|
const auto & hdr = hdrs[i];
|
|
if (UNLIKELY(hdr.size() != int(hdrSz))) { // ensure header looks the right size
|
|
// this should never happen.
|
|
Error() << "Header size from db height " << i + height << " is not " << hdrSz << " bytes! Database corruption likely! FIXME!";
|
|
throw RPCError("Server header store invalid", RPC::Code_InternalError);
|
|
}
|
|
// fast, in-place conversion to hex
|
|
Util::ToHexFastInPlace(hdr, hexHeaders.data() + offset, hdrHexSz);
|
|
}
|
|
QVariantMap resp{
|
|
{"hex" , QString(hexHeaders)}, // we cast to QString to prevent null for empty string ""
|
|
{"count", unsigned(hdrs.size())},
|
|
{"max", MAX_COUNT}
|
|
};
|
|
if (count && cp_height) {
|
|
// Note: it's possible for a reorg to happen and the chain height to be shortened in parellel in such
|
|
// a way that lastHeight > chainHeight or cp_height > chainHeight, thus making this merkle branch query
|
|
// below illegal. In that case the getHeadersBranchAndRoot function will bubble up an exception about a
|
|
// short header count, which is what we want.
|
|
const auto lastHeight = height + count - 1;
|
|
const auto [branch, root] = getHeadersBranchAndRoot(lastHeight, cp_height);
|
|
resp["branch"] = branch;
|
|
resp["root"] = root;
|
|
}
|
|
return resp;
|
|
});
|
|
}
|
|
void Server::rpc_blockchain_estimatefee(Client *c, const RPC::Message &m)
|
|
{
|
|
QVariantList l = m.paramsList();
|
|
assert(!l.isEmpty());
|
|
bool ok;
|
|
int n = l.front().toInt(&ok);
|
|
if (!ok || n < 0)
|
|
throw RPCError(QString("%1 parameter should be a single non-negative integer").arg(m.method));
|
|
if (bitcoinDInfo.subversion.startsWith("/Bitcoin ABC") && bitcoinDInfo.version >= Version(0, 20, 2)) {
|
|
// Bitcoin ABC v0.20.2 has taken out the nblocks argument
|
|
generic_async_to_bitcoind(c, m.id, "estimatefee", QVariantList(),[](const RPC::Message &response){
|
|
return response.result();
|
|
});
|
|
} else {
|
|
// BUcash, earlier ABC, and others support the nblocks argument
|
|
generic_async_to_bitcoind(c, m.id, "estimatefee", QVariantList{unsigned(n)},[](const RPC::Message &response){
|
|
return response.result();
|
|
});
|
|
}
|
|
}
|
|
void Server::rpc_blockchain_headers_subscribe(Client *c, const RPC::Message &m) // fully implemented
|
|
{
|
|
// helper used both for this response and for notifications
|
|
static const auto mkResp = [](unsigned height, const QByteArray & header) -> QVariantMap {
|
|
return QVariantMap{
|
|
{ "height" , height },
|
|
{ "hex" , Util::ToHexFast(header) }
|
|
};
|
|
};
|
|
Storage::Header hdr;
|
|
const auto [height, hhash] = storage->latestTip(&hdr);
|
|
// we assume everything is peachy and don't check header size, etc as we can't really get here until we have synched at least *some* headers.
|
|
if (!c->isSubscribedToHeaders) {
|
|
c->isSubscribedToHeaders = true;
|
|
// connect to signal. Will be emitted directly to object until it dies.
|
|
connect(this, &Server::newHeader, c, [c, meth=m.method](unsigned height, const QByteArray &header){
|
|
// the notification is a list of size 1, with a dict in it. :/
|
|
c->sendNotification(meth, QVariantList({mkResp(height, header)}));
|
|
});
|
|
Debug() << c->prettyName(false, false) << " is now subscribed to headers";
|
|
} else {
|
|
Debug() << c->prettyName(false, false) << " was already subscribed to headers, ignoring duplicate subscribe request";
|
|
}
|
|
emit c->sendResult(m.id, mkResp(unsigned(std::max(0, height)), hdr));
|
|
}
|
|
void Server::rpc_blockchain_relayfee(Client *c, const RPC::Message &m)
|
|
{
|
|
// This value never changes unless bitcoind is restarted, in which case we will pick up the new value when it comes
|
|
// back. See: refreshBitcoinDNetworkInfo()
|
|
emit c->sendResult(m.id, bitcoinDInfo.relayFee);
|
|
}
|
|
void Server::rpc_blockchain_scripthash_get_balance(Client *c, const RPC::Message &m)
|
|
{
|
|
QVariantList l(m.paramsList());
|
|
assert(!l.isEmpty());
|
|
const QByteArray sh = validateHashHex( l.front().toString() );
|
|
if (sh.length() != HashLen)
|
|
throw RPCError("Invalid scripthash");
|
|
generic_do_async(c, m.id, [sh, this] {
|
|
const auto [amt, uamt] = storage->getBalance(sh);
|
|
/* Note: ElectrumX protocol docs are incorrect. They claim a string in coin units is returned here.
|
|
* It is not. Instead a number in satoshis is returned!
|
|
* Incorrect docs: https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-balance */
|
|
QVariantMap resp{
|
|
{ "confirmed" , qlonglong(amt / amt.satoshi()) },
|
|
{ "unconfirmed" , qlonglong(uamt / uamt.satoshi()) },
|
|
};
|
|
return resp;
|
|
});
|
|
}
|
|
|
|
/// called from get_mempool and get_history to retrieve the mempool for a hashx synchronously. Returns the
|
|
/// QVariantMap suitable for placing into the resulting response.
|
|
QVariantList Server::getHistoryCommon(const QByteArray &sh, bool mempoolOnly)
|
|
{
|
|
QVariantList resp;
|
|
const auto items = storage->getHistory(sh, !mempoolOnly, true); // these are already sorted
|
|
for (const auto & item : items) {
|
|
QVariantMap m{
|
|
{ "tx_hash" , Util::ToHexFast(item.hash) },
|
|
{ "height", int(item.height) },
|
|
};
|
|
if (item.fee.has_value())
|
|
m["fee"] = qlonglong(item.fee.value() / bitcoin::Amount::satoshi());
|
|
resp.push_back(m);
|
|
}
|
|
return resp;
|
|
}
|
|
|
|
void Server::rpc_blockchain_scripthash_get_history(Client *c, const RPC::Message &m)
|
|
{
|
|
QVariantList l(m.paramsList());
|
|
assert(!l.isEmpty());
|
|
const QByteArray sh = validateHashHex( l.front().toString() );
|
|
if (sh.length() != HashLen)
|
|
throw RPCError("Invalid scripthash");
|
|
generic_do_async(c, m.id, [sh, this] {
|
|
return getHistoryCommon(sh, false);
|
|
});
|
|
}
|
|
void Server::rpc_blockchain_scripthash_get_mempool(Client *c, const RPC::Message &m)
|
|
{
|
|
QVariantList l(m.paramsList());
|
|
assert(!l.isEmpty());
|
|
const QByteArray sh = validateHashHex( l.front().toString() );
|
|
if (sh.length() != HashLen)
|
|
throw RPCError("Invalid scripthash");
|
|
generic_do_async(c, m.id, [sh, this] {
|
|
return getHistoryCommon(sh, true);
|
|
});
|
|
}
|
|
void Server::rpc_blockchain_scripthash_listunspent(Client *c, const RPC::Message &m)
|
|
{
|
|
QVariantList l = m.paramsList();
|
|
assert(!l.isEmpty());
|
|
const QByteArray sh = validateHashHex( l.front().toString() );
|
|
if (sh.length() != HashLen)
|
|
throw RPCError("Invalid scripthash");
|
|
generic_do_async(c, m.id, [sh, this] {
|
|
QVariantList resp;
|
|
const auto items = storage->listUnspent(sh); // these are already sorted
|
|
for (const auto & item : items) {
|
|
resp.push_back(QVariantMap{
|
|
{ "tx_hash" , Util::ToHexFast(item.hash) },
|
|
{ "tx_pos" , item.tx_pos },
|
|
{ "height", item.height }, // confirmed height. TODO: should be 0 for mempool regardless of unconf. parent status. Note this differs from get_mempool or get_history
|
|
{ "value", qlonglong(item.value / item.value.satoshi()) }, // amount (int64) in satoshis
|
|
});
|
|
}
|
|
return resp;
|
|
});
|
|
}
|
|
void Server::rpc_blockchain_scripthash_subscribe(Client *c, const RPC::Message &m)
|
|
{
|
|
QVariantList l = m.paramsList();
|
|
assert(l.size() == 1);
|
|
QByteArray sh = validateHashHex( l.front().toString() );
|
|
if (sh.length() != HashLen)
|
|
throw RPCError("Invalid scripthash");
|
|
/// Note: potential race condition here whereby notification can arrive BEFORE the status result. In practice this
|
|
/// is fine since clients will cope with the situation, but... ideally, fixme.
|
|
const auto [wasNew, optStatus] = storage->subs()->subscribe(c, sh, [c,method=m.method](const HashX &sh, const StatusHash &status) {
|
|
QVariant statusHexMaybeNull; // if empty we simply notify as 'null' (this is unlikely in practice but may happen on reorg)
|
|
if (!status.isEmpty())
|
|
statusHexMaybeNull = Util::ToHexFast(status);
|
|
const QByteArray shHex = Util::ToHexFast(sh);
|
|
emit c->sendNotification(method, QVariantList{shHex, statusHexMaybeNull});
|
|
});
|
|
if (wasNew) ++c->nShSubs;
|
|
if (!optStatus.has_value()) {
|
|
// no known/cached status -- do the work ourselves asynch in the thread pool.
|
|
generic_do_async(c, m.id, [sh, this] {
|
|
QVariant ret;
|
|
const auto status = storage->subs()->getFullStatus(sh);
|
|
storage->subs()->maybeCacheStatusResult(sh, status);
|
|
if (!status.isEmpty()) // if empty we return `null`, otherwise we return hex encoded bytes as the immediate status.
|
|
ret = Util::ToHexFast(status);
|
|
return ret;
|
|
});
|
|
} else {
|
|
// SubsMgr reported a cached status -- immediately return that as the result!
|
|
QVariant result;
|
|
if (!optStatus.value().isEmpty())
|
|
// not empty, so we return the hex-encoded string
|
|
result = QString(Util::ToHexFast(optStatus.value()));
|
|
Debug() << "Sending cached status to client for scripthash: " << Util::ToHexFast(sh) << " status: " << result.toString(); // remove/comment-out this after testing complete
|
|
emit c->sendResult(m.id, result); ///< may be 'null' if status was empty (indicates no history for scripthash)
|
|
}
|
|
}
|
|
void Server::rpc_blockchain_scripthash_unsubscribe(Client *c, const RPC::Message &m)
|
|
{
|
|
QVariantList l = m.paramsList();
|
|
assert(l.size() == 1);
|
|
QByteArray sh = validateHashHex( l.front().toString() );
|
|
if (sh.length() != HashLen)
|
|
throw RPCError("Invalid scripthash");
|
|
const bool result = storage->subs()->unsubscribe(c, sh);
|
|
if (result) --c->nShSubs;
|
|
emit c->sendResult(m.id, QVariant(result));
|
|
}
|
|
void Server::rpc_blockchain_transaction_broadcast(Client *c, const RPC::Message &m)
|
|
{
|
|
QVariantList l = m.paramsList();
|
|
assert(l.size() == 1);
|
|
QByteArray rawtxhex = l.front().toString().left(kMaxTxHex).toUtf8(); // limit raw hex to sane length.
|
|
// no need to validate hex here -- bitcoind does validation for us!
|
|
generic_async_to_bitcoind(c, m.id, "sendrawtransaction", QVariantList{ rawtxhex },
|
|
// print to log, echo bitcoind's reply to client
|
|
[size=rawtxhex.length()/2, cid = c->id](const RPC::Message & reply){
|
|
const QVariant ret = reply.result();
|
|
Log() << "Broadcast tx for client " << cid << ", size: " << size << " bytes, response: " << ret.toString();
|
|
return ret;
|
|
},
|
|
// error func, throw an RPCError that's formatted in a particular way
|
|
[](const RPC::Message & errResponse) {
|
|
throw RPCError(QString("the transaction was rejected by network rules.\n\n"
|
|
// Note: ElectrumX here would also spit back the [txhex] after the final newline.
|
|
// We do not do that, since it's a waste of bandwidth and also Electron Cash
|
|
// ignores that information anyway.
|
|
"%1\n").arg(errResponse.errorMessage()),
|
|
RPC::Code_App_BadRequest /**< ex does this here.. inconsistent with transaction.get,
|
|
* so for now we emulate that until we verify that EC
|
|
* will be ok with us changing it to Code_App_DaemonError */
|
|
);
|
|
}
|
|
);
|
|
// <-- do nothing right now, return without replying. Will respond when daemon calls us back in callbacks above.
|
|
}
|
|
void Server::rpc_blockchain_transaction_get(Client *c, const RPC::Message &m)
|
|
{
|
|
QVariantList l = m.paramsList();
|
|
assert(l.size() <= 2);
|
|
QByteArray txHash = validateHashHex( l.front().toString() );
|
|
if (txHash.length() != HashLen)
|
|
throw RPCError("Invalid tx hash");
|
|
bool verbose = false;
|
|
if (l.size() == 2) {
|
|
const auto [verbArg, verbArgOk] = parseBoolSemiLooselyButNotTooLoosely( l.back() );
|
|
if (!verbArgOk)
|
|
throw RPCError("Invalid verbose argument; expected boolean");
|
|
verbose = verbArg;
|
|
}
|
|
generic_async_to_bitcoind(c, m.id, "getrawtransaction", QVariantList{ Util::ToHexFast(txHash), verbose },
|
|
// use the default success func, which just echoes the bitcoind reply to the client
|
|
BitcoinDSuccessFunc(),
|
|
// error func, throw an RPCError
|
|
[](const RPC::Message & errResponse) {
|
|
// EX does this weird thing.. we do it too for now until we can verify not doing it won't break old EC
|
|
// clients... TODO: see if this can be removed in favor of a more canonical error message.
|
|
throw RPCError(formatBitcoinDErrorResponseToLookLikeDumbElectrumXPythonRepr(errResponse),
|
|
RPC::Code_App_DaemonError);
|
|
}
|
|
);
|
|
// <-- do nothing right now, return without replying. Will respond when daemon calls us back in callbacks above.
|
|
}
|
|
|
|
namespace {
|
|
/// Note: pos must be within the txHashes array, otherwise a BadArgs exception will be thrown.
|
|
/// Input txHashes should be in bitcoind memory order.
|
|
/// Output is a QVariantList already reversed and hex encoded, suitable for putting into the results map as 'merkle'.
|
|
/// Used by the below two _id_from_pos and _get_merkle rpc methods.
|
|
QVariantList getMerkleForTxHashes(const std::vector<QByteArray> & txHashes, unsigned pos) {
|
|
QVariantList branchList;
|
|
|
|
// next, compute the branch and root for the tx hashes which are now in bitcoind memory order
|
|
auto pair = Merkle::branchAndRoot(txHashes, pos);
|
|
auto & [branch, root] = pair;
|
|
|
|
// now, build our results for json as a QVariantList, reversing the memory back to hex memory order, and hex encoding it.
|
|
branchList.reserve(int(branch.size()));
|
|
for (auto & h : branch) {
|
|
// reverse each hash in place and then hex encode it, and pust it to branchList
|
|
std::reverse(h.begin(), h.end());
|
|
h = Util::ToHexFast(h);
|
|
branchList.push_back(h);
|
|
}
|
|
return branchList;
|
|
}
|
|
}
|
|
|
|
void Server::rpc_blockchain_transaction_get_merkle(Client *c, const RPC::Message &m)
|
|
{
|
|
QVariantList l = m.paramsList();
|
|
assert(l.size() == 2);
|
|
QByteArray txHash = validateHashHex( l.front().toString() );
|
|
if (txHash.length() != HashLen)
|
|
throw RPCError("Invalid tx hash");
|
|
bool ok = false;
|
|
unsigned height = l.back().toUInt(&ok);
|
|
if (!ok || height >= Storage::MAX_HEADERS)
|
|
throw RPCError("Invalid height argument; expected non-negative numeric value");
|
|
generic_do_async(c, m.id, [txHash, height, this] () mutable {
|
|
auto txHashes = storage->txHashesForBlockInBitcoindMemoryOrder(height);
|
|
std::reverse(txHash.begin(), txHash.end()); // we need to compare to bitcoind memory order so reverse specified hash
|
|
constexpr unsigned NO_POS = ~0U;
|
|
unsigned pos = NO_POS;
|
|
for (unsigned i = 0; i < txHashes.size(); ++i) {
|
|
if (txHashes[i] == txHash) {
|
|
pos = i;
|
|
break;
|
|
}
|
|
}
|
|
if (pos == NO_POS)
|
|
throw RPCError(QString("No transaction matching the requested hash found at height %1").arg(height));
|
|
|
|
const auto branchList = getMerkleForTxHashes(txHashes, pos);
|
|
|
|
QVariantMap resp = {
|
|
{ "block_height" , height },
|
|
{ "pos" , pos },
|
|
{ "merkle" , branchList }
|
|
};
|
|
|
|
return resp;
|
|
});
|
|
}
|
|
|
|
void Server::rpc_blockchain_transaction_id_from_pos(Client *c, const RPC::Message &m)
|
|
{
|
|
QVariantList l = m.paramsList();
|
|
assert(!l.isEmpty());
|
|
|
|
bool ok = false;
|
|
unsigned height = l.front().toUInt(&ok); // arg0
|
|
if (!ok || height >= Storage::MAX_HEADERS)
|
|
throw RPCError("Invalid height argument; expected non-negative numeric value");
|
|
unsigned pos = l.at(1).toUInt(&ok); // arg1
|
|
constexpr unsigned MAX_POS = Storage::MAX_HEADERS;
|
|
if (!ok || pos >= MAX_POS)
|
|
throw RPCError("Invalid tx_pos argument; expected non-negative numeric value");
|
|
bool merkle = false;
|
|
if (l.size() == 3) { //optional arg2
|
|
const auto [arg, argOk] = parseBoolSemiLooselyButNotTooLoosely( l.back() );
|
|
if (!argOk)
|
|
throw RPCError("Invalid merkle argument; expected boolean");
|
|
merkle = arg;
|
|
}
|
|
generic_do_async(c, m.id, [height, pos, merkle, this] {
|
|
static const QString missingErr("No transaction at position %1 for height %2");
|
|
if (merkle) {
|
|
// merkle=true is a dict, see: https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-id-from-pos
|
|
// get all hashes for the block (we need them for merkle)
|
|
auto txHashes = storage->txHashesForBlockInBitcoindMemoryOrder(height);
|
|
if (pos >= txHashes.size()) {
|
|
// out of range, or block not found
|
|
throw RPCError(missingErr.arg(pos).arg(height));
|
|
}
|
|
// save the requested tx_hash now, which we will return as tx_hash of the response dictionary
|
|
// (we need to reverse it for outputting to hex since we received it in bitcoind internal memory order).
|
|
const QByteArray txHashHex = Util::ToHexFast(Util::reversedCopy(txHashes[pos]));
|
|
|
|
const auto branchList = getMerkleForTxHashes(txHashes, pos);
|
|
|
|
QVariantMap res = {
|
|
{ "tx_hash" , txHashHex },
|
|
{ "merkle" , branchList },
|
|
};
|
|
|
|
return QVariant(res);
|
|
} else {
|
|
// no merkle, just return the tx_hash immediately without going async
|
|
const auto opt = storage->hashForHeightAndPos(height, pos);
|
|
if (!opt.has_value() || opt.value().length() != HashLen)
|
|
throw RPCError(missingErr.arg(pos).arg(height));
|
|
const auto txHashHex = Util::ToHexFast(opt.value());
|
|
return QVariant(txHashHex);
|
|
}
|
|
});
|
|
}
|
|
void Server::rpc_mempool_get_fee_histogram(Client *c, const RPC::Message &m)
|
|
{
|
|
const auto hist = storage->mempoolHistogram();
|
|
QVariantList result;
|
|
for (const auto & [feeRate, cumSize] : hist) {
|
|
result.push_back(QVariantList{feeRate, cumSize});
|
|
}
|
|
emit c->sendResult(m.id, result);
|
|
}
|
|
// --- Server::StaticData Definitions ---
|
|
#define HEY_COMPILER_PUT_STATIC_HERE(x) decltype(x) x
|
|
#define PR RPC::Method::PosParamRange
|
|
HEY_COMPILER_PUT_STATIC_HERE(Server::StaticData::dispatchTable);
|
|
HEY_COMPILER_PUT_STATIC_HERE(Server::StaticData::methodMap);
|
|
HEY_COMPILER_PUT_STATIC_HERE(Server::StaticData::registry){
|
|
/* ==> Note: Add stuff to this table when adding new RPC methods.
|
|
{ {"rpc.name", allow_requests, allow_notifications, PosParamRange, (QSet<QString> note: {} means undefined optional)}, &method_to_call } */
|
|
{ {"server.add_peer", true, false, PR{1,1}, RPC::KeySet{} }, &Server::rpc_server_add_peer },
|
|
{ {"server.banner", true, false, PR{0,0}, }, &Server::rpc_server_banner },
|
|
{ {"server.donation_address", true, false, PR{0,0}, }, &Server::rpc_server_donation_address },
|
|
{ {"server.features", true, false, PR{0,0}, }, &Server::rpc_server_features },
|
|
{ {"server.peers.subscribe", true, false, PR{0,0}, }, &Server::rpc_server_peers_subscribe },
|
|
{ {"server.ping", true, false, PR{0,0}, }, &Server::rpc_server_ping },
|
|
{ {"server.version", true, false, PR{2,2}, }, &Server::rpc_server_version },
|
|
|
|
{ {"blockchain.block.header", true, false, PR{1,2}, }, &Server::rpc_blockchain_block_header },
|
|
{ {"blockchain.block.headers", true, false, PR{2,3}, }, &Server::rpc_blockchain_block_headers },
|
|
{ {"blockchain.estimatefee", true, false, PR{1,1}, }, &Server::rpc_blockchain_estimatefee },
|
|
{ {"blockchain.headers.subscribe", true, false, PR{0,0}, }, &Server::rpc_blockchain_headers_subscribe },
|
|
{ {"blockchain.relayfee", true, false, PR{0,0}, }, &Server::rpc_blockchain_relayfee },
|
|
|
|
{ {"blockchain.scripthash.get_balance", true, false, PR{1,1}, }, &Server::rpc_blockchain_scripthash_get_balance },
|
|
{ {"blockchain.scripthash.get_history", true, false, PR{1,1}, }, &Server::rpc_blockchain_scripthash_get_history },
|
|
{ {"blockchain.scripthash.get_mempool", true, false, PR{1,1}, }, &Server::rpc_blockchain_scripthash_get_mempool },
|
|
{ {"blockchain.scripthash.listunspent", true, false, PR{1,1}, }, &Server::rpc_blockchain_scripthash_listunspent },
|
|
{ {"blockchain.scripthash.subscribe", true, false, PR{1,1}, }, &Server::rpc_blockchain_scripthash_subscribe },
|
|
{ {"blockchain.scripthash.unsubscribe", true, false, PR{1,1}, }, &Server::rpc_blockchain_scripthash_unsubscribe },
|
|
|
|
{ {"blockchain.transaction.broadcast", true, false, PR{1,1}, }, &Server::rpc_blockchain_transaction_broadcast },
|
|
{ {"blockchain.transaction.get", true, false, PR{1,2}, }, &Server::rpc_blockchain_transaction_get },
|
|
{ {"blockchain.transaction.get_merkle", true, false, PR{2,2}, }, &Server::rpc_blockchain_transaction_get_merkle },
|
|
{ {"blockchain.transaction.id_from_pos",true, false, PR{2,3}, }, &Server::rpc_blockchain_transaction_id_from_pos },
|
|
|
|
{ {"mempool.get_fee_histogram", true, false, PR{0,0}, }, &Server::rpc_mempool_get_fee_histogram },
|
|
};
|
|
#undef PR
|
|
#undef HEY_COMPILER_PUT_STATIC_HERE
|
|
/*static*/
|
|
void Server::StaticData::init()
|
|
{
|
|
if (!dispatchTable.empty())
|
|
return;
|
|
for (const auto & r : registry) {
|
|
if (!r.member) {
|
|
Error() << "Runtime check failed: RPC Method " << r.method << " has a nullptr for its .member! See Server class! FIXME!";
|
|
std::_Exit(EXIT_FAILURE);
|
|
}
|
|
methodMap[r.method] = r;
|
|
dispatchTable[r.method] = r.member;
|
|
}
|
|
}
|
|
// --- /Server::StaticData Definitions ---
|
|
// --- /RPC METHODS ---
|
|
|
|
// --- SSL Server support ---
|
|
ServerSSL::ServerSSL(const QHostAddress & address_, quint16 port_, const std::shared_ptr<const Options> & opts,
|
|
const std::shared_ptr<Storage> & storage_, const std::shared_ptr<BitcoinDMgr> & bitcoindmgr_)
|
|
: Server(address_, port_, opts, storage_, bitcoindmgr_), cert(opts->sslCert), key(opts->sslKey)
|
|
{
|
|
if (cert.isNull() || key.isNull())
|
|
throw BadArgs("ServerSSL cannot be instantiated: Key or cert are null!");
|
|
if (!QSslSocket::supportsSsl())
|
|
throw BadArgs("ServerSSL cannot be instantiated: Missing SSL support!");
|
|
connect(this, &ServerSSL::ready, this, []{
|
|
Debug() << "SSL ready emitted";
|
|
});
|
|
setObjectName(prettyName());
|
|
_thread.setObjectName(prettyName());
|
|
}
|
|
ServerSSL::~ServerSSL() {}
|
|
QString ServerSSL::prettyName() const
|
|
{
|
|
return QString("Ssl%1").arg(AbstractTcpServer::prettyName());
|
|
}
|
|
void ServerSSL::incomingConnection(qintptr socketDescriptor)
|
|
{
|
|
QSslSocket *socket = new QSslSocket(this);
|
|
if (socket->setSocketDescriptor(socketDescriptor)) {
|
|
socket->setLocalCertificate(cert);
|
|
socket->setPrivateKey(key);
|
|
socket->setProtocol(QSsl::SslProtocol::AnyProtocol);
|
|
connect(socket, &QSslSocket::encrypted, this, &ServerSSL::ready);
|
|
connect(socket, QOverload<const QList<QSslError> &>::of(&QSslSocket::sslErrors),
|
|
this, [](const QList<QSslError> & errors) {
|
|
for (auto e : errors)
|
|
Warning() << "SSL error: " << e.errorString();
|
|
});
|
|
addPendingConnection(socket);
|
|
socket->startServerEncryption();
|
|
} else {
|
|
Warning() << "setSocketDescriptor returned false -- unable to initiate SSL for client: " << socket->errorString();
|
|
delete socket;
|
|
}
|
|
}
|
|
|
|
// --- /SSL Server support ---
|
|
|
|
/*static*/ std::atomic_int Client::numClients{0};
|
|
|
|
Client::Client(const RPC::MethodMap & mm, IdMixin::Id id_in, Server *srv, QTcpSocket *sock, int maxBuffer)
|
|
: RPC::LinefeedConnection(mm, id_in, sock, /* ensure sane --> */ qMax(maxBuffer, Options::maxBufferMin)),
|
|
srv(srv)
|
|
{
|
|
const auto N = ++numClients;
|
|
socket = sock;
|
|
stale_threshold = 10 * 60 * 1000; // 10 mins stale threshold; after which clients get disconnected for being idle (for now... TODO: make this configurable)
|
|
pingtime_ms = int(stale_threshold); // this determines how often the pingtimer fires
|
|
Q_ASSERT(socket->state() == QAbstractSocket::ConnectedState);
|
|
status = Connected ; // we are always connected at construction time.
|
|
errorPolicy = ErrorPolicySendErrorMessage;
|
|
setObjectName(QString("Client.%1").arg(id_in));
|
|
on_connected();
|
|
Log() << "New " << prettyName(false, false) << ", " << N << Util::Pluralize(" client", N) << " total";
|
|
}
|
|
|
|
Client::~Client()
|
|
{
|
|
--numClients;
|
|
Debug() << __PRETTY_FUNCTION__;
|
|
socket = nullptr; // NB: we are a child of socket. this line here is added in case some day I make AbstractClient delete socket on destruct.
|
|
}
|
|
|
|
void Client::do_disconnect(bool graceful)
|
|
{
|
|
const bool wasConnected = socket ? socket->state() == QAbstractSocket::ConnectedState : false;
|
|
AbstractConnection::do_disconnect(graceful); // if 'graceful' *AND* was connected, a disconnected state will be entered later at which point we will delete socket.
|
|
if (socket && (!graceful || !wasConnected))
|
|
/// delete the socket if we weren't connected or if !graceful.
|
|
/// If graceful && connected, then a disconnect signal will be sent later and then we will
|
|
/// reenter here and delete the socket.
|
|
socket->deleteLater(); // side-effect: will implicitly delete 'this' because we are a child of the socket!
|
|
else if (socket && graceful)
|
|
Debug() << __FUNCTION__ << " (graceful); delayed socket delete (wait for disconnect) ...";
|
|
}
|
|
|
|
void Client::do_ping()
|
|
{
|
|
// Don't send clients pings.
|
|
// Instead, rely on them to ping us else disconnect them if idle for too long.
|
|
// The below just checks idle.
|
|
if (Util::getTime() - lastGood >= stale_threshold) {
|
|
Debug() << prettyName() << ": idle timeout after " << ((stale_threshold)/1e3) << " sec., will close connection";
|
|
emit sendError(true, RPC::Code_Custom+1, "Idle time exceeded");
|
|
return;
|
|
}
|
|
}
|