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) andStartResendingCopyCommandput 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. TheCancel*methods are local control operations, and everything else is a local computation performed by the daemon over the local socket.
| Method | Purpose |
|---|---|
| NewKeypair / new_keypair | Generates a new BACAP keypair from a 32-byte seed. |
| 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. |
| 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. |
| Method | Purpose |
|---|---|
| TombstoneRange / tombstone_range | Creates encrypted tombstone envelopes for a range of consecutive boxes. |
| 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. |
| 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. |
| Method | Purpose |
|---|---|
| GetPigeonholeGeometry / pigeonhole_geometry | Returns the negotiated Pigeonhole geometry, so that callers can size payloads to its maximum plaintext payload length. |
| 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_foundandno_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=Truetostart_resending_encrypted_messageinstead.
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=Truetostart_resending_encrypted_messageinstead.
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) errorpub 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,
) -> TombstoneRangeResultasync 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) errorpub 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) errorpub 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_geometryattribute, 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.Geometrypub 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_geometryAuxiliary 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.
| 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. |
| 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) |
| 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) |
| 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. |
| 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. |
| 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.
- It establishes a network connection (TCP or Unix socket).
- It receives an initial connection status from daemon.
- It receives an initial PKI document.
- 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::newreturns an already-connected handle. Go and Python construct the client first and connect afterwards viaDial()/start(), allowing the application to set up event sinks (in Go) or callbacks (in Python) before any traffic flows.
func (t *ThinClient) Dial() errorpub 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.
- It sends a close notification to the daemon.
- It closes the network connection.
- 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() errorpub 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() boolpub fn is_connected(&self) -> booldef 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() errorpub 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.InstanceTokenuniquely 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-levelNewPKIDocumentEventcarrying a raw CBORPayload []byteis used internally between daemon and thin client. Applications should consumeNewDocumentEvent.) -
MessageSentEvent: emitted when a
SendMessagerequest 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
SendMessagecall is received. Fields (Go):MessageID *[MessageIDLength]byte,SURBID *[SURBIDLength]byte,Payload []byte,ReplyIndex *uint8,ErrorCode uint8.ReplyIndexidentifies 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 benilwhen not applicable. The same value is accepted as thereply_indexparameter ofStartResendingEncryptedMessage, 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.IsGracefulis true precisely when aShutdownEventpreceded 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::Receivercarrying the same event stream. The Python binding has no equivalent method: Python applications instead register async callbacks on theConfigconstructor and receive events through those.
func (t *ThinClient) EventSink() chan Eventpub fn event_sink(&self) -> EventSinkReceiverStopEventSink
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.Documentpub 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_documentfor the current document, orget_pki_document_rawfor 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_servicesinhelpers.rs, rather than as a method onThinClient.
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) errorpub 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) errorpub 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
geometryattribute (aGeometry), 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.Geometrypub 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.geometryUtility
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]bytepub 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]bytepub 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]bytepub 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
configattribute onThinClient. The Rust binding exposes no configuration accessor; Rust callers read the negotiated geometries through the dedicatedsphinx_geometryandpigeonhole_geometrymethods.
func (t *ThinClient) GetConfig() *ConfigShutdown
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.LoggerData 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.
QueryIDcorrelates a reply with the request that produced it; the bindings manage it for you.ErrorCodeis 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.