Adds a dev-only harness that closes the "needs a live daemon" gap for the new chat wire paths, fund-safely on an isolated regtest chain: - tools/chat_regtest_smoke.cpp (+ an EXCLUDE_FROM_ALL CMake target): a small driver linking the chat library — derive identity / compose a message or a Control memo / extract + decrypt harvested memos. Secret-safe. - tools/chat_regtest.py: starts a throwaway regtest dragonxd (own datadir, -regtest, no network — nothing touches ~/.hush), funds a shielded note, then drives a full round-trip through the real daemon: compose -> z_sendmany the memo outputs -> mine -> z_listreceivedbyaddress -> feed back to the driver. Verified on dragonxd v1.3.0: a chunked 1529-byte message (8 memo outputs in one tx) and a self-addressed Control(del) memo both round-trip verbatim and reassemble/extract correctly — i.e. the real node accepts a multi-output 0-value chat tx and returns its memos intact. Paths are env-overridable (DRAGONX_REGTEST_DAEMON_DIR / DRAGONX_CHAT_SMOKE / DRAGONX_REGTEST_RUN). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
133 lines
5.5 KiB
C++
133 lines
5.5 KiB
C++
// DragonX Wallet — regtest chat smoke driver (dev tool, EXCLUDE_FROM_ALL; never shipped).
|
|
//
|
|
// Does the CHAT-side work only — identity derivation, outgoing-memo compose, and
|
|
// harvest extract + decrypt — linking the real chat library. The daemon transport
|
|
// (z_sendmany / generate / z_listreceivedbyaddress) is driven by the surrounding
|
|
// regtest runner script, which pipes memos in and out of this tool. Together they
|
|
// verify the one thing unit tests cannot: that the real dragonxd accepts a multi-
|
|
// output 0-value chat tx and round-trips its memos verbatim so a chunked (tier-2)
|
|
// message and a self-addressed Control memo reassemble end to end.
|
|
//
|
|
// Modes (memos for `extract` arrive on stdin, one per line):
|
|
// pubkey <mnemonic>
|
|
// compose <mnemonic> <peerPubKeyHex> <peerZaddr> <myReplyZaddr> <cid> <plaintext>
|
|
// control <mnemonic> <selfZaddr> <cid> <op> <label>
|
|
// extract <mnemonic>
|
|
//
|
|
// SECRET-SAFE: never prints the mnemonic or any key; only public pubkeys, memos, and
|
|
// recovered plaintext (which the runner feeds back the same message it sent).
|
|
|
|
#include "chat/chat_crypto.h"
|
|
#include "chat/chat_identity.h"
|
|
#include "chat/chat_outgoing.h"
|
|
#include "chat/chat_protocol.h"
|
|
|
|
#include <cstdio>
|
|
#include <iostream>
|
|
#include <string>
|
|
|
|
using namespace dragonx::chat;
|
|
|
|
namespace {
|
|
|
|
bool derive(const char* mnemonic, ChatKeyPair& keys, std::string& pubHex)
|
|
{
|
|
auto r = deriveChatIdentityFromSecret(mnemonic, keys, /*featureEnabled=*/true);
|
|
if (r.status != ChatIdentityStatus::Ready) {
|
|
std::fprintf(stderr, "[chat-smoke] identity derive failed: %s\n", r.error_name);
|
|
return false;
|
|
}
|
|
pubHex = r.public_key_hex;
|
|
return true;
|
|
}
|
|
|
|
// Strip the fixed-512 memo's trailing NUL padding (and any stray whitespace) that the
|
|
// daemon's memoStr carries back, so the header JSON / chunk hex parses cleanly.
|
|
std::string trimMemo(std::string s)
|
|
{
|
|
while (!s.empty() && (s.back() == '\0' || s.back() == '\n' || s.back() == '\r' || s.back() == ' '))
|
|
s.pop_back();
|
|
return s;
|
|
}
|
|
|
|
void emit(const OutgoingChatMemos& memos)
|
|
{
|
|
std::printf("RECIP %s\n", memos.recipientZaddr.c_str());
|
|
std::printf("HEADER %s\n", memos.headerMemo.c_str());
|
|
for (const auto& p : memos.payloadMemos) std::printf("PAYLOAD %s\n", p.c_str());
|
|
std::fprintf(stderr, "[chat-smoke] composed %zu payload output(s)\n", memos.payloadMemos.size());
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
if (argc < 2) { std::fprintf(stderr, "usage: chat_regtest_smoke <pubkey|compose|control|extract> ...\n"); return 2; }
|
|
const std::string mode = argv[1];
|
|
|
|
if (mode == "pubkey") {
|
|
if (argc < 3) return 2;
|
|
ChatKeyPair keys; std::string pub;
|
|
if (!derive(argv[2], keys, pub)) return 1;
|
|
std::printf("%s\n", pub.c_str());
|
|
return 0;
|
|
}
|
|
|
|
if (mode == "compose") {
|
|
if (argc < 8) { std::fprintf(stderr, "compose needs 6 args\n"); return 2; }
|
|
ChatKeyPair keys; std::string pub;
|
|
if (!derive(argv[2], keys, pub)) return 1;
|
|
OutgoingChatMemos memos;
|
|
const auto st = buildOutgoingMessage(keys, pub, /*myReply=*/argv[5], /*peerPub=*/argv[3],
|
|
/*peerZ=*/argv[4], /*cid=*/argv[6], /*plain=*/argv[7], memos);
|
|
if (st != ChatComposeStatus::Ok) { std::fprintf(stderr, "[chat-smoke] compose status=%d\n", (int)st); return 1; }
|
|
emit(memos);
|
|
return 0;
|
|
}
|
|
|
|
if (mode == "control") {
|
|
if (argc < 7) { std::fprintf(stderr, "control needs 5 args\n"); return 2; }
|
|
ChatKeyPair keys; std::string pub;
|
|
if (!derive(argv[2], keys, pub)) return 1;
|
|
OutgoingChatMemos memos;
|
|
const auto st = buildOutgoingControl(pub, /*selfZ=*/argv[3], /*cid=*/argv[4],
|
|
/*op=*/argv[5], /*label=*/argv[6], memos);
|
|
if (st != ChatComposeStatus::Ok) { std::fprintf(stderr, "[chat-smoke] control status=%d\n", (int)st); return 1; }
|
|
emit(memos);
|
|
return 0;
|
|
}
|
|
|
|
if (mode == "extract") {
|
|
if (argc < 3) return 2;
|
|
ChatKeyPair keys; std::string pub;
|
|
if (!derive(argv[2], keys, pub)) return 1;
|
|
HushChatTransactionInput tx; tx.txid = "regtest-tx";
|
|
std::string line; std::size_t pos = 0;
|
|
while (std::getline(std::cin, line)) {
|
|
const std::string m = trimMemo(line);
|
|
if (!m.empty()) tx.outputs.push_back({pos++, m});
|
|
}
|
|
auto ext = extractHushChatTransactionMetadata(tx, /*featureEnabled=*/true);
|
|
std::fprintf(stderr, "[chat-smoke] %zu memo output(s) in -> %zu metadata, %zu issue(s)\n",
|
|
tx.outputs.size(), ext.metadata.size(), ext.issues.size());
|
|
int recovered = 0;
|
|
for (const auto& md : ext.metadata) {
|
|
if (md.type == HushChatHeaderType::Message) {
|
|
std::string plain;
|
|
const auto cs = decryptIncoming(keys, md.sender_public_key_hex,
|
|
md.secretstream_header_hex, md.payload_memo, plain);
|
|
if (cs == ChatCryptoStatus::Ok) { std::printf("PLAINTEXT %s\n", plain.c_str()); ++recovered; }
|
|
else std::fprintf(stderr, "[chat-smoke] decrypt failed status=%d\n", (int)cs);
|
|
} else if (md.type == HushChatHeaderType::Control) {
|
|
std::printf("CONTROL op=%s cid=%s label=%s\n",
|
|
md.op.c_str(), md.conversation_id.c_str(), md.payload_memo.c_str());
|
|
++recovered;
|
|
}
|
|
}
|
|
return recovered > 0 ? 0 : 1;
|
|
}
|
|
|
|
std::fprintf(stderr, "unknown mode: %s\n", mode.c_str());
|
|
return 2;
|
|
}
|