基于事件流的去中心化社交应用:Linera Protocol Social 示例深度解析
【免费下载链接】linera-protocolMain repository for the Linera protocol项目地址: https://gitcode.com/GitHub_Trending/li/linera-protocol
导读
examples/social是 Linera Protocol 仓库中一个完整的去中心化社交应用示例,其核心价值在于演示如何在 Linera 的多链(microchain)架构上使用**事件流(Event Streams)**机制,实现「关注-发帖-订阅-点赞-评论」的跨链社交闭环。读完本文,你将掌握:Social 示例的数据结构与 ABI 设计、事件流在Subscribe/Post/Like/Comment中的底层调用链、从零启动本地网络并运行两个用户链的完整实操流程,以及对应的 GraphQL 查询与前端接入方式。
一、示例概览:每个微链就是一个用户
Social 示例(examples/social/README.md)的设计模型非常简洁:每条微链(microchain)代表一个用户,即该链的所有者(owner)。用户可以订阅其他用户,也可以发布文本帖子,帖子会被推送给自己的所有订阅者。
从源码结构看,示例由四个 Rust 文件组成:
- contract.rs:合约端,处理操作(Operation)、跨链消息(Message)与事件流更新;
- service.rs:服务端,将应用状态与操作暴露为 GraphQL;
- state.rs:应用状态定义(
RootView); - lib.rs:ABI 定义,包括
Operation、Message、Event、Post、Key等核心类型。
对应地,Cargo.toml 声明了两个二进制目标:social_contract(合约)与social_service(服务),依赖linera-sdk、async-graphql、serde、bcs等,与仓库其他示例保持一致。
二、应用状态与数据结构
2.1 状态:SocialState
每条链上应用的状态(state.rs)包含两部分:
#[derive(RootView, async_graphql::SimpleObject)] #[view(context = ViewStorageContext)] pub struct SocialState { /// 本链自己发布的帖子 pub own_posts: LogView<OwnPost>, /// 从所订阅作者处收到的帖子 pub received_posts: CustomMapView<Key, Post>, }own_posts用LogView按发布顺序追加保存;received_posts用CustomMapView<Key, Post>以Key为索引保存收到的帖子。
2.2 帖子索引:Key
收到的帖子按Key索引(lib.rs),由三元组构成:
| 字段 | 类型 | 含义 |
|---|---|---|
timestamp | Timestamp | 帖子被包含进作者链上区块时的时间戳 |
author | ChainId | 发布该帖子的链 |
index | u32 | 该作者此前发布的帖子数量(即本条帖子在作者帖子日志中的序号) |
值得关注的是,Key实现了CustomSerialize(lib.rs),对时间戳和索引做按位取反后再序列化,从而保证序列化后的字典序对应「时间倒序 → 作者 → 索引倒序」,使订阅者可以按时间倒序高效地浏览最新帖子。
2.3 帖子、评论与事件类型
OwnPost(timestamp+text+ 可选image_url):帖子正文,在作者与索引已知的上下文中使用;Post(key+text+image_url+likes: u32+comments: Vec<Comment>):订阅者视角下带互动数据的完整帖子;Comment(text+ 评论者chain_id);Event(lib.rs):应用向事件流发射的事件,包括Post { post, index }、Like { key }、Comment { key, comment },以及检查点(checkpoint)时携带最近帖子的Summary { recent_posts }。
三、核心操作与跨链消息
3.1 操作(Operation)
用户通过区块内的操作驱动应用(lib.rs):
pub enum Operation { Subscribe { chain_id: ChainId }, Unsubscribe { chain_id: ChainId }, Post { text: String, image_url: Option<String> }, Like { key: Key }, Comment { key: Key, comment: String }, }在 contract.rs 的execute_operation中:
Subscribe/Unsubscribe:调用runtime.subscribe_to_events(chain_id, app_id, STREAM_NAME)/unsubscribe_from_events(...),其中流名常量STREAM_NAME: &[u8] = b"posts"(contract.rs),即订阅/退订目标链上名为posts的事件流;Post:调用execute_post_operation,取系统时间作为timestamp,写入own_posts,并向流发射Event::Post { post, index }(contract.rs);Like/Comment:若目标帖子作者是其他链,则通过runtime.send_message(chain_id, ...)向作者链发送Message::Like/Message::Comment;若点赞/评论的是自己的帖子,则直接发射对应事件(contract.rs)。
3.2 跨链消息(Message)
Message(lib.rs)只有Like与Comment两种变体,且始终发给帖子的作者链(ChainId),由作者链处理后转发为事件:
async fn execute_message(&mut self, message: Message) { match message { Message::Like { key } => self.runtime.emit(STREAM_NAME.into(), &Event::Like { key }), Message::Comment { key, comment } => self .runtime .emit(STREAM_NAME.into(), &Event::Comment { key, comment }), }; }这与 README 中的描述一致:Subscribe、Unsubscribe、Posts是订阅/退订/发帖时产生的跨链消息变体,前两者直接发往要订阅/退订的链,发帖则写入事件流。
3.3 事件流消费:process_streams与summarize_events
订阅者链收到流更新后,process_streams(contract.rs)会校验stream_id(流名与application_id),随后对新增的每个索引调用runtime.read_event读取事件:
Event::Post→execute_post_event,将帖子按Key写入received_posts;Event::Like→execute_like_event,将对应帖子likes += 1;Event::Comment→execute_comment_event,向帖子追加一条带评论者chain_id的评论;Event::Summary→ 将recent_posts中尚未拥有的帖子逐个补齐(配合if get(&key).is_none()守卫,避免覆盖订阅者已记录的点赞与评论)。
summarize_events(contract.rs)在检查点时生成摘要:取自己最近SUMMARY_POST_COUNT = 10条帖子,打包成Event::Summary发射到流上。这样在检查点之后才加入的订阅者,也能通过摘要补齐此前可能已被丢弃的旧事件。
四、端到端运行指南
以下步骤完整继承自 examples/social/README.md,并已在当前仓库源码中逐一验证其对应实现。
4.1 环境准备
在 Linera 仓库根目录下设置路径与辅助函数:
export PATH="$PWD/target/debug:$PATH" source /dev/stdin <<<"$(linera net helper 2>/dev/null)"4.2 启动本地网络与水龙头
FAUCET_PORT=8079 FAUCET_URL=http://localhost:$FAUCET_PORT linera_spawn linera net up --with-faucet --faucet-port $FAUCET_PORT # 如果使用测试网,则改为: # LINERA_TMP_DIR=$(mktemp -d) # FAUCET_URL=https://faucet.testnet-XXX.linera.net # XXX 为具体编号开启用户应用日志:
export LINERA_APPLICATION_LOGS=true4.3 创建两个用户钱包与链
export LINERA_WALLET_1="$LINERA_TMP_DIR/wallet_1.json" export LINERA_KEYSTORE_1="$LINERA_TMP_DIR/keystore_1.json" export LINERA_STORAGE_1="rocksdb:$LINERA_TMP_DIR/client_1.db" export LINERA_WALLET_2="$LINERA_TMP_DIR/wallet_2.json" export LINERA_KEYSTORE_2="$LINERA_TMP_DIR/keystore_2.json" export LINERA_STORAGE_2="rocksdb:$LINERA_TMP_DIR/client_2.db" linera --with-wallet 1 wallet init --faucet $FAUCET_URL linera --with-wallet 2 wallet init --faucet $FAUCET_URL INFO_1=($(linera --with-wallet 1 wallet request-chain --faucet $FAUCET_URL)) INFO_2=($(linera --with-wallet 2 wallet request-chain --faucet $FAUCET_URL)) CHAIN_1="${INFO_1[0]}" CHAIN_2="${INFO_2[0]}" OWNER_1="${INFO_1[3]}" OWNER_2="${INFO_2[3]}"说明:linera --with-wallet 1等价于显式指定--wallet "$LINERA_WALLET_1" --keystore "$LINERA_KEYSTORE_1" --storage "$LINERA_STORAGE_1",可显著减少重复输入。这里CHAIN_1/CHAIN_2是两个用户的链 ID,后续 GraphQL 与前端 URL 都会用到。
4.4 编译并发布应用
APP_ID=$(linera --with-wallet 1 project publish-and-create examples/social)该命令会编译examples/social下的合约与服务,发布字节码并在链 1 上创建应用实例,返回的APP_ID用于后续所有 GraphQL 请求。
4.5 启动两个节点服务
分别在不同端口启动两个钱包对应的节点服务:
linera --with-wallet 1 service --port 8080 & # 等待其完成启动 sleep 2 linera --with-wallet 2 service --port 8081 & # 等待其完成启动 sleep 2说明:README 注释指出,等待(
sleep)是为了规避部分竞态条件(对应仓库历史上的 issue 1176/1177),在实际脚本化操作时建议保留足够等待时间或轮询端口就绪。
4.6 用 GraphQL 完成订阅与发帖
在两个浏览器标签页分别打开两个 URL(对应link入口)。在 8081 标签页执行:
echo "http://localhost:8081/chains/$CHAIN_2/applications/$APP_ID"然后使用以下 mutation,让用户 2 订阅用户 1 的链:
mutation { subscribe( chainId: "$CHAIN_1" ) }在 8080 标签页执行:
echo "http://localhost:8080/chains/$CHAIN_1/applications/$APP_ID"然后发布一条帖子(imageUrl为可选参数):
mutation { post( text: "Linera Social is the new Mastodon!" imageUrl: "https://example.com/logo.svg" # 可选 ) }4.7 验证订阅者收到帖子
回到 8081 标签页(此时用户 2 是订阅者),查询收到的帖子:
query { receivedPosts { keys { timestamp author index } } }应能列出 1 条记录,包含timestamp、author、index。随后用该三元组查看帖子详情:
query { receivedPosts { entry(key: { timestamp: 1705504131018960, author: "$CHAIN_1", index: 0 }) { value { key { timestamp author index } text imageUrl comments { text chainId } likes } } } }返回结果大致如下(时间戳以实际值为准):
{ "data": { "receivedPosts": { "entry": { "value": { "key": { "timestamp": 1705504131018960, "author": "$CHAIN_1", "index": 0 }, "text": "Linera Social is the new Mastodon!", "imageUrl": "https://example.com/logo.svg", "comments": [], "likes": 0 } } } } }五、GraphQL 服务层与 React 前端
5.1 服务端 schema 组装
service.rs 在收到查询请求时,将SocialState(根视图,提供ownPosts、receivedPosts查询)与Operation::mutation_root(...)(自动生成的 mutation 根)组装成 async-graphql schema 并执行:
let schema = Schema::build( self.state.clone(), Operation::mutation_root(self.runtime.clone()), EmptySubscription, ) .finish(); schema.execute(request).await这正是 README 中subscribe、post等 mutation 与receivedPosts查询能直接可用的原因——Operation上的GraphQLMutationRoot派生宏(见 lib.rs)自动生成了 GraphQL 变更入口。
5.2 React 前端
web-frontend 是一个基于 React + Apollo Client + Tailwind 的界面,其 README 说明:应用需先在网络上部署,且节点服务运行在 8000 端口(即把上文--port 8080/8081换成 8000),并需先完成根 README 中的操作。
访问格式为:
http://localhost:3000/$CHAIN_1/$APP_ID/$OWNER_1/$PORT例如 README 给出的实际形态:
http://localhost:3000/aee928d4bf3880353b4a3cd9b6f88e6cc6e5ed050860abae439e7782e9b2dfe8?app=aee928d4bf3880353b4a3cd9b6f88e6cc6e5ed050860abae439e7782e9b2dfe8000000000000000000000000aee928d4bf3880353b4a3cd9b6f88e6cc6e5ed050860abae439e7782e9b2dfe8020000000000000000000000&owner=019d0849200869c4c88493db9966afad44194714cf76ed5d0639a7cdc37cb286&port=8080前端核心逻辑在 App.tsx:加载时通过RECEIVED_POSTS查询拉取receivedPosts.entries渲染帖子列表,同时通过节点服务的notifications(chainId: ...)订阅监听链上新事件,一旦有新通知就重新查询(fetchPolicy: 'network-only')以实时刷新。发帖组件 NewPost.tsx 通过post(text: $message, imageUrl: $image)mutation 发布文字与可选图片 URL。当前版本的操作范围是「发布帖子、点赞与评论(仅在创建链上)」。
前端可用脚本(在examples/social/web-frontend目录下执行):npm run tailwind(Tailwind 构建)、npm run compile(由codegen.ts生成 GraphQL 类型)、npm start(开发模式,默认 3000 端口)、npm run build(生产构建)、npx prettier --write .(格式化)。
六、测试验证
仓库为 Social 示例提供了集成测试 tests/cross_chain.rs,覆盖了本文描述的完整跨链流程:
- 用
TestValidator::with_current_module::<social::SocialAbi, (), ()>()启动测试验证器,创建链 1、链 2; - 在链 1 上
create_application创建应用; - 链 1 提交
Operation::Subscribe { chain_id: chain2.id() }订阅链 2; - 链 2 提交
Operation::Post { text: "Linera is the new Mastodon".to_string(), image_url: None }发帖; chain1.handle_new_events()让链 1 处理新事件;- 查询链 2 的
ownPosts断言帖子文本,查询链 1 的receivedPosts.keys断言作者为链 2 的 ID。
此外,tests/format.rs 与快照 format__format.snap 用于锁定 BCS 序列化格式(源自 lib.rs 中SocialApplication::formats()对Operation、Message、Event、Key、OwnPost、Post、Comment的格式追踪),保证 ABI 的向后兼容性;lib.rs 中的单测 则验证了Key自定义序列化的往返一致性。
七、小结与扩展方向
通过 Social 示例可以提炼出在 Linera 上构建去中心化应用的三条核心经验:
- 事件流是「一对多」内容分发的高效原语:作者链只需
emit一次,订阅者通过process_streams消费同一流,无需作者向每个订阅者单独发消息; - 跨链互动走「作者中心」路由:点赞、评论等互动通过
send_message先汇聚到作者链,再由作者链发射事件,从而保证互动数据的一致性归集; - 检查点摘要(
Summary)保证后到订阅者不错过历史:summarize_events定期把最近 10 条帖子打包,配合received_posts的判空守卫,实现了「迟到补课」且不覆盖本地互动记录。
若想进一步扩展,可参照 linera-sdk 的事件流 API(subscribe_to_events、unsubscribe_from_events、emit、read_event)与 linera-execution 的运行时实现,将流名、摘要条数等常量参数化,或为Like/Comment增加取消与编辑能力。
【免费下载链接】linera-protocolMain repository for the Linera protocol项目地址: https://gitcode.com/GitHub_Trending/li/linera-protocol
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考