fix(parsers): harden RPC/price/updater parsing against valid-but-unhandled input
Audit of the full-node RPC response parsers and updater release-body parsers (find -> adversarially-verify workflow) surfaced three worth fixing; three others guard formats the project doesn't emit and are backstopped by signature verification, so they're documented rather than churned. - Price (Medium): parseCoinGeckoPriceResponse used .value(key, 0.0), which throws type_error on a PRESENT null. CoinGecko emits null for usd_24h_change/ usd_24h_vol on illiquid tokens (DRGX is one) while still returning a valid spot price; the outer catch turned that into no price update at all. Read null-tolerantly so the valid usd/btc survives. - Daemon updater (Medium): parseDaemonChecksums blanked '|'/backtick but not markdown emphasis, so a bolded **archive.zip** checksum row was dropped and a valid, correctly-signed release would be refused. Also blank '*'/'_' (cannot cause a wrong-asset match; the 64-hex + .zip-suffix tests are unchanged). - Opid poll (Low, severe failure mode): parseOperationStatusPoll read id/status via .value() (throws on a present non-string) and the call site parsed OUTSIDE its try/catch, so a throw left opid_poll_in_progress_ stuck true and wedged all z-operation polling for the session. Type-check the reads and parse inside the guard. (dragonxd can't emit non-string id/status; this is defense-in-depth.) Regression tests: CoinGecko null field; opid non-string id/status; **bold** checksum row. Suite green (1/1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -426,12 +426,21 @@ std::optional<NetworkRefreshService::PriceRefreshResult> NetworkRefreshService::
|
||||
if (!parsed.contains("dragonx-2")) return std::nullopt;
|
||||
|
||||
const auto& data = parsed["dragonx-2"];
|
||||
// CoinGecko emits JSON null (not an omitted key) for fields it can't currently compute —
|
||||
// commonly usd_24h_change on illiquid/newly-listed tokens — while still returning a valid
|
||||
// spot price in the same object. .value(key, default) throws type_error on a PRESENT null,
|
||||
// which the outer catch turns into "no price update at all", so read null-tolerantly and
|
||||
// keep the valid usd/btc rather than discarding the whole refresh.
|
||||
auto num = [&data](const char* key, double def) {
|
||||
auto it = data.find(key);
|
||||
return (it != data.end() && it->is_number()) ? it->get<double>() : def;
|
||||
};
|
||||
PriceRefreshResult result;
|
||||
result.market.price_usd = data.value("usd", 0.0);
|
||||
result.market.price_btc = data.value("btc", 0.0);
|
||||
result.market.change_24h = data.value("usd_24h_change", 0.0);
|
||||
result.market.volume_24h = data.value("usd_24h_vol", 0.0);
|
||||
result.market.market_cap = data.value("usd_market_cap", 0.0);
|
||||
result.market.price_usd = num("usd", 0.0);
|
||||
result.market.price_btc = num("btc", 0.0);
|
||||
result.market.change_24h = num("usd_24h_change", 0.0);
|
||||
result.market.volume_24h = num("usd_24h_vol", 0.0);
|
||||
result.market.market_cap = num("usd_market_cap", 0.0);
|
||||
|
||||
char buf[64];
|
||||
// Runs on the RPC worker thread — std::localtime shares a process-wide static tm, so use the
|
||||
@@ -1101,12 +1110,16 @@ NetworkRefreshService::OperationStatusPollResult NetworkRefreshService::parseOpe
|
||||
std::set<std::string> reported;
|
||||
for (const auto& op : result) {
|
||||
if (!op.is_object()) continue;
|
||||
std::string opid = op.value("id", std::string());
|
||||
// Type-checked reads: .value(key, default) throws if the key is PRESENT with a non-string
|
||||
// type, which would abort the whole poll (and wedge it for the session — see the call site).
|
||||
if (!op.contains("id") || !op["id"].is_string()) continue;
|
||||
std::string opid = op["id"].get<std::string>();
|
||||
if (opid.empty()) continue;
|
||||
if (requested.find(opid) == requested.end()) continue; // not one of ours — ignore
|
||||
reported.insert(opid);
|
||||
|
||||
std::string status = op.value("status", std::string());
|
||||
std::string status = (op.contains("status") && op["status"].is_string())
|
||||
? op["status"].get<std::string>() : std::string();
|
||||
if (status == "success") {
|
||||
parsed.doneOpids.push_back(opid);
|
||||
parsed.anySuccess = true;
|
||||
|
||||
Reference in New Issue
Block a user