深度解析 Linera meta-counter 测试夹具:跨应用调用(Cross-Application Calls)与预言机(Oracle)机制
【免费下载链接】linera-protocolMain repository for the Linera protocol项目地址: https://gitcode.com/GitHub_Trending/li/linera-protocol
导读
meta-counter 是 Linera 协议仓库中一个极其精简但作用关键的测试应用(fixture)。根据它的官方 README,该应用唯一的存在目的就是"测试跨应用调用(cross-application calls)与预言机(oracles)"。本文以 linera-sdk/tests/fixtures/meta-counter/README.md 为核心骨架,结合其完整的合约、服务与 ABI 实现,以及 linera-core 中的集成测试,系统讲解 Linera SDK 中跨应用调用、预言机查询、跨链消息投递、消息回弹(bouncing)与燃料授权等底层机制。读完本文,你将掌握 Linera 应用之间如何互相调用、如何发起被区块记录的服务查询,以及这些能力在官方测试中是如何被验证的。
一、这个"只有一句话"的 README 背后是什么
linera-sdk/tests/fixtures/meta-counter/README.md全文只有一句:
This application is only used for testing cross-application calls and oracles.
它的定位是Linera SDK 测试夹具(fixture),不面向生产使用,而是作为测试"应用调用应用"这条核心链路的载体。meta-counter 本身没有状态,它的全部逻辑就是:接收外部操作(Operation),把操作转换成发给另一个应用(counter)的调用或消息,从而在测试中检验 Linera 的以下核心能力:
- 跨应用调用(cross-application call):一个合约通过
call_application同步调用另一个合约的操作; - 预言机(oracle):合约通过
query_service向另一个应用的服务发起 GraphQL 查询,查询结果被记录在区块的oracle_responses中; - 跨链消息(cross-chain message):通过
send_message把消息投递到目标链; - 消息投递控制:认证(authentication)、追踪(tracking)、燃料授权(fuel grant);
- 消息回弹(bouncing):消息被目标链拒绝后弹回原链的处理语义。
这个目录虽然 README 简短,但代码结构完整,共三个源文件加一个Cargo.toml:
linera-sdk/tests/fixtures/meta-counter/ ├── Cargo.toml # 定义 meta_counter_contract 与 meta_counter_service 两个二进制 ├── README.md └── src/ ├── lib.rs # ABI:Operation / Message 类型定义 ├── contract.rs # 合约实现 └── service.rs # 服务实现其中 Cargo.toml 声明的两个 bin 目标:
meta_counter_contract→src/contract.rsmeta_counter_service→src/service.rs
这正是 Linera 应用的标准形态:合约与服务编译为两个独立的 Wasm 模块,通过同一个 ABI 相连。值得注意的是该 crate 把counter(即 examples/counter 中的计数合约)作为直接依赖,并通过linera-sdk.workspace = true引用工作区 SDK——这是它能"调用另一个应用"的前提。
二、ABI 层:Operation 与 Message 如何描述一次跨应用交互
src/lib.rs 定义了MetaCounterAbi,同时实现了ContractAbi与ServiceAbi:
pub struct MetaCounterAbi; impl ContractAbi for MetaCounterAbi { type Operation = Operation; type Response = (); } impl ServiceAbi for MetaCounterAbi { type Query = Request; type QueryResponse = Response; }2.1 Operation:用户如何驱动 meta-counter
Operation是用户通过链上区块提交的"指令",它的字段几乎一一对应了 Linera SDK 消息投递的每一项能力开关:
#[derive(Debug, Serialize, Deserialize)] pub struct Operation { pub recipient_id: ChainId, // 消息接收链 pub authenticated: bool, // 是否携带调用者认证 pub is_tracked: bool, // 是否启用消息追踪(用于跨链送达确认) pub query_service: bool, // 是否在发送前做一次服务预言机查询 pub fuel_grant: u64, // 为消息额外授予的 wasm_fuel pub message: Message, // 实际要投递的消息体 }同时提供了两个便捷构造器:
Operation::increment(recipient_id, value, query_service):构造一条Message::Increment(value),默认authenticated=false、is_tracked=false、fuel_grant=0,仅允许选择是否附带服务查询;Operation::fail(recipient_id):构造一条Message::Fail,用于测试"消息在目标链上执行失败(panic)"的场景。
2.2 Message:跨链传递的载荷
#[derive(Debug, Serialize, Deserialize)] pub enum Message { Increment(u64), Fail, }Increment(u64)会被转发成对 counter 应用的CounterOperation::Increment { value }调用;Fail则会在执行时故意panic!,用于验证失败消息的处理语义(见下文第四节)。
三、合约实现:instantiate / execute_operation / execute_message 三段式
src/contract.rs 实现了Contracttrait。它最值得注意的一点是:meta-counter 通过"应用参数(application parameters)"获得被调用应用(counter)的ApplicationId:
impl MetaCounterContract { fn counter_id(&mut self) -> ApplicationId<counter::CounterAbi> { self.runtime.application_parameters() } }也就是说,meta-counter 部署时不需要硬编码目标应用的 ID,而是在CreateApplication时通过参数传入(详见第六节的测试证据)。这也对应了合约的Parameters关联类型:
type Message = Message; type InstantiationArgument = (); type Parameters = ApplicationId<counter::CounterAbi>; type EventValue = String;3.1 instantiate:初始化即验证 + 自投消息 + 事件流
async fn instantiate(&mut self, _argument: ()) { // Validate that the application parameters were configured correctly. self.counter_id(); // 向自身发送一条 no-op 消息,用于测试"初始化时发送消息"的合约。 let this_chain = self.runtime.chain_id(); self.runtime.emit( StreamName(b"announcements".to_vec()), &"instantiated".to_string(), ); self.runtime.send_message(this_chain, Message::Increment(0)); }这里展示了三个 SDK 能力:
- 参数校验:调用
counter_id()只是为了确认参数可解析,若参数配置错误,初始化直接失败; - 事件流(event stream):
self.runtime.emit(StreamName(b"announcements".to_vec()), &"instantiated".to_string())向名为announcements的事件流写入一条事件。在 linera-core/src/unit_tests/wasm_client_tests.rs 中可以看到测试断言:创建 meta-counter 的区块事件恰好包含一条announcements流上值为"instantiated"的事件; - 初始化即发消息:向自身链发送
Message::Increment(0),因为值为 0,所以不会改变 counter 的值——专门用来覆盖"合约在 instantiate 阶段发送消息"的代码路径。
3.2 execute_operation:把 Operation 变成一条受控的消息
async fn execute_operation(&mut self, operation: Operation) { // 操作执行时,origin timestamp 必须为空 assert!( self.runtime.message_origin_timestamp().is_none(), "Origin timestamp must not be set when executing an operation" ); let Operation { recipient_id, authenticated, is_tracked, query_service, fuel_grant, message } = operation; let mut message = self.runtime.prepare_message(message).with_grant(Resources { wasm_fuel: fuel_grant, ..Resources::default() }); if authenticated { message = message.with_authentication(); } if is_tracked { message = message.with_tracking(); } if query_service { // 预言机查询:结果会被记录进区块 let counter_id = self.counter_id(); self.runtime.query_service(counter_id, "query { value }".into()); } message.send_to(recipient_id); }这一方法集中展示了 Linera 的**消息构建器(MessageBuilder)**模式:
prepare_message(message)创建构建器,with_grant(Resources { wasm_fuel, .. })为消息授予燃料配额;with_authentication()让消息携带调用者的认证信息(接收方可通过authenticated_caller_id/ 账户权限检查确认来源);with_tracking()启用消息追踪;- 若
query_service为真,则在发送前执行一次预言机查询(详见第五节); - 最后
send_to(recipient_id)把消息投递到目标链。
这些 API 的源码实现在 linera-sdk/src/contract/runtime.rs(prepare_message)与 同文件 L499-L517(with_tracking/with_authentication/with_grant/send_to)。
3.3 execute_message:处理收到的消息(含回弹与失败语义)
async fn execute_message(&mut self, message: Message) { let is_bouncing = self.runtime.message_is_bouncing() .expect("Message delivery status has to be available when executing a message"); let origin_timestamp = self.runtime.message_origin_timestamp() .expect("Origin timestamp has to be available when executing a message"); assert!(origin_timestamp <= self.runtime.system_time(), "Origin timestamp must not be in the future"); if is_bouncing { log::trace!("receiving a bouncing message {message:?}"); return; } match message { Message::Fail => { panic!("Message failed intentionally"); } Message::Increment(value) => { let counter_id = self.counter_id(); let operation = counter::CounterOperation::Increment { value }; self.runtime.call_application(true, counter_id, &operation); } } }这里是消息语义的核心展示:
message_is_bouncing():返回true表示该消息此前被目标链拒绝、如今"弹回"原链。对弹回的消息直接返回,不做业务处理。对应 SDK 实现见 runtime.rs L203-L207;message_origin_timestamp():返回消息在源链区块上的时间戳,合约据此断言"来源时间不能晚于当前系统时间",防止未来消息;对应实现见 runtime.rs L219-L223;Message::Fail分支故意panic!("Message failed intentionally")——一个未追踪的失败消息会导致目标链拒绝该入站消息,从而触发"回弹"机制;而Message::Increment分支则是真正的跨应用调用:self.runtime.call_application(true, counter_id, &operation)以"已认证"方式同步调用 counter 应用的操作。
跨应用调用的 SDK 实现见 runtime.rs L291-L308:它把Operation序列化后通过contract_wit::try_call_application调用目标应用,再反序列化响应返回。
四、服务实现:把预言机查询"转发"给 counter
src/service.rs 非常简短,但点明了预言机查询的服务端路径:
impl Service for MetaCounterService { type Parameters = ApplicationId<counter::CounterAbi>; async fn new(runtime: ServiceRuntime<Self>) -> Self { MetaCounterService { runtime } } async fn handle_query(&self, request: Request) -> Response { let counter_id = self.runtime.application_parameters(); self.runtime.query_application(counter_id, &request) } }当合约端执行runtime.query_service(counter_id, "query { value }".into())时,运行时会在当前链的上下文内启动 counter 的 service,把 GraphQL 请求转发给它;meta-counter 的 service 又把请求二次转发给 counter 应用的服务(query_application),从而读取 counter 的内部视图值。
五、预言机(Oracle)机制:查询结果如何进入区块
预言机是 Linera 的一个重要设计:合约可以发起"出链"查询,查询结果由验证者执行并写入区块,成为区块的一部分(oracle_responses),从而保证确定性。
在 wasm_client_tests.rs 中,测试构造了Operation::increment(receiver_id, 5, true)(query_service=true),并断言执行后的区块:
let responses = &block.body.oracle_responses; let [_, responses] = &responses[..] else { panic!("Unexpected oracle responses: {responses:?}"); }; let [OracleResponse::Service(json)] = &responses[..] else { ... }; let response_json = serde_json::from_slice::<serde_json::Value>(json).unwrap(); assert_eq!(response_json["data"], json!({"value": 10}));这段测试验证了三条事实:
- 区块中确实出现了
oracle_responses,且类型为OracleResponse::Service(json)——即服务查询型预言机响应; - 查询执行于区块构建阶段(在跨链消息送达接收链之前),所以读到的是 counter 当前的初始值
10,而不是后来Increment(5)的结果; - 随后接收链
process_inbox处理消息、counter 被call_application递增 5 后,对 meta-counter 服务的查询{ value }才返回{"value": 5}(见同文件 L360-L376)。两次查询结果10vs5的差异,精确地体现了"预言机查询发生在区块内、跨链消息执行发生在后续区块"的时序。
而合约端的发起方式正是execute_operation中的:
self.runtime.query_service(counter_id, "query { value }".into());其 SDK 实现在 runtime.rs L358。
六、测试证据:meta-counter 如何在官方测试中被使用
6.1 客户端级集成测试(wasm_client_tests)
wasm_client_tests.rs 先发布counter与meta-counter两个模块,并绑定 ABI 类型:
let module_id2 = publisher.publish_wasm_example("meta-counter").await?; let module_id2 = module_id2.with_abi::<meta_counter::MetaCounterAbi, ApplicationId<CounterAbi>, ()>();创建应用时(L321-L329),CreateApplication的参数即 counter 的应用 ID、required_application_ids里也声明了对 counter 的依赖——这正是合约里application_parameters()能拿到ApplicationId<counter::CounterAbi>的来源。
随后测试覆盖了三条核心路径:
- 跨应用调用 + 预言机(L342-L376):
Operation::increment(receiver_id, 5, true)配合fuel_grant = 1000000,验证区块 oracle 响应与最终 counter 值; - 未追踪消息失败 → 回弹(L379-L403):
Operation::fail(receiver_id)产生的消息在接收链上执行失败(panic),接收链因此拒绝该入站消息,incoming_bundles[0].action == MessageAction::Reject,消息弹回源链; - 追踪消息失败(L406 起):
operation.is_tracked = true的失败消息,验证追踪模式下的送达/失败处理。
6.2 工作线程级测试(wasm_worker_tests)
wasm_worker_tests.rs 展示了更底层的操作:直接加载两个模块的字节码(L283-L298)、把 blobs 写入 worker 存储(L310-L320)、发布模块(L322-L338)、创建 counter 与 meta-counter 应用(L340-L390,meta-counter 的parameters为 counter 的ApplicationId、required_application_ids为[counter_app_id]),最后通过Operation::fail验证失败消息的传递(L392 起)。这些测试与客户端测试互为印证,覆盖了"内存/服务/rocksdb/scylladb"多种存储后端的相同语义(见 wasm_client_tests.rs L222-L260 的四个变体)。
七、总结:meta-counter 揭示的 Linera SDK 关键 API 清单
以这个夹具为窗口,可以梳理出 Linera SDK 合约侧的核心编程模型:
| 能力 | 调用方式 | 源码位置 |
|---|---|---|
| 读取应用参数(获知被调用方 ID) | runtime.application_parameters() | contract.rs |
| 跨应用同步调用 | runtime.call_application(authenticated, app_id, &operation) | runtime.rs L291 |
| 预言机服务查询 | runtime.query_service(app_id, query) | runtime.rs L358 |
| 发送跨链消息 | runtime.send_message(chain_id, msg)/prepare_message(msg).send_to(...) | runtime.rs L244-L255 |
| 消息认证 / 追踪 / 燃料授权 | with_authentication()/with_tracking()/with_grant(Resources) | runtime.rs L499-L517 |
| 检测消息回弹 | runtime.message_is_bouncing() | runtime.rs L203 |
| 读取消息来源时间戳 | runtime.message_origin_timestamp() | runtime.rs L219 |
| 写事件流 | runtime.emit(StreamName, &value) | runtime.rs L311 |
meta-counter 虽然只是 linera-sdk/tests/fixtures 下的一个测试用小程序,但它用最少的代码同时覆盖了 Linera 应用互联中最重要的三类能力——应用间调用、预言机查询与跨链消息的完整生命周期。如果你的目标是编写自己的多合约应用,这个夹具连同 examples/counter 是一份非常值得对照阅读的"最小可运行参照":它清楚地展示了Parameters如何充当"依赖注入"、Operation如何映射到消息投递选项、以及失败消息在 Linera 的"拒绝—回弹"模型中如何被处理。
【免费下载链接】linera-protocolMain repository for the Linera protocol项目地址: https://gitcode.com/GitHub_Trending/li/linera-protocol
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考