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:
@@ -2032,6 +2032,20 @@ void testNetworkRefreshResultModels()
|
||||
EXPECT_EQ(state.market.price_history.size(), static_cast<size_t>(1));
|
||||
}
|
||||
|
||||
// Regression: CoinGecko emits JSON null (not an omitted key) for fields it can't compute —
|
||||
// commonly usd_24h_change on illiquid tokens like DRGX — while still returning a valid spot
|
||||
// price. The parser must keep the valid usd/btc, not discard the whole update on the null.
|
||||
auto priceNull = Refresh::parseCoinGeckoPriceResponse(
|
||||
R"({"dragonx-2":{"usd":0.42,"btc":0.000009,"usd_24h_change":null,"usd_24h_vol":null,"usd_market_cap":50000}})",
|
||||
0);
|
||||
EXPECT_TRUE(priceNull.has_value());
|
||||
if (priceNull) {
|
||||
EXPECT_NEAR(priceNull->market.price_usd, 0.42, 0.00000001);
|
||||
EXPECT_NEAR(priceNull->market.price_btc, 0.000009, 0.00000001);
|
||||
EXPECT_NEAR(priceNull->market.change_24h, 0.0, 0.0001); // null -> default, not a throw
|
||||
EXPECT_NEAR(priceNull->market.market_cap, 50000.0, 0.0001);
|
||||
}
|
||||
|
||||
Refresh::markPriceRefreshStarted(state);
|
||||
Refresh::applyPriceRefreshFailure(state, "timeout");
|
||||
EXPECT_FALSE(state.market.price_loading);
|
||||
@@ -2213,6 +2227,20 @@ void testOperationStatusPollParsing()
|
||||
EXPECT_FALSE(malformed.anySuccess);
|
||||
EXPECT_TRUE(malformed.doneOpids.empty());
|
||||
EXPECT_TRUE(malformed.staleOpids.empty());
|
||||
|
||||
// Regression: a type-anomalous element (non-string "id"/"status") must be skipped, not throw —
|
||||
// a throw here would escape the parser and permanently wedge opid polling for the session. A
|
||||
// valid tracked opid alongside the anomaly must still be processed.
|
||||
auto typeSafe = Refresh::parseOperationStatusPoll(json::array({
|
||||
json{{"id", 12345}, {"status", "failed"}}, // non-string id -> skipped, no throw
|
||||
json{{"id", "op-ok"}, {"status", 7}}, // non-string status -> "" (not done)
|
||||
json{{"id", "op-good"}, {"status", "success"}, {"result", json{{"txid", "tx-good"}}}}
|
||||
}), {"op-ok", "op-good", "op-x"});
|
||||
EXPECT_TRUE(typeSafe.anySuccess);
|
||||
EXPECT_EQ(typeSafe.successTxidsByOpid.at("op-good"), std::string("tx-good"));
|
||||
EXPECT_TRUE(typeSafe.failureMessages.empty()); // the non-string-id "failed" was skipped
|
||||
EXPECT_EQ(typeSafe.staleOpids.size(), static_cast<size_t>(1)); // op-x absent; op-ok was seen (not stale)
|
||||
EXPECT_EQ(typeSafe.staleOpids[0], std::string("op-x"));
|
||||
}
|
||||
|
||||
void testSecureVaultScope()
|
||||
@@ -6260,6 +6288,12 @@ void testDaemonChecksumParsing()
|
||||
"| DragonX-1.0.2-Win64.ZIP | `dd6a554ac05c834da9910ae796215567e97c426f3aed15b54af1f7b90d48c43a` |");
|
||||
EXPECT_EQ(mixed.at("dragonx-1.0.2-win64.zip"),
|
||||
std::string("dd6a554ac05c834da9910ae796215567e97c426f3aed15b54af1f7b90d48c43a"));
|
||||
// Regression: a markdown-bolded filename (**archive.zip**) must still parse — otherwise a valid,
|
||||
// correctly-signed release whose body bolds the name would fail checksum lookup and be refused.
|
||||
const auto bold = parseDaemonChecksums(
|
||||
"| **dragonx-1.0.2-win64.zip** | `dd6a554ac05c834da9910ae796215567e97c426f3aed15b54af1f7b90d48c43a` |");
|
||||
EXPECT_EQ(bold.at("dragonx-1.0.2-win64.zip"),
|
||||
std::string("dd6a554ac05c834da9910ae796215567e97c426f3aed15b54af1f7b90d48c43a"));
|
||||
}
|
||||
|
||||
void testDaemonBasenamesAndVersionCore()
|
||||
|
||||
Reference in New Issue
Block a user