Files
ObsidianDragon/src/chat/chat_protocol.h
T
DanSandClaude Opus 4.8 cc08854237 feat(chat): tier-3 larger messages — protocol + split + cross-tx reassembly core
Foundation for messages too large for a single transaction (tier 2's 16-output
ceiling, ~4000 chars). A long message is now split across SEVERAL transactions
that share a random message id; each carries a slice of the one ciphertext with
GLOBAL chunk indices, and the receiver reassembles across the siblings.

Protocol: the Message header gains an optional "mid" (message id); "n" is reused
as the TOTAL chunk count and its bound raised to kHushChatMaxMessageChunks (64 ⇒
up to ~16 KB / 4 txs, tunable, ≤256 for the 2-hex index). The grouper, when a
tx's header carries "mid", emits a PARTIAL (its global-indexed chunks + message
id + total) instead of reassembling in-place; HushChatTransactionMetadata
carries those partials through. Crypto is untouched — one secretstream session
for the whole message, chunking is transport-only.

Compose: splitOutgoingMessage() returns one OutgoingChatMemos per transaction
(one element = the classic tier-1/2 single tx, byte-identical; several = tier 3).
buildOutgoingMessage (single-tx) is unchanged. Old clients bound "n" to 16 so a
tier-3 header fails to parse and its memos are dropped — graceful degradation.

Unit-tested (testHushChatLargeMessageTier3): a ~9000-char message splits into 3
txs, every output shuffled, slices gathered across siblings, reassembled and
decrypted back to the original; short message stays single-tx (no mid); the
total-chunk cap is enforced. The stateful accumulator (persisted, confirm-gated),
sequential broadcast, and progressive/incomplete UI follow in later commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 12:20:11 -05:00

154 lines
6.6 KiB
C++

#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
#ifndef DRAGONX_ENABLE_CHAT
#define DRAGONX_ENABLE_CHAT 0
#endif
namespace dragonx::chat {
enum class HushChatHeaderType {
Message,
ContactRequest,
Control // a self-addressed control signal (delete / restore / mark sensitive); carries an `op`
};
struct HushChatHeader {
int header_number = 0;
int version = 0;
std::string reply_zaddr;
std::string conversation_id;
HushChatHeaderType type = HushChatHeaderType::Message;
std::string op; // Control only ("del" / "res" / "sensitive" / "unsensitive" / future ops); empty otherwise
std::string secretstream_header_hex;
std::string public_key_hex;
// Message only: how many payload outputs this message's ciphertext is split across (header "n").
// 1 = the classic single-payload message (field omitted on the wire). >1 means the payloads are
// index-prefixed ciphertext chunks reassembled in order. For tier 2 all N chunks ride ONE tx; for
// tier 3 (message_id set) "n" is the TOTAL chunk count spread across several sibling transactions.
int parts = 1;
// Message only, larger-messages tier 3: when a message needs more chunks than one tx can carry, it
// is split across several transactions that all share this random id (header "mid"); each tx's
// payloads carry their GLOBAL chunk index (0..parts-1). Empty = a single-tx message (tier 1/2).
std::string message_id;
// Optional sender-stamped compose time (header "ts", Unix seconds). 0 = absent (older sender) → the
// receiver falls back to the tx/receive time. Lets both sides show the SAME (send) time.
std::int64_t sent_at = 0;
};
struct HushChatHeaderParseResult {
bool ok = false;
HushChatHeader header;
std::string error;
};
struct HushChatMemoOutput {
std::size_t position = 0;
std::string memo;
};
struct HushChatMemoPair {
HushChatHeader header;
std::size_t header_position = 0;
std::size_t payload_position = 0;
std::string payload_memo;
// Larger-messages tier 3: a single transaction that carries only PART of a bigger message (the rest
// ride sibling txs sharing message_id). payload_memo is then empty; partial_chunks holds this tx's
// (global index, ciphertext-hex chunk) pairs and total_parts is the whole message's chunk count.
std::string message_id;
int total_parts = 0;
std::vector<std::pair<int, std::string>> partial_chunks;
};
enum class HushChatMemoGroupingIssue {
InvalidHeader,
MissingPayload,
DuplicateHeader,
OversizedMemo
};
struct HushChatMemoGroupingIssueInfo {
HushChatMemoGroupingIssue issue = HushChatMemoGroupingIssue::InvalidHeader;
std::size_t position = 0;
std::string detail;
};
struct HushChatMemoGroupingResult {
std::vector<HushChatMemoPair> pairs;
std::vector<HushChatMemoGroupingIssueInfo> issues;
std::size_t ignored_memo_count = 0;
};
struct HushChatTransactionInput {
std::string txid;
std::vector<HushChatMemoOutput> outputs;
};
struct HushChatTransactionMetadata {
std::string txid;
HushChatHeaderType type = HushChatHeaderType::Message;
std::string op; // Control only: the control op ("del"/"res"/...); empty otherwise
std::string conversation_id;
std::string reply_zaddr;
std::size_t header_position = 0;
std::size_t payload_position = 0;
std::size_t payload_size = 0;
// Decrypt inputs carried through from the paired header + payload memos so the chat
// service can actually decrypt (a Message) or read the request (a ContactRequest).
std::string sender_public_key_hex; // header "p": peer crypto_kx public key (hex)
std::string secretstream_header_hex; // header "e": secretstream header (hex; empty for ContactRequest)
std::string payload_memo; // ciphertext hex (Message) or plaintext request text (ContactRequest)
std::int64_t sent_at = 0; // header "ts": sender compose time (Unix s); 0 = absent → use tx time
// Larger-messages tier 3 (a multi-transaction Message): message_id ties this tx's slice to its
// siblings, total_parts is the whole message's chunk count, and partial_chunks are this tx's
// (global index, ciphertext-hex) chunks. Empty message_id = an ordinary single-tx message (payload
// in payload_memo). The chat service accumulates partials by message_id and reassembles when whole.
std::string message_id;
int total_parts = 0;
std::vector<std::pair<int, std::string>> partial_chunks;
};
struct HushChatTransactionExtractionResult {
bool feature_enabled = false;
std::vector<HushChatTransactionMetadata> metadata;
std::vector<HushChatMemoGroupingIssueInfo> issues;
std::size_t ignored_memo_count = 0;
};
constexpr int kHushChatSupportedVersion = 0;
constexpr std::size_t kHushChatMemoByteLimit = 512;
// Larger-messages tier 2: the most payload outputs a single transaction may carry (one tx). Keeps the
// chat tx well under the shielded max_tx_size after Sietch decoy padding + proving cost. A message
// needing more chunks than this spills into sibling transactions (tier 3), up to kHushChatMaxMessageChunks.
constexpr int kHushChatMaxMessageParts = 16;
// Larger-messages tier 3: the most chunks a single message may span IN TOTAL, across all its sibling
// transactions (ceil(chunks / kHushChatMaxMessageParts) txs, each a separate 0-value chat send + fee).
// Bounds the on-chain cost / broadcast time of one message; beyond this compose returns TooLong. The
// 2-hex global part index (below) tops out at 256, so this must stay ≤ 256.
constexpr int kHushChatMaxMessageChunks = 64;
// Each multi-part payload chunk is prefixed with a 2-hex-char (1-byte) part index so the receiver can
// reorder chunks after the daemon shuffles a tx's outputs. 2 hex covers indices 0..255 (>> the cap).
constexpr std::size_t kHushChatPartIndexHexLen = 2;
constexpr std::size_t kHushChatPublicKeyHexLength = 64;
constexpr std::size_t kHushChatSecretstreamHeaderHexLength = 48;
constexpr bool hushChatFeatureEnabledAtBuild()
{
return DRAGONX_ENABLE_CHAT != 0;
}
HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo);
HushChatMemoGroupingResult groupHushChatMemoOutputs(const std::vector<HushChatMemoOutput>& outputs);
HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
const HushChatTransactionInput& transaction,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
const char* hushChatHeaderTypeName(HushChatHeaderType type);
const char* hushChatMemoGroupingIssueName(HushChatMemoGroupingIssue issue);
} // namespace dragonx::chat