3-RRC: Wire Encoding, Constants, and Numeric Assignments¶
Version: 0.1.2
This document exists so that two people, writing two separate RRC implementations, do not accidentally invent two different protocols while both claiming success.
1-RRC explained what the system is. 2-RRC explained what messages exist and how sessions behave. This document locks down the numbers, field keys, and expectations that appear on the wire so that CBOR blobs mean the same thing everywhere.
If you skip this document, your implementation will still work, but only with itself. That is not a victory.
Canonical Encoding Rules¶
All RRC messages are encoded as CBOR maps. The map keys are unsigned integers. String keys are not allowed. This is not because strings are evil, but because numeric keys are smaller, faster to parse, and harder to misspell.
All integer values are encoded as unsigned integers unless explicitly stated otherwise. Binary values are encoded as CBOR byte strings. Human-readable text is encoded as CBOR text strings using UTF-8.
Ordering of keys in the CBOR map does not matter. Receivers must not assume any specific ordering and must not reject messages solely because the sender arranged keys differently.
Unknown keys must be ignored. This is not optional. If your implementation explodes because it saw a field it did not understand, that is your fault.
CBOR Encoding Size Reference¶
Understanding CBOR encoding overhead is essential for staying within the Reticulum MTU constraints. This section provides the byte counts for RRC fields to help implementers calculate message sizes.
CBOR Encoding Basics¶
In CBOR, each value has a type byte that includes both the major type and additional information. For small values, this can be very compact.
Map overhead:
Maps with 0-23 fields: 1 byte for the map header
Integer keys (0-23):
Each key 0-23: 1 byte
Unsigned integer values:
Values 0-23: 1 byte (encoded directly in the type byte)
Values 24-255: 2 bytes (type byte + 1 data byte)
Values 256-65535: 3 bytes (type byte + 2 data bytes)
Values up to 2^32-1: 5 bytes (type byte + 4 data bytes)
Values up to 2^64-1: 9 bytes (type byte + 8 data bytes)
Byte strings:
Length 0-23: 1 byte (type+length) + N bytes data
Length 24-255: 2 bytes (type + length byte) + N bytes data
Length 256-65535: 3 bytes (type + 2 length bytes) + N bytes data
Text strings (UTF-8):
Same encoding as byte strings, but major type 3 instead of 2
Fixed Field Sizes¶
RRC mandates specific fixed lengths for core envelope fields to ensure predictable sizing and interoperability. The following measurements are based on actual CBOR encoding using Python’s cbor2 library in the reference implementation (rrcd).
CBOR Map Header: 1 byte (for maps with 0-23 fields)
Field Number |
Field Name |
Value Size |
Key |
Type/Length |
Total |
Notes |
|---|---|---|---|---|---|---|
0 |
Protocol Version |
1 byte |
1 |
— |
2 |
Value 0-23 fits in 1 byte |
1 |
Message Type |
1 byte |
1 |
— |
2 |
Values 0-255 |
2 |
Message ID |
8 bytes |
1 |
1 |
10 |
Byte string (fixed 8) |
3 |
Timestamp |
8 bytes |
1 |
1 |
10 |
Uint64 (ms since epoch) |
4 |
Sender Identity |
16 bytes |
1 |
1 |
18 |
Byte string (fixed 16) |
Total fixed overhead: 1 (map) + 2 + 2 + 10 + 10 + 18 = 43 bytes
Variable Field Sizes¶
Text strings are encoded as UTF-8, where characters may use 1-4 bytes each. CBOR overhead is determined by the total byte length of the UTF-8 encoded string, not the character count.
Field Number |
Field Name |
Type |
Overhead Formula |
Typical Overhead |
|---|---|---|---|---|
5 |
Room Name |
text string |
1 (key) + 1-2 (type+len) + N |
2-3 bytes |
6 |
Body |
varies |
1 (key) + 1-3 (type+len) + N |
2-4 bytes |
7 |
Nickname |
text string |
1 (key) + 1-2 (type+len) + N |
2-3 bytes |
CBOR text string overhead by length:
0-23 bytes: 1 byte (type+length encoded together)
24-255 bytes: 2 bytes (type + separate 1-byte length)
256-65535 bytes: 3 bytes (type + separate 2-byte length)
Concrete examples:
“lobby” (5 ASCII chars = 5 bytes): 1 key + 1 type+len + 5 data = 7 bytes total
“café” (4 chars = 5 UTF-8 bytes): 1 key + 1 type+len + 5 data = 7 bytes total
“学习” (2 Chinese chars = 6 UTF-8 bytes): 1 key + 1 type+len + 6 data = 8 bytes total
350-byte message body: 1 key + 3 type+len + 350 data = 354 bytes total
Minimum Envelope Overhead¶
A minimal RRC envelope with only required fields totals 43 bytes:
CBOR map header: 1 byte
Field 0 (Protocol Version): 2 bytes
Field 1 (Message Type): 2 bytes
Field 2 (Message ID): 10 bytes
Field 3 (Timestamp): 10 bytes
Field 4 (Sender Identity): 18 bytes
When adding optional variable fields:
Field 5 (Room Name): 2-3 bytes overhead + UTF-8 byte length
Field 6 (Body): 2-4 bytes overhead + content length
Field 7 (Nickname): 2-3 bytes overhead + UTF-8 byte length
Reticulum MTU Constraints¶
Reticulum packets have a default MTU of 500 bytes structured as:
[HEADER 2 bytes] [ADDRESSES 16/32 bytes] [CONTEXT 1 byte] [DATA 0-465 bytes]
Worst case (32-byte addresses): 465 bytes available for RRC envelope
Fixed overhead: 43 bytes
Remaining for variable fields: 422 bytes
Best case (16-byte addresses): 481 bytes available for RRC envelope
Fixed overhead: 43 bytes
Remaining for variable fields: 438 bytes
Implementers should always budget for the worst-case scenario (422 bytes for variable content).
Practical Size Budget Example¶
A worst-case MSG envelope with recommended hub limits:
Component |
Calculation |
Bytes |
|---|---|---|
Fixed overhead |
Map + Fields 0-4 |
43 |
Room name (64B) |
3 overhead + 64 data |
67 |
Nickname (32B) |
3 overhead + 32 data |
35 |
Body (350B) |
4 overhead + 350 data |
354 |
Total |
499 |
Result: ✓ Fits within 500-byte Reticulum MTU with 1 byte to spare.
Note: Room names and nicknames measured in bytes (not characters), accounting for UTF-8 encoding where characters may use 1-4 bytes each.
Envelope Structure¶
Every RRC message shares the same top-level envelope. This envelope is a CBOR map containing the following fields.
Field 0 - Protocol Version¶
This value identifies the RRC protocol version used to encode the message. For this specification, the value must be 1. Messages with other versions may be ignored or rejected.
Field 1 - Message Type¶
This is an unsigned integer identifying what kind of message this is. The numeric values for message types are defined later in this document.
Field 2 - Message Identifier¶
This is an 8-byte random identifier chosen by the sender using a cryptographically secure random source (e.g., os.urandom(8)). It must be unique within the scope of the sender’s current session. It does not need to be globally unique or persistent across reconnects. It exists so implementations can correlate messages if they care to.
Field 3 - Timestamp¶
This is an unsigned integer representing milliseconds since the Unix epoch, according to the sender’s clock. CBOR encodes this as a 64-bit unsigned integer (type byte + 8 data bytes). It is advisory only. No one is expected to agree on time.
Field 4 - Sender Identity¶
This is a 16-byte value containing the Reticulum identity hash of the sender encoded as a CBOR byte string. This field is mandatory even though the Link already authenticates the peer, because forwarded messages need an explicit source identifier.
Field 5 - Room Name¶
This is a text string identifying the room the message applies to. If a message does not apply to a room, this field may be omitted.
Field 6 - Body¶
This field contains the payload of the message. Its structure and meaning depend entirely on the message type.
Field 7 - Nickname¶
This is an optional text string containing a human-readable nickname for the sender.
It is advisory only. It may be omitted, empty, ridiculous, or all three. The hub may ignore it, sanitize it, replace it, or drop it on the floor entirely. Clients may display it if present, or may choose to rely on identity hashes, locally cached labels, or whatever else they think is a good idea.
If both sides send this field, they are not negotiating. They are making suggestions.
No other top-level fields are defined by this document.
Envelope field keys 0 through 39 are reserved for core protocol use. Extensions may define additional top-level fields using keys 40 through 63. Keys 64 and above are reserved for future specifications, which is a polite way of saying “don’t squat there unless you enjoy avoidable pain.”
Message Type Assignments¶
Message type values are fixed. They are not suggestions.
Type |
Name |
|---|---|
1 |
HELLO |
2 |
WELCOME |
10 |
JOIN |
11 |
JOINED |
12 |
PART |
13 |
PARTED |
20 |
MSG |
21 |
NOTICE |
30 |
PING |
31 |
PONG |
40 |
ERROR |
0 - 9: Link Control Messages¶
Type 0 - RESERVED¶
Type 0 is reserved and must not be used.
Type 1 - HELLO¶
The HELLO message uses type 1. This message is sent by the client to announce
itself after a Link is established.
Type 2 - WELCOME¶
The WELCOME message uses type 2. This message is sent by the hub to
acknowledge and accept the session.
10 - 19: Room Membership Messages¶
Type 10 - JOIN¶
The JOIN message uses type 10. This message is sent by the client to request
entry into a room.
Type 11 - JOINED¶
The JOINED message uses type 11. This message is sent by the hub to confirm
room membership.
Type 12 - PART¶
The PART message uses type 12. This message is sent by the client to leave a
room.
Type 13 - PARTED¶
The PARTED message uses type 13. This message is sent by the hub to confirm
that a client has left a room.
20 - 29: Messages Intended for Rooms and People¶
Type 20 - MSG¶
The MSG message uses type 20. This message carries chat content intended for a
room.
Type 21 - NOTICE¶
The NOTICE message uses type 21. This message carries informational or
non-conversational content.
30 - 39: Link Management Messages¶
Type 30 - PING¶
The PING message uses type 30. This message checks whether the peer is still
responsive.
Type 31 - PONG¶
The PONG message uses type 31. This message is the response to a PING.
40 and Up: Error and Status Messages¶
Type 40 - ERROR¶
The ERROR message uses type 40. This message reports a failure or refusal.
Values from 0 to 63 are reserved for core protocol use. Values from 64 upward are available for extensions, experiments, and future specifications. If you collide with yourself there, that is your own adventure.
Message Body Definitions¶
The body field exists so message-specific data does not leak into the envelope and turn everything into a special case.
HELLO body key assignments¶
For HELLO, the body is a CBOR map with unsigned integer keys. It may contain
session-specific data used for exchanging version information or program
capabilities, as well as human-facing metadata.
All body fields are advisory and optional. Any human-facing metadata is considered an extension of the protocol and is outside the scope of this specification.
Key |
Name |
Type |
Description |
|---|---|---|---|
0 |
Client Name |
text string |
Client software name (optional) |
1 |
Client Version |
text string |
Client version string (optional) |
2 |
Capabilities |
map/array |
Capability hints (optional) |
WELCOME body key assignments¶
For WELCOME, the body is also a CBOR map with unsigned integer keys. It may
contain session-specific data used for exchanging version information or program
capabilities, as well as human-facing metadata.
All body fields are advisory and optional. Any human-facing metadata is considered an extension of the protocol and is outside the scope of this specification.
Key |
Name |
Type |
Description |
|---|---|---|---|
0 |
Hub Name |
text string |
Human-friendly hub name |
1 |
Hub Version |
text string |
Hub version string (optional) |
2 |
Capabilities |
map/array |
Capability hints (optional) |
3 |
Hub Limits |
map |
Configured hub limits (optional) |
Capabilities Field¶
The Capabilities field (key 2) in both HELLO and WELCOME bodies is intended
for exchanging information about supported features. Its structure is left
open-ended to allow flexibility. Implementations may choose to use a CBOR map
with string keys representing capability names and boolean values indicating
support, or an array of strings listing supported capabilities.
Hub Limits Field¶
The Hub Limits field (key 3) in the WELCOME body is a CBOR map that may
contain information about the hub’s configured limits. Possible keys include:
max_nick_bytes: unsigned integer indicating the maximum length of nicknames in bytes.max_rooms_per_session: unsigned integer indicating the maximum number of rooms a client may join in a single session.max_room_name_bytes: unsigned integer indicating the maximum length of room names in bytes.max_msg_body_bytes: unsigned integer indicating the maximum size of message bodies in bytes.rate_limit_msgs_per_minute: unsigned integer indicating the maximum number of messages a client may send per minute.
These keys are only examples. Hubs may define additional limit keys as needed. Clients should treat this information as advisory and may choose to enforce limits locally based on the received values.
Other message body definitions:¶
Unknown body keys must be ignored. Keys 0 through 63 are reserved for core body fields defined by this document. Keys 64 and above may be used for extensions.
For JOIN, the body is empty or omitted. The room name is taken from the
envelope field.
For JOINED, the body may contain a list of current members. If present, this
list is advisory and not guaranteed to be complete or current.
For PART, the body is empty or omitted.
For PARTED, the body may be empty, omitted, or may contain the identity of the
client that parted, for confirmation purposes.
For MSG, the body contains the message payload. The simplest and most common
form is a text string. Implementations may choose to use structured payloads,
but hubs must treat the body as opaque data and forward it unchanged.
For NOTICE, the body follows the same rules as MSG but carries a different
semantic meaning.
For PING and PONG, the body may be omitted or may contain arbitrary data to
allow round-trip correlation. Receivers must echo the body back unchanged in the
corresponding response if it is present.
For ERROR, the body should be a text string describing the error in plain
language. It may optionally be a CBOR map if structured error information is
desired, but clients must handle simple text at minimum.
Room Name Normalization¶
Room names are encoded as text strings. Hubs should normalize room names internally, typically by converting them to lowercase. The exact normalization method is an implementation detail, but the observable behavior should be case-insensitive room matching.
Clients should not assume that room names preserve original casing.
Identity Encoding¶
The sender identity field contains the Reticulum identity hash encoded as a CBOR byte string. No additional structure is imposed by RRC.
Clients and hubs must not reinterpret, truncate, or re-encode this value. It is passed through as-is and treated as opaque data.
Error Handling on the Wire¶
Malformed messages may be ignored. Messages with unknown message types must be ignored. Messages with missing required envelope fields may be ignored or may trigger an ERROR, at the receiver’s discretion.
If a hub sends an ERROR and then closes the Link, that is considered valid behavior. Clients should not attempt to argue with a closed socket.
Forward Compatibility Rules¶
Implementations must follow three simple rules to remain compatible over time.
They must ignore unknown envelope keys. They must ignore unknown message types. They must not repurpose existing numeric assignments.
Breaking any of these rules means you are no longer speaking RRC, regardless of how confident you feel.
Examples¶
Example: Client Sending a Chat Message (MSG)¶
A client sending “Hello, world!” to the #lobby room:
Field Number |
Field Name |
Value |
|---|---|---|
0 |
Protocol Version |
1 |
1 |
Message Type |
20 (MSG) |
2 |
Message ID |
b”\x7a\x3f\x8e\x12\x45\xc9\xa1\x6d” (8 random bytes) |
3 |
Timestamp |
1737849600000 (2026-01-26 00:00:00 UTC) |
4 |
Sender Identity |
b”\x9c\x7e…\x4a\x2f” (16-byte RNS identity hash) |
5 |
Room Name |
“#lobby” |
6 |
Body |
“Hello, world!” |
7 |
Nickname |
“alice” |
Encoded size: ~43 (fixed) + 8 (room) + 15 (body) + 7 (nickname) = ~73 bytes
Example: Client Joining a Room (JOIN)¶
A client requesting to join the #general room:
Field Number |
Field Name |
Value |
|---|---|---|
0 |
Protocol Version |
1 |
1 |
Message Type |
10 (JOIN) |
2 |
Message ID |
b”\xb4\x5c\x2a\x89\x13\xd7\x6f\x1e” (8 random bytes) |
3 |
Timestamp |
1737849610000 |
4 |
Sender Identity |
b”\x9c\x7e…\x4a\x2f” (16-byte RNS identity hash) |
5 |
Room Name |
“#general” |
6 |
Body |
(omitted) |
7 |
Nickname |
“alice” |
Encoded size: ~43 (fixed) + 10 (room) + 7 (nickname) = ~60 bytes
Example: Hub Sending Welcome (WELCOME)¶
A hub welcoming a client and providing its limits:
Field Number |
Field Name |
Value |
|---|---|---|
0 |
Protocol Version |
1 |
1 |
Message Type |
2 (WELCOME) |
2 |
Message ID |
b”\x31\xa8\xf4\x5d\x92\xc6\x7b\x3e” (8 random bytes) |
3 |
Timestamp |
1737849590000 |
4 |
Sender Identity |
b”\x1f\x8a…\xb3\xc5” (16-byte hub identity hash) |
5 |
Room Name |
(omitted) |
6 |
Body |
{0: “ExampleHub”, 1: “0.1.0”, 3: {0: 32, 1: 64, …}} |
7 |
Nickname |
(omitted) |
Body breakdown:
Key 0: Hub Name = “ExampleHub”
Key 1: Hub Version = “0.1.0”
Key 3: Hub Limits = map with keys 0-4 (max_rooms=32, max_room_name=64, etc.)
Encoded size: ~43 (fixed) + ~40 (body with limits) = ~83 bytes
Example: Error Response (ERROR)¶
A hub rejecting a request due to rate limiting:
Field Number |
Field Name |
Value |
|---|---|---|
0 |
Protocol Version |
1 |
1 |
Message Type |
40 (ERROR) |
2 |
Message ID |
b”\xe7\x9a\x4c\x18\xd3\x2f\x61\xb5” (8 random bytes) |
3 |
Timestamp |
1737849625000 |
4 |
Sender Identity |
b”\x1f\x8a…\xb3\xc5” (16-byte hub identity hash) |
5 |
Room Name |
(omitted) |
6 |
Body |
“Rate limit exceeded. Try again later.” |
7 |
Nickname |
(omitted) |
Encoded size: ~43 (fixed) + ~42 (error message) = ~85 bytes