feat(chat): tier-3 reassembly accumulator in the chat service
Second half of tier-3 larger messages: the receive side. ChatService now buffers the slices of a multi-transaction message (keyed by the shared message id) as they arrive — across transactions and across scans — and reassembles the whole ciphertext once every chunk is present, then decrypts and surfaces it as one message. The buffer is NOT persisted: a rescan re-sends every slice, so it re-derives from chain exactly like the control reducer, and survives a restart mid-receive (the next full scan re-feeds the siblings and completes it). Ingest's per-message build/decrypt/guard logic is refactored into one shared lambda so a reassembled tier-3 message passes the identical block / on-chain-hide / sensitive-vault / tombstone checks as a single-tx message. A completed group is retired after it surfaces (deduped across scans by its stable canonical txid — the smallest sibling txid); a group withheld only because the sensitive vault is locked is kept so a later unlock + scan completes it. Our own slices (sender == us) are skipped — the local echo records the outgoing message. The pending map is bounded (256 groups) against a flood of never-completing partials. Unit-tested (testHushChatTier3Reassembly): a ~9000-char message's first slice alone surfaces nothing; once every slice has arrived it reassembles, decrypts to the original and surfaces once; a re-scan dedups; our own slices never ingest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -153,6 +153,22 @@ private:
|
||||
};
|
||||
std::unordered_map<std::string, ControlState> control_; // hide axis: del / res
|
||||
std::unordered_map<std::string, ControlState> sensitive_control_; // sensitive axis: sensitive / unsensitive
|
||||
|
||||
// Larger-messages tier 3: a big message split across several transactions arrives one slice at a
|
||||
// time (siblings share a message id). We accumulate the slices here until every chunk is present,
|
||||
// then reassemble + decrypt + emit the whole message. Not persisted — a rescan re-sends every slice
|
||||
// so the buffer re-derives from chain (like the control reducer); a completed group is retired.
|
||||
struct Tier3Pending {
|
||||
int total_parts = 0;
|
||||
std::string secretstream_header_hex; // "e", shared by every slice
|
||||
std::string sender_public_key_hex; // "p", shared
|
||||
std::string conversation_id;
|
||||
std::string reply_zaddr;
|
||||
std::int64_t sent_at = 0;
|
||||
std::string canonical_txid; // smallest sibling txid — the reassembled message's stable key
|
||||
std::unordered_map<int, std::string> chunks; // global index -> ciphertext-hex chunk
|
||||
};
|
||||
std::unordered_map<std::string, Tier3Pending> tier3_pending_; // message id -> accumulating slices
|
||||
// Monotonic reduce: route the op to its axis (hide vs sensitive), ignore unknown ops (forward-compat),
|
||||
// and within an axis a later (timestamp, txid) wins so a replay of an older op never flips the state.
|
||||
void applyControl(const std::string& cid, const std::string& op, const std::string& label,
|
||||
|
||||
Reference in New Issue
Block a user