feat(chat): add a Control memo header type for on-chain conversation signals

Introduces HushChatHeaderType::Control — a self-addressed control signal
(delete / restore / mark-sensitive) that rides a chat tx as a header memo
carrying an `op`, paired with a plaintext label payload so it groups like a
normal header/payload pair. The op VALUE is not restricted at parse time so a
future op reaches newer clients and is ignored by older ones; unknown header
types still fail-closed (grouped as an invalid header, never surfaced, never a
crash), preserving forward-compat.

Foundation for the on-chain chat delete/restore feature (Phase A).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 00:12:23 -05:00
co-authored by Claude Opus 4.8
parent a3b4f534d5
commit 529286777f
3 changed files with 58 additions and 1 deletions
+43
View File
@@ -15009,6 +15009,48 @@ void testHushChatReceivePath()
EXPECT_EQ((int)off.metadata.size(), 0);
}
// Control memos (self-addressed delete/restore/sensitive signal): a new header type carrying an `op`,
// parsed + surfaced like a message pair, with unknown future types ignored (forward-compat).
void testHushChatControlMemo()
{
using namespace dragonx::chat;
ChatKeyPair me;
ChatIdentityResult r = deriveChatIdentityFromSecret("ctrl-self", me, true);
EXPECT_TRUE(r.status == ChatIdentityStatus::Ready);
// A self-addressed control memo: header (t:"Ctrl", op:"del", e:"") + a plaintext label payload.
std::string headerMemo = std::string("{\"cid\":\"conv-42\",\"e\":\"\",\"h\":1,\"op\":\"del\",\"p\":\"")
+ r.public_key_hex + "\",\"t\":\"Ctrl\",\"v\":0,\"z\":\"zs-self\"}";
HushChatTransactionInput tx;
tx.txid = "txid-ctrl-1";
tx.outputs.push_back(HushChatMemoOutput{0, headerMemo});
tx.outputs.push_back(HushChatMemoOutput{1, "Bob"}); // plaintext label payload
auto ext = extractHushChatTransactionMetadata(tx, true);
EXPECT_EQ((int)ext.metadata.size(), 1);
const auto& m = ext.metadata[0];
EXPECT_TRUE(m.type == HushChatHeaderType::Control);
EXPECT_EQ(m.op, std::string("del"));
EXPECT_EQ(m.conversation_id, std::string("conv-42"));
EXPECT_EQ(m.payload_memo, std::string("Bob"));
// A control header with no "op" is rejected (invalid header -> no metadata).
std::string noOp = std::string("{\"cid\":\"c\",\"e\":\"\",\"h\":1,\"p\":\"")
+ r.public_key_hex + "\",\"t\":\"Ctrl\",\"v\":0,\"z\":\"zs-self\"}";
HushChatTransactionInput tx2; tx2.txid = "txid-ctrl-2";
tx2.outputs.push_back(HushChatMemoOutput{0, noOp});
tx2.outputs.push_back(HushChatMemoOutput{1, "x"});
EXPECT_EQ((int)extractHushChatTransactionMetadata(tx2, true).metadata.size(), 0);
// Forward-compat: an unknown future header type is ignored, never surfaced or crashing.
std::string unknown = std::string("{\"cid\":\"c\",\"e\":\"\",\"h\":1,\"p\":\"")
+ r.public_key_hex + "\",\"t\":\"Zzz9\",\"v\":0,\"z\":\"zs-self\"}";
HushChatTransactionInput tx3; tx3.txid = "txid-ctrl-3";
tx3.outputs.push_back(HushChatMemoOutput{0, unknown});
tx3.outputs.push_back(HushChatMemoOutput{1, "x"});
EXPECT_EQ((int)extractHushChatTransactionMetadata(tx3, true).metadata.size(), 0);
}
// ChatService: metadata batch -> decrypt -> threaded, deduped in-memory store.
void testHushChatService()
{
@@ -19959,6 +20001,7 @@ int main()
#endif
testHushChatCrypto();
testHushChatReceivePath();
testHushChatControlMemo();
testHushChatService();
testHushChatDatabase();
testHushChatDeleteConversation();