What’s public and private: the field reference
This is the precise companion to What’s public and private: a field-by-field inventory of what SithBit exposes. The plain-language overview lives there; this page is the exhaustive list for anyone who wants to verify exactly what a third party can read.
Ground rule: every on-chain account is a world-readable Solana account holding borsh-serialized plaintext. There is no on-chain confidentiality. Every field listed below is readable by anyone. The only on-chain privacy technique SithBit uses is hashing address strings so the string isn’t stored — the hash still confirms a guessed address (see message metadata is hashed, not hidden).
On-chain accounts (all fields world-readable)
| Account | Public fields | Notes |
|---|---|---|
| Mailbox | mail_count, default_postage, domain, no_ipfs | 1:1 with a wallet via its PDA, so the owning wallet is public too. no_ipfs reveals your storage preference. |
| Mailbox key | pub_key (base58 X25519) | Only present if you published a delegated key. It is a public key by nature. |
Message (Email) | sender (wallet), from_hash (blake3 of from), epoch (timestamp), cid (IPFS CID or a b3: local-only marker), bounty_lamports, expires_at, reply_to_hash | No address strings. Recipient = the wallet the PDA seeds on. cid takes its b3: form rather than an IPFS CID when the recipient set no_ipfs. Human From:/To:/Subject: live only in the sealed body. Survives DeleteMail. |
| Frombox | required_postage, stamps | Keyed by a PDA on (blake3(from), recipient wallet) — the from string is never stored, only its hash. |
| Alias | address (the target wallet) | The alias→wallet mapping is fully public. The PDA seeds on blake3(normalized alias) (the name lowercased and stripped of any domain suffix, as the aliases chapter and the blake3 appendix describe), not the name itself, so the name isn’t recoverable from account state. It is still effectively public because CreateAlias carries it in plaintext instruction data, which is permanent ledger content. |
| MailDomain | is_active, authority, rent_payer, domain (cleartext DNS name) | The domain name is stored in the clear. |
| Postoffice | fee fields, root_ksk, delegate_address, commitment_root | The postmaster (owner) key is not on-chain — only an opaque Merkle commitment_root. See the postoffice admin keys. |
| AliasListing | holder (seller), price_lamports, created_at, expires_at, mode, antisnipe_window_secs, high_bid_lamports, high_bidder | Marketplace state, including the winning bidder’s wallet — who is buying what, for how much. |
| DomainListing | holder (seller), price_lamports, created_at, expires_at | Same, for domains. |
| AliasEscrow | recipient, fee_lamports, offered_at, expires_at | A pending alias hand-off: who the alias is offered to, at what fee, and the offer’s window. |
| AliasBid | bidder, amount_lamports, bid_at | Auction state: every bidder’s wallet and bid amount are public while the auction runs. |
| ParticipantBeacon | tags (bitmap), detail_cid_len, detail_cid, created_at, updated_at, owner (wallet) | Marketplace opt-in is fully public by design: the wallet, its self-attested tags, and the CID of its rich-profile blob. The blob’s content is encrypted; its existence and update times are not. |
| SenderAttestation | domain (cleartext DNS name), wallet, attested_at | Publicly binds a sending wallet to a domain — that is its purpose (the client trust mark). One account per (domain, wallet). |
| SenderReputation | wallet, cumulative_spend_lamports | A sender wallet’s cumulative first-contact postage spend is public; the individual recipients behind it are not stored here. |
| PinLease | cid_hash (blake3 of the leased CID), holder (wallet), created_at | Publicly binds the holder wallet to a message CID — anyone who knows a CID can see who leased it (and its lamport balance reveals the deposit). The CID itself is stored only as a hash, but message CIDs are public on their Email accounts anyway. |
| PendingMailboxClose | requested_at | The PDA seeds on the closing wallet, so that a wallet is closing its mailbox — and when the timelock lapses — is public. |
| PendingDeactivation | requested_at | Same shape, for a domain sitting in its deactivation notice window. |
| PendingReclaim | requested_at, authority, payer | A DNSSEC-proof reclaim in flight: the incoming authority wallet is public before the handover finalizes. The recorded payer — the wallet that funded the request, and the one its rent refunds to — is public too, and need not be the incoming authority. |
What a send leaks on-chain
SendMail writes the Email account above, so one send exposes every field
in that row — and the recipient wallet besides, which is not a stored
field at all but the PDA seed the account is keyed to. What the row cannot show
is the instructions around it. No address string appears in any of them — the
from address travels as its blake3 hash throughout. The frombox instructions
(CreateFrombox/UpdateFrombox/AddStamps) likewise carry
only blake3(from), never the plaintext address; the two purchase variants
additionally carry the buyer’s optional max_price_lamports ceiling, a figure
about the buyer’s own tolerance rather than about either identity.
ReclaimFromboxStamps carries no payload at all — the program recomputes the
hash from the signer’s address bytes. SOL amounts and
account balances are visible as on any Solana transaction.
Off-chain surfaces
“Off-chain” means not on the public ledger — it does not always mean private.
- The sealed body (private plaintext, public ciphertext). The body and
subject are sealed with crypto_box_seal to the
recipient’s wallet or delegated key, so the plaintext is private. By
default the ciphertext is pinned to public IPFS, where
anyone with the CID can fetch it (ciphertext only) — a harvest-now,
decrypt-later surface.
no_ipfskeeps it in the operator’s store instead. - The operator’s store. Your mail server holds the sealed body and, for IMAP / POP delivery, serves it to you. A curious or compromised operator can read whatever their store holds in whatever form it holds it.
- Mail credentials and account settings. Your IMAP/POP password, and the
account service’s session token, timezone, and do-not-disturb schedule, live
in the operator’s account store, not on-chain. The schedule is never served
to anonymous callers — they get only the yes/no “away right now” answer —
unless the owner opts in via
expose_dnd_schedule; see What the refused sender sees. - The relaying MX operator. Mail relayed through a domain’s mail server
passes through that operator, who sees the SMTP envelope and headers in the
clear and whose
fromclaim the chain trusts. This is a trusted role — see the domain authority is fully trusted for relayed mail. - DMARC evaluation rows and ingested reports. This instance’s own
DMARC evaluation rows (the aggregation table the
[spooler.dmarc_report]worker drains into outbound RUA reports) and ingested DMARC aggregate reports (thedmarc_rua/blobs the account API’sGET /v1/admin/dmarc-reportssurface serves) each carry asource_ipper row. Both prune by default —dmarc_retention_daysandreport_retention_daysboth default to 90 days (configuration reference) — and either can be set to0to keep rows or reports forever. The second setting also governs lingering TLS-RPT pending rows, not only DMARC reports. - Erasure of the off-chain copy. An operator can remove everything the
store holds for one wallet with
DELETE /v1/admin/accounts/{wallet}on the account API’s admin surface. The call looks the exact wallet up first; a match sweeps: mailboxes and every message copy, the offload pins those copies held (enqueued for the background unpin worker, which only drains where the chain pipeline is enabled — never released inline), the per-reader wraps, the wallet-keyed leases and rate-limit windows, the per-sender chain-publication windows for the addresses that deployment configures, and last the account row and its satellites — the mail password and timezone are columns on the row itself, while the DND schedule and any pending login challenge are satellite tables deleted immediately before it.204when an account row matches exactly and the sweep ran,404when none matches — and in that case nothing is swept, so data left behind for a wallet with no matching account row is not cleaned up by this route. The audit event recordsoutcome="success"only for a completed sweep; a404recordsoutcome="failure", the same as a store error would. Three things it cannot reach: the on-chain transactions, which are permanent ledger content (the message accounts are not in that group — for mail this deployment published, the sweep enqueues aDeleteMailjob per copy that closes the account on-chain, though the transactions that created it remain); IPFS copies pinned by third parties; and the send-rate windows it cannot name — achain/sender/window charged under a domain absent from the API’smail.local_domainsor under an alias form, plus the bucket every null-envelope-sender message (bounces, usually) is charged against, which is deployment-wide rather than any one account’s and so is never swept. - Data export (data portability). A wallet’s own
GET /v1/account/export(plain JWT) or an operator’sGET /v1/admin/accounts/{wallet}/export(admin-authenticated, audited as a read) streams a JSON document of everything the store holds under that wallet: account facts, away-schedule exclusions, quota usage, and every mailbox with its messages. Message bodies are excluded — each message carries a pointer to its bytes, the store’s blob key plus the chain CID once the copy has been pinned, never the bytes themselves — and so are credential material (the sealed secret, the mail password, the pin-provider secret) and the wallet-keyed lease and rate-window rows.
See also
- What’s public and private — the plain-language overview.
- Trust assumptions and threat model — the adversarial view, including message metadata is hashed, not hidden.
- How sealed-box encryption works and How blake3 hashing works.
- Program & PDA reference — the full account layouts.