test(chat): regtest harness verifying on-chain memo round-trip (larger messages + Control)
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>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user