fix(fund): Tier-B parser/aggregation hardening — range-check amounts, int-satoshi sums, strict balance parse
Fund-correctness audit, Tier-B (parse/validation/display only — no send/sweep/encrypt call touched; each change can only reject bad input or remove drift, never over-report): - lite_result_parsers: cap every backend-reported amount at the 21,000,000 DRGX supply (note value, outgoing output value + running sum, received amount, the net-amount fallback magnitude), and replace the two bare `return false` paths in the outgoing-metadata loop — which left ok=false with an EMPTY error and silently rejected the whole tx list — with fail() carrying a reason. - network_refresh_service: accumulate per-address balances in integer satoshis (util::toSats) instead of summing doubles, so many notes can't drift by an ulp per addition (ten 0.1 notes now sum to exactly 1.0); skip non-finite amounts. - readBalanceString: strict decimal parse (digits / one '.' / optional leading '-') instead of std::stod, which silently accepted "1.5abc", whitespace, hex, exponents and nan/inf. Tests: testLiteParserAmountRangeChecks + integer-satoshi drift + strict-parse cases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,9 @@ bool requireArray(const json& value, const std::string& path, Result& result)
|
||||
return true;
|
||||
}
|
||||
|
||||
// 21,000,000 DRGX in zatoshi: no single backend-reported amount can legitimately exceed the supply.
|
||||
constexpr std::int64_t kMaxMoneyZat = static_cast<std::int64_t>(21000000) * 100000000LL;
|
||||
|
||||
bool jsonToUnsigned(const json& value, std::uint64_t& output)
|
||||
{
|
||||
try {
|
||||
@@ -282,6 +285,9 @@ bool parseSpendableOutput(const json& value,
|
||||
if (!readRequiredStringField(value, "created_in_txid", output.createdInTxid, result)) return false;
|
||||
if (!readOptionalSignedField(value, "created_in_block", output.createdInBlock, result)) return false;
|
||||
if (!readRequiredUnsignedField(value, "value", output.value, result)) return false;
|
||||
if (output.value > static_cast<std::uint64_t>(kMaxMoneyZat)) {
|
||||
return fail(result, LiteResultParserError::InvalidFieldValue, path + ".value", "note value exceeds the maximum supply");
|
||||
}
|
||||
if (!readOptionalBoolField(value, "spent", output.spent, result)) return false;
|
||||
if (!readOptionalBoolField(value, "unconfirmed_spent", output.unconfirmedSpent, result)) return false;
|
||||
output.spendable = !output.pending && !output.spent && !output.unconfirmedSpent;
|
||||
@@ -352,8 +358,13 @@ bool parseTransactionRecord(const json& value,
|
||||
if (!parseTransactionOutput(metadata[index], path + ".outgoing_metadata[" + std::to_string(index) + "]", parsedOutput, result)) return false;
|
||||
// Reject negative values and guard the running sum: signed overflow is UB, and a hostile
|
||||
// backend could otherwise overflow the total into a bogus (even negative) amount.
|
||||
if (parsedOutput.value < 0) return false;
|
||||
if (output.amount > std::numeric_limits<std::int64_t>::max() - parsedOutput.value) return false;
|
||||
const std::string outputPath = path + ".outgoing_metadata[" + std::to_string(index) + "]";
|
||||
if (parsedOutput.value < 0 || parsedOutput.value > kMaxMoneyZat) {
|
||||
return fail(result, LiteResultParserError::InvalidFieldValue, outputPath + ".value", "outgoing output value is negative or exceeds the maximum supply");
|
||||
}
|
||||
if (output.amount > kMaxMoneyZat - parsedOutput.value) {
|
||||
return fail(result, LiteResultParserError::InvalidFieldValue, path + ".outgoing_metadata", "outgoing output total exceeds the maximum supply");
|
||||
}
|
||||
output.amount += parsedOutput.value;
|
||||
output.outgoingMetadata.push_back(std::move(parsedOutput));
|
||||
}
|
||||
@@ -362,7 +373,8 @@ bool parseTransactionRecord(const json& value,
|
||||
// totals aren't understated. Display-only; tolerant of a missing/odd field.
|
||||
if (output.amount == 0 && value.contains("amount") && !value.at("amount").is_null()) {
|
||||
std::int64_t net = 0;
|
||||
if (jsonToSigned(value.at("amount"), net) && net != std::numeric_limits<std::int64_t>::min()) {
|
||||
if (jsonToSigned(value.at("amount"), net) && net != std::numeric_limits<std::int64_t>::min()
|
||||
&& (net < 0 ? -net : net) <= kMaxMoneyZat) {
|
||||
output.amount = net < 0 ? -net : net;
|
||||
}
|
||||
}
|
||||
@@ -372,6 +384,9 @@ bool parseTransactionRecord(const json& value,
|
||||
output.direction = LiteTransactionDirection::Receive;
|
||||
if (!readRequiredStringField(value, "address", output.address, result)) return false;
|
||||
if (!readRequiredSignedField(value, "amount", output.amount, result)) return false;
|
||||
if (output.amount < 0 || output.amount > kMaxMoneyZat) {
|
||||
return fail(result, LiteResultParserError::InvalidFieldValue, path + ".amount", "received amount is negative or exceeds the maximum supply");
|
||||
}
|
||||
if (!readOptionalStringField(value, "memo", output.memo, result)) return false;
|
||||
if (!readOptionalSignedField(value, "position", output.position, result)) return false;
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user