Messaging Data Structures
The core application supported by River Protocol is messaging. River Stream Nodes are tasked with serving stream events, which are represented as data using protobufs. Stream events themselves may contain message-like events or metadata to support event validation.
Protocol Messaging Protobuf
The data schema of the River protocol can be defined declaratively by protobuf.proto. Below is an elided snapshot of protocol.proto
as of January 2024 containing only schema definitions used in a messaging context within the River protocol.
syntax = "proto3";
package river;
option go_package = "github.com/river-build/river/protocol";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
/**
* StreamEvent is a single event in the stream.
*/
message StreamEvent {
/**
* Address of the creator of the event.
* For user - address of the user's primary wallet.
* For server - address of the server's keypair in staking smart contract.
*
* For the event to be valid:
* If delegate_sig is present, creator_address must match delegate_sig.
* If delegate_sig is not present, creator_address must match event signature in the Envelope.
*/
bytes creator_address = 1;
/**
* delegate_sig allows event to be signed by device keypair
* which is linked to the user's primary wallet.
*
* delegate_sig constains signature of the public key of the device keypair.
* User's primary wallet is used to produce this signature.
*
* If present, for the event to be valid:
* 1. creator_address must match delegate_sig's signer public key
* 2. delegate_sig should be the signature of Envelope.signature's public key.
*
* Server nodes sign node-produced events with their own keypair and do not
* need to use delegate_sig.
*/
bytes delegate_sig = 2;
/** Salt ensures that similar messages are not hashed to the same value. genId() from id.ts may be used. */
bytes salt = 3;
/** Hash of a preceding miniblock. Null for the inception event. Must be a recent miniblock */
optional bytes prev_miniblock_hash = 4;
/** CreatedAt is the time when the event was created.
NOTE: this value is set by clients and is not reliable for anything other than displaying
the value to the user. Never use this value to sort events from different users. */
int64 created_at_epoch_ms = 5;
/** Variable-type payload.
* Payloads should obey the following rules:
* - payloads should have their own unique type
* - each payload should have a oneof content field
* - each payload should have an inception field inside the content oneof
* - each payload should have a unique Inception type
* - payloads can't violate previous type recursively to inception payload
*/
oneof payload {
MiniblockHeader miniblock_header = 100;
CommonPayload common_payload = 101;
SpacePayload space_payload = 102;
ChannelPayload channel_payload = 103;
UserPayload user_payload = 104;
UserSettingsPayload user_settings_payload = 105;
UserDeviceKeyPayload user_device_key_payload = 106;
UserToDevicePayload user_to_device_payload = 107;
MediaPayload media_payload = 108;
DmChannelPayload dm_channel_payload = 109;
GdmChannelPayload gdm_channel_payload = 110;
}
}
/**
* SpacePayload
*/
message SpacePayload {
message Snapshot {
// inception
Inception inception = 1;
// streamId: Channel
map<string, Channel> channels = 2;
// userId: Membership
map<string, Membership> memberships = 3;
// userId: Username
map<string, WrappedEncryptedData> usernames = 4;
// userId: Displayname
map<string, WrappedEncryptedData> display_names = 5;
}
message Inception {
string stream_id = 1;
StreamSettings settings = 2;
}
message Channel {
ChannelOp op = 1;
string channel_id = 2;
EventRef origin_event = 3;
EncryptedData channel_properties = 4;
}
oneof content {
Inception inception = 1;
Channel channel = 2;
Membership membership = 3;
EncryptedData username = 4;
EncryptedData display_name = 5;
}
}
/**
* ChannelPayload
*/
message ChannelPayload {
message Snapshot {
// inception
Inception inception = 1;
// userId: Membership
map<string, Membership> memberships = 2;
}
message Inception {
string stream_id = 1;
string space_id = 3;
/**
* channel_name and channel_topic from this payload will be used to
* create associated with that channel space event for stream as we agreed
* that channel names and topics will be delivered using space stream
*/
EncryptedData channel_properties = 4;
StreamSettings settings = 5;
}
oneof content {
Inception inception = 1;
EncryptedData message = 2;
Membership membership = 3;
}
}
/**
* DmChannelPayload
*/
message DmChannelPayload {
message Snapshot {
Inception inception = 1;
map<string, Membership> memberships = 2;
map<string, WrappedEncryptedData> usernames = 3;
map<string, WrappedEncryptedData> display_names = 4;
}
message Inception {
string stream_id = 1;
string first_party_id = 2;
string second_party_id = 3;
StreamSettings settings = 4;
}
oneof content {
Inception inception = 1;
Membership membership = 2;
EncryptedData message = 3;
EncryptedData username = 4;
EncryptedData display_name = 5;
}
}
/**
* GdmChannelPayload
*/
message GdmChannelPayload {
message Snapshot {
Inception inception = 1;
map<string, Membership> memberships = 2;
map<string, WrappedEncryptedData> usernames = 3;
map<string, WrappedEncryptedData> display_names = 4;
WrappedEncryptedData channel_properties = 5;
}
message Inception {
string stream_id = 1;
EncryptedData channel_properties = 2;
StreamSettings settings = 3;
}
oneof content {
Inception inception = 1;
Membership membership = 2;
EncryptedData message = 3;
EncryptedData username = 4;
EncryptedData display_name = 5;
EncryptedData channel_properties = 6;
}
}
/**
* MediaPayload
*/
message MediaPayload {
message Snapshot {
Inception inception = 1;
}
message Inception {
string stream_id = 1;
string channel_id = 2;
int32 chunk_count = 3;
StreamSettings settings = 4;
}
message Chunk {
bytes data = 1;
int32 chunk_index = 2;
}
oneof content {
Inception inception = 1;
Chunk chunk = 2;
}
}
message Membership {
MembershipOp op = 1;
string user_id = 2;
}
message EncryptedData {
/**
* Ciphertext of the encryption envelope.
*/
string ciphertext = 1;
/**
* Encryption algorithm used to encrypt this event.
*/
string algorithm = 2;
/**
* Sender device public key identifying the sender's device.
*/
string sender_key = 3;
/**
* The ID of the session used to encrypt the message.
*/
string session_id = 4;
/**
* Optional checksum of the cleartext data.
*/
optional string checksum = 5;
}
message WrappedEncryptedData {
EncryptedData data = 1;
int64 event_num = 2;
bytes event_hash = 3;
}
enum MembershipOp {
SO_UNSPECIFIED = 0;
SO_INVITE = 1;
SO_JOIN = 2;
SO_LEAVE = 3;
}
enum ChannelOp {
CO_UNSPECIFIED = 0;
CO_CREATED = 1;
CO_DELETED = 2;
CO_UPDATED = 4;
}
SpacePayload
StreamEvent
messages encapsulate all message types handled by River Nodes. The primary unit of account for messaging is described in SpacePayload
. A newly minted Space is represented on-chain with the SpaceFactory contract as well of in River Nodes and the River Chain, which stores the streamId
of the Space.
There exists a 1-to-many relation between Spaces as defined in data by the SpacePayload
message and Channels
. Furthermore, Memberships
, usernames
, and display_names
are stored and snapshotted within the streamId
of a Space.
SpacePayload
is made polymorphic using oneof
in the above definition. River Nodes apply rules to each message after unpacking the payload
field from the StreamEvent
, which is the message transmitted to the River Node over the wire. These rules validate the legality of the event
for writes that addEvent
to the stream.
Channel messages
Channel messages are the primary messages used to facilitate group chat within Spaces. They are defined in ChannelPayload
in protocol.proto
. Each ChannelPayload
message is typed as EncryptedData
, which is a message that defines the payload of ciphertext.
Remember, River nodes never see plain text of messages. The protocol defines any message-like event or event that requires encryption with an EncryptedData
or WrappedEncryptedData
message type in protocol.proto
.
DM Channel messages
Direct messages are the second messaging primitive supported by the River protocol. The data structure representing direct messaging is defined by the DmChannelPayload
protobuf. Each DM is created with the pair of users privy to the DM conversation. Note that just like with channel messages, DM’s are EncryptedData
messages from the node’s vantage point.
GDM Channel messages
Group direct message messages are supported by the River protocol as their own streams as well. Each Group DM is created with the list of members that forms the initial Membership roster. Though outside the specification of the protocol, the membership roster is meant to convey entitlement to encryption keys, such that clients can implement logic to share keys only with members of the same group dm.
DM’s, and GDM’s are all created as new streams in the RiverNode using the createStream
rpc client method. Unlike Spaces and Channels though, there exists no on-chain record of these streams.
Media files are supported as separate streams in River protocol associated with a parent channel_id
. Messages added to a River node containing media must pass validation criteria that proves the creator
is a member of that channel_id
to prevent man-in-the-middle type attacks.
Media files can be quite large in size and as such are chunked in byte arrays on nodes and therefore support pagination by clients.