Two dead-code removals surfaced by the audit; no runtime behavior change.
1. Delete the lite-lifecycle "readiness" scaffold (~1,586 lines). The pure
dry-run evaluateLiteWalletServerLifecycleReadiness (whose own status text
says "wallet lifecycle execution is still disabled in this scaffold") had
zero callers; executeLiteWalletServerSelectionUi had zero callers; and
executeLiteWalletLifecycleUiRequest's only caller was a fallback in
settings_page reached only when app->liteWallet() is null. Deleted
lite_wallet_server_lifecycle_readiness.{h,cpp} and the readiness /
UI-execution machinery + enums + translation tables from both adapters,
keeping the genuinely-live helpers (liteConnectionSettingsFromAppSettings,
applyLiteConnectionSettingsToAppSettings, redactLiteServerSelectionValue,
and the LiteWalletLifecycleUiExecutionInput DTO the live create/open/
restore path still uses). The null-backend fallback now sets a plain
"Lite wallet backend unavailable" status. This is exactly the
readiness/scaffold pattern CLAUDE.md forbids regrowing.
2. Split the HushChat fixture/capture-manifest/seed-projection tooling
(~2,050 lines, incl. the libsodium seed-projection) out of
chat_protocol.cpp (1854 -> 284) and chat_protocol.h (586 -> 105) into a
new chat_fixture_tooling.{h,cpp} compiled ONLY into the HushChatFixtureCheck
dev tool — never into the app, lite, or test binaries (verified via nm).
The app keeps only the runtime slice (memo parsing + tx metadata
extraction) that network_refresh_service actually calls. Also moved the
decrypt-preflight/hex helpers, which likewise had no runtime caller.
Note: the HushChat split is on gated-off experimental code and should be
coordinated with the pending dormant-HushChat-content handling (commit
af06b8b) before the lite PR — done here as a pure mechanical split, no redesign.
Full-node + Lite + HushChatFixtureCheck build clean; ctest 1/1; hygiene clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
285 lines
9.6 KiB
C++
285 lines
9.6 KiB
C++
#include "chat_protocol.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <optional>
|
|
#include <utility>
|
|
|
|
namespace dragonx::chat {
|
|
namespace {
|
|
|
|
bool isHexString(const std::string& value)
|
|
{
|
|
for (unsigned char ch : value) {
|
|
if (!std::isxdigit(ch)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool isCiphertextPayloadCandidate(const std::string& value)
|
|
{
|
|
return !value.empty() && value.size() <= kHushChatMemoByteLimit &&
|
|
value.size() % 2 == 0 && isHexString(value);
|
|
}
|
|
|
|
bool isContactPayloadCandidate(const std::string& value)
|
|
{
|
|
return !value.empty() && value.size() <= kHushChatMemoByteLimit && value.front() != '{';
|
|
}
|
|
|
|
bool readRequiredString(const nlohmann::json& object,
|
|
const char* key,
|
|
std::string& value,
|
|
std::string& error)
|
|
{
|
|
auto it = object.find(key);
|
|
if (it == object.end()) {
|
|
error = std::string("missing field: ") + key;
|
|
return false;
|
|
}
|
|
if (!it->is_string()) {
|
|
error = std::string("field is not a string: ") + key;
|
|
return false;
|
|
}
|
|
value = it->get<std::string>();
|
|
return true;
|
|
}
|
|
|
|
bool readRequiredInt(const nlohmann::json& object,
|
|
const char* key,
|
|
int& value,
|
|
std::string& error)
|
|
{
|
|
auto it = object.find(key);
|
|
if (it == object.end()) {
|
|
error = std::string("missing field: ") + key;
|
|
return false;
|
|
}
|
|
if (!it->is_number_integer()) {
|
|
error = std::string("field is not an integer: ") + key;
|
|
return false;
|
|
}
|
|
value = it->get<int>();
|
|
return true;
|
|
}
|
|
|
|
HushChatHeaderParseResult fail(std::string error)
|
|
{
|
|
HushChatHeaderParseResult result;
|
|
result.error = std::move(error);
|
|
return result;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
const char* hushChatHeaderTypeName(HushChatHeaderType type)
|
|
{
|
|
switch (type) {
|
|
case HushChatHeaderType::Message:
|
|
return "Memo";
|
|
case HushChatHeaderType::ContactRequest:
|
|
return "Cont";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
const char* hushChatMemoGroupingIssueName(HushChatMemoGroupingIssue issue)
|
|
{
|
|
switch (issue) {
|
|
case HushChatMemoGroupingIssue::InvalidHeader:
|
|
return "InvalidHeader";
|
|
case HushChatMemoGroupingIssue::MissingPayload:
|
|
return "MissingPayload";
|
|
case HushChatMemoGroupingIssue::DuplicateHeader:
|
|
return "DuplicateHeader";
|
|
case HushChatMemoGroupingIssue::OversizedMemo:
|
|
return "OversizedMemo";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo)
|
|
{
|
|
if (memo.empty()) return fail("empty memo");
|
|
if (memo.size() > kHushChatMemoByteLimit) return fail("memo exceeds HushChat memo byte limit");
|
|
if (memo.front() != '{') return fail("memo is not a HushChat header JSON object");
|
|
|
|
nlohmann::json object;
|
|
try {
|
|
object = nlohmann::json::parse(memo);
|
|
} catch (const nlohmann::json::parse_error& e) {
|
|
return fail(std::string("invalid JSON: ") + e.what());
|
|
}
|
|
if (!object.is_object()) return fail("header memo JSON is not an object");
|
|
|
|
HushChatHeader header;
|
|
std::string type;
|
|
std::string error;
|
|
if (!readRequiredInt(object, "h", header.header_number, error)) return fail(error);
|
|
if (!readRequiredInt(object, "v", header.version, error)) return fail(error);
|
|
if (!readRequiredString(object, "z", header.reply_zaddr, error)) return fail(error);
|
|
if (!readRequiredString(object, "cid", header.conversation_id, error)) return fail(error);
|
|
if (!readRequiredString(object, "t", type, error)) return fail(error);
|
|
if (!readRequiredString(object, "e", header.secretstream_header_hex, error)) return fail(error);
|
|
if (!readRequiredString(object, "p", header.public_key_hex, error)) return fail(error);
|
|
|
|
if (header.header_number < 1) return fail("header number must be positive");
|
|
if (header.version != kHushChatSupportedVersion) return fail("unsupported HushChat version");
|
|
if (header.reply_zaddr.empty()) return fail("reply z-address is empty");
|
|
if (header.conversation_id.empty()) return fail("conversation id is empty");
|
|
|
|
if (type == "Memo") {
|
|
header.type = HushChatHeaderType::Message;
|
|
} else if (type == "Cont") {
|
|
header.type = HushChatHeaderType::ContactRequest;
|
|
} else {
|
|
return fail("unknown HushChat header type");
|
|
}
|
|
|
|
if (header.public_key_hex.size() != kHushChatPublicKeyHexLength || !isHexString(header.public_key_hex)) {
|
|
return fail("public key must be 32 bytes encoded as hex");
|
|
}
|
|
|
|
if (header.type == HushChatHeaderType::Message) {
|
|
if (header.secretstream_header_hex.size() != kHushChatSecretstreamHeaderHexLength ||
|
|
!isHexString(header.secretstream_header_hex)) {
|
|
return fail("message header must include a 24 byte secretstream header encoded as hex");
|
|
}
|
|
} else if (!header.secretstream_header_hex.empty()) {
|
|
return fail("contact request header must not include a secretstream header");
|
|
}
|
|
|
|
HushChatHeaderParseResult result;
|
|
result.ok = true;
|
|
result.header = std::move(header);
|
|
return result;
|
|
}
|
|
|
|
HushChatMemoGroupingResult groupHushChatMemoOutputs(const std::vector<HushChatMemoOutput>& outputs)
|
|
{
|
|
struct OrderedOutput {
|
|
std::size_t input_index = 0;
|
|
HushChatMemoOutput output;
|
|
};
|
|
|
|
std::vector<OrderedOutput> ordered;
|
|
ordered.reserve(outputs.size());
|
|
for (std::size_t index = 0; index < outputs.size(); ++index) {
|
|
ordered.push_back(OrderedOutput{index, outputs[index]});
|
|
}
|
|
std::stable_sort(ordered.begin(), ordered.end(), [](const OrderedOutput& left, const OrderedOutput& right) {
|
|
if (left.output.position == right.output.position) return left.input_index < right.input_index;
|
|
return left.output.position < right.output.position;
|
|
});
|
|
|
|
HushChatMemoGroupingResult result;
|
|
std::optional<HushChatMemoOutput> pending_header_output;
|
|
std::optional<HushChatHeader> pending_header;
|
|
|
|
auto addIssue = [&](HushChatMemoGroupingIssue issue, std::size_t position, std::string detail) {
|
|
result.issues.push_back(HushChatMemoGroupingIssueInfo{issue, position, std::move(detail)});
|
|
};
|
|
|
|
auto clearPendingAsMissing = [&]() {
|
|
if (!pending_header_output) return;
|
|
addIssue(HushChatMemoGroupingIssue::MissingPayload,
|
|
pending_header_output->position,
|
|
"header did not have a matching payload memo");
|
|
pending_header_output.reset();
|
|
pending_header.reset();
|
|
};
|
|
|
|
for (const auto& entry : ordered) {
|
|
const auto& output = entry.output;
|
|
if (output.memo.size() > kHushChatMemoByteLimit) {
|
|
addIssue(HushChatMemoGroupingIssue::OversizedMemo,
|
|
output.position,
|
|
"memo exceeds HushChat memo byte limit");
|
|
continue;
|
|
}
|
|
|
|
if (!output.memo.empty() && output.memo.front() == '{') {
|
|
auto parsed = parseHushChatHeaderMemo(output.memo);
|
|
if (!parsed.ok) {
|
|
addIssue(HushChatMemoGroupingIssue::InvalidHeader, output.position, parsed.error);
|
|
continue;
|
|
}
|
|
|
|
if (pending_header_output) {
|
|
addIssue(HushChatMemoGroupingIssue::DuplicateHeader,
|
|
output.position,
|
|
"encountered another HushChat header before a payload");
|
|
clearPendingAsMissing();
|
|
}
|
|
|
|
pending_header_output = output;
|
|
pending_header = std::move(parsed.header);
|
|
continue;
|
|
}
|
|
|
|
if (!pending_header_output || !pending_header) {
|
|
++result.ignored_memo_count;
|
|
continue;
|
|
}
|
|
|
|
const bool payload_matches = pending_header->type == HushChatHeaderType::Message
|
|
? isCiphertextPayloadCandidate(output.memo)
|
|
: isContactPayloadCandidate(output.memo);
|
|
if (!payload_matches) {
|
|
++result.ignored_memo_count;
|
|
continue;
|
|
}
|
|
|
|
HushChatMemoPair pair;
|
|
pair.header = std::move(*pending_header);
|
|
pair.header_position = pending_header_output->position;
|
|
pair.payload_position = output.position;
|
|
pair.payload_memo = output.memo;
|
|
result.pairs.push_back(std::move(pair));
|
|
pending_header_output.reset();
|
|
pending_header.reset();
|
|
}
|
|
|
|
clearPendingAsMissing();
|
|
return result;
|
|
}
|
|
|
|
HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
|
|
const HushChatTransactionInput& transaction,
|
|
bool featureEnabled)
|
|
{
|
|
HushChatTransactionExtractionResult result;
|
|
result.feature_enabled = featureEnabled;
|
|
if (!featureEnabled || transaction.txid.empty()) return result;
|
|
|
|
auto grouped = groupHushChatMemoOutputs(transaction.outputs);
|
|
result.ignored_memo_count = grouped.ignored_memo_count;
|
|
result.issues.reserve(grouped.issues.size());
|
|
for (const auto& issue : grouped.issues) {
|
|
result.issues.push_back(HushChatMemoGroupingIssueInfo{
|
|
issue.issue,
|
|
issue.position,
|
|
hushChatMemoGroupingIssueName(issue.issue)
|
|
});
|
|
}
|
|
|
|
result.metadata.reserve(grouped.pairs.size());
|
|
for (const auto& pair : grouped.pairs) {
|
|
HushChatTransactionMetadata metadata;
|
|
metadata.txid = transaction.txid;
|
|
metadata.type = pair.header.type;
|
|
metadata.conversation_id = pair.header.conversation_id;
|
|
metadata.reply_zaddr = pair.header.reply_zaddr;
|
|
metadata.header_position = pair.header_position;
|
|
metadata.payload_position = pair.payload_position;
|
|
metadata.payload_size = pair.payload_memo.size();
|
|
result.metadata.push_back(std::move(metadata));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
} // namespace dragonx::chat
|