feat(chat): composer + contact requests — outgoing construction (Phase 4)
Add the outgoing side of HushChat: compose messages and start conversations.
The wire construction is the byte-exact inverse of the receive parser and is
proven by a self-consistent round-trip (build → parse → decrypt).
- src/chat/chat_outgoing.{h,cpp} (pure): buildOutgoingMessage encrypts via
encryptOutgoing and buildOutgoingContactRequest carries plaintext; both emit
the header memo JSON ({h,v,z,cid,t,e,p}, which nlohmann serializes
alphabetically to match SilentDragonXLite) + the payload memo, validating the
512-byte memo limit, the 64-hex peer key, and the "no leading '{'" rule for
request text. My public key goes in header "p"; the peer's key is the
encryption recipient.
- ChatService: composeMessage/composeContactRequest (encrypt with the held
identity) + recordOutgoing (echo an Outgoing ChatMessage into the store + DB —
we never harvest our own sends, so this is the only local record).
- App: sendChatMessage(cid,text) sources the peer z-addr + public key from the
conversation, composes, and records a random-id echo; startChatConversation
(zaddr,text) mints a random cid + composes a plaintext contact request;
chatReplyZaddr() picks a spendable z-addr. broadcastChatMemos() is the Phase-5
transport seam (network delivery + real-SDXL interop verification land there).
- Chat tab: a message composer (shown once the peer's key is known, else a
"waiting for reply" hint) + a "New conversation" modal that sends a contact
request to a z-address.
- Tests: outgoing round-trip through the receive parser + decrypt, contact-request
passthrough, validation guards, and ChatService compose + recordOutgoing echo.
Adversarially reviewed (crypto/interop, secret hygiene, logic, UI) — 0 findings.
Gated by DRAGONX_ENABLE_CHAT (default OFF). Verified: Linux + Windows(mingw)
build with chat ON, ctest 100%, hygiene clean; caches restored to the OFF default.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5849,6 +5849,89 @@ void testHushChatDatabase()
|
||||
scrub(dbPath); scrub(dbPath2);
|
||||
}
|
||||
|
||||
// Phase 4: outgoing memo construction round-trips through the receive parser + decrypt, and
|
||||
// ChatService compose/recordOutgoing echoes into the store.
|
||||
void testHushChatOutgoing()
|
||||
{
|
||||
using namespace dragonx::chat;
|
||||
|
||||
ChatKeyPair alice, bob;
|
||||
ChatIdentityResult ra = deriveChatIdentityFromSecret("out-alice", alice, true);
|
||||
ChatIdentityResult rb = deriveChatIdentityFromSecret("out-bob", bob, true);
|
||||
const std::string aliceZ = "zs-alice-reply";
|
||||
const std::string bobZ = "zs-bob";
|
||||
const std::string cid = "conv-out";
|
||||
|
||||
// Encrypted message Alice -> Bob, then round-trip through the receive path.
|
||||
OutgoingChatMemos memos;
|
||||
EXPECT_TRUE(buildOutgoingMessage(alice, ra.public_key_hex, aliceZ, rb.public_key_hex, bobZ, cid,
|
||||
"hello bob", memos) == ChatComposeStatus::Ok);
|
||||
EXPECT_EQ(memos.recipientZaddr, bobZ);
|
||||
EXPECT_TRUE(!memos.headerMemo.empty() && memos.headerMemo.front() == '{');
|
||||
// Header keys serialize alphabetically (cid before e).
|
||||
EXPECT_TRUE(memos.headerMemo.find("\"cid\"") < memos.headerMemo.find("\"e\""));
|
||||
|
||||
HushChatTransactionInput tx;
|
||||
tx.txid = "txout1";
|
||||
tx.outputs.push_back({0, memos.headerMemo}); // header at the lower position
|
||||
tx.outputs.push_back({1, memos.payloadMemo});
|
||||
auto extracted = extractHushChatTransactionMetadata(tx, true);
|
||||
EXPECT_EQ((int)extracted.metadata.size(), 1);
|
||||
|
||||
ChatService bobSvc;
|
||||
bobSvc.setIdentity(bob);
|
||||
EXPECT_EQ(bobSvc.ingest(extracted.metadata, {}, 5), 1);
|
||||
std::vector<ChatMessage> conv = bobSvc.store().conversation(cid);
|
||||
EXPECT_EQ((int)conv.size(), 1);
|
||||
EXPECT_EQ(conv[0].body, std::string("hello bob"));
|
||||
EXPECT_EQ(conv[0].peer_public_key_hex, ra.public_key_hex); // Bob learns Alice's key
|
||||
EXPECT_EQ(conv[0].peer_zaddr, aliceZ);
|
||||
|
||||
// Plaintext contact request Alice -> Bob.
|
||||
OutgoingChatMemos creq;
|
||||
EXPECT_TRUE(buildOutgoingContactRequest(ra.public_key_hex, aliceZ, bobZ, "conv-cr", "add me?",
|
||||
creq) == ChatComposeStatus::Ok);
|
||||
HushChatTransactionInput tx2;
|
||||
tx2.txid = "txout2";
|
||||
tx2.outputs.push_back({0, creq.headerMemo});
|
||||
tx2.outputs.push_back({1, creq.payloadMemo});
|
||||
auto ex2 = extractHushChatTransactionMetadata(tx2, true);
|
||||
EXPECT_EQ((int)ex2.metadata.size(), 1);
|
||||
EXPECT_TRUE(ex2.metadata[0].type == HushChatHeaderType::ContactRequest);
|
||||
bobSvc.ingest(ex2.metadata, {}, 6);
|
||||
EXPECT_EQ((int)bobSvc.store().conversation("conv-cr").size(), 1);
|
||||
EXPECT_EQ(bobSvc.store().conversation("conv-cr")[0].body, std::string("add me?"));
|
||||
|
||||
// Validation guards.
|
||||
OutgoingChatMemos dummy;
|
||||
EXPECT_TRUE(buildOutgoingMessage(alice, ra.public_key_hex, aliceZ, "tooshort", bobZ, cid, "x", dummy)
|
||||
== ChatComposeStatus::BadPeerKey);
|
||||
EXPECT_TRUE(buildOutgoingMessage(alice, ra.public_key_hex, aliceZ, rb.public_key_hex, bobZ, cid, "", dummy)
|
||||
== ChatComposeStatus::EmptyBody);
|
||||
EXPECT_TRUE(buildOutgoingContactRequest(ra.public_key_hex, aliceZ, bobZ, "c", "{bad", dummy)
|
||||
== ChatComposeStatus::BadRequestText);
|
||||
|
||||
// ChatService compose + recordOutgoing echo.
|
||||
OutgoingChatMemos svcMemos;
|
||||
EXPECT_TRUE(bobSvc.composeMessage(bobZ, ra.public_key_hex, aliceZ, cid, "reply!", svcMemos)
|
||||
== ChatComposeStatus::Ok);
|
||||
ChatMessage echo;
|
||||
echo.direction = ChatDirection::Outgoing;
|
||||
echo.kind = ChatMessageKind::Message;
|
||||
echo.conversation_id = cid;
|
||||
echo.peer_zaddr = aliceZ;
|
||||
echo.peer_public_key_hex = ra.public_key_hex;
|
||||
echo.body = "reply!";
|
||||
echo.timestamp = 7;
|
||||
echo.txid = "out:local1";
|
||||
echo.payload_position = 0;
|
||||
EXPECT_TRUE(bobSvc.recordOutgoing(echo));
|
||||
std::vector<ChatMessage> conv2 = bobSvc.store().conversation(cid);
|
||||
EXPECT_EQ((int)conv2.size(), 2);
|
||||
EXPECT_TRUE(conv2.back().direction == ChatDirection::Outgoing);
|
||||
EXPECT_EQ(conv2.back().body, std::string("reply!"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
@@ -5943,6 +6026,7 @@ int main()
|
||||
testHushChatReceivePath();
|
||||
testHushChatService();
|
||||
testHushChatDatabase();
|
||||
testHushChatOutgoing();
|
||||
testAddressChecksumValidation();
|
||||
testLiteServerProbeLive();
|
||||
testXmrigLiveInstall();
|
||||
|
||||
Reference in New Issue
Block a user