feat(chat): stamp sender compose-time in message header (clock-clamped)
Carry the sender's compose time as an optional "ts" (Unix seconds) in the plaintext header JSON that rides outside the AEAD, and prefer it as the displayed message time so both ends show the same send time regardless of when the tx confirms. Parse "ts" leniently. On ingest, clamp: reject a "ts" implausibly in the future vs the receive/block time (1h skew tolerated) so a wrong/ahead peer clock can't pin messages to the bottom of a thread; a past compose time is fine (the note buffer may broadcast a queued message later, and a confirmed tx's block time is always >= compose time). Tests cover the round-trip and the future-clock clamp. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6198,12 +6198,17 @@ void testHushChatOutgoing()
|
||||
|
||||
ChatService bobSvc;
|
||||
bobSvc.setIdentity(bob);
|
||||
EXPECT_EQ(bobSvc.ingest(extracted.metadata, {}, 5), 1);
|
||||
// Realistic receive-time fallback (the header "ts" is a real std::time stamp; an artificially-tiny
|
||||
// fallback would trip the future-plausibility clamp in ingest and reject the real ts).
|
||||
EXPECT_EQ(bobSvc.ingest(extracted.metadata, {}, static_cast<std::int64_t>(std::time(nullptr))), 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);
|
||||
// The header now carries the sender's compose time (header "ts"); ingest prefers it over the fallback
|
||||
// (5) so both ends show the same send time. A real std::time() stamp is well past year-2001.
|
||||
EXPECT_TRUE(conv[0].timestamp > 1000000000);
|
||||
|
||||
// Plaintext contact request Alice -> Bob.
|
||||
OutgoingChatMemos creq;
|
||||
@@ -6240,7 +6245,9 @@ void testHushChatOutgoing()
|
||||
echo.peer_zaddr = aliceZ;
|
||||
echo.peer_public_key_hex = ra.public_key_hex;
|
||||
echo.body = "reply!";
|
||||
echo.timestamp = 7;
|
||||
// A reply is sent AFTER the received message; the incoming "hello bob" now carries a real compose-time
|
||||
// "ts" (~now), so stamp the echo just after it (not the old artificial 7, which would sort before it).
|
||||
echo.timestamp = static_cast<std::int64_t>(std::time(nullptr)) + 10;
|
||||
echo.txid = "out:local1";
|
||||
echo.payload_position = 0;
|
||||
EXPECT_TRUE(bobSvc.recordOutgoing(echo));
|
||||
@@ -6248,6 +6255,27 @@ void testHushChatOutgoing()
|
||||
EXPECT_EQ((int)conv2.size(), 2);
|
||||
EXPECT_TRUE(conv2.back().direction == ChatDirection::Outgoing);
|
||||
EXPECT_EQ(conv2.back().body, std::string("reply!"));
|
||||
|
||||
// Future-clock clamp: a header "ts" implausibly ahead of the receive time is rejected (falls back to
|
||||
// the receive time), so a wrong/ahead-clocked peer can't pin their messages to the bottom of a thread.
|
||||
{
|
||||
const std::int64_t nowSec = static_cast<std::int64_t>(std::time(nullptr));
|
||||
const std::int64_t futureTs = nowSec + 10 * 24 * 3600; // 10 days ahead — well past the 1h tolerance
|
||||
nlohmann::json h;
|
||||
h["h"] = 1; h["v"] = 0; h["z"] = aliceZ; h["cid"] = "conv-clamp";
|
||||
h["t"] = "Cont"; h["e"] = ""; h["p"] = ra.public_key_hex; h["ts"] = futureTs;
|
||||
HushChatTransactionInput ctx;
|
||||
ctx.txid = "txclamp";
|
||||
ctx.outputs.push_back({0, h.dump()}); // header at the lower position
|
||||
ctx.outputs.push_back({1, "please add"}); // contact-request plaintext payload (must not start with '{')
|
||||
auto exc = extractHushChatTransactionMetadata(ctx, true);
|
||||
EXPECT_EQ((int)exc.metadata.size(), 1);
|
||||
EXPECT_EQ(exc.metadata[0].sent_at, futureTs); // the future ts parses through
|
||||
bobSvc.ingest(exc.metadata, {}, nowSec); // realistic receive time
|
||||
auto cc = bobSvc.store().conversation("conv-clamp");
|
||||
EXPECT_EQ((int)cc.size(), 1);
|
||||
EXPECT_EQ(cc[0].timestamp, nowSec); // clamped to receive time, NOT the future ts
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: the transport encoding (chatSendOutputs) — header first, "utf8:" for full-node / raw for
|
||||
|
||||
Reference in New Issue
Block a user