v5.1.6: Fix block sync stall for fresh nodes, REST API refactor
Build All Platforms / build-windows-qt (push) Waiting to run
Build All Platforms / build-windows-daemon (push) Waiting to run
Build All Platforms / build-linux-qt (push) Waiting to run
Build All Platforms / build-macos (push) Waiting to run
Build All Platforms / release (push) Blocked by required conditions
Build All Platforms / build-linux-daemon (push) Failing after 25m20s

Critical fix: revert initial sync from getheaders back to getblocks.
The headers-first change (05b1fd1) broke chain continuation — after
downloading the first 2000 blocks, fresh nodes would stall because
the getheaders path has no orphan-based continuation mechanism.
The getblocks/inv/orphan cycle is required for full chain sync.

Also adds getblocks fallback to the headers handler so if headers
are used via other paths, sync still continues.

Other changes:
- Extract REST API into separate rest.cpp/rest.h
- Add REST rate limiting and CORS support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 23:33:09 -07:00
parent 6a8fde3e92
commit 3ed9a42b3c
13 changed files with 1071 additions and 207 deletions
+18 -183
View File
@@ -1020,187 +1020,10 @@ static string JSONRPCExecBatch(const Array& vReq)
return write_string(Value(ret), false) + "\n";
}
// REST API support
extern Object blockToJSON(const CBlock& block, const CBlockIndex* blockindex, bool fPrintTransactionDetail);
extern void TxToJSON(const CTransaction& tx, const uint256 hashBlock, json_spirit::Object& entry);
// REST API support (implementation in rest.cpp)
#include "rest.h"
static string HTTPReplyREST(int nStatus, const string& strMsg, const string& contentType = "application/json")
{
const char *cStatus;
if (nStatus == HTTP_OK) cStatus = "OK";
else if (nStatus == HTTP_BAD_REQUEST) cStatus = "Bad Request";
else if (nStatus == HTTP_NOT_FOUND) cStatus = "Not Found";
else if (nStatus == HTTP_INTERNAL_SERVER_ERROR) cStatus = "Internal Server Error";
else cStatus = "";
return strprintf(
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n"
"Connection: close\r\n"
"Content-Length: %"PRIszu"\r\n"
"Content-Type: %s\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Server: Triangles-json-rpc/%s\r\n"
"\r\n"
"%s",
nStatus,
cStatus,
rfc1123Time().c_str(),
strMsg.size(),
contentType.c_str(),
FormatFullVersion().c_str(),
strMsg.c_str());
}
static bool HandleRESTRequest(const string& strURI, string& strReply, string& strContentType, int& nStatus)
{
// Parse: /rest/<resource>[/<param>][.format]
vector<string> parts;
string uri = strURI;
// Remove query string if present
size_t qpos = uri.find('?');
if (qpos != string::npos) uri = uri.substr(0, qpos);
boost::split(parts, uri, boost::is_any_of("/"));
// parts[0]="" parts[1]="rest" parts[2]="resource" parts[3]="param.format"
if (parts.size() < 3) {
nStatus = HTTP_NOT_FOUND;
strReply = "{\"error\":\"Not found\"}";
return true;
}
string resource = parts[2];
// Get param and format from last path component
string lastPart = parts.size() > 3 ? parts[parts.size()-1] : "";
string param, format = "json";
size_t dotPos = lastPart.rfind('.');
if (dotPos != string::npos) {
param = lastPart.substr(0, dotPos);
format = lastPart.substr(dotPos+1);
} else {
param = lastPart;
}
strContentType = (format == "hex") ? "text/plain" : "application/json";
nStatus = HTTP_OK;
try {
LOCK(cs_main);
if (resource == "chaininfo") {
Object obj, diff;
obj.push_back(Pair("chain", fTestNet ? string("test") : string("main")));
obj.push_back(Pair("blocks", (int)nBestHeight));
obj.push_back(Pair("bestblockhash", hashBestChain.GetHex()));
diff.push_back(Pair("proof-of-work", GetDifficulty()));
diff.push_back(Pair("proof-of-stake", GetDifficulty(GetLastBlockIndex(pindexBest, true))));
obj.push_back(Pair("difficulty", diff));
obj.push_back(Pair("moneysupply", ValueFromAmount(pindexBest->nMoneySupply)));
strReply = write_string(Value(obj), false) + "\n";
}
else if (resource == "block" && !param.empty()) {
uint256 hash(param);
if (mapBlockIndex.count(hash) == 0) {
nStatus = HTTP_NOT_FOUND;
strReply = "{\"error\":\"Block not found\"}";
return true;
}
CBlock block;
CBlockIndex* pblockindex = mapBlockIndex[hash];
block.ReadFromDisk(pblockindex, true);
if (format == "hex") {
CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION);
ssBlock << block;
strReply = HexStr(ssBlock.begin(), ssBlock.end()) + "\n";
} else {
Object obj = blockToJSON(block, pblockindex, false);
strReply = write_string(Value(obj), false) + "\n";
}
}
else if (resource == "blockheader" && !param.empty()) {
uint256 hash(param);
if (mapBlockIndex.count(hash) == 0) {
nStatus = HTTP_NOT_FOUND;
strReply = "{\"error\":\"Block not found\"}";
return true;
}
CBlockIndex* pblockindex = mapBlockIndex[hash];
Object result;
result.push_back(Pair("hash", pblockindex->GetBlockHash().GetHex()));
result.push_back(Pair("confirmations", pindexBest->nHeight - pblockindex->nHeight + 1));
result.push_back(Pair("height", pblockindex->nHeight));
result.push_back(Pair("version", pblockindex->nVersion));
result.push_back(Pair("merkleroot", pblockindex->hashMerkleRoot.GetHex()));
result.push_back(Pair("time", (boost::int64_t)pblockindex->GetBlockTime()));
result.push_back(Pair("nonce", (boost::uint64_t)pblockindex->nNonce));
result.push_back(Pair("bits", HexBits(pblockindex->nBits)));
result.push_back(Pair("difficulty", GetDifficulty(pblockindex)));
result.push_back(Pair("flags", strprintf("%s%s",
pblockindex->IsProofOfStake() ? "proof-of-stake" : "proof-of-work",
pblockindex->GeneratedStakeModifier() ? " stake-modifier" : "")));
if (pblockindex->pprev)
result.push_back(Pair("previousblockhash", pblockindex->pprev->GetBlockHash().GetHex()));
if (pblockindex->pnext)
result.push_back(Pair("nextblockhash", pblockindex->pnext->GetBlockHash().GetHex()));
strReply = write_string(Value(result), false) + "\n";
}
else if (resource == "tx" && !param.empty()) {
uint256 hash(param);
CTransaction tx;
uint256 hashBlock = 0;
if (!GetTransaction(hash, tx, hashBlock))
{
nStatus = HTTP_NOT_FOUND;
strReply = "{\"error\":\"Transaction not found\"}";
return true;
}
if (format == "hex") {
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << tx;
strReply = HexStr(ssTx.begin(), ssTx.end()) + "\n";
} else {
Object obj;
obj.push_back(Pair("txid", tx.GetHash().GetHex()));
TxToJSON(tx, hashBlock, obj);
strReply = write_string(Value(obj), false) + "\n";
}
}
else if (resource == "blockhashbyheight" && !param.empty()) {
int nHeight = atoi(param.c_str());
if (nHeight < 0 || nHeight > nBestHeight) {
nStatus = HTTP_NOT_FOUND;
strReply = "{\"error\":\"Block height out of range\"}";
return true;
}
CBlockIndex* pblockindex = FindBlockByHeight(nHeight);
Object obj;
obj.push_back(Pair("blockhash", pblockindex->phashBlock->GetHex()));
strReply = write_string(Value(obj), false) + "\n";
}
else if (resource == "mempool") {
vector<uint256> vtxid;
mempool.queryHashes(vtxid);
Array a;
BOOST_FOREACH(const uint256& hash, vtxid)
a.push_back(hash.ToString());
strReply = write_string(Value(a), false) + "\n";
}
else {
nStatus = HTTP_NOT_FOUND;
strReply = "{\"error\":\"Unknown REST endpoint\"}";
}
}
catch (std::exception& e) {
nStatus = HTTP_INTERNAL_SERVER_ERROR;
strReply = strprintf("{\"error\":\"%s\"}", e.what());
}
catch (...) {
nStatus = HTTP_INTERNAL_SERVER_ERROR;
strReply = "{\"error\":\"Internal server error\"}";
}
return true;
}
// Old HandleRESTRequest removed - now in rest.cpp
/**
* Handle SSE (Server-Sent Events) stream connection.
@@ -1294,20 +1117,32 @@ void ThreadRPCServer3(void* parg)
ReadHTTP(conn->stream(), mapHeaders, strRequest);
// Handle REST API requests (unauthenticated, read-only)
// Handle REST API requests
string strHTTPMethod = mapHeaders.count("_method") ? mapHeaders["_method"] : "POST";
string strURI = mapHeaders.count("_uri") ? mapHeaders["_uri"] : "/";
if (strHTTPMethod == "GET" && strURI.substr(0, 6) == "/rest/")
if (IsRESTPath(strURI) || (strHTTPMethod == "OPTIONS" && IsRESTPath(strURI)))
{
if (!GetBoolArg("-rest", false))
{
conn->stream() << HTTPReplyREST(HTTP_FORBIDDEN, "{\"error\":\"REST API not enabled. Start with -rest=1\"}") << std::flush;
break;
}
// Rate limit public (non-wallet) endpoints
if (strURI.find("/rest/wallet/") == string::npos)
{
string strPeerIP = conn->peer_address_to_string();
if (!CheckRESTRateLimit(strPeerIP))
{
conn->stream() << HTTPReplyREST(429, "{\"error\":\"Rate limit exceeded. Try again later.\"}") << std::flush;
break;
}
}
string strReply, strContentType;
int nRESTStatus;
HandleRESTRequest(strURI, strReply, strContentType, nRESTStatus);
HandleRESTRequest(strHTTPMethod, strURI, strRequest, mapHeaders, strReply, strContentType, nRESTStatus);
conn->stream() << HTTPReplyREST(nRESTStatus, strReply, strContentType) << std::flush;
break;
}