news 2026/9/10 14:50:46

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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)、模块动态发布与运行时 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,它同时实现ContractAbiServiceAbi

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的四个字段含义依次为:

  1. contract_bytes:目标应用的合约字节码Vec<u8>);
  2. service_bytes:目标应用的service 字节码Vec<u8>);
  3. initialization_value:创建计数器应用时的初始化值u64,对应InstantiationArgument);
  4. 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():计数器应用无链上参数;
  • InstantiationArgumentu64:即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

第一个参数authenticatedtrue,表示这是一个认证调用:被调用方(计数器合约)在执行时收到的消息来源是可信的,且调用方会成为被调用消息的认证人。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); }

测试的关键验证点:

  1. 字节码来源真实counter-no-graphql并非伪造数据,而是通过ActiveChain::build_bytecode_files_in实际编译examples/counter-no-graphql得到的 Wasm 产物,再用find_bytecode_files_in读取原始字节——这保证了Bytecode::new拿到的是真实可执行模块。
  2. 状态断言:最终查询返回48 = 43 + 5,证明初始化值(43)确实生效、Increment(5)确实被调用,且 create-and-call 的查询代理链路(Queryquery_application)正确。
  3. 一次操作完成全部工作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 Note)

README 明确指出了一个容易被忽视的语义陷阱:

在同一个操作中执行publish_modulecreate_applicationcall_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.rsQuery分支)获取;
  • 若你的业务逻辑依赖"创建后立即可用",请以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 中实现"合约创建合约并调用"的四要素:

  1. ABI 分层:通过ContractAbi/ServiceAbi分别定义操作与查询接口,字节码用Vec<u8>在操作中传递(lib.rs);
  2. 运行时四件套publish_module发布模块 →create_application创建实例 →query_service查询 service →call_application(true, id, &op)认证调用,调用链完整定义于 linera-sdk/src/contract/runtime.rs;
  3. 状态持有目标 ID:用RegisterView<Option<ApplicationId<...>>>记住动态创建的应用,供后续查询与调用(state.rs);
  4. 牢记查询语义:同一操作内的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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 14:50:38

JumpServer 开源 PAM 平台深度指南:架构、组件与快速部署实战

JumpServer 开源 PAM 平台深度指南&#xff1a;架构、组件与快速部署实战 【免费下载链接】jumpserver JumpServer is an open-source Privileged Access Management (PAM) platform that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kuberne…

作者头像 李华
网站建设 2026/9/10 14:50:37

趣味项目驱动技术成长:实战经验与避坑指南

1. 项目概述&#xff1a;当趣味遇上实战去年在团队内部搞了个"周五创意日"活动&#xff0c;每周五下午大家都要放下手头工作&#xff0c;用2小时完成一个趣味小项目。没想到这个看似玩闹的活动&#xff0c;竟然成了我们技术提升最快的催化剂。从最初简单的爬虫小工具…

作者头像 李华
网站建设 2026/9/10 14:49:13

T型逆变器微电网VSG与PQ控制Simulink仿真实践

1. 项目背景与核心价值在新能源发电占比不断提升的今天&#xff0c;微电网作为分布式电源接入的有效解决方案&#xff0c;正受到越来越多的关注。我最近完成了一个关于两台T型逆变器构建局域微电网的Simulink仿真项目&#xff0c;其中采用了VSG&#xff08;虚拟同步发电机&…

作者头像 李华
网站建设 2026/9/10 14:48:07

昇腾/GE更新可刷新特征内存基址API

UpdateGraphRefreshableFeatureMemoryBase 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提…

作者头像 李华
网站建设 2026/9/10 14:46:45

RustDesk 1.4.5客户端本地编译与硬件加速优化实战

1. 项目概述&#xff1a;RustDesk 1.4.5客户端本地编译实战 在远程桌面工具领域&#xff0c;RustDesk凭借其开源特性和低延迟表现赢得了大量技术用户的青睐。最近在调试一个需要硬件加速的远程医疗场景时&#xff0c;我发现官方预编译版本对NVIDIA显卡的H.264编码器支持不够理想…

作者头像 李华