Complete API reference for the Katzenpost thin client libraries (Go, Rust, Python)

Thin Client API Reference

This is the complete API reference for the Katzenpost thin client. The thin client is an interface to the kpclientd daemon, which handles all cryptographic and network operations. The thin client communicates with the daemon over a local socket using CBOR-encoded messages.

This document is machine-generated. Do not edit this file directly, as local changes will be overwritten by the next generation pass. The canonical source files are located in website/tools/thin-client-api-gen/. Edits should be applied to binding docstrings (in the source trees) or groups.yaml / overlay/*.md (in the generator).

There are three thin-client implementations:

  • a Go reference (katzenpost/client/thin),
  • a Rust binding (thin_client/src), and
  • a Python binding (thin_client/katzenpost_thinclient).

The official API documentation for each binding is found in the following locations.

Language Official API documentation Official API release
Go pkg.go.dev git tags
Rust docs.rs crates.io
Python Python Thin Client API PyPI

This reference describes the following pinned binding versions:

Binding Repository Tag
Go reference katzenpost/client/thin v0.0.90
Rust thin_client/src 0.0.23
Python thin_client/katzenpost_thinclient 0.0.23

For pinned versions of the full stack (including kpclientd, katzenqt, and the server-side components), see Build from source.

For conceptual background on Pigeonhole, see Understanding Pigeonhole. For task-oriented usage guides, see Thin Client How-to Guide. The design and its security analysis are presented in the Echomix paper.

The reference is organized in two parts: the Pigeonhole API, the message-passing layer most applications build on, followed by the Core Thin-Client API covering configuration, connection management, events, PKI queries, and direct messaging.


Pigeonhole API

The Pigeonhole API is the message-passing layer of the thin client: applications write to and read from BACAP-encrypted storage streams held on the mixnet’s storage replicas, reached through couriers. Much of this API wraps the BACAP scheme, with the daemon performing all BACAP operations on the application’s behalf. For conceptual background, see Understanding Pigeonhole; for worked examples in all three languages, see the Thin Client How-to Guide.

Most Pigeonhole methods cause no mixnet traffic. Only StartResendingEncryptedMessage (and its two variants) and StartResendingCopyCommand put traffic on the mixnet; they are marked Sends mixnet traffic below and deliver through the daemon’s stop-and-wait ARQ, described in The Pigeonhole ARQ. The Cancel* methods are local control operations, and everything else is a local computation performed by the daemon over the local socket.

Keypairs and Capabilities

Method Purpose
NewKeypair / new_keypair Generates a new BACAP keypair from a 32-byte seed.

Preparing Reads and Writes

Method Purpose
EncryptRead / encrypt_read Creates an encrypted read request for a single Pigeonhole box.
EncryptWrite / encrypt_write Creates an encrypted write request for a single Pigeonhole box.

Sending and ARQ Transport

Method Purpose
StartResendingEncryptedMessage / start_resending_encrypted_message Sends an encrypted read or write request to a courier via the ARQ mechanism. Sends mixnet traffic.
StartResendingEncryptedMessageReturnBoxExists Same as the default variant, but returns BoxAlreadyExists as an error instead of treating it as success. Sends mixnet traffic. (Go, Rust)
StartResendingEncryptedMessageNoRetry Same as the default variant, but returns BoxIDNotFound immediately instead of retrying indefinitely. Sends mixnet traffic. (Go, Rust)
CancelResendingEncryptedMessage / cancel_resending_encrypted_message Cancels an in-flight StartResendingEncryptedMessage operation.

Tombstones

Method Purpose
TombstoneRange / tombstone_range Creates encrypted tombstone envelopes for a range of consecutive boxes.

Copy Commands

Method Purpose
CreateCourierEnvelopesFromPayload / create_courier_envelopes_from_payload Packs a single payload for one destination into copy stream elements.
CreateCourierEnvelopesFromMultiPayload / create_courier_envelopes_from_multi_payload Packs multiple payloads for different destinations into copy stream elements.
CreateCourierEnvelopesFromTombstoneRange / create_courier_envelopes_from_tombstone_range Packs tombstone envelopes for a range of consecutive destination boxes into copy stream elements.
StartResendingCopyCommand / start_resending_copy_command Sends a copy command to a courier via ARQ and blocks until the courier acknowledges completion. Sends mixnet traffic.
CancelResendingCopyCommand / cancel_resending_copy_command Cancels an in-flight copy command.

Contact Vouchers

Method Purpose
VoucherMint / voucher_mint Mints a Voucher from the joiner’s MessageStream write capability.
VoucherInduct / voucher_induct Verifies a published VoucherPayload and seals a reply to the joiner.
VoucherOpen / voucher_open Opens the inductor’s sealed reply with the joiner’s voucher secret key.
VoucherDeriveStream / voucher_derive_stream Derives the VoucherStream capabilities from the Voucher.

Pigeonhole Geometry

Method Purpose
GetPigeonholeGeometry / pigeonhole_geometry Returns the negotiated Pigeonhole geometry, so that callers can size payloads to its maximum plaintext payload length.

Auxiliary Index Helpers

Method Purpose
NextMessageBoxIndex / next_message_box_index Returns the next message box index in the sequence.
GetMessageBoxIndexCounter / get_message_box_index_counter Returns the BACAP Idx64 counter embedded in a MessageBoxIndex, the sequence number of a box within its stream.

Looking for connection management, events, PKI queries, or direct messaging? Those live in the Core Thin-Client API below, which opens with its own method index.


Keypairs and Capabilities

NewKeypair / new_keypair

NewKeypair creates a new keypair for use with the Pigeonhole protocol.

This method generates a WriteCap and ReadCap from the provided seed using the BACAP (blinding-and-capability) protocol. The WriteCap should be stored securely for writing messages, while the ReadCap can be shared with others to allow them to read messages.

func (t *ThinClient) NewKeypair(seed []byte) (writeCap *bacap.WriteCap, readCap *bacap.ReadCap, firstMessageIndex *bacap.MessageBoxIndex, err error)
pub async fn new_keypair(
    &self,
    seed: &[u8; 32],
) -> Result<KeypairResult, ThinClientError>
async def new_keypair(self, seed: bytes) -> KeypairResult:

Preparing Reads and Writes

EncryptRead / encrypt_read

EncryptRead encrypts a read operation for a given read capability.

This method prepares an encrypted read request that can be sent to the courier service to retrieve a message from a Pigeonhole box. The returned ciphertext should be sent via StartResendingEncryptedMessage.

func (t *ThinClient) EncryptRead(readCap *bacap.ReadCap, messageBoxIndex *bacap.MessageBoxIndex) (messageCiphertext []byte, envelopeDescriptor []byte, envelopeHash *[32]byte, nextMessageBoxIndex *bacap.MessageBoxIndex, err error)
pub async fn encrypt_read(
    &self,
    read_cap: &[u8],
    message_box_index: &[u8],
) -> Result<EncryptReadResult, ThinClientError>
async def encrypt_read(self, read_cap: bytes, message_box_index: bytes) -> EncryptReadResult:

EncryptWrite / encrypt_write

EncryptWrite encrypts a write operation for a given write capability.

This method prepares an encrypted write request that can be sent to the courier service to store a message in a Pigeonhole box. The returned ciphertext should be sent via StartResendingEncryptedMessage.

func (t *ThinClient) EncryptWrite(plaintext []byte, writeCap *bacap.WriteCap, messageBoxIndex *bacap.MessageBoxIndex) (messageCiphertext []byte, envelopeDescriptor []byte, envelopeHash *[32]byte, nextMessageBoxIndex *bacap.MessageBoxIndex, err error)
pub async fn encrypt_write(
    &self,
    plaintext: &[u8],
    write_cap: &[u8],
    message_box_index: &[u8],
) -> Result<EncryptWriteResult, ThinClientError>
async def encrypt_write(self, plaintext: bytes, write_cap: bytes, message_box_index: bytes) -> EncryptWriteResult:

Sending and ARQ Transport

The senders in this section deliver envelopes through the daemon’s stop-and-wait ARQ. The Pigeonhole ARQ describes the retransmission behavior and what each operation costs in mixnet round trips.

StartResendingEncryptedMessage / start_resending_encrypted_message

Sends mixnet traffic.

StartResendingEncryptedMessage sends an encrypted read or write request to a courier through the daemon’s stop-and-wait ARQ and blocks until the operation completes, fails, or is cancelled via CancelResendingEncryptedMessage. The daemon retransmits until the courier answers; see https://katzenpost.network/docs/pigeonhole_explained/#the-pigeonhole-arq for the retransmission behavior and per-operation round-trip costs.

A write completes on the courier’s ACK, a single mixnet round trip, and by default treats BoxAlreadyExists as idempotent success. A read is two-phased: after the ACK the daemon collects the payload with a fresh SURB, decrypts it, and returns the plaintext; by default a read retries BoxIDNotFound until the box is written.

Note: The Python binding exposes the two variant behaviors below as keyword-only flags on this method (no_retry_on_box_id_not_found and no_idempotent_box_already_exists) rather than as separate methods.

func (t *ThinClient) StartResendingEncryptedMessage(readCap *bacap.ReadCap, writeCap *bacap.WriteCap, messageBoxIndex []byte, replyIndex *uint8, envelopeDescriptor []byte, messageCiphertext []byte, envelopeHash *[32]byte) (*StartResendingResult, error)
pub async fn start_resending_encrypted_message(
    &self,
    read_cap: Option<&[u8]>,
    write_cap: Option<&[u8]>,
    message_box_index: Option<&[u8]>,
    reply_index: Option<u8>,
    envelope_descriptor: &[u8],
    message_ciphertext: &[u8],
    envelope_hash: &[u8; 32],
) -> Result<StartResendingResult, ThinClientError>
async def start_resending_encrypted_message(self, read_cap: 'bytes|None', write_cap: 'bytes|None', message_box_index: 'bytes|None', reply_index: 'int|None', envelope_descriptor: bytes, message_ciphertext: bytes, envelope_hash: bytes, *, no_retry_on_box_id_not_found: bool = False, no_idempotent_box_already_exists: bool = False) -> StartResendingResult:

StartResendingEncryptedMessageReturnBoxExists

Sends mixnet traffic.

Available in: Go, Rust.

StartResendingEncryptedMessageReturnBoxExists behaves exactly like StartResendingEncryptedMessage save that it returns ErrBoxAlreadyExists when the replica reports that the destination box has already been written, rather than swallowing the condition as idempotent success.

This variant costs an additional mixnet round trip: the BoxAlreadyExists code is carried by the replica’s reply rather than the courier’s ACK, so the daemon must dispatch a second SURB before it can return the answer.

An in-flight call may be cancelled via CancelResendingEncryptedMessage.

Note: In Python, pass no_idempotent_box_already_exists=True to start_resending_encrypted_message instead.

func (t *ThinClient) StartResendingEncryptedMessageReturnBoxExists(readCap *bacap.ReadCap, writeCap *bacap.WriteCap, messageBoxIndex []byte, replyIndex *uint8, envelopeDescriptor []byte, messageCiphertext []byte, envelopeHash *[32]byte) (*StartResendingResult, error)
pub async fn start_resending_encrypted_message_return_box_exists(
    &self,
    read_cap: Option<&[u8]>,
    write_cap: Option<&[u8]>,
    message_box_index: Option<&[u8]>,
    reply_index: Option<u8>,
    envelope_descriptor: &[u8],
    message_ciphertext: &[u8],
    envelope_hash: &[u8; 32],
) -> Result<StartResendingResult, ThinClientError>

StartResendingEncryptedMessageNoRetry

Sends mixnet traffic.

Available in: Go, Rust.

StartResendingEncryptedMessageNoRetry behaves exactly like StartResendingEncryptedMessage save that it disables the daemon’s automatic retry of ErrBoxIDNotFound: the caller learns at once that the box has not been written yet, rather than blocking until it appears.

An in-flight call may be cancelled via CancelResendingEncryptedMessage.

Note: In Python, pass no_retry_on_box_id_not_found=True to start_resending_encrypted_message instead.

func (t *ThinClient) StartResendingEncryptedMessageNoRetry(readCap *bacap.ReadCap, writeCap *bacap.WriteCap, messageBoxIndex []byte, replyIndex *uint8, envelopeDescriptor []byte, messageCiphertext []byte, envelopeHash *[32]byte) (*StartResendingResult, error)
pub async fn start_resending_encrypted_message_no_retry(
    &self,
    read_cap: Option<&[u8]>,
    write_cap: Option<&[u8]>,
    message_box_index: Option<&[u8]>,
    reply_index: Option<u8>,
    envelope_descriptor: &[u8],
    message_ciphertext: &[u8],
    envelope_hash: &[u8; 32],
) -> Result<StartResendingResult, ThinClientError>

CancelResendingEncryptedMessage / cancel_resending_encrypted_message

CancelResendingEncryptedMessage cancels ARQ resending for an encrypted message.

The daemon stops retransmitting the operation identified by envelopeHash, the blocked StartResendingEncryptedMessage caller returns ErrStartResendingCancelled, and the operation is removed from in-flight tracking so it is not replayed after a reconnect.

func (t *ThinClient) CancelResendingEncryptedMessage(envelopeHash *[32]byte) error
pub async fn cancel_resending_encrypted_message(
    &self,
    envelope_hash: &[u8; 32],
) -> Result<(), ThinClientError>
async def cancel_resending_encrypted_message(self, envelope_hash: bytes) -> None:

Tombstones

A tombstone is a signed empty payload that deletes a box’s contents. See Tombstones in Understanding Pigeonhole.

TombstoneRange / tombstone_range

TombstoneRange prepares the encrypted envelopes needed to tombstone a consecutive range of Pigeonhole boxes beginning at the supplied MessageBoxIndex. A tombstone is a signed empty payload that deletes a box’s contents; see https://katzenpost.network/docs/pigeonhole_explained/#tombstones.

This method does not itself touch the network: it returns the envelopes for the caller to dispatch one by one, typically via StartResendingEncryptedMessage. To tombstone a single box, pass maxCount=1.

func (c *ThinClient) TombstoneRange(
	writeCap *bacap.WriteCap,
	start *bacap.MessageBoxIndex,
	maxCount uint32,
) (result *TombstoneRangeResult, err error)
pub async fn tombstone_range(
    &self,
    write_cap: &[u8],
    start: &[u8],
    max_count: u32,
) -> TombstoneRangeResult
async def tombstone_range(self, write_cap: bytes, start: bytes, max_count: int) -> TombstoneRangeResult:

Copy Commands

A copy command atomically writes a batch of envelopes to existing streams by way of a temporary copy stream, which a courier executes and then tombstones. See Copy commands in Understanding Pigeonhole for the workflow and its use cases.

CreateCourierEnvelopesFromPayload / create_courier_envelopes_from_payload

CreateCourierEnvelopesFromPayload packs a payload of arbitrary size (up to 10 MB) into properly sized CopyStreamElement chunks for one destination channel. Each chunk is a serialized CopyStreamElement, ready to be written to a box via EncryptWrite followed by StartResendingEncryptedMessage. The caller marks the boundaries of the stream with the isStart and isLast flags.

This method is stateless: no daemon state is kept between calls. It causes no mixnet traffic. See https://katzenpost.network/docs/pigeonhole_explained/#copy-commands for the copy command workflow the chunks feed into.

func (t *ThinClient) CreateCourierEnvelopesFromPayload(payload []byte, destWriteCap *bacap.WriteCap, destStartIndex *bacap.MessageBoxIndex, isStart bool, isLast bool) (envelopes [][]byte, nextDestIndex *bacap.MessageBoxIndex, err error)
pub async fn create_courier_envelopes_from_payload(
    &self,
    payload: &[u8],
    dest_write_cap: &[u8],
    dest_start_index: &[u8],
    is_start: bool,
    is_last: bool,
) -> Result<CreateEnvelopesResult, ThinClientError>
async def create_courier_envelopes_from_payload(self, payload: bytes, dest_write_cap: bytes, dest_start_index: bytes, is_start: bool, is_last: bool) -> 'CreateEnvelopesResult':

CreateCourierEnvelopesFromMultiPayload / create_courier_envelopes_from_multi_payload

CreateCourierEnvelopesFromMultiPayload packs payloads bound for several destination channels into a single stream of CopyStreamElement chunks. This is more space-efficient than calling CreateCourierEnvelopesFromPayload once per destination, because it avoids padding the final box of each destination independently.

This method is stateless; the buffer argument carries any residual state across calls. Pass nil for buffer on the first call and the buffer returned by the previous call thereafter; set isLast on the final call to flush the remainder.

func (t *ThinClient) CreateCourierEnvelopesFromMultiPayload(destinations []DestinationPayload, isStart bool, isLast bool, buffer []byte) (*CreateEnvelopesResult, error)
pub async fn create_courier_envelopes_from_multi_payload(
    &self,
    destinations: Vec<(&[u8], &[u8], &[u8])>,
    is_start: bool,
    is_last: bool,
    buffer: Option<Vec<u8>>,
) -> Result<CreateEnvelopesResult, ThinClientError>
async def create_courier_envelopes_from_multi_payload(self, destinations: 'List[Dict[str, Any]]', is_start: bool, is_last: bool, buffer: 'bytes | None' = None) -> 'CreateEnvelopesResult':

CreateCourierEnvelopesFromTombstoneRange / create_courier_envelopes_from_tombstone_range

CreateCourierEnvelopesFromTombstoneRange creates tombstone CourierEnvelopes for a range of destination indices, encoded as copy stream elements ready to be written to a temporary copy stream channel.

This combines tombstone creation with the copy stream encoding of CreateCourierEnvelopesFromPayload.

The buffer parameter enables stateless continuation across multiple calls without wasting space in the last box. Pass nil on the first call, then pass the returned nextBuffer to the next call.

func (t *ThinClient) CreateCourierEnvelopesFromTombstoneRange(
	destWriteCap *bacap.WriteCap,
	destStartIndex *bacap.MessageBoxIndex,
	maxCount uint32,
	isStart bool,
	isLast bool,
	buffer []byte,
) (envelopes [][]byte, nextBuffer []byte, nextDestIndex *bacap.MessageBoxIndex, err error)
pub async fn create_courier_envelopes_from_tombstone_range(
    &self,
    dest_write_cap: &[u8],
    dest_start_index: &[u8],
    max_count: u32,
    is_start: bool,
    is_last: bool,
    buffer: Option<Vec<u8>>,
) -> Result<CreateEnvelopesResult, ThinClientError>
async def create_courier_envelopes_from_tombstone_range(self, dest_write_cap: bytes, dest_start_index: bytes, max_count: int, is_start: bool, is_last: bool, buffer: 'bytes | None' = None) -> 'CreateEnvelopesResult':

StartResendingCopyCommand / start_resending_copy_command

Sends mixnet traffic.

StartResendingCopyCommand sends a copy command to a courier through the daemon’s stop-and-wait ARQ and blocks until the courier acknowledges completion. The copy command hands the courier the write capability of a temporary copy stream; the courier executes the stream’s envelopes to their destination boxes and tombstones the temporary stream. See https://katzenpost.network/docs/pigeonhole_explained/#copy-commands for the workflow and its all-or-nothing semantics.

An in-flight call may be cancelled via CancelResendingCopyCommand.

func (t *ThinClient) StartResendingCopyCommand(writeCap *bacap.WriteCap) error
pub async fn start_resending_copy_command(
    &self,
    write_cap: &[u8],
    courier_identity_hash: Option<&[u8]>,
    courier_queue_id: Option<&[u8]>,
) -> Result<(), ThinClientError>
async def start_resending_copy_command(self, write_cap: bytes, courier_identity_hash: 'bytes|None' = None, courier_queue_id: 'bytes|None' = None) -> None:

CancelResendingCopyCommand / cancel_resending_copy_command

CancelResendingCopyCommand cancels ARQ resending for a copy command.

The daemon stops retransmitting the copy command identified by writeCapHash (the blake2b-256 hash of the serialized write capability), and the operation is removed from in-flight tracking so it is not replayed after a reconnect.

func (t *ThinClient) CancelResendingCopyCommand(writeCapHash *[32]byte) error
pub async fn cancel_resending_copy_command(
    &self,
    write_cap_hash: &[u8; 32],
) -> Result<(), ThinClientError>
async def cancel_resending_copy_command(self, write_cap_hash: bytes) -> None:

Contact Vouchers

VoucherMint / voucher_mint

VoucherMint mints a Voucher from the joiner’s MessageStream write cap. The returned reply carries the Voucher to hand over out of band, the payload to publish to VoucherStream box 0, the rendezvous stream caps, and the reply keypair; persist VoucherSecretKey to open the inductor’s reply later.

func (t *ThinClient) VoucherMint(messageWriteCap []byte, displayName string) (*VoucherMintReply, error)
pub async fn voucher_mint(
    &self,
    message_write_cap: &[u8],
    display_name: &str,
) -> Result<VoucherMintResult, ThinClientError>
async def voucher_mint(self, message_write_cap: bytes, display_name: str) -> VoucherMintResult:

VoucherInduct / voucher_induct

VoucherInduct verifies a published VoucherPayload and seals a reply to the joiner. The returned reply carries the joiner’s salt-mutated read cap (the live read cap to hand the group), the sealed reply to write to VoucherStream box 1, and the salt the inductor minted.

func (t *ThinClient) VoucherInduct(voucher, voucherPayload, whoReply []byte) (*VoucherInductReply, error)
pub async fn voucher_induct(
    &self,
    voucher: &[u8],
    voucher_payload: &[u8],
    who_reply: &[u8],
) -> Result<VoucherInductResult, ThinClientError>
async def voucher_induct(self, voucher: bytes, voucher_payload: bytes, who_reply: bytes) -> VoucherInductResult:

VoucherOpen / voucher_open

VoucherOpen opens the inductor’s sealed reply with the joiner’s voucher secret key, recovers the salt, and mutates the joiner’s MessageStream write cap by it. The returned reply carries the opaque WhoReply, the salt, and the salt-mutated write cap with which the joiner writes real messages.

func (t *ThinClient) VoucherOpen(voucherSecretKey, sealedReply, messageWriteCap []byte) (*VoucherOpenReply, error)
pub async fn voucher_open(
    &self,
    voucher_secret_key: &[u8],
    sealed_reply: &[u8],
    message_write_cap: &[u8],
) -> Result<VoucherOpenResult, ThinClientError>
async def voucher_open(self, voucher_secret_key: bytes, sealed_reply: bytes, message_write_cap: bytes) -> VoucherOpenResult:

VoucherDeriveStream / voucher_derive_stream

VoucherDeriveStream derives the VoucherStream caps from the Voucher, which the inductor needs to read box 0 before inducting.

func (t *ThinClient) VoucherDeriveStream(voucher []byte) (*VoucherDeriveStreamReply, error)
pub async fn voucher_derive_stream(
    &self,
    voucher: &[u8],
) -> Result<VoucherStreamResult, ThinClientError>
async def voucher_derive_stream(self, voucher: bytes) -> VoucherStreamResult:

Pigeonhole Geometry

GetPigeonholeGeometry / pigeonhole_geometry

GetPigeonholeGeometry returns the Pigeonhole geometry the daemon supplied during the connection handshake. It is nil until Dial() has completed.

Note: The Python binding has no getter method yet; it exposes the Pigeonhole geometry as the public pigeonhole_geometry attribute, cached from the daemon’s connection handshake so applications can size their payloads. A getter method is planned for the next release.

func (t *ThinClient) GetPigeonholeGeometry() *pigeonholeGeo.Geometry
pub fn pigeonhole_geometry(&self) -> PigeonholeGeometry
# Public attribute, cached from the connection handshake;
# None until the daemon connects. A getter method is planned
# for the next release.
geo = client.pigeonhole_geometry

Auxiliary Index Helpers

Most applications never need these methods: every call that consumes a message box index already returns the next index alongside its result.

NextMessageBoxIndex / next_message_box_index

NextMessageBoxIndex returns the message box index that follows messageBoxIndex in its BACAP stream. The computation happens in the daemon and causes no mixnet traffic.

Most callers never need this method: EncryptRead, EncryptWrite, and the copy stream constructors already return the next index alongside their results.

func (t *ThinClient) NextMessageBoxIndex(messageBoxIndex *bacap.MessageBoxIndex) (nextMessageBoxIndex *bacap.MessageBoxIndex, err error)
pub async fn next_message_box_index(
    &self,
    message_box_index: &[u8],
) -> Result<Vec<u8>, ThinClientError>
async def next_message_box_index(self, message_box_index: bytes) -> bytes:

GetMessageBoxIndexCounter / get_message_box_index_counter

GetMessageBoxIndexCounter returns the BACAP Idx64 counter embedded in a MessageBoxIndex. Callers can use this to order or compare two indexes without having to know bacap.MessageBoxIndex’s binary layout.

func (t *ThinClient) GetMessageBoxIndexCounter(messageBoxIndex *bacap.MessageBoxIndex) (uint64, error)
pub async fn get_message_box_index_counter(
    &self,
    message_box_index: &[u8],
) -> Result<u64, ThinClientError>
async def get_message_box_index_counter(self, message_box_index: bytes) -> int:

Core Thin-Client API

Everything below is the transport and control plumbing shared by all thin-client applications: constructing and connecting a client, consuming daemon events, querying the PKI, and sending direct (non-Pigeonhole) messages into the mixnet.

Connection Management

Method Purpose
Dial / new / start Connect to the kpclientd daemon.
Close / stop Disconnect from the daemon and shut down the thin client.
IsConnected / is_connected Returns whether the daemon is currently connected to the mixnet.
Disconnect / disconnect Disconnect from the daemon without shutting down.

Events

Method Purpose
EventSink / event_sink Returns a channel (Go) or receiver (Rust) that yields events from the daemon. (Go, Rust)
StopEventSink Stops delivering events on the given channel. (Go)

PKI and Service Discovery

Method Purpose
PKIDocument / pki_document Returns the current PKI consensus document, which contains the network topology and available services.
GetPKIDocumentRaw / get_pki_document_raw Returns the signed PKI document for a given epoch with every directory authority signature intact, so callers may verify the document themselves.
GetDirectoryAuthorities / get_directory_authorities Returns the directory authority descriptors the client daemon is configured to trust, including their identity keys and addresses.
PKIDocumentForEpoch / pki_document_for_epoch Returns the cached PKI document for a specific epoch, or an error if the daemon has not retained a document for that epoch. (Go, Python)
GetService / get_service Returns a random instance of the named service from the PKI document.
GetServices / get_services Returns all instances of a service with the given capability name. (Go, Python)

Direct Messaging

Method Purpose
SendMessage / send_message Sends a message with a SURB (Single Use Reply Block) that allows the destination service to reply. Sends mixnet traffic.
SendMessageWithoutReply / send_message_without_reply Sends a fire-and-forget message with no SURB. Sends mixnet traffic.
BlockingSendMessage / blocking_send_message Sends a message and blocks until a reply is received or the timeout expires. Sends mixnet traffic.

Sphinx Geometry

Method Purpose
GetSphinxGeometry / sphinx_geometry Returns the Sphinx geometry the daemon supplied during the connection handshake, describing the packet and payload sizes of the mixnet’s Sphinx packet format.

Utility

Method Purpose
NewMessageID / new_message_id Returns a new random message ID (16 bytes).
NewSURBID / new_surb_id Returns a new random SURB ID for correlating message replies.
NewQueryID / new_query_id Returns a new random query ID for correlating requests and replies within the thin client protocol.
GetConfig Returns the client’s configuration object, including the Sphinx and Pigeonhole geometries negotiated with the daemon. (Go)
Shutdown Cleanly shuts down the ThinClient instance and stops its background workers. (Go)
GetLogger Returns a logger instance with the given prefix, using the thin client’s configured logging backend. (Go)

Configuration and Construction

The thin client is configured via a TOML file that specifies only how to reach the local daemon. We usually name this configuration file thinclient.toml.

cfg, err := thin.LoadFile("thinclient.toml")
if err != nil {
    log.Fatal(err)
}

logging := &config.Logging{Level: "INFO"}
client := thin.NewThinClient(cfg, logging)
let config = Config::new("thinclient.toml")?;
let client = ThinClient::new(config).await?;
config = Config("thinclient.toml")
client = ThinClient(config)

The thinclient.toml file

thinclient.toml tells the thin client only where to reach the local daemon. The complete file is simply:

[Dial]
  [Dial.Tcp]
    Address = "localhost:64331"
    Network = "tcp"

[Dial] selects the daemon transport. Set exactly one of the two forms (the Network key is an optional refinement of the TCP form):

Key Type Meaning
[Dial.Unix] Address string Filesystem path of the daemon’s Unix socket.
[Dial.Tcp] Address string host:port of the daemon’s TCP listener.
[Dial.Tcp] Network string Optional: "tcp", "tcp4", or "tcp6" (default "tcp").

Concurrency

The Go ThinClient is safe for concurrent use by multiple goroutines. Because its connection state, current PKI document, and in-flight request tracking are guarded internally, the cancel-from-another-goroutine patterns shown in the how-to guide are sound. The Rust and Python bindings are async. An instance is driven from its runtime (a Tokio task or an asyncio event loop) and follows that runtime’s ordinary conventions rather than offering an independent thread-safety guarantee.


Connection Management

Dial / new / start

Dial establishes a connection to the client daemon and initializes the client.

This method performs the complete connection handshake with the client daemon with the following actions.

  1. It establishes a network connection (TCP or Unix socket).
  2. It receives an initial connection status from daemon.
  3. It receives an initial PKI document.
  4. It starts background workers for event handling.

The client supports both online and offline modes. In offline mode (when the daemon is not connected to the mixnet), channel preparation operations will work but actual message transmission will fail.

After successful connection, the client will automatically handle:

  • PKI document updates
  • connection status changes
  • event distribution to application code

Note: The Rust binding folds the connect step into its constructor, so ThinClient::new returns an already-connected handle. Go and Python construct the client first and connect afterwards via Dial() / start(), allowing the application to set up event sinks (in Go) or callbacks (in Python) before any traffic flows.

func (t *ThinClient) Dial() error
pub async fn new(config: Config) -> Result<Arc<Self>, Box<dyn std::error::Error>>
async def start(self, loop: asyncio.AbstractEventLoop) -> None:

Close / stop

Close gracefully shuts down the thin client and closes the daemon connection.

This method performs a clean shutdown with the following actions.

  1. It sends a close notification to the daemon.
  2. It closes the network connection.
  3. It stops all background workers.

After calling Close(), the ThinClient instance should not be used further. Any ongoing operations will be interrupted and may return errors.

func (t *ThinClient) Close() error
pub async fn stop(&self)
def stop(self) -> None:

IsConnected / is_connected

IsConnected returns true if the client daemon is connected to the mixnet.

This indicates whether the daemon has an active connection to the mixnet infrastructure. When false, the client is in “offline mode” where channel operations (prepare operations) will work but actual message transmission will fail.

func (t *ThinClient) IsConnected() bool
pub fn is_connected(&self) -> bool
def is_connected(self) -> bool:

Disconnect / disconnect

Disconnect closes the connection without sending ThinClose. The daemon preserves all state for this client’s app ID, allowing the client to reconnect and resume with the same session token.

func (t *ThinClient) Disconnect() error
pub async fn disconnect(&self)
def disconnect(self) -> None:

Events

The thin client emits events for connection status changes, PKI document updates, and message replies. Go uses an event channel; Rust uses a broadcast receiver; Python uses async callbacks supplied to the Config constructor.

// Get a channel that receives all events
eventCh := client.EventSink()
defer client.StopEventSink(eventCh)

for ev := range eventCh {
    switch ev.(type) {
    case *thin.ConnectionStatusEvent:
        // ...
    case *thin.NewDocumentEvent:
        // ...
    case *thin.MessageReplyEvent:
        // ...
    }
}
// Get a receiver that yields all events as CBOR BTreeMaps
let mut event_rx = client.event_sink();

tokio::spawn(async move {
    while let Some(event) = event_rx.recv().await {
        // Inspect event["type"] and dispatch
    }
});
# Pass async callback functions to the Config constructor.
# Each callback receives a dict with event-specific keys.
# All callbacks are optional; omitted events are ignored.

async def on_connection_status(event):
    print(f"Connected: {event['is_connected']}")

async def on_message_reply(event):
    print(f"Reply for SURBID {event['surbid']!r}: {event['payload']!r}")

config = Config(
    "thinclient.toml",
    on_connection_status=on_connection_status,
    on_message_reply=on_message_reply,
)
client = ThinClient(config)

Event types

  • ConnectionStatusEvent: emitted when the daemon’s connection to the mixnet changes. Fields (Go): IsConnected bool, Err error, InstanceToken [16]byte. InstanceToken uniquely identifies the daemon process and lets clients notice daemon restarts.

  • NewDocumentEvent: emitted when a new PKI consensus document is received from the directory authorities. The Go binding exposes the parsed document as Document *cpki.Document. (The lower-level NewPKIDocumentEvent carrying a raw CBOR Payload []byte is used internally between daemon and thin client. Applications should consume NewDocumentEvent.)

  • MessageSentEvent: emitted when a SendMessage request has been transmitted by the daemon. Fields (Go): MessageID *[MessageIDLength]byte, SURBID *[SURBIDLength]byte, SentAt time.Time, ReplyETA time.Duration, Err string.

  • MessageReplyEvent: emitted when a reply to a SendMessage call is received. Fields (Go): MessageID *[MessageIDLength]byte, SURBID *[SURBIDLength]byte, Payload []byte, ReplyIndex *uint8, ErrorCode uint8. ReplyIndex identifies which of the box’s two replicas answered. Each box is sharded across K=2 replicas, and the value (0 or 1) is the position within that pair of the replica whose response was used. It is chiefly of interest for Pigeonhole channel reads and may be nil when not applicable. The same value is accepted as the reply_index parameter of StartResendingEncryptedMessage, where it likewise selects the replica of the pair to address.

  • ShutdownEvent: emitted when the daemon signals that it is shutting down. Carrying no fields, it precedes the loss of the local socket and is what causes the DaemonDisconnectedEvent (see below) to report IsGraceful = true. Treat it as advance notice of the disconnect; no action is required, since the thin client reconnects and replays in-flight requests on its own.

  • DaemonDisconnectedEvent: emitted by the thin client (not the daemon) when the local socket connection to the daemon is lost. Fields (Go): IsGraceful bool, Err error. IsGraceful is true precisely when a ShutdownEvent preceded the disconnect.

EventSink / event_sink

Available in: Go, Rust.

EventSink returns a buffered channel that receives all events from the thin client.

This method creates a new event channel that will receive copies of all events generated by the thin client, including the following.

  • PKI document updates
  • message sent confirmations
  • message replies
  • channel operation results

The returned channel is buffered with capacity 1. Events are never silently dropped; the fan-out worker blocks until the subscriber accepts each event, matching the “no loss” contract that the Rust and Python thin clients uphold. Consequently an application that stops consuming from its sink will stall the entire fan-out (including events destined for other subscribers). Applications must drain promptly or call StopEventSink() to release their subscription.

Important: Always call StopEventSink() when done with the channel to prevent resource leaks and ensure proper cleanup.

Note: The event sink channel is NOT closed when the client shuts down. Consumers should also select on HaltCh() to detect shutdown, or they can check for a ShutdownEvent in the event stream.

Note: The Rust binding returns an mpsc::Receiver carrying the same event stream. The Python binding has no equivalent method: Python applications instead register async callbacks on the Config constructor and receive events through those.

func (t *ThinClient) EventSink() chan Event
pub fn event_sink(&self) -> EventSinkReceiver

StopEventSink

Available in: Go.

StopEventSink stops sending events to the specified channel and cleans up resources.

This method removes the channel from the event distribution system and should be called when the application is done processing events from a channel returned by EventSink(). Failure to call this method may result in resource leaks and continued event processing overhead.

Note: Rust subscribers are released by dropping the mpsc::Receiver, so the binding exposes no explicit teardown method. Python’s callback model owns no per-subscriber resources either, and so likewise needs no equivalent.

func (t *ThinClient) StopEventSink(ch chan Event)

PKI and Service Discovery

PKIDocument / pki_document

PKIDocument returns the thin client’s current PKI document.

The PKI document contains the current network topology, service information, and cryptographic parameters for the current epoch. This document is automatically updated when the client daemon receives new PKI information.

func (t *ThinClient) PKIDocument() *cpki.Document
pub async fn pki_document(&self) -> Result<BTreeMap<Value, Value>, ThinClientError>
def pki_document(self) -> 'Dict[str,Any] | None':

GetPKIDocumentRaw / get_pki_document_raw

GetPKIDocumentRaw returns the cert.Certificate-wrapped signed PKI document for the requested epoch, with every directory authority signature intact. Pass epoch == 0 to request the document that the daemon believes is current.

The thin client receives the stripped PKI document by default (as pushed in NewPKIDocumentEvent). Use this method when the caller needs to verify the directory authority signatures itself. The payload can be deserialized and verified with core/pki.FromPayload.

func (t *ThinClient) GetPKIDocumentRaw(epoch uint64) ([]byte, uint64, error)
pub async fn get_pki_document_raw(
    &self,
    epoch: u64,
) -> Result<(Vec<u8>, u64), ThinClientError>
async def get_pki_document_raw(self, epoch: int = 0) -> 'Tuple[bytes,int]':

GetDirectoryAuthorities / get_directory_authorities

GetDirectoryAuthorities returns the directory authority descriptors the client daemon is configured with.

A thin client holds only its dial transport configuration and never sees the daemon’s voting authority peer list. This method surfaces it, so a caller may, for instance, map a PKI document’s signature fingerprints (the keys of its Signatures map) to human-readable authority identifiers via each descriptor’s IdentityKeyHash.

func (t *ThinClient) GetDirectoryAuthorities() ([]*DirectoryAuthority, error)
pub async fn get_directory_authorities(
    &self,
) -> Result<Vec<DirectoryAuthority>, ThinClientError>
async def get_directory_authorities(self) -> 'List[Dict[str,Any]]':

PKIDocumentForEpoch / pki_document_for_epoch

Available in: Go, Python.

PKIDocumentForEpoch returns the PKI document for a specific epoch from cache.

This method provides access to PKI documents from previous epochs that are cached by the client. This is important for maintaining consistency during epoch transitions where different participants might be using PKI documents from different epochs, which can lead to different envelope hashes and communication failures.

The client automatically caches the last 5 epochs of PKI documents. If the requested epoch is not in cache, the current document is returned as a fallback.

Note: The Rust binding does not expose a per-epoch accessor; Rust callers use pki_document for the current document, or get_pki_document_raw for the signed document at a given epoch.

func (t *ThinClient) PKIDocumentForEpoch(epoch uint64) (*cpki.Document, error)
def pki_document_for_epoch(self, epoch: int) -> 'Dict[str,Any]':

GetService / get_service

GetService returns a randomly selected service matching the specified capability.

This method is a convenience wrapper around GetServices() that randomly selects one service from all available services with the given capability. This provides automatic load balancing across available service instances.

func (t *ThinClient) GetService(serviceName string) (*common.ServiceDescriptor, error)
pub async fn get_service(
    &self,
    service_name: &str,
) -> Result<ServiceDescriptor, ThinClientError>
def get_service(self, service_name: str) -> ServiceDescriptor:

GetServices / get_services

Available in: Go, Python.

GetServices returns all services matching the specified capability name.

This method searches the current PKI document for services that provide the specified capability. Services in Katzenpost are identified by their capability names (e.g., “echo”, “courier”, “keyserver”).

Note: The Rust binding exposes the same lookup as the free function find_services in helpers.rs, rather than as a method on ThinClient.

func (t *ThinClient) GetServices(capability string) ([]*common.ServiceDescriptor, error)
def get_services(self, capability: str) -> 'List[ServiceDescriptor]':

Direct Messaging

SendMessage / send_message

Sends mixnet traffic.

SendMessage sends a message with reply capability using the legacy API.

This method sends a message with a Single Use Reply Block (SURB) that allows the destination to send a reply. The method is asynchronous: it only blocks until the daemon receives the send request, not until the message is actually transmitted or a reply is received.

To receive replies, applications must monitor events from EventSink() and look for MessageReplyEvent instances with matching SURB IDs.

func (t *ThinClient) SendMessage(surbID *[sConstants.SURBIDLength]byte, payload []byte, destNode *[32]byte, destQueue []byte) error
pub async fn send_message(
    &self,
    surb_id: Vec<u8>,
    payload: &[u8],
    dest_node: Vec<u8>,
    dest_queue: Vec<u8>,
) -> Result<(), ThinClientError>
async def send_message(self, surb_id: bytes, payload: bytes | str, dest_node: bytes, dest_queue: bytes) -> None:

SendMessageWithoutReply / send_message_without_reply

Sends mixnet traffic.

SendMessageWithoutReply sends a fire-and-forget message using the legacy API.

This method sends a message without any reply capability. The message is encapsulated in a Sphinx packet and sent through the mixnet, but no response can be received. This is suitable for notifications or one-way communication.

func (t *ThinClient) SendMessageWithoutReply(payload []byte, destNode *[32]byte, destQueue []byte) error
pub async fn send_message_without_reply(
    &self,
    payload: &[u8],
    dest_node: Vec<u8>,
    dest_queue: Vec<u8>,
) -> Result<(), ThinClientError>
async def send_message_without_reply(self, payload: bytes | str, dest_node: bytes, dest_queue: bytes) -> None:

BlockingSendMessage / blocking_send_message

Sends mixnet traffic.

BlockingSendMessage sends a message and blocks until a reply is received.

This method provides a synchronous request-response pattern by automatically generating a SURB ID, sending the message, and waiting for the reply. It blocks until either a reply is received or the context times out.

This is convenient for simple request-response interactions but lacks the advanced features of the Pigeonhole channel API such as message ordering, channel persistence, and offline operation support.

func (t *ThinClient) BlockingSendMessage(ctx context.Context, payload []byte, destNode *[32]byte, destQueue []byte) ([]byte, error)
pub async fn blocking_send_message(
    &self,
    payload: &[u8],
    dest_node: Vec<u8>,
    dest_queue: Vec<u8>,
    timeout: std::time::Duration,
) -> Result<Vec<u8>, ThinClientError>
async def blocking_send_message(self, payload: bytes | str, dest_node: bytes, dest_queue: bytes, timeout_seconds: float = 30.0) -> bytes:

Sphinx Geometry

GetSphinxGeometry / sphinx_geometry

GetSphinxGeometry returns the Sphinx geometry the daemon supplied during the connection handshake. It is nil until Dial() has completed.

Note: The Python binding has no getter method yet; it exposes the Sphinx geometry as the public geometry attribute (a Geometry), cached from the daemon’s connection handshake so applications can size their payloads. A getter method is planned for the next release.

func (t *ThinClient) GetSphinxGeometry() *geo.Geometry
pub fn sphinx_geometry(&self) -> Geometry
# Public attribute, cached from the connection handshake;
# None until the daemon connects. A getter method is planned
# for the next release.
geo = client.geometry

Utility

NewMessageID / new_message_id

NewMessageID generates a new cryptographically random message identifier.

Message IDs are used to correlate requests with responses in both legacy and channel APIs. Each message should have a unique ID to prevent confusion and enable proper event correlation.

func (t *ThinClient) NewMessageID() *[MessageIDLength]byte
pub fn new_message_id() -> Vec<u8>
def new_message_id() -> bytes:

NewSURBID / new_surb_id

NewSURBID generates a new SURB.

SURB IDs are used in the legacy API to correlate reply messages with their original requests. Each SURB should have a unique ID.

func (t *ThinClient) NewSURBID() *[sConstants.SURBIDLength]byte
pub fn new_surb_id() -> Vec<u8>
def new_surb_id(self) -> bytes:

NewQueryID / new_query_id

NewQueryID generates a new cryptographically random query identifier.

Query IDs are used in the channel API to correlate channel operation requests with their responses. Each query should have a unique ID.

func (t *ThinClient) NewQueryID() *[QueryIDLength]byte
pub fn new_query_id() -> Vec<u8>
def new_query_id(self) -> bytes:

GetConfig

Available in: Go.

GetConfig returns the client’s configuration.

Note: Python callers read the configuration through the plain config attribute on ThinClient. The Rust binding exposes no configuration accessor; Rust callers read the negotiated geometries through the dedicated sphinx_geometry and pigeonhole_geometry methods.

func (t *ThinClient) GetConfig() *Config

Shutdown

Available in: Go.

Shutdown cleanly shuts down the ThinClient instance.

This method stops all background workers and closes the connection to the client daemon. It is equivalent to calling Halt() and is provided for compatibility. For proper cleanup, prefer using Close().

Note: The Rust and Python bindings expose a single teardown method, stop, documented under Close / stop above.

func (t *ThinClient) Shutdown()

GetLogger

Available in: Go.

GetLogger returns a logger instance with the specified prefix.

This allows applications to create loggers that integrate with the thin client’s logging system and maintain consistent log formatting.

Note: The Rust and Python bindings leave logging to the host application and expose no equivalent accessor.

func (t *ThinClient) GetLogger(prefix string) *logging.Logger

Data Types

The Pigeonhole methods return structured results whose fields are enumerated below. These are the Go reference structs from katzenpost/client/thin; they are authoritative. The Rust and Python bindings return the equivalent data through their own result types, with the same fields rendered in snake_case (for example WriteCap becomes write_cap, and NextMessageBoxIndex becomes next_message_box_index).

The following two fields recur throughout and are protocol plumbing rather than application data.

  • QueryID correlates a reply with the request that produced it; the bindings manage it for you.
  • ErrorCode is zero on success and otherwise names the failure. The bindings translate a non-zero code into the language-native error documented under Replica and Courier Errors; application code inspects the raised error or returned sentinel rather than this byte directly.

NewKeypair result (Rust/Python: KeypairResult)

NewKeypairReply is the reply to a NewKeypair request.

Field Type Description
QueryID *[QueryIDLength]byte QueryID is used for correlating this reply with the NewKeypair request.
WriteCap *bacap.WriteCap WriteCap is the write capability that should be stored for the channel.
ReadCap *bacap.ReadCap ReadCap is the read capability that can be shared with others to allow them to read messages from this channel.
FirstMessageIndex *bacap.MessageBoxIndex FirstMessageIndex is the first message index that should be used when writing messages to the channel.
ErrorCode uint8 ErrorCode indicates the reason for a failure to create a new keypair if any. Otherwise it is set to zero for success.

EncryptWrite result (Rust/Python: EncryptWriteResult)

EncryptWriteReply is the reply to an EncryptWrite request.

Field Type Description
QueryID *[QueryIDLength]byte QueryID is used for correlating this reply with the EncryptWrite request
MessageCiphertext []byte MessageCiphertext is the encrypted message ciphertext that should be sent to the courier service.
EnvelopeDescriptor []byte EnvelopeDescriptor contains the serialized EnvelopeDescriptor that contains the private key material needed to decrypt the envelope reply.
EnvelopeHash *[32]byte EnvelopeHash is the hash of the CourierEnvelope that was sent to the mixnet and is used to resume the write operation.
NextMessageBoxIndex *bacap.MessageBoxIndex NextMessageBoxIndex is the next message box index to use for subsequent write operations. This is computed by the daemon using BACAP’s NextIndex.
ErrorCode uint8 ErrorCode indicates the reason for a failure to encrypt the write if any. Otherwise it is set to zero for success.

EncryptRead result (Rust/Python: EncryptReadResult)

EncryptReadReply is the reply to an EncryptRead request.

Field Type Description
QueryID *[QueryIDLength]byte QueryID is used for correlating this reply with the EncryptRead request.
MessageCiphertext []byte MessageCiphertext is the encrypted message ciphertext that should be sent to the courier service.
EnvelopeDescriptor []byte EnvelopeDescriptor contains the serialized EnvelopeDescriptor that contains the private key material needed to decrypt the envelope reply.
EnvelopeHash *[32]byte EnvelopeHash is the hash of the CourierEnvelope that was sent to the mixnet and is used to resume the read operation.
NextMessageBoxIndex *bacap.MessageBoxIndex NextMessageBoxIndex is the next message box index to use for subsequent read operations. This is computed by the daemon using BACAP’s NextIndex.
ErrorCode uint8 ErrorCode indicates the reason for a failure to encrypt the read if any. Otherwise it is set to zero for success.

StartResendingEncryptedMessage result (Rust/Python: StartResendingResult)

StartResendingEncryptedMessageReply is the reply to a StartResendingEncryptedMessage request.

Field Type Description
QueryID *[QueryIDLength]byte QueryID is used for correlating this reply with the StartResendingEncryptedMessage request.
Plaintext []byte Plaintext is the plaintext message that was read from the channel.
ErrorCode uint8 ErrorCode indicates the reason for a failure to start resending the encrypted message if any. Otherwise it is set to zero for success.
CourierIdentityHash *[32]byte CourierIdentityHash is the 32-byte hash of the identity key of the courier that was selected to handle this message. Callers can watch PKI document updates for this courier disappearing from consensus and cancel+re-encrypt if it does.
CourierQueueID []byte CourierQueueID is the queue ID of the courier that was selected.

StartResendingCopyCommand result

StartResendingCopyCommandReply is the reply to a StartResendingCopyCommand request.

Field Type Description
QueryID *[QueryIDLength]byte QueryID is used for correlating this reply with the StartResendingCopyCommand request.
ErrorCode uint8 ErrorCode indicates the reason for a failure to execute the copy command if any. Otherwise it is set to zero for success.
ReplicaErrorCode uint8 ReplicaErrorCode is the Pigeonhole replica ErrorCode that caused the Copy command to abort on the courier. Meaningful only when ErrorCode indicates a Copy failure and the courier identified a specific replica-side reason (e.g., ReplicaErrorBoxAlreadyExists).
FailedEnvelopeIndex uint64 FailedEnvelopeIndex is the 1-based sequential position in the copy stream of the envelope whose write triggered the abort. 0 if not applicable. Not a BACAP message index.

NextMessageBoxIndex result

NextMessageBoxIndexReply is the reply to a NextMessageBoxIndex request.

Field Type Description
QueryID *[QueryIDLength]byte QueryID is used for correlating this reply with the NextMessageBoxIndex request.
NextMessageBoxIndex *bacap.MessageBoxIndex NextMessageBoxIndex is the incremented message box index.
ErrorCode uint8 ErrorCode indicates the reason for a failure to increment the index if any. Otherwise it is set to zero for success.

GetMessageBoxIndexCounter result

GetMessageBoxIndexCounterReply is the reply to a GetMessageBoxIndexCounter request.

Field Type Description
QueryID *[QueryIDLength]byte QueryID is used for correlating this reply with the GetMessageBoxIndexCounter request.
Counter uint64 Counter is the BACAP Idx64 value read out of the requested MessageBoxIndex.
ErrorCode uint8 ErrorCode indicates the reason for a failure to read the counter if any. Otherwise it is set to zero for success.

GetPKIDocumentRaw result

GetPKIDocumentReply is the reply to a GetPKIDocument request. The Payload field carries the cert.Certificate-wrapped signed PKI document exactly as the daemon received it from the gateway, retaining every directory authority signature so that callers may verify it themselves.

Field Type Description
QueryID *[QueryIDLength]byte QueryID is used for correlating this reply with the GetPKIDocument request.
Payload []byte Payload is the cert.Certificate-wrapped signed PKI document, or nil on failure. Use core/pki.FromPayload to deserialize and verify it against the directory authorities’ public keys.
Epoch uint64 Epoch is the epoch of the returned document. When the request asked for the current epoch this echoes the epoch the daemon believes is current.
ErrorCode uint8 ErrorCode indicates the reason for a failure to return a signed PKI document if any. Otherwise it is set to zero for success.

CreateCourierEnvelopesFromPayload result

CreateCourierEnvelopesFromPayloadReply is sent in response to a CreateCourierEnvelopesFromPayload request. It provides multiple serialized CopyStreamElements, one for each chunk of the payload.

Field Type Description
QueryID *[QueryIDLength]byte QueryID is used for correlating this reply with the CreateCourierEnvelopesFromPayload request that created it.
Envelopes [][]byte Envelopes is a slice of serialized CopyStreamElements, one per chunk.
NextDestIndex *bacap.MessageBoxIndex NextDestIndex is the next destination message box index after all boxes consumed by this call. Use this as DestStartIndex in subsequent calls to continue writing to the same destination stream.
ErrorCode uint8 ErrorCode indicates the success or failure of the envelope creation. A value of ThinClientSuccess indicates successful creation.

CreateCourierEnvelopesFromMultiPayload result

CreateCourierEnvelopesFromPayloadsReply is sent in response to a CreateCourierEnvelopesFromPayloads request. It provides multiple serialized CopyStreamElements packed efficiently from multiple destination payloads.

Field Type Description
QueryID *[QueryIDLength]byte QueryID is used for correlating this reply with the CreateCourierEnvelopesFromPayloads request that created it.
Envelopes [][]byte Envelopes is a slice of serialized CopyStreamElements containing all the courier envelopes from all destinations packed efficiently together.
Buffer []byte Buffer contains any data buffered by the encoder that hasn’t been output yet. This can be persisted for crash recovery and restored via SetStreamBuffer.
NextDestIndices []*bacap.MessageBoxIndex NextDestIndices contains the next destination message box index for each destination, in the same order as the destinations in the request. Use these as StartIndex in subsequent calls to continue writing to the same destination streams.
ErrorCode uint8 ErrorCode indicates the success or failure of the envelope creation. A value of ThinClientSuccess indicates successful creation.

CreateCourierEnvelopesFromTombstoneRange result

CreateCourierEnvelopesFromTombstoneRangeReply is sent in response to a CreateCourierEnvelopesFromTombstoneRange request. It provides serialized CopyStreamElements containing tombstone courier envelopes.

Field Type Description
QueryID *[QueryIDLength]byte QueryID is used for correlating this reply with the request.
Envelopes [][]byte Envelopes is a slice of serialized CopyStreamElements.
Buffer []byte Buffer is the residual encoder buffer to pass to the next call. Nil when IsLast was true in the request.
NextDestIndex *bacap.MessageBoxIndex NextDestIndex is the next destination message box index after all tombstones created by this call.
ErrorCode uint8 ErrorCode indicates the success or failure of the operation.

DestinationPayload (parameter)

DestinationPayload specifies a payload and its destination channel for multi-channel writes.

Passed into CreateCourierEnvelopesFromMultiPayload, one per destination channel.

Field Type Description
Payload []byte Payload is the data to be written to this destination.
WriteCap *bacap.WriteCap WriteCap is the write capability for the destination channel.
StartIndex *bacap.MessageBoxIndex StartIndex is the starting index in the destination channel.

Transport and Lifecycle Errors

These errors can in principle be raised by any method that performs I/O against the daemon or the mixnet.

Condition Go Rust Python
Daemon not connected to mixnet Ad-hoc error with message “cannot send message in offline mode - daemon not connected to mixnet”. (No sentinel; check IsConnected() first.) ThinClientError::OfflineMode(String) ThinClientOfflineError
Operation timed out context.DeadlineExceeded (from ctx.Err()) ThinClientError::Timeout(String) asyncio.TimeoutError
Operation canceled by caller context.Canceled (from ctx.Err()) No distinct variant; uses higher-level cancellation. asyncio.CancelledError
Local socket to kpclientd lost returned on the next I/O; thin client attempts reconnect with exponential backoff ditto (receive DaemonDisconnectedEvent on the event sink) ditto (receive DaemonDisconnectedEvent on the event sink)
CBOR (de)serialisation failure wrapped error ThinClientError::CborError(serde_cbor::Error) serde-layer exception bubbles up

The Go binding does not provide a named sentinel for offline mode. Applications that must distinguish “daemon offline” from other errors should test IsConnected() before sending, not compare error values after the fact. The Rust and Python bindings provide proper sentinels testable with matches! / isinstance.


Replica and Courier Errors

The errors below can be returned by StartResendingEncryptedMessage and its variants. They are defined in pigeonhole/errors.go.

Errors specific to reads (when readCap is set)

Error Go Rust Python
Box not found (retries exhausted) ErrBoxIDNotFound ThinClientError::BoxNotFound BoxIDNotFoundError
MKEM decryption failed ErrMKEMDecryptionFailed ThinClientError::MkemDecryptionFailed MKEMDecryptionFailedError
BACAP decryption failed ErrBACAPDecryptionFailed ThinClientError::BacapDecryptionFailed BACAPDecryptionFailedError
Tombstone (box was deleted) ErrTombstone ThinClientError::Tombstone TombstoneError

Errors specific to writes (when writeCap is set)

Error Go Rust Python
Storage full ErrStorageFull ThinClientError::StorageFull StorageFullError

Errors on both reads and writes

Error Go Rust Python
Operation canceled ErrStartResendingCancelled ThinClientError::StartResendingCancelled StartResendingCancelledError
Invalid box ID ErrInvalidBoxID ThinClientError::InvalidBoxId InvalidBoxIDError
Invalid signature ErrInvalidSignature ThinClientError::InvalidSignature InvalidSignatureError
Invalid tombstone signature ErrInvalidTombstoneSignature ThinClientError::InvalidTombstoneSignature InvalidTombstoneSignatureError
Database failure ErrDatabaseFailure ThinClientError::DatabaseFailure DatabaseFailureError
Invalid payload ErrInvalidPayload ThinClientError::InvalidPayload InvalidPayloadError
Invalid epoch ErrInvalidEpoch ThinClientError::InvalidEpoch InvalidEpochError
Replication failed ErrReplicationFailed ThinClientError::ReplicationFailed ReplicationFailedError
Replica internal error ErrReplicaInternalError ThinClientError::ReplicaInternalError ReplicaInternalError
Box already exists (writes only, when non-idempotent variant used) ErrBoxAlreadyExists ThinClientError::BoxAlreadyExists BoxAlreadyExistsError

Copy-command failure

StartResendingCopyCommand can return a diagnostic error carrying the underlying replica error code and the 1-based sequential envelope index at which processing stopped:

Binding Error
Go ErrCopyCommandFailed (see CopyCommandFailedError struct for fields)
Rust ThinClientError::CopyCommandFailed { replica_error_code, failed_envelope_index }
Python CopyCommandFailedError(replica_error_code, failed_envelope_index)

Expected Outcomes vs Real Failures

Some errors from StartResendingEncryptedMessage represent completed operations, not failures. Use IsExpectedOutcome(err) (Go), err.is_expected_outcome() (Rust), or is_expected_outcome(exc) (Python) to distinguish them:

Error Why it may be expected
BoxIDNotFound / BoxNotFound Polling for a message that hasn’t been written yet.
BoxAlreadyExists Retrying an idempotent write that already succeeded.
Tombstone Reading a box that was intentionally deleted.

These should generally not trigger retries in your application.