fuels-rs 中配置可配置常量(Configurable Constants):Sway 部署期参数覆盖完整指南
【免费下载链接】fuels-rsFuel Network Rust SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-rs
在 Fuel 链上部署合约或脚本时,Sway 的configurable常量允许开发者在部署期改变程序字节码中内嵌的默认值,而无需重新编译链上代码。fuels-rs(Fuel Network Rust SDK)会为每一个 configurable 生成类型安全的with_XXXbuilder 方法,配合abigen!与Contract::load_from/deploy调用链,即可实现“一套字节码、多次参数化部署”。本文基于 configurable-constants.md 展开,结合仓库中的 Sway 示例合约、端到端测试与代码生成源码,完整讲解从 Sway 声明到 Rust 侧覆盖、部署再到链上验证的整个流程。
一、什么是 configurable 常量
在 Sway 智能合约(contract)、脚本(script)与谓词(predicate)中,都可以通过configurable块声明一组常量。这些常量在编译时拥有默认值,但它们的存放位置(字节码内的 offset)会被编译器记录在 ABI 中,使得 SDK 能在部署/提交前对字节码做定点修补(patching)。
仓库中用于演示的合约位于 e2e/sway/contracts/configurables/src/main.sw,其声明的常量几乎覆盖了 Sway 的主流类型:
contract; #[allow(dead_code)] enum EnumWithGeneric<D> { VariantOne: D, VariantTwo: (), } struct StructWithGeneric<D> { field_1: D, field_2: u64, } configurable { BOOL: bool = true, U8: u8 = 8, U16: u16 = 16, U32: u32 = 32, U64: u64 = 63, U256: u256 = 0x0000000000000000000000000000000000000000000000000000000000000008u256, B256: b256 = 0x0101010101010101010101010101010101010101010101010101010101010101, STR_4: str[4] = __to_str_array("fuel"), TUPLE: (u8, bool) = (8, true), ARRAY: [u32; 3] = [253, 254, 255], STRUCT: StructWithGeneric<u8> = StructWithGeneric { field_1: 8, field_2: 16, }, ENUM: EnumWithGeneric<bool> = EnumWithGeneric::VariantOne(true), }该合约还暴露了一个只读方法return_configurables(),用于把全部常量打包成一个元组返回,方便链上验证当前实际生效的配置(见下文第五节)。与此平行的脚本示例位于 e2e/sway/scripts/script_configurables/src/main.sw,其configurable声明与合约版完全一致,仅将abi换成了直接返回元组的fn main()。
上表覆盖的类型可归纳为:
| 常量 | Sway 类型 | 默认值 | Rust 侧对应类型 |
|---|---|---|---|
BOOL | bool | true | bool |
U8 | u8 | 8 | u8 |
U16 | u16 | 16 | u16 |
U32 | u32 | 32 | u32 |
U64 | u64 | 63 | u64 |
U256 | u256 | 0x…08 | U256 |
B256 | b256 | 0x01…01 | Bits256 |
STR_4 | str[4] | "fuel" | SizedAsciiString<4> |
TUPLE | (u8, bool) | (8, true) | (u8, bool) |
ARRAY | [u32; 3] | [253, 254, 255] | [u32; 3] |
STRUCT | StructWithGeneric<u8> | field_1: 8, field_2: 16 | 同名生成结构体 |
ENUM | EnumWithGeneric<bool> | VariantOne(true) | 同名生成枚举 |
二、abigen! 如何为 configurable 生成 Rust 代码
在 Rust 侧调用abigen!时,代码生成器会同时读取 ABI 中的configurables信息。从 bindings/contract.rs 可以看到,生成逻辑会将合约名与Configurables拼成配置结构体名:
let configuration_struct_name = ident(&format!("{name}Configurables")); let constant_configuration_code = generate_code_for_configurable_constants(&configuration_struct_name, &abi.configurables)?;也就是说,合约MyContract对应生成MyContractConfigurables,脚本MyScript对应生成MyScriptConfigurables(脚本绑定见 bindings/script.rs)。生成逻辑集中在 abigen/configurables.rs,其关键行为是:
- 每个 configurable 生成一个专用
with_方法。方法名由format!("with_{}", configurable.name)产生,例如STR_4→with_STR_4。该值保留了原常量名的大小写,并通过#[allow(non_snake_case)]抑制 lint 警告。 - 方法接受与 Sway 声明完全一致的 Rust 类型,类型由 ABI 中的
type_application解析而来,支持结构体、枚举、元组、数组、字符串乃至泛型实例等复杂类型。 - 方法把值编码并记录下来。生成的伪代码如下:
pub fn with_STR_4(mut self, value: SizedAsciiString<4>) -> fuels::prelude::Result<Self> { let encoded = self.encoder.encode(&[ <SizedAsciiString<4> as fuels::core::traits::Tokenizable>::into_token(value) ])?; self.offsets_with_data.push(fuels::core::Configurable { offset: <ABI 中记录的字节偏移>, data: encoded, }); Ok(self) }- 生成的配置结构体本身由三部分组成:
#[derive(Clone, Debug, Default)] pub struct MyContractConfigurables { offsets_with_data: Vec<::fuels::core::Configurable>, encoder: ::fuels::core::codec::ABIEncoder, }并附带From<MyContractConfigurables> for fuels::core::Configurables与From<…> for Vec<fuels::core::Configurable>转换实现,使其可以直接塞进部署配置。
注意:每次调用
with_XXX都会把“新值”加入offsets_with_data列表,因此该方法消费并返回self,天然支持链式调用;对同一个常量重复调用时,后者会追加在列表末尾。
三、编码后的数据如何写回字节码
with_XXX只负责编码与记录,真正改写字节码的是Configurables类型。packages/fuels-core/src/lib.rs 中定义了底层数据结构:
#[derive(Debug, Clone, Default, PartialEq)] pub struct Configurable { /// 数据在二进制中的偏移量(单位:字节) pub offset: u64, /// 与该 configurable 对应的已编码数据 pub data: Vec<u8>, } #[derive(Debug, Clone, Default, PartialEq)] pub struct Configurables { pub offsets_with_data: Vec<Configurable>, }其核心方法是update_constants_in,逻辑极其直接:读取每个Configurable.offset,把data整段覆盖写回程序二进制对应位置:
pub fn update_constants_in(&self, binary: &mut [u8]) { for c in &self.offsets_with_data { let offset = c.offset as usize; binary[offset..offset + c.data.len()].copy_from_slice(&c.data) } }这样部署上链的就不再是 ABI 里那份“默认值字节码”,而是打过补丁的新版本。Configurables还提供with_shifted_offsets(shift),当同一份字节码被塞进 loader/包装结构、常量相对偏移整体平移时,可对全部 offset 统一加减修正。
四、在部署合约时覆盖默认值
结合 e2e/tests/configurables.rs 中contract_configurables测试的完整写法,覆盖流程分三步。
4.1 生成绑定并准备新值
abigen!(Contract( name = "MyContract", abi = "e2e/sway/contracts/configurables/out/release/configurables-abi.json" )); let wallet = launch_provider_and_get_wallet().await?; let str_4: SizedAsciiString<4> = "FUEL".try_into()?; let new_struct = StructWithGeneric { field_1: 16u8, field_2: 32, }; let new_enum = EnumWithGeneric::VariantTwo;其中SizedAsciiString<4>只能容纳恰好 4 个 ASCII 字符,长度不符会在try_into()时返回错误;结构体与枚举则使用 abigen 生成的同名类型。
4.2 链式设置 configurable
let configurables = MyContractConfigurables::default() .with_BOOL(false)? .with_U8(7)? .with_U16(15)? .with_U32(31)? .with_U64(63)? .with_U256(U256::from(8))? .with_B256(Bits256([2; 32]))? .with_STR_4(str_4.clone())? .with_TUPLE((7, false))? .with_ARRAY([252, 253, 254])? .with_STRUCT(new_struct.clone())? .with_ENUM(new_enum.clone())?;- 每个
with_XXX都返回Result<Self>,因此需要?;它们会推进同一个 ABI 编码器,方法内任何编码失败(如超出编码器限制)都会在此时报错,而不是拖到部署阶段。 - 只调用其中某几个也是允许的,未覆盖的常量继续沿用 Sway 源码中的默认值。
4.3 注入配置并部署
let contract_id = Contract::load_from( "sway/contracts/configurables/out/release/configurables.bin", LoadConfiguration::default().with_configurables(configurables), )? .deploy_if_not_exists(&wallet, TxPolicies::default()) .await? .contract_id; let contract_instance = MyContract::new(contract_id, wallet.clone());LoadConfiguration(定义于 packages/fuels-programs/src/contract/regular.rs)聚合了部署所需的全部可选参数:storage(存储槽)、configurables与salt。通过with_configurables(configurables)传入的MyContractConfigurables会先经Into<Configurables>转换,再随字节码一起进入Contract内部。
从源码看,Contract::load_from最终调用Regular::new(binary, config.configurables),把字节码与配置绑定在同一种code_types::Regular中;只有当真正读取代码(如生成部署交易)时才会执行update_constants_in,从而避免“拿到裸字节码却忘了打补丁”这类隐患。仓库在code_types模块上的注释也明确写道:将 code 设为私有字段正是为了杜绝绕过 configurable 直接取用原始代码的误操作。
若不需要预编译产物路径,也可以用deploy系列 API:把configurables构建好后,经deploy_if_not_exists/deploy提交即可,二者对 configurable 的处理是同一套机制。例如同文件中的contract_manual_configurables测试展示了先Contract::load_from(..., LoadConfiguration::default())再.with_configurables(configurables)的等价写法。
五、链上验证:读取实际生效的值
部署只是写入,是否生效还需要链上证据。演示合约的return_configurables()会把全部 12 个常量打包返回,测试随后逐一断言:
let response = contract_instance .methods() .return_configurables() .call() .await?; let expected_value = ( false, // BOOL 被覆盖为 false 7, // U8 被覆盖为 7 15, // U16 被覆盖为 15 31, // U32 被覆盖为 31 63, // U64 保持默认 63 U256::from(8), // U256 保持默认 Bits256([2; 32]), // B256 被覆盖 str_4, // STR_4 被覆盖为 "FUEL" (7, false), // TUPLE 被覆盖 [252, 253, 254], // ARRAY 被覆盖 new_struct, // STRUCT 被覆盖 new_enum, // ENUM 被覆盖 ); assert_eq!(response.value, expected_value);同一目录下还有contract_default_configurables测试:它完全不调用任何with_XXX,直接以默认配置部署,期望值则全部等于 Sway 源码中的默认值(true, 8, 16, 32, 63, U256::from(8), Bits256([1; 32]), "fuel", (8, true), [253, 254, 255], …)。这组对照测试恰好说明:不覆盖则行为完全由 Sway 默认值决定,覆盖后则按新值写回,双向验证了机制的正确性。
六、脚本(Script)与谓词(Predicate)中的 configurable
configurable 并不局限于合约部署。abigen!(Script(...))会为脚本生成MyScriptConfigurables,调用侧流程见 e2e/tests/configurables.rs 中script_configurables测试:
abigen!(Script( name = "MyScript", abi = "e2e/sway/scripts/script_configurables/out/release/script_configurables-abi.json" )); let wallet = launch_provider_and_get_wallet().await?; let bin_path = "sway/scripts/script_configurables/out/release/script_configurables.bin"; let instance = MyScript::new(wallet, bin_path); // ...准备与合约版相同的新值... let configurables = MyScriptConfigurables::new(EncoderConfig { max_tokens: 5, ..Default::default() }) .with_BOOL(false)? // ...其余 with_XXX 链式调用... .with_ENUM(new_enum.clone())?; let response = instance .with_configurables(configurables) .main() .call() .await?;与合约分支的两个差异值得注意:
- 脚本配置结构体用
MyScriptConfigurables::new(EncoderConfig { … })构造,而非::default()。因为脚本通常要被打包进 loader 再执行(如script_default_configurables测试中的convert_into_loader()),编码器参数更常需要显式控制。 - 调用点为
instance.with_configurables(configurables),绑定代码会在内部调用Executable::from_bytes(binary).with_configurables(...)(见 bindings/script.rs)。当脚本被嵌入 loader 时,常量 offset 会整体平移,此时正是Configurables::with_shifted_offsets发挥作用的场景——绑定生成代码在convert_into_loader等路径中会使用平移后的偏移量重新定位。
谓词(predicate)的代码生成同样接入了generate_code_for_configurable_constants(见 abigen/bindings/predicate.rs),因此“用新常量构建谓词”的用法与脚本一致。若需为谓词/脚本源码配套的预编译二进制,可参考 deploying/the-fuelvm-binary-file.md 与 preuploading-code.md 中关于构建与预上传流程的说明。
七、EncoderConfig:编码器上限与报错排查
with_XXX在编码阶段就会受EncoderConfig约束。ABIEncoder(packages/fuels-core/src/codec/abi_encoder.rs)内置bounded_encoder(bounded_encoder.rs),通过max_depth(最大嵌套深度)与max_tokens(最大 token 数)两个计数器防止恶意/超限数据造成资源滥用。
仓库中的configurable_encoder_config_is_applied测试精确演示了这一点:当用默认MyScriptConfigurables::default()设置一个结构体常量时一切正常;而一旦换成EncoderConfig { max_tokens: 1, ..Default::default() },同一个with_STRUCT调用会立刻报错,错误信息中包含:
token limit `1` reached while encoding. Try increasing it由此可以总结两条工程经验:
- 默认配置足够覆盖绝大多数常规类型;仅当 configurable 涉及深嵌套的复杂泛型/大体积数据时才需要调大
max_tokens/max_depth。 - 编码错误发生在
with_XXX调用点而非部署时,便于在测试阶段尽早暴露配置问题。
八、使用要点小结
- Sway 侧:在
configurable { NAME: type = default, … }中声明,编译出的二进制与 ABI(含各常量 offset)是 SDK 修补的基础;u128目前在示例中被注释并标注了上游 Sway issue 的 TODO,使用时需注意版本对u128configurable 的支持情况。 - Rust 侧:
abigen!依据合约/脚本/谓词名生成<Name>Configurables,其中每个with_XXX均与源码同名同类型,天然具备编译期类型检查。 - 注入时机:合约走
LoadConfiguration::with_configurables(或Contract::load_from(...).with_configurables(...)),脚本/谓词走实例的.with_configurables(...);实际写回发生在生成部署交易、读取字节码的那一刻,通过Configurables::update_constants_in按 offset 覆盖。 - 验证闭环:让链上方法把常量原样返回,与构造端期望值做
assert_eq!,即可确认覆盖生效、默认值路径未被破坏。
更多相关材料可继续阅读:deploying/index.md(合约部署总览)、deploying/storage-slots.md(与存储槽并存的其他部署期配置)、configurables.rs(含默认值/覆盖值/脚本/编码器限制四组端到端测试)、configurables.rs(代码生成实现) 以及 packages/fuels-core/src/lib.rs 中的Configurable/Configurables底层类型。
【免费下载链接】fuels-rsFuel Network Rust SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-rs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考