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)、模块动态发布与运行时 API(publish_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/src |
counter-no-graphql | 被动态创建的目标应用(简单计数器) | examples/counter-no-graphql/src |
counter-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: RegisterView<Option<ApplicationId<CounterNoGraphQlAbi>>>, }RegisterView<Option<ApplicationId<CounterNoGraphQlAbi>>>意味着:初始为None,执行一次CreateAndCall操作后保存所创建计数器应用的ApplicationId,后续查询通过它转发到计数器应用。
ABI 定义:无 GraphQL 的序列化接口
lib.rs 定义了CreateAndCallAbi,它同时实现ContractAbi与ServiceAbi:
impl 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(Vec<u8>, Vec<u8>, u64, u64), } pub enum CreateAndCallRequest { Query, CreateAndCall(Vec<u8>, Vec<u8>, u64, u64), }CreateAndCallOperation::CreateAndCall的四个字段含义依次为:
contract_bytes:目标应用的合约字节码(Vec<u8>);service_bytes:目标应用的service 字节码(Vec<u8>);initialization_value:创建计数器应用时的初始化值(u64,对应InstantiationArgument);increment_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: 将 Vec<u8> 包装为 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_module)
ContractRuntime::publish_module的签名(见 linera-sdk/src/contract/runtime.rs):
pub fn publish_module( &mut self, contract: Bytecode, service: Bytecode, vm_runtime: VmRuntime, formats: Option<Vec<u8>>, ) -> ModuleId要点:
Bytecode::new(bytes)仅是对Vec<u8>的包装;原始字节来自测试中对counter-no-graphql构建产物的读取(见下文测试部分)。VmRuntime::Wasm指定目标模块的虚拟机运行时。第二个参数传None表示不附加 BCS 编码的Formats描述(ABI 格式描述,用于链上校验)。- 返回的
ModuleId是发布后模块的链上唯一标识,后续创建应用必须引用它。
Step 2:动态创建应用(create_application)
pub fn create_application<Abi, Parameters, InstantiationArgument>( &mut self, module_id: ModuleId, parameters: &Parameters, argument: &InstantiationArgument, required_application_ids: Vec<ApplicationId>, ) -> ApplicationId<Abi>泛型参数在此例中的实例化为create_application::<CounterNoGraphQlAbi, (), u64>,含义:
Parameters为():计数器应用无链上参数;InstantiationArgument为u64:即initialization_value(计数器初始值 43);required_application_ids传空vec![]:被创建应用不依赖其他应用。
返回值ApplicationId<CounterNoGraphQlAbi>被保存进状态self.state.value,供后续查询使用。
Step 3:查询目标 service(query_service)
pub fn query_service<A: ServiceAbi + Send>( &mut self, application_id: ApplicationId<A>, query: A::Query, ) -> A::QueryResponse这里向刚创建的计数器应用发送CounterRequest::Query,并断言返回 0。这验证了"刚创建的应用使用传入的初始化参数执行了instantiate"——因为create_application的初始化尚未对查询可见(详见下文"关于 query_application 默认值的重要说明")。
同时需要留意query_service在 runtime.rs 中的文档约束:
- 它要求所有验证者对查询计算出一致结果,否则区块提案可能失败,因此只适合确定性的查询;
- 它不能用于 fast block(fast block 只能由普通 owner 提议,不能由 super owner 提议)。
Step 4:认证调用目标合约(call_application)
pub fn call_application<A: ContractAbi + Send>( &mut self, authenticated: bool, application: ApplicationId<A>, 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 = 48
test_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/ActiveChain,wasmerfeature 使测试能在宿主机上直接执行 Wasm 字节码(不需要外部虚拟机进程)。
关于 query_application 默认值的重要说明(README Note)
README 明确指出了一个容易被忽视的语义陷阱:
在同一个操作中执行
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分别定义操作与查询接口,字节码用Vec<u8>在操作中传递(lib.rs); - 运行时四件套:
publish_module发布模块 →create_application创建实例 →query_service查询 service →call_application(true, id, &op)认证调用,调用链完整定义于 linera-sdk/src/contract/runtime.rs; - 状态持有目标 ID:用
RegisterView<Option<ApplicationId<...>>>记住动态创建的应用,供后续查询与调用(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),仅供参考