Sway 合约外部代码执行run_external实战可升级合约与代理模式实现指南【免费下载链接】sway Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/swaySway 语言在std-lib中提供了run_external函数允许合约在执行期间加载并运行任意外部合约代码这一机制是可升级合约Upgradeable Contracts与代理Proxy模式的核心基石。本文以官方文档《External Code Execution》为主线结合仓库中的完整示例 examples/upgradeable_proxy/proxy/src/main.sw 与 SDK 集成测试 run_external_proxy 测试深入讲解如何用run_external构建代理合约、理解其与普通合约调用的本质区别、处理 fallback 函数以及规避关键限制。读完本文你将能够独立设计并实现一个可在链上安全更换逻辑的 Sway 可升级合约。认识run_external运行时加载并跳转执行外部合约std-lib的execution模块sway-lib-std/src/execution.sw定义了外部代码执行的唯一入口/// Load and run the contract with the provided ContractId. /// /// Contract code will be loaded using LDC and jumped into. /// Unlike a normal contract call, the context of the contract running /// run_external is retained for the loaded code. /// /// As this function never returns to the original code that calls it, it returns !. #[inline(never)] pub fn run_external(load_target: ContractId) - ! { asm( load_target: load_target, word, length, ssp_saved, cur_stack_size, ) { csiz length load_target; move ssp_saved ssp; sub cur_stack_size sp ssp; cfs cur_stack_size; ldc load_target zero length i0; addi word zero i64; aloc word; sw hp ssp_saved i0; } __jmp_mem() }从源码中可以提炼出几个关键事实函数接收一个ContractId它永不返回——返回类型是!never 类型加载外部代码后直接通过__jmp_mem()跳转执行执行权不再回到调用它的函数。底层使用 FuelVM 的LDCload code指令将目标合约代码载入内存然后跳转到MEM[$hp]处执行。__jmp_mem内建函数在 sway-core/src/semantic_analysis/ast_node/expression/intrinsic_function.rs 中被描述为“Jumps toMEM[$hp]”与run_external的 asm 块中sw hp ssp_saved i0把跳转地址写入堆指针指向的内存完全对应。与普通合约调用不同加载的代码会保留调用方代理合约的上下文继续执行——这是可升级合约能够工作的根本原因下一节会详述其影响。在使用上你只需要在合约中引入并调用它use std::execution::run_external; // 在某个存储了 ContractId 的函数中 run_external(target_contract_id)可升级合约一个完整的 Proxy 示例可升级合约Upgradeable Contract的设计目标是合约部署后其逻辑可以被更新。经典做法是代理 实现两层结构用户始终与代理合约交互代理合约的存储中保存当前实现合约的ContractId当需要升级时只需更换这个ContractId指向的新实现即可代理合约地址与用户资产不动。代理合约仓库中的 examples/upgradeable_proxy/proxy/src/main.sw 给出了一个最小但完整的代理实现contract; use std::execution::run_external; abi Proxy { #[storage(write)] fn set_target_contract(id: ContractId); #[storage(read)] fn double_input(_value: u64) - u64; } #[namespace(my_storage_namespace)] storage { target_contract: OptionContractId None, } impl Proxy for Contract { #[storage(write)] fn set_target_contract(id: ContractId) { storage.target_contract.write(Some(id)); } #[storage(read)] fn double_input(_value: u64) - u64 { let target storage.target_contract.read().unwrap(); run_external(target) } }这个代理合约只有两个函数职责边界非常清晰set_target_contract(id: ContractId)将传入的实现合约ContractId写入存储变量target_contract。升级逻辑本质上就是再次调用它指向新版本实现。double_input(_value: u64)从存储读出target_contract直接调用run_external(target)。run_external会按函数选择器函数名 签名在目标合约中寻找同名函数并执行——如果目标合约中存在同名的double_input那么外部实现合约中的double_input代码就会被执行并返回u64结果。注意代理的 ABI 中声明了存储读写权限标注#[storage(write)]/#[storage(read)]这是 Sway 强制要求开发者在 ABI 中显式声明存储访问意图的一部分。存储命名空间代理合约的最佳实践请注意代理合约存储块上的这一行#[namespace(my_storage_namespace)] storage { target_contract: OptionContractId None, }Sway 官方文档明确建议所有 Sway 代理合约都应使用namespace属性。原因是run_external会让实现合约代码在代理的存储上下文中运行而实现合约既能看到自己的存储也能访问代理的存储上下文若不用命名空间隔离实现合约的存储槽位与代理的存储槽位可能发生冲突storage collision导致数据被意外覆盖或错误读取。加一层命名空间即可有效规避该风险。实现合约对应地examples/upgradeable_proxy/implementation/src/main.sw 是代理所指向的实现合约contract; abi Implementation { #[storage(write)] fn double_input(value: u64) - u64; } storage { value: u64 0, // to stay compatible, this has to stay the same in the next version } impl Implementation for Contract { #[storage(write)] fn double_input(value: u64) - u64 { let new_value value * 2; storage.value.write(new_value); new_value } }实现合约只有一个double_input函数把入参乘以 2写入存储变量value并返回新值。注释 to stay compatible, this has to stay the same in the next version 直接呼应了文档中关于升级兼容性的关键约束——升级时存储变量的声明顺序和类型必须保持一致详见后文限制一节。把两个合约串起来看完整调用流用户调用代理的set_target_contract将实现合约 ID 存入代理存储用户调用代理的double_input(21)代理读取target_contract执行run_external实现合约的double_input代码被加载并在代理的上下文中执行21 * 2 42value被写入代理的存储42 作为返回值穿透回用户。与普通合约调用有何不同run_external不是Contract::call之类的普通跨合约调用二者在两个关键维度上有本质区别。不需要对方的 ABI普通跨合约调用如通过abi定义 contract_id接口调用或 SDK 层的with_contract_ids调用要求调用方在编译期就知晓目标合约的 ABI。而使用run_external时代理合约对实现合约的唯一认知就是它的ContractId——它不需要、也不持有任何 ABI 定义。这带来一个重大收益实现合约的接口可以自由演进。只要新实现里仍然存在被调用的同名函数以及同样的存储布局代理无需重新部署指向新合约即可完成升级。这正是不可能通过普通静态合约调用实现的动态升级能力。存储上下文写进代理而不是实现第二个区别也是可升级合约模式中最容易被误解的一点run_external保留的是调用方代理的存储上下文加载进来的代码写存储写的是代理的存储。这意味着在上面示例中value变量被更新在代理合约的存储里而不是实现合约自己的存储里。文档中明确举例如果你直接调用实现合约去读value得到的结果与通过代理读到的结果是不一样的——因为代理加载代码后是在它自己的上下文自己的存储中执行。这一特性的工程价值在于所有状态始终沉淀在固定的代理地址上升级实现合约不会丢失用户数据实现合约可以被设计为无状态仅仅是可替换的逻辑代码包多个代理可以复用同一份实现代码各自维护独立状态如同一个逻辑模板的不同实例。从源码层面佐证run_external的实现见上文 asm 块在跳转前通过cfscall frame shift等指令调整调用帧使被加载代码嫁接到当前调用上下文上继续运行这正是保留调用方上下文的底层机制。Fallback 函数目标中不存在对应函数时如果外部代码加载后目标合约中不存在与调用匹配的函数名会发生什么若目标合约定义了fallback函数则fallback会被触发执行若没有fallback函数交易将 revert文档明确强调此行为。fallback 场景在仓库的 SDK 测试项目中有完整实现。代理侧 test/src/sdk-harness/test_projects/run_external_proxy/src/main.sw 定义了does_not_exist_in_the_target// ANCHOR: does_not_exist_in_the_target fn does_not_exist_in_the_target(_foo: u64) - u64 { run_external(TARGET) } // ANCHOR_END: does_not_exist_in_the_target该测试代理使用configurable常量TARGET在部署时注入目标合约 ID而非存储。目标合约 test/src/sdk-harness/test_projects/run_external_target/src/main.sw 中并没有does_not_exist_in_the_target但它定义了一个 fallback// ANCHOR: fallback #[fallback] fn fallback() - u64 { use std::call_frames::*; __log(3); __log(called_method()); __log(double_value); __log(called_method() double_value); let foo called_args::u64(); foo * 3 } // ANCHOR_END: fallback因此当代理调用does_not_exist_in_the_target(42)时执行流程变为run_external跳入目标合约未找到同名函数目标合约的fallback被触发fallback 通过std::call_frames模块读取被调用方法的元数据与参数返回foo * 3 126。在 fallback 中读取参数call_frames 模块fallback 函数没有显式参数列表那么代理调用时传入的参数如何取到答案是std-lib的call_frames模块sway-lib-std/src/call_frames.sw。fallback 内可通过两个关键函数恢复调用上下文/// Get the called method name from the current call frame. pub fn called_method() - str { use ::codec::decode_first_param; decode_first_param::str() } /// Get the called arguments from the current call frame. pub fn called_argsT() - T where T: AbiDecode, { use ::codec::decode_second_param; decode_second_param::T() }called_method()返回当前调用帧中被调用的方法名str。上面 fallback 中用__log(called_method() double_value)来断言/观测当前调用确实命中了double_value。called_args::T()按类型T解码第二个调用参数。示例中let foo called_args::u64();即取回代理does_not_exist_in_the_target(_foo: u64)传入的_foo值类型需与真实参数类型一致T: AbiDecode。call_frames模块还基于调用帧偏移提供了msg_asset_id()、code_size()、first_param()、second_param()、get_previous_frame_pointer()、get_contract_id_from_call_frame()等工具可进一步读取当前调用帧携带的资产、代码大小等信息。测试如何验证这一切test/src/sdk-harness/test_projects/run_external_proxy/mod.rs 中的run_external_can_proxy_call集成测试用 Rust SDKfuels-rs端到端验证了三条路径large_value()代理run_external到目标返回值与目标合约中硬编码的b256常量一致注意调用时通过.with_contract_ids([target_id])向交易附加目标合约以便 FuelVM 允许加载该合约代码double_value(42)命中目标合约同名函数返回84does_not_exist_in_the_target(42)目标无此函数落入 fallback返回12642 * 3。测试还通过__log打印的 receipts 观测了调用链上的日志顺序代理、目标、fallback 各打印了标记直接印证了代码在代理上下文执行 fallback 兜底的行为。限制使用run_external前必须知道的边界官方文档列出run_external的三条限制结合源码与示例整理如下只能对外部合约使用。Script、Predicate 以及 Library 代码无法被run_external加载执行——它的加载目标是链上已部署的合约代码LDC按ContractId取代码因此可升级模式只能以合约对合约的方式落地。升级时必须保持存储布局稳定。如果更换实现合约新的实现必须保持与旧版本相同的存储变量声明顺序和类型因为之前的数据已经按旧布局写入代理存储。示例实现合约中的注释 to stay compatible, this has to stay the same in the next version 就是这一约束的代码级提醒。在实践中建议把存储变量视为不可变契约只做语义兼容的追加设计绝不在升级时重排或改类型。调用栈使用受限。你不能在另一个调用帧中先使用调用栈再调用run_external——只能在使用run_external的同一个调用帧内使用调用栈。这与run_external跳转式执行、接管当前调用帧的机制直接相关它接管/复用当前调用帧后之前帧的栈状态已不再有效。组合实战建议将以上机制组合起来一个生产级的 Sway 可升级合约通常包含以下要素代理合约持有#[namespace(...)]存储内含OptionContractId目标指针提供set_target_contract或带权限校验的升级函数如限制为 owner 可调与业务转发函数实现合约无状态或独立存储的逻辑代码包函数签名与代理 ABI 对齐升级时保证存储声明顺序与类型不变fallback 设计为不存在的函数提供兜底逻辑如统一 revert 或转发到通用处理器避免无 fallback 时直接 revert 的硬失败部署与测试借助 examples/upgradeable_proxy 目录中的代理/实现双 Forc 项目结构配合 SDK 集成测试参考 run_external_proxy/mod.rs验证代理转发、存储归属与 fallback 三条路径。小结run_external是 Sway 生态中实现可升级合约与代理模式的基石能力它允许合约在运行时按ContractId加载任意合约代码并在自身上下文中执行因而实现了逻辑可换、状态与地址不变。与普通合约调用相比它免除了 ABI 依赖、保留调用方存储上下文并通过fallbackcall_frames提供动态方法分发的灵活性。与此同时合约-only 的目标、存储布局的稳定性要求与调用栈限制构成了使用它时必须遵守的边界。理解这些机制你就能在 Sway 中构建出可靠、可升级的合约体系。【免费下载链接】sway Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考