Merge pull request #3 from logos-messaging/proto

Protobuf bindings for rust
This commit is contained in:
kaichao 2026-01-09 10:15:14 +08:00 committed by GitHub
commit 7995313ca1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 600 additions and 1 deletions

33
.github/workflows/rust.yaml vendored Normal file
View File

@ -0,0 +1,33 @@
name: Rust
on:
pull_request:
branches:
- main
paths-ignore:
- "**README.md"
- ".gitignore"
- "LICENSE"
jobs:
test:
name: Cargo Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Check Rust bindings
working-directory: gen/rust
run: cargo check --all-targets
- name: Test Rust bindings
working-directory: gen/rust
run: cargo test

View File

@ -1 +1,59 @@
# chat_proto
# Chat Protobuf Definitionss
This repository contains the canonical **protobuf definitions** for the logoschat (Logos Chat Protocol) used by LibChat and related components.
It is **schema-only**:
- No application logic
- Stable, versioned wire formats
- Intended to be consumed by multiple languages
Generated bindings (e.g. Rust) are produced from these schemas.
---
## Prerequisites
This project uses [Buf](https://buf.build) for protobuf linting and code generation.
### Install Buf
On macOS and Linux:
```sh
brew install bufbuild/buf/buf
```
For other platforms, See https://buf.build/docs/cli/installation/.
## Repository Structure
```
protos/ # Protobuf source files
buf.yaml # Buf module configuration
buf.gen.yaml # Code generation configuration
gen/
└── rust/ # Generated Rust bindings (prost)
```
## Generate Rust bindings
```
buf generate
```
This will generate Rust code under: `gen/rust/`, The generated crate can be used directly as a dependency in Rust projects.
## Usage (Rust)
Add in Cargo.toml:
```
chat-proto = { git = "https://github.com/logos-messaging/chat_proto" }
```
Example import:
```
use chat_proto::logoschat::{
inbox::InboxV1Frame,
invite::InvitePrivateV1,
encryption::EncryptedPayload,
};
```

6
buf.gen.yaml Normal file
View File

@ -0,0 +1,6 @@
version: v2
plugins:
- remote: buf.build/community/neoeinstein-prost:v0.5.0
out: gen/rust/src
opt:
- bytes=.

10
buf.yaml Normal file
View File

@ -0,0 +1,10 @@
version: v2
modules:
- path: protos
lint:
use:
- DEFAULT
breaking:
use:
- FILE

1
gen/rust/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target/

96
gen/rust/Cargo.lock generated Normal file
View File

@ -0,0 +1,96 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "bytes"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "chat-proto"
version = "0.1.0"
dependencies = [
"bytes",
"prost",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "proc-macro2"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
dependencies = [
"unicode-ident",
]
[[package]]
name = "prost"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-derive"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "quote"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"

10
gen/rust/Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "chat-proto"
version = "0.1.0"
edition = "2021"
[dependencies]
prost = "0.14"
[dev-dependencies]
bytes = "1.3"

109
gen/rust/src/lib.rs Normal file
View File

@ -0,0 +1,109 @@
// #![allow(clippy::all)]
// #![allow(warnings)]
pub mod logoschat {
pub mod convos {
pub mod private_v1 {
include!("logoschat/convos/private_v1/logoschat.convos.private_v1.rs");
}
}
pub mod encryption {
include!("logoschat/encryption/logoschat.encryption.rs");
}
pub mod envelope {
include!("logoschat/envelope/logoschat.envelope.rs");
}
pub mod inbox {
include!("logoschat/inbox/logoschat.inbox.rs");
}
pub mod invite {
include!("logoschat/invite/logoschat.invite.rs");
}
pub mod reliability {
include!("logoschat/reliability/logoschat.reliability.rs");
}
}
#[cfg(test)]
mod tests {
use super::logoschat::{
encryption::{encrypted_payload::Encryption, EncryptedPayload, Plaintext},
inbox::{inbox_v1_frame::FrameType, InboxV1Frame, Note},
invite::InvitePrivateV1,
};
use bytes::Bytes;
use prost::Message;
#[test]
fn test_encrypted_payload_roundtrip() {
let payload = EncryptedPayload {
encryption: Some(Encryption::Plaintext(Plaintext {
payload: Bytes::from_static(b"hello world"),
})),
};
// Encode to bytes
let mut buf = Vec::new();
payload.encode(&mut buf).expect("Encoding failed");
// Decode back
let decoded = EncryptedPayload::decode(&buf[..]).expect("Decoding failed");
match decoded.encryption {
Some(Encryption::Plaintext(p)) => {
assert_eq!(p.payload, Bytes::from_static(b"hello world"));
}
_ => panic!("Expected plaintext variant"),
}
}
#[test]
fn test_inbox_frame_roundtrip() {
let note = Note {
text: "This is a test note".to_string(),
};
let frame = InboxV1Frame {
recipient: "alice".to_string(),
frame_type: Some(FrameType::Note(note.clone())),
};
let mut buf = Vec::new();
frame.encode(&mut buf).expect("Encoding failed");
let decoded = InboxV1Frame::decode(&buf[..]).expect("Decoding failed");
match decoded.frame_type {
Some(FrameType::Note(n)) => {
assert_eq!(n.text, note.text);
}
_ => panic!("Expected Note variant"),
}
}
#[test]
fn test_invite_private_roundtrip() {
let invite = InvitePrivateV1 {
initiator: Bytes::from_static(b"initiator"),
initiator_ephemeral: Bytes::from_static(b"ephemeral"),
participant: Bytes::from_static(b"participant"),
participant_ephemeral_id: 42,
discriminator: "test_discriminator".to_string(),
initial_message: None, // skipping encrypted payload for simplicity
};
let mut buf = Vec::new();
invite.encode(&mut buf).expect("Encoding failed");
let decoded = InvitePrivateV1::decode(&buf[..]).expect("Decoding failed");
assert_eq!(decoded.initiator, Bytes::from_static(b"initiator"));
assert_eq!(decoded.participant_ephemeral_id, 42);
assert_eq!(decoded.discriminator, "test_discriminator");
}
}

View File

@ -0,0 +1,31 @@
// @generated
// This file is @generated by prost-build.
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct Placeholder {
#[prost(uint32, tag="1")]
pub counter: u32,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct PrivateV1Frame {
#[prost(string, tag="1")]
pub conversation_id: ::prost::alloc::string::String,
#[prost(bytes="bytes", tag="2")]
pub sender: ::prost::bytes::Bytes,
/// Sender reported timestamp
#[prost(int64, tag="3")]
pub timestamp: i64,
#[prost(oneof="private_v1_frame::FrameType", tags="10, 11")]
pub frame_type: ::core::option::Option<private_v1_frame::FrameType>,
}
/// Nested message and enum types in `PrivateV1Frame`.
pub mod private_v1_frame {
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)]
pub enum FrameType {
#[prost(bytes, tag="10")]
Content(::prost::bytes::Bytes),
/// ....
#[prost(message, tag="11")]
Placeholder(super::Placeholder),
}
}
// @@protoc_insertion_point(module)

View File

@ -0,0 +1,39 @@
// @generated
// This file is @generated by prost-build.
/// TODO: This also encompasses plaintexts, is there a better name?
/// Alternatives: ???
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct EncryptedPayload {
#[prost(oneof="encrypted_payload::Encryption", tags="1, 2")]
pub encryption: ::core::option::Option<encrypted_payload::Encryption>,
}
/// Nested message and enum types in `EncryptedPayload`.
pub mod encrypted_payload {
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)]
pub enum Encryption {
#[prost(message, tag="1")]
Plaintext(super::Plaintext),
#[prost(message, tag="2")]
Doubleratchet(super::Doubleratchet),
}
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct Plaintext {
#[prost(bytes="bytes", tag="1")]
pub payload: ::prost::bytes::Bytes,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct Doubleratchet {
/// 32 byte array
#[prost(bytes="bytes", tag="1")]
pub dh: ::prost::bytes::Bytes,
#[prost(uint32, tag="2")]
pub msg_num: u32,
#[prost(uint32, tag="3")]
pub prev_chain_len: u32,
#[prost(bytes="bytes", tag="4")]
pub ciphertext: ::prost::bytes::Bytes,
#[prost(string, tag="5")]
pub aux: ::prost::alloc::string::String,
}
// @@protoc_insertion_point(module)

View File

@ -0,0 +1,16 @@
// @generated
// This file is @generated by prost-build.
// /////////////////////////////////////////////////////////////////////////////
// Payload Framing Messages
// /////////////////////////////////////////////////////////////////////////////
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct EnvelopeV1 {
#[prost(string, tag="1")]
pub conversation_hint: ::prost::alloc::string::String,
#[prost(uint64, tag="2")]
pub salt: u64,
#[prost(bytes="bytes", tag="5")]
pub payload: ::prost::bytes::Bytes,
}
// @@protoc_insertion_point(module)

View File

@ -0,0 +1,25 @@
// @generated
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct Note {
#[prost(string, tag="1")]
pub text: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct InboxV1Frame {
#[prost(string, tag="1")]
pub recipient: ::prost::alloc::string::String,
#[prost(oneof="inbox_v1_frame::FrameType", tags="10, 11")]
pub frame_type: ::core::option::Option<inbox_v1_frame::FrameType>,
}
/// Nested message and enum types in `InboxV1Frame`.
pub mod inbox_v1_frame {
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)]
pub enum FrameType {
#[prost(message, tag="10")]
InvitePrivateV1(super::super::invite::InvitePrivateV1),
#[prost(message, tag="11")]
Note(super::Note),
}
}
// @@protoc_insertion_point(module)

View File

@ -0,0 +1,18 @@
// @generated
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct InvitePrivateV1 {
#[prost(bytes="bytes", tag="1")]
pub initiator: ::prost::bytes::Bytes,
#[prost(bytes="bytes", tag="2")]
pub initiator_ephemeral: ::prost::bytes::Bytes,
#[prost(bytes="bytes", tag="3")]
pub participant: ::prost::bytes::Bytes,
#[prost(int32, tag="4")]
pub participant_ephemeral_id: i32,
#[prost(string, tag="5")]
pub discriminator: ::prost::alloc::string::String,
#[prost(message, optional, tag="6")]
pub initial_message: ::core::option::Option<super::encryption::EncryptedPayload>,
}
// @@protoc_insertion_point(module)

View File

@ -0,0 +1,32 @@
// @generated
// This file is @generated by prost-build.
// /////////////////////////////////////////////////////////////////////////////
// SDS Payloads
// /////////////////////////////////////////////////////////////////////////////
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct HistoryEntry {
/// Unique identifier of the SDS message, as defined in `Message`
#[prost(string, tag="1")]
pub message_id: ::prost::alloc::string::String,
/// Optional information to help remote parties retrieve this SDS
#[prost(bytes="bytes", tag="2")]
pub retrieval_hint: ::prost::bytes::Bytes,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ReliablePayload {
#[prost(string, tag="2")]
pub message_id: ::prost::alloc::string::String,
#[prost(string, tag="3")]
pub channel_id: ::prost::alloc::string::String,
#[prost(int32, tag="10")]
pub lamport_timestamp: i32,
#[prost(message, repeated, tag="11")]
pub causal_history: ::prost::alloc::vec::Vec<HistoryEntry>,
#[prost(bytes="bytes", tag="12")]
pub bloom_filter: ::prost::bytes::Bytes,
/// Optional field causes errors in nim protobuf generation. Removing for now as optional is implied anways.
#[prost(bytes="bytes", tag="20")]
pub content: ::prost::bytes::Bytes,
}
// @@protoc_insertion_point(module)

26
protos/encryption.proto Normal file
View File

@ -0,0 +1,26 @@
syntax = "proto3";
package logoschat.encryption;
// TODO: This also encompasses plaintexts, is there a better name?
// Alternatives: ???
message EncryptedPayload {
oneof encryption {
encryption.Plaintext plaintext = 1;
encryption.Doubleratchet doubleratchet = 2;
}
}
message Plaintext {
bytes payload=1;
}
message Doubleratchet {
bytes dh = 1; // 32 byte array
uint32 msgNum = 2;
uint32 prevChainLen = 3;
bytes ciphertext = 4;
string aux = 5;
}

16
protos/envelope.proto Normal file
View File

@ -0,0 +1,16 @@
syntax = "proto3";
package logoschat.envelope;
///////////////////////////////////////////////////////////////////////////////
// Payload Framing Messages
///////////////////////////////////////////////////////////////////////////////
message EnvelopeV1 {
string conversation_hint = 1;
uint64 salt = 2;
bytes payload = 5;
}

17
protos/inbox.proto Normal file
View File

@ -0,0 +1,17 @@
syntax = "proto3";
package logoschat.inbox;
import "invite.proto";
message Note{
string text = 1;
}
message InboxV1Frame {
string recipient = 1;
oneof frame_type {
invite.InvitePrivateV1 invite_private_v1 = 10;
Note note = 11;
}
}

14
protos/invite.proto Normal file
View File

@ -0,0 +1,14 @@
syntax = "proto3";
package logoschat.invite;
import "encryption.proto";
message InvitePrivateV1 {
bytes initiator = 1;
bytes initiator_ephemeral = 2;
bytes participant = 3;
int32 participant_ephemeral_id= 4;
string discriminator = 5;
encryption.EncryptedPayload initial_message = 6;
}

19
protos/private_v1.proto Normal file
View File

@ -0,0 +1,19 @@
syntax = "proto3";
package logoschat.convos.private_v1;
message Placeholder {
uint32 counter = 1;
}
message PrivateV1Frame {
string conversation_id = 1;
bytes sender = 2;
int64 timestamp = 3; // Sender reported timestamp
oneof frame_type {
bytes content = 10;
Placeholder placeholder = 11;
// ....
}
}

23
protos/reliability.proto Normal file
View File

@ -0,0 +1,23 @@
syntax = "proto3";
package logoschat.reliability;
///////////////////////////////////////////////////////////////////////////////
// SDS Payloads
///////////////////////////////////////////////////////////////////////////////
message HistoryEntry {
string message_id = 1; // Unique identifier of the SDS message, as defined in `Message`
bytes retrieval_hint = 2; // Optional information to help remote parties retrieve this SDS
// message; For example, A Waku deterministic message hash or routing payload hash
}
message ReliablePayload {
string message_id = 2;
string channel_id = 3;
int32 lamport_timestamp = 10;
repeated HistoryEntry causal_history = 11;
bytes bloom_filter = 12;
// Optional field causes errors in nim protobuf generation. Removing for now as optional is implied anways.
bytes content = 20;
}