QMail Upload — Group 6, Code 70

Stores one file fragment (stripe) for one email under a 16-byte GUID. The sender calls upload in parallel against all storage RAIDAs before invoking tell to notify the recipient’s beacon.

Quick reference

Command Group6 (CHAT/QMail)
Command Code70
Server functioncmd_qmail_upload in cmd_qmail.c
Wire structureqmail_upload_req_t in qmail_structs.h
Body layoutPreamble (48) + Payload (38) + Data (N) + Terminator (2)
Response bodyNone (status only)
EncryptionRequired — AES-128 keyed by sender’s coin AN
TransportTCP recommended (data length up to ~10 MB per stripe)
Server-side limitQMAIL_MAX_STRIPE_SIZE = 10 MiB

Purpose

The sender splits each QMail object into stripes using Reed-Solomon-style erasure coding (typically 7 data + 1 parity = 8 stripes). One upload request is sent to each storage RAIDA in parallel, each carrying one stripe. The recipient later downloads the stripes from those RAIDAs and reassembles the object.

An email is normally a group of multiple files under the same GUID: private CBDF meta (file_type = 0), body/content (file_type = 1), and zero to several attachments (file_type = 10..255). Each file is uploaded separately under the same GUID; the storage server distinguishes them by the file_type byte, which selects the on-disk file extension.

Private metadata

Subject, preview text, attachment names, labels, and other human-readable display metadata belong in the private file_type=0 CBDF meta object, not in the Tell envelope. The Tell carries only public bootstrap data and the receiver downloads file_type=0 first.

Request body

All offsets are relative to the start of the decrypted body (after the 32-byte routing header that the executor framework wraps every request in).

Preamble (48 bytes, offsets 0–47)

The standard QMail preamble. See QMail Overview — Universal preamble for the byte-level breakdown.

Command payload (38 bytes, offsets 48–85)

File GUID (16 bytes, part 1 of 2) 48 55 File GUID (part 2 of 2) 56 63 Locker Code (16 bytes, part 1 of 2) 64 71 Locker Code (part 2 of 2) 72 79 FT 80 SD 81 DL (Data Length, BE) 82 85
OffsetSizeFieldDescription
48–6316File GUID16-byte identifier for the email this stripe belongs to. Same GUID on every stripe of every file in the email.
64–7916Locker CodeASCII storage-payment locker key, null-padded to 16 bytes (e.g. "X7KQ-M3PL-9RVB" + zeros). When a storage fee is due, the server claims this locker by downloading its coins. An all-zero locker is accepted when the fee is zero (welfare allowance or rate 0); it is rejected with ERROR_PAYMENT_REQUIRED only when a fee is actually due. See Payment & pricing.
801File TypeSelects which file inside the email this stripe belongs to. See File Types table below.
811Storage DurationStorage retention code. Server logs but does not currently enforce.
82–854Data LengthLength of the stripe data that follows, in bytes (big-endian).
86…NStripe DataThe actual stripe bytes. Length must equal the Data Length field.
last 22TerminatorFixed 3E 3E. Server rejects with ERROR_INVALID_PACKET_LENGTH if missing.

Total body length: 48 + 38 + Data Length + 2. The server validates this exactly — one byte off and the request is rejected with ERROR_INVALID_PACKET_LENGTH.

File types

The file_type byte at offset 80 picks the storage filename suffix. The server’s mapping (cmd_qmail_upload in cmd_qmail.c):

The same file_type value is later carried in the tell file manifest entry for this file, and the recipient passes it back in download to select which stored file to fetch. New QMail beta messages must upload file_type=0 private meta and file_type=1 body/content; attachments use file type 10 (0x0A) and up. The Tell manifest lists private meta first, body second, then attachments.

CodeSuffixPurpose
0.metaPrivate CBDF meta: subject, preview, attachment names, private object references, recipient-visible display metadata.
1.qmailBody/content object. For split CBDF QMail, this is the content tail that follows the private meta section.
2.styleStyle information applied to the text file (CBDF Styles section).
3.textMarked-up body content (CBDF Text section).
4.resourceAn array of images, videos, tables, and other media compacted into one file, with CBDF key/values separating the media files and listing their names (CBDF Resources section).
5.logicCode that adds functionality to a document, analogous to how JavaScript adds functionality to HTML pages (CBDF Logic section).
6–9.blobReserved for future use.
10.0.binFirst attachment.
11.1.binSecond attachment.
12–255.{N−10}.binSubsequent attachments.

Payment & pricing

Command 70 does not put a fee amount on the wire. The server is paid by the coins in the locker identified by the 16-byte locker code. Payment behavior is controlled by the server’s [qmail] payment_mode_upload setting (off | shadow | enforce). Tell and object-transfer payments are not enforced in this release.

Payment modes

ModeBehavior
off (default)No upload payment processing. The stripe is stored without claiming a locker.
shadowRead-only peek of the locker plus would-charge logging. Never rejects for payment.
enforceSynchronous claim-by-download before durable store. Underpayment rejects the upload; transport/backend claim failures are retryable.

Enforce ordering (CMD 70)

  1. Welfare / zero-fee short-circuit — if the computed fee for this stripe is 0, no claim is performed; an all-zero locker is accepted.
  2. All-zero locker gate (fee due) — when a fee is due, an all-zero locker is rejected with ERROR_PAYMENT_REQUIRED before payment-record lookup or claim. (Zero-fee / welfare path already returned in step 1.)
  3. Durable payment-record lookup — records are keyed (locker, GUID, sender). A prior successful claim for the same tuple covers retries of the same GUID for free. The same locker presented for a different GUID finds no covering record (coins already taken) and fails unpaid.
  4. Synchronous claim-by-download — the server downloads the entire locker contents (exclusive: coins can be taken only once). This is not escrow; nothing is left in the locker for reuse. The client funds each locker with the exact computed fee; if the locker holds more than the fee, the excess is still consumed.
  5. Value check — claimed value < required fee → ERROR_PAYMENT_REQUIRED and nothing is stored. Transport or backend claim failure → ERROR_PAYMENT_PROCESSING (retryable; no durable zero-value record is written for transport failures).

In enforce mode, unpaid or underfunded content that was accepted outside the synchronous path can be deleted by a sweep after a grace window (default 3600 s). Content stored before enforcement cutover is permanently protected via legacy markers.

Storage fee formula (interim, until DRD serves prices)

Rate and welfare are server-configurable ([qmail] upload_fee_per_mb, upload_welfare_mb); the client mirrors the same formula. Defaults:

  • Rate: 0.01 CC per MiB (220 bytes) of per-server stripe data, per server.
  • Rounding: always ceil up to the next 0.01 CC (at most two decimal places in the billed amount).
  • Welfare: if per-server stripe size ≤ welfare allowance (default 1 MiB), fee is 0 for that server.
  • Representation: fee amounts are decimal CloudCoins; the representation layer supports 0.0001 CC granularity. All REST amounts are exact decimal JSON numbers (not double-rounded units fields).

Example: 25 MiB striped over 10 servers ⇒ 2.5 MiB per server ⇒ 2.5 × 0.01 = 0.025 ⇒ ceil to 0.03 CC per server.

When the fee is zero (welfare or rate 0), the client sends an all-zero locker code and the server accepts it. When a fee is due, the client funds a locker with exactly that amount and places its key in the locker-code field.

Response

No body. The status byte in the response header indicates the outcome.

Status codes

DecimalHexSymbolMeaning
2500xFASTATUS_SUCCESSStripe stored under QMAIL_PUBLIC_UPLOAD_ROOT/G1/G2/GUID/00000000GUID.suffix; .acl sidecar written. In enforce mode, payment was accepted (zero-fee, covering payment record, or successful claim) before the write.
80x08ERROR_COIN_NOT_FOUNDSender’s (denom, SN) is not loaded on this RAIDA.
160x10ERROR_INVALID_PACKET_LENGTHBody too short (< 88 bytes), missing terminator, or 48 + 38 + data_length + 2 doesn’t equal the actual body size.
400x28ERROR_INVALID_SN_OR_DENOMINATIONDenomination outside −8…+6.
1670xA7ERROR_PAYMENT_PROCESSINGEnforce mode: claim transport/backend failure (retryable). Nothing durable is stored for this attempt; the client should retry the same GUID (payment records make successful same-GUID retries free).
1690xA9ERROR_PAYMENT_REQUIREDEnforce mode: a fee is due and the locker is all-zero, empty after claim, underfunded, or the locker was already claimed for a different GUID. Nothing is stored. Same-GUID retries that already have a covering payment record succeed without re-claiming.
1940xC2ERROR_FILESYSTEMServer could not create the storage directory or write the file (disk full, permission denied, path-traversal check failed).
2000xC8ERROR_INVALID_ANPreamble AN does not match the server’s stored AN for the (denom, SN). Most common cause: client sent a zero-AN preamble.

Server-side storage layout

The storage path is sharded by the first two bytes of the GUID:

Root: /opt/raidax/QMail_Data/public_uploads/
Path: ROOT/{G1}/{G2}/{GUID_hex}/00000000{GUID_hex}.{suffix}

  G1       = hex(file_guid[0])    e.g. "a3"
  G2       = hex(file_guid[1])    e.g. "f7"
  GUID_hex = hex(file_guid)       32 lowercase hex chars
  suffix   = chosen by file_type  (.qmail, .meta, .{N}.bin, .blob)

Beside each stored file the server writes a .acl sidecar:

{file_path}.acl  (22 bytes, Phase I "allow all" format)
  byte 0      version       = 0x01
  byte 1      flags         = 0x01 (ACL_ALLOW_ALL)
  bytes 2-17  owner_guid    = the email GUID (16 bytes)
  byte 18     owner_denom   = sender denomination
  bytes 19-22 owner_sn      = sender SN (4 bytes)

The ACL sidecar exists today as a placeholder; download currently logs its absence but does not enforce. Future phases will read the sidecar to gate downloads by (recipient SN, recipient AN).

Example request body (hex)

Preamble (48 bytes, offsets 0-47):
A1 B2 C3 D4 E5 F6 07 08 09 0A 0B 0C XX XX XX XX  <- challenge: 12 random + 4 CRC32 BE
00 00 00 00 00 00 00 00                            <- session_id (zeros)
00 06                                              <- coin_type
01                                                 <- denomination = 1
00 00 0B 19                                        <- serial_number = 2841 BE
00                                                 <- reserved (was Device ID)
[16 bytes of authenticity AN]                      <- AN

Payload (38 bytes, offsets 48-85):
[16 bytes File GUID]
58 37 4B 51 2D 4D 33 50 4C 2D 39 52 56 42 00 00    <- locker_code "X7KQ-M3PL-9RVB" + 2 nulls
01                                                 <- file_type = 1 (.qmail)
00                                                 <- storage_duration
00 00 00 50                                        <- data_length = 80 BE

Stripe data (80 bytes):
[80 bytes of stripe content]

Terminator:
3E 3E

Total body size: 48 + 38 + 80 + 2 = 168 bytes.

Common mistakes

Locker code padding

The locker key is an ASCII string up to 16 bytes long. If your key is shorter than 16 bytes, null-pad the remainder — do not space-pad or repeat the key. The server trims trailing nulls before passing the key to the locker download.

Zero locker when fee is due

An all-zero locker code is accepted when the computed storage fee is zero (welfare allowance or rate 0). In enforce mode it is rejected with ERROR_PAYMENT_REQUIRED only when a fee is actually due. Do not send a zero locker for a chargeable stripe.

Body length must be exact

The server computes expected = 48 + 38 + data_length + 2 and compares to the actual decrypted body size. Off-by-one (e.g. forgetting the terminator) returns ERROR_INVALID_PACKET_LENGTH.

Per-RAIDA AN

Each RAIDA holds a different AN for the same (denom, SN). The preamble’s AN field must contain this RAIDA’s 16-byte AN, not a master key or the AN for some other RAIDA. Sending the wrong AN returns ERROR_INVALID_AN.

Points of confusion

  1. Preamble size. The current value is QMAIL_PREAMBLE_SIZE = 48. Older specs say 49 — that was a typo from when byte 31 was Device ID and counted as a separate field. Byte range 0–47 is 48 bytes; the server enforces 48.
  2. Reserved byte 31. Documented as “Reserved (was Device ID)”. The server reads it but does nothing with it. Available for protocol extension; clients should write zero today.
  3. File type 2 vs 10. File type 2 is reserved for non-email blob storage (web pages, IM, etc.) and gets the .blob suffix. Attachments start at file type 10. Don’t use 2 for attachments.
  4. Locker payment is mode-dependent, not always async. Default payment_mode_upload=off skips payment entirely. shadow peeks and logs only. enforce runs a synchronous claim-by-download before store (welfare → reject all-zero when fee due → payment record → claim → value check). Claim failures are ERROR_PAYMENT_PROCESSING (retryable) or ERROR_PAYMENT_REQUIRED (unpaid/underfunded). See Payment & pricing.
  5. Storage duration is currently advisory. The byte is logged but the server does not yet enforce retention. Don’t rely on the server to delete expired files.