feat(chat): message model, in-memory store, and receive service

Phase 1 Steps 4-6 (the receive pipeline, minus the App/sync wiring which
lands next).

- chat_message: the in-memory ChatMessage model (direction, kind, txid, cid,
  peer zaddr/pubkey, body, timestamp, payload_position). No libsodium.
- chat_store: threads messages by conversation_id and dedups by
  (txid, payload_position) so re-scanning the chain never double-inserts.
- chat_service: owns the long-lived chat identity keypair (move-disabled,
  wiped on destruction/clear) and the store. ingest() decrypts each Message
  (drops undecryptable ones silently — no plaintext/memo logging), passes a
  ContactRequest's plaintext through, and threads the result. No-op without an
  identity.

Tests: a metadata batch decrypts into a threaded conversation; re-ingest
dedups; a contact request carries through; a wrong identity decrypts nothing;
no-identity ingest is a no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 20:11:05 -05:00
parent d043538e2f
commit ba03de938e
7 changed files with 282 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
#include "chat/chat_crypto.h"
#include "chat/chat_identity.h"
#include "chat/chat_service.h"
#include "daemon/daemon_controller.h"
#include "data/transaction_history_cache.h"
#include "daemon/lifecycle_adapters.h"
@@ -5662,6 +5663,74 @@ void testHushChatReceivePath()
EXPECT_EQ((int)off.metadata.size(), 0);
}
// ChatService: metadata batch -> decrypt -> threaded, deduped in-memory store.
void testHushChatService()
{
using namespace dragonx::chat;
ChatKeyPair alice, bob;
ChatIdentityResult ra = deriveChatIdentityFromSecret("alice-svc", alice, true);
ChatIdentityResult rb = deriveChatIdentityFromSecret("bob-svc", bob, true);
// Two Alice->Bob messages in one conversation, as harvested metadata.
std::vector<HushChatTransactionMetadata> batch;
for (int i = 0; i < 2; ++i) {
std::string e, ct;
EXPECT_TRUE(encryptOutgoing(alice, rb.public_key_hex, i == 0 ? "first" : "second", e, ct) == ChatCryptoStatus::Ok);
HushChatTransactionMetadata m;
m.txid = std::string("tx") + std::to_string(i);
m.type = HushChatHeaderType::Message;
m.conversation_id = "conv-x";
m.reply_zaddr = "zs-alice";
m.sender_public_key_hex = ra.public_key_hex;
m.secretstream_header_hex = e;
m.payload_memo = ct;
m.payload_position = 1;
batch.push_back(m);
}
ChatService svc;
EXPECT_TRUE(!svc.hasIdentity());
EXPECT_EQ(svc.ingest(batch), 0); // no identity -> nothing ingested
svc.setIdentity(bob);
EXPECT_TRUE(svc.hasIdentity());
EXPECT_EQ(svc.ingest(batch, 1000), 2);
EXPECT_EQ((int)svc.store().size(), 2);
EXPECT_EQ(svc.ingest(batch, 1000), 0); // re-scan dedups
EXPECT_EQ((int)svc.store().size(), 2);
std::vector<ChatMessage> conv = svc.store().conversation("conv-x");
EXPECT_EQ((int)conv.size(), 2);
EXPECT_EQ(conv[0].body, std::string("first"));
EXPECT_EQ(conv[1].body, std::string("second"));
EXPECT_TRUE(conv[0].direction == ChatDirection::Incoming);
EXPECT_EQ(conv[0].peer_public_key_hex, ra.public_key_hex);
EXPECT_EQ(conv[0].timestamp, (std::int64_t)1000);
// A contact request carries plaintext through without decryption.
std::vector<HushChatTransactionMetadata> creq(1);
creq[0].txid = "txc";
creq[0].type = HushChatHeaderType::ContactRequest;
creq[0].conversation_id = "conv-y";
creq[0].sender_public_key_hex = ra.public_key_hex;
creq[0].payload_memo = "hi, add me";
creq[0].payload_position = 1;
EXPECT_EQ(svc.ingest(creq), 1);
std::vector<ChatMessage> cy = svc.store().conversation("conv-y");
EXPECT_EQ((int)cy.size(), 1);
EXPECT_TRUE(cy[0].kind == ChatMessageKind::ContactRequest);
EXPECT_EQ(cy[0].body, std::string("hi, add me"));
// Wrong identity can't decrypt -> messages dropped silently.
ChatService svc2;
ChatKeyPair mallory;
deriveChatIdentityFromSecret("mallory-svc", mallory, true);
svc2.setIdentity(mallory);
EXPECT_EQ(svc2.ingest(batch, 1000), 0);
EXPECT_TRUE(svc2.store().empty());
}
} // namespace
int main()
@@ -5754,6 +5823,7 @@ int main()
testAtomicFileWrite();
testHushChatCrypto();
testHushChatReceivePath();
testHushChatService();
testAddressChecksumValidation();
testLiteServerProbeLive();
testXmrigLiveInstall();