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 messages

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.