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>
154 lines
6.6 KiB
C++
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
|