ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Linera 应用内动态创建并调用合约:create-and-call 示例深度解析

2026/9/10 14:50:50 拓冰建站 浏览量
Linera 应用内动态创建并调用合约:create-and-call 示例深度解析 Linera 应用内动态创建并调用合约create-and-call 示例深度解析【免费下载链接】linera-protocolMain repository for the Linera protocol项目地址: https://gitcode.com/GitHub_Trending/li/linera-protocol本文基于 Linera 协议仓库中的create-and-call测试夹具fixture讲解如何在一条链上实现一个合约动态发布新模块、创建另一个应用实例并直接调用它的完整流程。该示例以 counter-no-graphql 计数器应用为目标对象是理解 Linera 应用间互操作cross-application call、模块动态发布与运行时 APIpublish_module/create_application/call_application/query_service的最佳入门案例。读完本文你将掌握 Linera SDK 中动态创建应用的完整 API 调用链、其语义约束如认证调用、查询默认值问题以及如何用linera-sdk的测试框架端到端验证这一流程。示例要解决的核心问题在常规的 Linera 应用开发中应用依赖的模块通常在部署时静态确定。但真实业务如工厂合约、可插拔插件、链上孵化器往往需要在合约执行过程中动态地发布字节码、创建新应用并立刻调用它。create-and-call就是验证这一能力的极简范例它自己是一个调度者合约接收一个CreateAndCall操作操作载荷中包含目标应用的合约字节码与 service 字节码即被创建应用的 Wasm 二进制以及初始化参数和调用参数执行时它依次完成发布模块 → 用该模块创建应用实例 → 查询其 service → 调用其合约操作并把调用结果作为自己的操作响应返回。其位置在 linera-sdk/tests/fixtures/create-and-call目录下包含完整的src/合约、service、状态、ABI与tests/端到端测试。整体架构两个应用的协作整个示例由两个应用组成应用角色代码位置create-and-call调度者动态创建并调用目标应用linera-sdk/tests/fixtures/create-and-call/srccounter-no-graphql被动态创建的目标应用简单计数器examples/counter-no-graphql/srccounter-no-graphql是一个不依赖 GraphQL 的极简计数器它以u64初始化通过Increment(u64)操作累加并以u64返回当前值见 counter-no-graphql/src/contract.rs。选择它作为目标是因为其 ABI 足够简单u64初始化、u64参数、u64返回可以清晰展示动态创建 动态调用的每一个步骤而不被业务逻辑干扰。而create-and-call的状态也非常简单——它只需要记住被自己创建出来的应用 ID// linera-sdk/tests/fixtures/create-and-call/src/state.rs #[derive(RootView)] #[view(context ViewStorageContext)] pub struct CreateAndCallState { pub value: RegisterViewOptionApplicationIdCounterNoGraphQlAbi, }RegisterViewOptionApplicationIdCounterNoGraphQlAbi意味着初始为None执行一次CreateAndCall操作后保存所创建计数器应用的ApplicationId后续查询通过它转发到计数器应用。ABI 定义无 GraphQL 的序列化接口lib.rs 定义了CreateAndCallAbi它同时实现ContractAbi与ServiceAbiimpl ContractAbi for CreateAndCallAbi { type Operation CreateAndCallOperation; type Response u64; } impl ServiceAbi for CreateAndCallAbi { type Query CreateAndCallRequest; type QueryResponse u64; } #[derive(StableEnum)] pub enum CreateAndCallOperation { CreateAndCall(Vecu8, Vecu8, u64, u64), } pub enum CreateAndCallRequest { Query, CreateAndCall(Vecu8, Vecu8, u64, u64), }CreateAndCallOperation::CreateAndCall的四个字段含义依次为contract_bytes目标应用的合约字节码Vecu8service_bytes目标应用的service 字节码Vecu8initialization_value创建计数器应用时的初始化值u64对应InstantiationArgumentincrement_value创建完成后调用计数器Increment操作的增量u64。注意#[derive(StableEnum)]它保证枚举在升级场景下的稳定序列化是 Linera ABI 的标准实践。Debug手写实现则避免在日志中打印巨大字节码内容只输出字节数。合约核心四步动态调用链contract.rs 的execute_operation是全文的枢纽它把一次CreateAndCall操作拆解为四个有注释标记的步骤async fn execute_operation(mut self, operation: CreateAndCallOperation) - u64 { let CreateAndCallOperation::CreateAndCall( contract_bytes, service_bytes, initialization_value, increment_value, ) operation; // Step 1: 将 Vecu8 包装为 Bytecode并以 Wasm 运行时发布模块 let contract_bytecode Bytecode::new(contract_bytes); let service_bytecode Bytecode::new(service_bytes); let module_id self.runtime .publish_module(contract_bytecode, service_bytecode, VmRuntime::Wasm, None); // Step 2: 用初始化值创建应用实例 let application_id self .runtime .create_application::CounterNoGraphQlAbi, (), u64( module_id, (), initialization_value, vec![], ); self.state.value.set(Some(application_id)); // Step 3: 查询 service应返回本合约初始化之前的值即 0 let counter_request CounterRequest::Query; let value self.runtime.query_service(application_id, counter_request); assert_eq!(value, 0); // Step 4: 以 Increment 操作调用合约返回其响应 let counter_operation CounterOperation::Increment(increment_value); self.runtime .call_application(true, application_id, counter_operation) }Step 1动态发布模块publish_moduleContractRuntime::publish_module的签名见 linera-sdk/src/contract/runtime.rspub fn publish_module( mut self, contract: Bytecode, service: Bytecode, vm_runtime: VmRuntime, formats: OptionVecu8, ) - ModuleId要点Bytecode::new(bytes)仅是对Vecu8的包装原始字节来自测试中对counter-no-graphql构建产物的读取见下文测试部分。VmRuntime::Wasm指定目标模块的虚拟机运行时。第二个参数传None表示不附加 BCS 编码的Formats描述ABI 格式描述用于链上校验。返回的ModuleId是发布后模块的链上唯一标识后续创建应用必须引用它。Step 2动态创建应用create_applicationpub fn create_applicationAbi, Parameters, InstantiationArgument( mut self, module_id: ModuleId, parameters: Parameters, argument: InstantiationArgument, required_application_ids: VecApplicationId, ) - ApplicationIdAbi泛型参数在此例中的实例化为create_application::CounterNoGraphQlAbi, (), u64含义Parameters为()计数器应用无链上参数InstantiationArgument为u64即initialization_value计数器初始值 43required_application_ids传空vec![]被创建应用不依赖其他应用。返回值ApplicationIdCounterNoGraphQlAbi被保存进状态self.state.value供后续查询使用。Step 3查询目标 servicequery_servicepub fn query_serviceA: ServiceAbi Send( mut self, application_id: ApplicationIdA, query: A::Query, ) - A::QueryResponse这里向刚创建的计数器应用发送CounterRequest::Query并断言返回 0。这验证了刚创建的应用使用传入的初始化参数执行了instantiate——因为create_application的初始化尚未对查询可见详见下文关于 query_application 默认值的重要说明。同时需要留意query_service在 runtime.rs 中的文档约束它要求所有验证者对查询计算出一致结果否则区块提案可能失败因此只适合确定性的查询它不能用于 fast blockfast block 只能由普通 owner 提议不能由 super owner 提议。Step 4认证调用目标合约call_applicationpub fn call_applicationA: ContractAbi Send( mut self, authenticated: bool, application: ApplicationIdA, call: A::Operation, ) - A::Response第一个参数authenticated传true表示这是一个认证调用被调用方计数器合约在执行时收到的消息来源是可信的且调用方会成为被调用消息的认证人。call_application的返回值A::Response这里是u64即累加后的计数值直接作为execute_operation的返回值向上传递形成应用 → 应用 → 用户的响应链。service 端查询转发与操作调度service.rs 演示了从 service 侧驱动整个流程的两种方式async fn handle_query(self, request: CreateAndCallRequest) - u64 { match request { CreateAndCallRequest::Query { let application_id self.state.value.get().expect(An application_id); let counter_request CounterRequest::Query; self.runtime .query_application(application_id, counter_request) } CreateAndCallRequest::CreateAndCall(bytecode, calldata, initial_value, increment) { let operation CreateAndCallOperation::CreateAndCall( bytecode, calldata, initial_value, increment, ); self.runtime.schedule_operation(operation); 0 } } }Query分支把查询转发给已保存的计数器应用query_application实现查询计数器即查询 create-and-call的代理语义。CreateAndCall分支service 不做业务计算而是通过schedule_operation把操作调度回合约执行随后立即返回 0。这正是 Linera 中service 发起操作的标准模式service 负责构造与调度合约负责确定性执行。端到端测试完整验证 43 5 48test_create_and_call.rs 是原 README 提到的test_create_and_call_end_to_end对应实现当前仓库中的实际函数名为test_create_and_call它在内存测试验证器TestValidator上完整复现创建并调用流程#[tokio::test(flavor multi_thread)] async fn test_create_and_call() { let (validator, create_call_module_id) TestValidator::with_current_module::create_and_call::CreateAndCallAbi, (), ()().await; let mut chain validator.new_chain().await; // Step 1: 编译 examples/counter-no-graphql 并读取其字节码 let counter_no_graphql_path std::path::Path::new(../../../../examples/counter-no-graphql); let counter_no_graphql_path std::fs::canonicalize(counter_no_graphql_path).unwrap(); ActiveChain::build_bytecode_files_in(counter_no_graphql_path); let (counter_contract, counter_service) ActiveChain::find_bytecode_files_in(counter_no_graphql_path).await; let counter_contract_bytes counter_contract.bytes.to_vec(); let counter_service_bytes counter_service.bytes.to_vec(); // Step 2: 部署 create-and-call 应用 let application_id chain .create_application(create_call_module_id, (), (), vec![]) .await; // Step 3: 以初始化值 43、增量 5 发起 CreateAndCall 操作 let create_and_call_operation create_and_call::CreateAndCallOperation::CreateAndCall( counter_contract_bytes, counter_service_bytes, 43, 5, ); chain.add_block(|block| block.with_operation(application_id, create_and_call_operation)).await; // Step 4: 查询期望 48 43 5 let query_request create_and_call::CreateAndCallRequest::Query; let outcome chain.query(application_id, query_request).await; assert_eq!(outcome.response, 48); }测试的关键验证点字节码来源真实counter-no-graphql并非伪造数据而是通过ActiveChain::build_bytecode_files_in实际编译examples/counter-no-graphql得到的 Wasm 产物再用find_bytecode_files_in读取原始字节——这保证了Bytecode::new拿到的是真实可执行模块。状态断言最终查询返回48 43 5证明初始化值43确实生效、Increment(5)确实被调用且 create-and-call 的查询代理链路Query→query_application正确。一次操作完成全部工作CreateAndCall操作把发布模块、创建应用、调用合约合并进单个区块的单个操作中执行展示了动态应用创建的最小闭环。Cargo.toml中测试所需的依赖也值得注意[target.cfg(not(target_arch wasm32)).dev-dependencies] linera-sdk { workspace true, features [test, wasmer] }testfeature 提供TestValidator/ActiveChainwasmerfeature 使测试能在宿主机上直接执行 Wasm 字节码不需要外部虚拟机进程。关于 query_application 默认值的重要说明README NoteREADME 明确指出了一个容易被忽视的语义陷阱在同一个操作中执行publish_module、create_application、call_application时我们可以调用query_application但它使用的是默认值。这正是contract.rsStep 3 中assert_eq!(value, 0)的由来新创建应用的instantiate(43)虽然会在链上状态中生效但在同一操作的执行上下文中对目标应用的查询仍返回默认初始化状态RegisterView的默认值 0而不是instantiate已写入的 43。合约代码用这一断言显式冻结了这个行为// Step 3: Call the service. It should return the value before // the initialization of this contract and thus zero. let value self.runtime.query_service(application_id, counter_request); assert_eq!(value, 0);这一约束的实际含义不要在创建 调用同一操作内依赖query_application返回新应用的最终状态若要读取初始化后的值必须在后续区块/操作中通过查询本例service.rs的Query分支获取若你的业务逻辑依赖创建后立即可用请以call_application的返回值而非query_service作为数据来源如本示例 Step 4 返回的计数值。如何使用与验证create-and-call是 SDK 测试夹具主要价值在于测试与学习运行方式为执行其端到端测试# 在仓库根目录下运行 cargo test -p create-and-call --test test_create_and_call或直接运行 crate 的全部测试cargo test -p create-and-call测试会自动编译examples/counter-no-graphql、启动内存TestValidator并在单链上完成部署调度者 → 动态发布目标模块 → 创建计数器实例 → 调用并断言 48。该测试验证了 README.md 中所述的全部行为也是你把它改造成自己工厂/孵化器合约时的最佳起点。小结可复用的动态应用调用模式从源码结构可以提炼出在 Linera 中实现合约创建合约并调用的四要素ABI 分层通过ContractAbi/ServiceAbi分别定义操作与查询接口字节码用Vecu8在操作中传递lib.rs运行时四件套publish_module发布模块 →create_application创建实例 →query_service查询 service →call_application(true, id, op)认证调用调用链完整定义于 linera-sdk/src/contract/runtime.rs状态持有目标 ID用RegisterViewOptionApplicationId...记住动态创建的应用供后续查询与调用state.rs牢记查询语义同一操作内的query_application返回默认值最终状态请通过后续查询或call_application返回值获取。把握住这四点你就掌握了 Linera 动态应用组合dynamic application composition的完整骨架可以在此基础上构建工厂合约、插件系统或任何需要链上按需实例化子应用的业务。【免费下载链接】linera-protocolMain repository for the Linera protocol项目地址: https://gitcode.com/GitHub_Trending/li/linera-protocol创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考