Fulcrum/Servers.cpp
Calin Culianu b6b40cac5c
Added bitcoindmgr reference to Servers.cpp, added rpc methods
- blockchain.transaction.get is now fully implemented
- blockchain.transaction.broadcast is now fully implemented

Note we emulate electrumx's quirky/inconsistent behavior as much as
possible within reason until we can determine we can "do the right
thing" and it won't break existing clients.
2019-12-19 16:59:21 +02:00

998 lines
48 KiB
C++

#include "BitcoinD.h"
#include "Servers.h"
#include "Storage.h"
#include <QByteArray>
#include <QCoreApplication>
#include <QtNetwork>
#include <QString>
#include <QTextCodec>
#include <QTextStream>
#include <QTimer>
#include <cstdlib>
#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 (id: %2)").arg(hostPort()).arg(id);
}
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 kMaxServerVersion = 80, ///< the maximum server version length we accept to prevent memory exhaustion attacks
kMaxBuffer = 4*1000*1000, ///< =4MB. The max buffer we use in Client (ElectronX client). TODO: Make this tune-able and configurable!
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, std::shared_ptr<Storage> s, std::shared_ptr<BitcoinDMgr> bdm)
: AbstractTcpServer(a, p), storage(std::move(s)), bitcoindmgr(std::move(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.userAgent, client->info.protocolVersion});
map["errCt"] = client->info.errCt;
map["nRequestsRcv"] = client->info.nRequestsRcv;
map["isSubscribedToHeaders"] = client->isSubscribedToHeaders;
// 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();
conns.push_back(connect(this, &Server::tellClientScriptHashStatus, this, &Server::_tellClientScriptHashStatus));
}
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);
// if deleted, we need to purge it from map
auto on_destroyed = [clientId, 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));
}
};
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";
emit clientDisconnected(client->id);
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);
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();
}
void Server::killClient(quint64 clientId)
{
killClient(clientsById.take(clientId));
}
void Server::onMessage(quint64 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;
// call ptr to member
(this->*member)(c, m);
}
} else {
Debug() << "Unknown client: " << clientId;
}
}
void Server::onErrorMessage(quint64 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(quint64 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;
}
}
}
// --- RPC METHODS ---
void Server::rpc_server_version(Client *c, const RPC::Message &m)
{
if (QVariantList l = m.paramsList(); m.isRequest() && l.size() == 2) {
c->info.userAgent = l[0].toString().left(kMaxServerVersion);
c->info.protocolVersion = l[1].toString().left(kMaxServerVersion);
Trace() << "Client (id: " << c->id << ") sent version: \"" << c->info.userAgent << "\" / \"" << c->info.protocolVersion << "\"";
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
Error() << "Bad server version message! Other code should have handled this. FIXME! Json: " << m.toJsonString();
return;
}
emit c->sendResult(m.id, m.method, QStringList({QString("%1/%2").arg(APPNAME).arg(VERSION), QString("1.4")}));
}
void Server::rpc_server_ping(Client *c, const RPC::Message &m)
{
if (m.isRequest()) {
Trace() << "Got ping from client (id: " << c->id << "), responding...";
emit c->sendResult(m.id, m.method);
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
Error() << "Bad client ping message! This shouldn't happen. FIXME! Json: " << m.toJsonString();
}
}
void Server::rpc_blockchain_block_header(Client *c, const RPC::Message &m)
{
if (QVariantList l = m.paramsList(); m.isRequest() && l.size() >= 1 && l.size() <= 2) {
bool ok;
const unsigned height = l.front().toUInt(&ok);
if (!ok) {
emit c->sendError(false, RPC::Code_InvalidParams, "Invalid height", m.id);
return;
}
const unsigned cp_height = l.size() > 1 ? l.back().toUInt(&ok) : 0;
// TODO SUPPORT CP_HEIGHT!
if (!ok || cp_height != 0) {
emit c->sendError(false, RPC::Code_InvalidParams, "cp_height not yet supported", m.id);
return;
}
QString err;
const auto optHdr = storage->headerForHeight(height, &err);
if (QByteArray hdr; err.isEmpty() && optHdr.has_value() && !(hdr = optHdr.value()).isEmpty())
c->sendResult(m.id, m.method, hdr);
else
c->sendError(false, RPC::Code_App_BadRequest, err.isEmpty() ? "Unknown error" : err, m.id);
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
Error() << "Bad block.header message! This shouldn't happen. FIXME! Json: " << m.toJsonString();
}
}
void Server::rpc_blockchain_block_headers(Client *c, const RPC::Message &m)
{
if (QVariantList l = m.paramsList(); m.isRequest() && l.size() >= 2 && l.size() <= 3) {
bool ok;
const unsigned height = l.front().toUInt(&ok);
if (!ok) {
emit c->sendError(false, RPC::Code_InvalidParams, "Invalid height", m.id);
return;
}
const unsigned count = l[1].toUInt(&ok);
if (!ok) {
emit c->sendError(false, RPC::Code_InvalidParams, "Invalid count", m.id);
return;
}
const unsigned cp_height = l.size() > 2 ? l.back().toUInt(&ok) : 0;
// TODO SUPPORT CP_HEIGHT!
if (!ok || cp_height != 0) {
emit c->sendError(false, RPC::Code_InvalidParams, "cp_height not yet supported", m.id);
return;
}
constexpr unsigned MAX_HEADERS = 2016; ///< TODO: make this cofigurable. this is the current electrumx limit, for now.
// 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(MAX_HEADERS, 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!";
emit c->sendError(false, RPC::Code_InternalError, "server header store invalid", m.id);
return;
}
// fast, in-place conversion to hex
Util::ToHexFastInPlace(hdr, hexHeaders.data() + offset, hdrHexSz);
}
QVariantMap resp{
{"hex" , hexHeaders},
{"count", unsigned(hdrs.size())},
{"max", MAX_HEADERS}
};
if (hexHeaders.isEmpty()) {
// special-case: on empty results, prevent sending null (empty QByteArray ends up as null), but prefer
// sending "" so as to not confuse clients if they see null but are expecting only string "".
resp["hex"] = QString("");
}
c->sendResult(m.id, m.method, resp);
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
Error() << "Bad block.headers message! This shouldn't happen. FIXME! Json: " << m.toJsonString();
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
}
}
void Server::rpc_blockchain_estimatefee(Client *c, const RPC::Message &m)
{
if (QVariantList l = m.paramsList(); m.isRequest() && l.size() == 1) {
// TODO: Implement this
Trace() << "Got estimatefee from client (id: " << c->id << "), responding...";
constexpr double dummyReply = 0.00001000;
emit c->sendResult(m.id, m.method, dummyReply);
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
Error() << "Bad client estimtefee message! This shouldn't happen. FIXME! Json: " << m.toJsonString();
}
}
void Server::rpc_blockchain_headers_subscribe(Client *c, const RPC::Message &m) // fully implemented
{
if (QVariantList l = m.paramsList(); m.isRequest() && l.size() == 0) {
// 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){
c->sendNotification(meth, mkResp(height, header));
});
Debug() << c->prettyName() << " is now subscribed to headers";
} else {
Debug() << c->prettyName() << " was already subscribed to headers, ignoring duplicate subscribe request";
}
emit c->sendResult(m.id, m.method, mkResp(unsigned(std::max(0, height)), hdr));
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
Error() << "Bad client headers.subscribe message! This shouldn't happen. FIXME! Json: " << m.toJsonString();
}
}
void Server::rpc_blockchain_relayfee(Client *c, const RPC::Message &m)
{
// TODO: Implement this
Trace() << "Got relayfee from client (id: " << c->id << "), responding...";
constexpr double dummyReply = 0.00001000;
emit c->sendResult(m.id, m.method, dummyReply);
}
void Server::rpc_blockchain_scripthash_get_balance(Client *c, const RPC::Message &m)
{
if (QVariantList l = m.paramsList(); m.isRequest() && l.size() == 1) {
const QByteArray sh = validateHashHex( l.front().toString() );
if (sh.length() != HashLen) {
emit c->sendError(false, RPC::Code_App_BadRequest, "Invalid scripthash", m.id);
return;
}
const bitcoin::Amount amt = 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" , 0 }, /* TODO: unconfirmed */
};
emit c->sendResult(m.id, m.method, resp);
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
Error() << "Bad client get_balance message! This shouldn't happen. FIXME! Json: " << m.toJsonString();
}
}
void Server::rpc_blockchain_scripthash_get_history(Client *c, const RPC::Message &m)
{
if (QVariantList l = m.paramsList(); m.isRequest() && l.size() == 1) {
const QByteArray sh = validateHashHex( l.front().toString() );
if (sh.length() != HashLen) {
emit c->sendError(false, RPC::Code_App_BadRequest, "Invalid scripthash", m.id);
return;
}
QVariantList resp;
const auto items = storage->getHistory(sh); // these are already sorted
for (const auto & item : items) {
resp.push_back(QVariantMap{
{ "tx_hash" , Util::ToHexFast(item.hash) },
{ "height", item.height },
// mempool tx's here would also have "fee"! (basically the contents of get_mempool concatenated) <--- TODO
});
}
emit c->sendResult(m.id, m.method, resp);
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
Error() << "Bad client get_balance message! This shouldn't happen. FIXME! Json: " << m.toJsonString();
}
}
void Server::rpc_blockchain_scripthash_get_mempool(Client *c, const RPC::Message &m)
{
if (QVariantList l = m.paramsList(); m.isRequest() && l.size() == 1) {
const QByteArray sh = validateHashHex( l.front().toString() );
if (sh.length() != HashLen) {
emit c->sendError(false, RPC::Code_App_BadRequest, "Invalid scripthash", m.id);
return;
}
// NOT YET IMPLEMENTED. TODO: Implement!
emit c->sendError(false, RPC::Code_InternalError, "not yet implemented", m.id);
/* Not yet implemented.. this is what the possible implementation would look like
QVariantList resp;
const auto items = storage->getMempool(sh); // these are already sorted
for (const auto & item : items) {
resp.push_back(QVariantMap{
{ "tx_hash" , Util::ToHexFast(item.hash) },
{ "height", item.height }, // should be 0 for "no unconfirmed parent", -1 for "has unconfirmed parent"
{ "fee", item.fee }, // fee (int64) in satoshis
});
}
emit c->sendResult(m.id, m.method, resp);
*/
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
Error() << "Bad client get_mempool message! This shouldn't happen. FIXME! Json: " << m.toJsonString();
}
}
void Server::rpc_blockchain_scripthash_listunspent(Client *c, const RPC::Message &m)
{
if (QVariantList l = m.paramsList(); m.isRequest() && l.size() == 1) {
const QByteArray sh = validateHashHex( l.front().toString() );
if (sh.length() != HashLen) {
emit c->sendError(false, RPC::Code_App_BadRequest, "Invalid scripthash", m.id);
return;
}
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
});
}
emit c->sendResult(m.id, m.method, resp);
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
Error() << "Bad client listunspent message! This shouldn't happen. FIXME! Json: " << m.toJsonString();
}
}
void Server::rpc_blockchain_scripthash_subscribe(Client *c, const RPC::Message &m)
{
if (QVariantList l = m.paramsList(); m.isRequest() && l.size() == 1) {
constexpr bool testTimer = false;
if constexpr (!testTimer) {
emit c->sendError(false, RPC::Code_InternalError, "not yet implemented", m.id);
} else {
// TESTING TODO FIXME THIS IS FOR TESTING ONLY
const auto clientId = c->id;
QByteArray sh = validateHashHex( l.front().toString() );
if (sh.length() != HashLen) {
emit c->sendError(false, RPC::Code_InvalidParams, "Invalid scripthash", m.id);
return;
}
emit tellClientScriptHashStatus(clientId, m.id, QByteArray(HashLen, 0));
QTimer *t = new QTimer(c);
connect(t, &QTimer::timeout, this, [sh, clientId, this] {
auto val = QRandomGenerator::global()->generate64();
emit tellClientScriptHashStatus(clientId, RPC::Message::Id(), QByteArray(reinterpret_cast<char *>(&val), sizeof(val)), sh);
});
t->setSingleShot(false); t->start(3000);
}
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
Error() << "Bad subscribe message! This shouldn't happen. FIXME! Json: " << m.toJsonString();
}
}
void Server::rpc_blockchain_scripthash_unsubscribe(Client *c, const RPC::Message &m)
{
if (QVariantList l = m.paramsList(); m.isRequest() && l.size() == 1) {
// this isn't really implemented. this is a stub.
QByteArray sh = validateHashHex( l.front().toString() );
if (sh.length() != HashLen) {
emit c->sendError(false, RPC::Code_InvalidParams, "Invalid scripthash", m.id);
return;
}
emit c->sendResult(m.id, m.method, QVariant(true)); // dummy response. in future returns true if unsub'd (was sub'd), false otherwise.
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
Error() << "Bad unsubscribe message! This shouldn't happen. FIXME! Json: " << m.toJsonString();
}
}
void Server::rpc_blockchain_transaction_broadcast(Client *c, const RPC::Message &m)
{
if (QVariantList l = m.paramsList(); m.isRequest() && 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!
const auto internalReqId = newId();
bitcoindmgr->submitRequest(c, internalReqId, "sendrawtransaction", QVariantList{ rawtxhex },
// success callback
[c,id=m.id,method=m.method](const RPC::Message &response){
emit c->sendResult(id, method, response.result());
},
// error callback
[c,id=m.id](const RPC::Message &errResponse){
emit c->sendError(false, 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 */
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()),
id);
},
// failure callback
[c,id=m.id](const RPC::Message::Id &, const QString & failureReason){
emit c->sendError(false, RPC::Code_InternalError, QString("internal error: %1").arg(failureReason), id);
}
);
// <-- do nothing right now, return without replying. Will respond when daemon calls us back in callbacks above.
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
Error() << "Bad broadcast message! This shouldn't happen. FIXME! Json: " << m.toJsonString();
}
}
void Server::rpc_blockchain_transaction_get(Client *c, const RPC::Message &m)
{
if (QVariantList l = m.paramsList(); m.isRequest() && l.size() >= 1 && l.size() <= 2) {
QByteArray txHash = validateHashHex( l.front().toString() );
if (txHash.length() != HashLen) {
emit c->sendError(false, RPC::Code_App_BadRequest, "Invalid tx hash", m.id);
return;
}
bool verbose = false;
if (l.size() == 2) {
const auto [verbArg, verbArgOk] = parseBoolSemiLooselyButNotTooLoosely( l.back() );
if (!verbArgOk) {
emit c->sendError(false, RPC::Code_App_BadRequest, "Invalid verbose argument; expected boolean", m.id);
return;
}
verbose = verbArg;
}
const auto internalReqId = newId();
bitcoindmgr->submitRequest(c, internalReqId, "getrawtransaction", QVariantList{ Util::ToHexFast(txHash), verbose },
// success callback
[c,id=m.id,method=m.method](const RPC::Message &response){
emit c->sendResult(id, method, response.result());
},
// error callback
[c,id=m.id](const RPC::Message &errResponse){
emit c->sendError(false, RPC::Code_App_DaemonError,
formatBitcoinDErrorResponseToLookLikeDumbElectrumXPythonRepr(errResponse),
id);
},
// failure callback
[c,id=m.id](const RPC::Message::Id &, const QString & failureReason){
emit c->sendError(false, RPC::Code_InternalError, QString("internal error: %1").arg(failureReason), id);
}
);
// <-- do nothing right now, return without replying. Will respond when daemon calls us back in callbacks above.
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
Error() << "Bad transaction.get message! This shouldn't happen. FIXME! Json: " << m.toJsonString();
}
}
void Server::rpc_blockchain_transaction_get_merkle(Client *c, const RPC::Message &m)
{
if (QVariantList l = m.paramsList(); m.isRequest() && l.size() == 2) {
// this isn't really implemented. this is a stub
QByteArray txHash = validateHashHex( l.front().toString() );
if (txHash.length() != HashLen) {
emit c->sendError(false, RPC::Code_App_BadRequest, "Invalid tx hash", m.id);
return;
}
bool ok = false;
[[maybe_unused]] unsigned height = l.back().toUInt(&ok);
if (!ok) {
emit c->sendError(false, RPC::Code_App_BadRequest, "Invalid height argument; expected numeric value", m.id);
return;
}
// TODO:.. implement this properly....
emit c->sendError(false, RPC::Code_InternalError, "not yet implemented", m.id);
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
Error() << "Bad get_merkle message! This shouldn't happen. FIXME! Json: " << m.toJsonString();
}
}
void Server::rpc_blockchain_transaction_id_from_pos(Client *c, const RPC::Message &m)
{
if (QVariantList l = m.paramsList(); m.isRequest() && l.size() >= 2 && l.size() <= 3) {
// this is partialluy implemented for merkle=false TODO: fully implement
bool ok = false;
unsigned height = l.front().toUInt(&ok); // arg0
if (!ok) {
emit c->sendError(false, RPC::Code_App_BadRequest, "Invalid height argument; expected numeric value", m.id);
return;
}
unsigned pos = l.at(1).toUInt(&ok); // arg1
if (!ok) {
emit c->sendError(false, RPC::Code_App_BadRequest, "Invalid tx_pos argument; expected numeric value", m.id);
return;
}
bool merkle = false;
if (l.size() == 3) { //optional arg2
const auto [arg, argOk] = parseBoolSemiLooselyButNotTooLoosely( l.back() );
if (!argOk) {
emit c->sendError(false, RPC::Code_App_BadRequest, "Invalid merkle argument; expected boolean", m.id);
return;
}
merkle = arg;
}
if (merkle) {
// TODO:.. implement this properly for merkle=true ....
emit c->sendError(false, RPC::Code_InternalError, "not yet implemented", m.id);
return;
}
const auto opt = storage->hashForHeightAndPos(height, pos);
if (!opt.has_value() || opt.value().length() != HashLen) {
emit c->sendError(false, RPC::Code_App_BadRequest, QString("no tx for height %1 at position %2").arg(height).arg(pos), m.id);
return;
}
const auto txHashHex = Util::ToHexFast(opt.value());
// merkle=false is just the txHash. Merkle=true would be a dict, see: https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-id-from-pos
emit c->sendResult(m.id, m.method, txHashHex);
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
Error() << "Bad id_from_pos message! This shouldn't happen. FIXME! Json: " << m.toJsonString();
}
}
void Server::rpc_mempool_get_fee_histogram(Client *c, const RPC::Message &m)
{
if (QVariantList l = m.paramsList(); m.isRequest() && l.size() == 0) {
// this is a stub
emit c->sendResult(m.id, m.method, QVariantList());
} else {
// TODO: remove this soon as this should never be reached. It is just here to detect bugs in our own RPC code
emit c->sendError(false, RPC::Code_InternalError, "internal error", m.id);
Error() << "Bad get_fee_histogram message! This shouldn't happen. FIXME! Json: " << m.toJsonString();
}
}
// --- 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.ping", true, false, PR{0,0}, RPC::KeySet{} }, &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 ---
void Server::_tellClientScriptHashStatus(quint64 clientId, const RPC::Message::Id & refId, const QByteArray & status, const QByteArray & scriptHash)
{
if (Client *client = getClient(clientId); client) {
if (scriptHash.isEmpty())
// immediate scripthash status result
client->sendResult(refId, "blockchain.scripthash.subscribe", status.toHex());
else {
// notification, no id.
client->sendNotification("blockchain.scripthash.subscribe", QVariantList{scriptHash.toHex(), status.toHex()});
}
} else {
Debug() << "ClientId: " << clientId << " not found.";
}
}
Client::Client(const RPC::MethodMap & mm, quint64 id_in, Server *srv, QTcpSocket *sock)
: RPC::LinefeedConnection(mm, id_in, sock, kMaxBuffer), srv(srv)
{
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();
Debug() << prettyName() << " new client";
}
Client::~Client()
{
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;
}
}