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:
@@ -81,6 +81,8 @@ const char* hushChatHeaderTypeName(HushChatHeaderType type)
|
|||||||
return "Memo";
|
return "Memo";
|
||||||
case HushChatHeaderType::ContactRequest:
|
case HushChatHeaderType::ContactRequest:
|
||||||
return "Cont";
|
return "Cont";
|
||||||
|
case HushChatHeaderType::Control:
|
||||||
|
return "Ctrl";
|
||||||
}
|
}
|
||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
@@ -138,7 +140,15 @@ HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo)
|
|||||||
header.type = HushChatHeaderType::Message;
|
header.type = HushChatHeaderType::Message;
|
||||||
} else if (type == "Cont") {
|
} else if (type == "Cont") {
|
||||||
header.type = HushChatHeaderType::ContactRequest;
|
header.type = HushChatHeaderType::ContactRequest;
|
||||||
|
} else if (type == "Ctrl") {
|
||||||
|
header.type = HushChatHeaderType::Control;
|
||||||
|
// A control signal carries a non-empty op ("del"/"res"/...); the op VALUE isn't restricted here so
|
||||||
|
// a future op reaches newer clients and is ignored by the reducer on older ones (forward-compat).
|
||||||
|
if (!readRequiredString(object, "op", header.op, error)) return fail(error);
|
||||||
|
if (header.op.empty()) return fail("control op is empty");
|
||||||
} else {
|
} else {
|
||||||
|
// Unknown type (older client seeing a newer memo type): fail here so it is grouped as an invalid
|
||||||
|
// header and ignored, never crashing. Keep this the catch-all.
|
||||||
return fail("unknown HushChat header type");
|
return fail("unknown HushChat header type");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,6 +309,7 @@ HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
|
|||||||
HushChatTransactionMetadata metadata;
|
HushChatTransactionMetadata metadata;
|
||||||
metadata.txid = transaction.txid;
|
metadata.txid = transaction.txid;
|
||||||
metadata.type = pair.header.type;
|
metadata.type = pair.header.type;
|
||||||
|
metadata.op = pair.header.op; // Control only; empty for Message/ContactRequest
|
||||||
metadata.conversation_id = pair.header.conversation_id;
|
metadata.conversation_id = pair.header.conversation_id;
|
||||||
metadata.reply_zaddr = pair.header.reply_zaddr;
|
metadata.reply_zaddr = pair.header.reply_zaddr;
|
||||||
metadata.header_position = pair.header_position;
|
metadata.header_position = pair.header_position;
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ namespace dragonx::chat {
|
|||||||
|
|
||||||
enum class HushChatHeaderType {
|
enum class HushChatHeaderType {
|
||||||
Message,
|
Message,
|
||||||
ContactRequest
|
ContactRequest,
|
||||||
|
Control // a self-addressed control signal (delete / restore / mark sensitive); carries an `op`
|
||||||
};
|
};
|
||||||
|
|
||||||
struct HushChatHeader {
|
struct HushChatHeader {
|
||||||
@@ -21,6 +22,7 @@ struct HushChatHeader {
|
|||||||
std::string reply_zaddr;
|
std::string reply_zaddr;
|
||||||
std::string conversation_id;
|
std::string conversation_id;
|
||||||
HushChatHeaderType type = HushChatHeaderType::Message;
|
HushChatHeaderType type = HushChatHeaderType::Message;
|
||||||
|
std::string op; // Control only ("del" / "res" / "sensitive" / "unsensitive" / future ops); empty otherwise
|
||||||
std::string secretstream_header_hex;
|
std::string secretstream_header_hex;
|
||||||
std::string public_key_hex;
|
std::string public_key_hex;
|
||||||
// Optional sender-stamped compose time (header "ts", Unix seconds). 0 = absent (older sender) → the
|
// Optional sender-stamped compose time (header "ts", Unix seconds). 0 = absent (older sender) → the
|
||||||
@@ -73,6 +75,7 @@ struct HushChatTransactionInput {
|
|||||||
struct HushChatTransactionMetadata {
|
struct HushChatTransactionMetadata {
|
||||||
std::string txid;
|
std::string txid;
|
||||||
HushChatHeaderType type = HushChatHeaderType::Message;
|
HushChatHeaderType type = HushChatHeaderType::Message;
|
||||||
|
std::string op; // Control only: the control op ("del"/"res"/...); empty otherwise
|
||||||
std::string conversation_id;
|
std::string conversation_id;
|
||||||
std::string reply_zaddr;
|
std::string reply_zaddr;
|
||||||
std::size_t header_position = 0;
|
std::size_t header_position = 0;
|
||||||
|
|||||||
@@ -15009,6 +15009,48 @@ void testHushChatReceivePath()
|
|||||||
EXPECT_EQ((int)off.metadata.size(), 0);
|
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.
|
// ChatService: metadata batch -> decrypt -> threaded, deduped in-memory store.
|
||||||
void testHushChatService()
|
void testHushChatService()
|
||||||
{
|
{
|
||||||
@@ -19959,6 +20001,7 @@ int main()
|
|||||||
#endif
|
#endif
|
||||||
testHushChatCrypto();
|
testHushChatCrypto();
|
||||||
testHushChatReceivePath();
|
testHushChatReceivePath();
|
||||||
|
testHushChatControlMemo();
|
||||||
testHushChatService();
|
testHushChatService();
|
||||||
testHushChatDatabase();
|
testHushChatDatabase();
|
||||||
testHushChatDeleteConversation();
|
testHushChatDeleteConversation();
|
||||||
|
|||||||
Reference in New Issue
Block a user