首页
/ Union 跨链桥的链身份体系:ucs04 crate 中 UCS04 通用链 ID 标准解析、构建期代码生成与工程应用

Union 跨链桥的链身份体系:ucs04 crate 中 UCS04 通用链 ID 标准解析、构建期代码生成与工程应用

2026-09-04 12:07:19作者:俞予舒Fleming

本文围绕 Union 项目中的 lib/ucs04/README.md 展开,系统讲解 UCS04 通用链 ID(Universal Chain ID)标准的 Rust 实现。UCS04 是 Union 跨链桥生态中用于唯一标识一条网络的基础标准:用「链族名称 + 链 ID」两段式标识符消除 testnet 与 mainnet 之间的歧义。读完后,你将理解 UniversalChainId 的解析规则与校验逻辑、well-known.json 如何通过构建脚本在编译期生成常量与枚举,以及该 crate 在部署查询、chain-kitchen 域名推导和 Voyager 轻客户端插件等模块中的真实调用方式。

一、UCS04 是什么:两段式链身份标识

原始文档 lib/ucs04/README.md 给出了标准的核心定义:

Union 使用一个由两部分组成的标识符——链族名称(chain family name)与链 ID(chain ID)——来唯一标识一条网络,以此避免跨环境(testnet、mainnet)的歧义。

即通用链 ID 的字符串形态为:

<chain_family_name>.<chain_id>

文档给出的示例为 ethereum.1babylon.bbn-test-5。这种设计的价值在于:不同链上 1 这条 ID 的含义完全不同(Ethereum 主网是 1,Optimism 是 10,Babylon 是 bbn-1),单靠数字或字符串本身无法跨生态消歧;而加上链族前缀后,每个 ID 都落在一个明确命名的命名空间里。

值得注意的一点是:chain ID 部分本身可以不是纯数字。从 lib/ucs04/well-known.json 的实际内容可以看到,Cosmos 系链使用 bbn-test-6osmosis-1 这类可读字符串,而 Starknet 甚至使用 0x534e5f4d41494e 这样的十六进制表示。这说明 UCS04 对 <chain_id> 采取了宽松的字符串语义,把格式约束留给链族自身,只在分隔符上做了硬性规定。

二、crate 整体构成与 Cargo 配置

ucs04 crate 位于 lib/ucs04/,目录极为精简:

Cargo.toml 中有几处值得注意的配置:

include = ["well-known.json"]   # 发布到 crates.io 时保证数据文件随包分发

[dependencies]
serde     = { workspace = true, features = ["derive"], optional = true }
thiserror = { workspace = true }

[build-dependencies]
heck       = "0.5.0"           # 大小写风格转换(PascalCase / UPPER_SNAKE_CASE)
serde_json = { workspace = true }

[features]
serde = ["dep:serde"]
  • serde可选特性:默认不启用,只有需要跨边界(如反序列化部署配置、CLI 参数)序列化 UniversalChainId 时才打开;
  • heck 仅服务于构建期,把 well-known.json 中的家族名转换成合法的 Rust 标识符;
  • src/lib.rs 声明了 #![no_std] 并手动引入 extern crate alloc,说明该 crate 刻意保持无标准库依赖,可以嵌入对资源敏感的环境。

三、well-known.json:已知链 ID 的权威映射

lib/ucs04/well-known.json 定义了 Union 生态中所有已知的通用链 ID,是「链族 → 支持的链 ID 列表」的规范映射,用于在跨环境场景下消歧。其结构在原始文档中描述为:

  • <chain family name>:链族名称(如 ethereumbabylon);
  • 其值为该链族支持的链 ID 列表,链族名与链 ID 组合后形成通用链 ID,如 ethereum.1

当前仓库中该文件的完整内容如下(共 24 个链族):

{
  "aptos": ["2"],
  "arbitrum": ["42161", "421614"],
  "babylon": ["bbn-1", "bbn-test-6"],
  "base": ["8453", "84532"],
  "berachain": ["80069", "80084", "80094"],
  "bob": ["60808", "808813"],
  "bsc": ["56", "97"],
  "corn": ["21000000", "21000001"],
  "dydx": ["dydx-testnet-4"],
  "ethereum": ["11155111", "560048", "1"],
  "intento": ["intento-dev-1"],
  "mantra": ["mantra-dukong-1"],
  "movement": ["250", "27"],
  "neutron": ["neutron-1", "pion-1"],
  "optimism": ["10", "11155420"],
  "osmosis": ["osmo-test-5", "osmosis-1"],
  "scroll": ["534351"],
  "sei": ["pacific-1", "atlantic-2", "1328", "1329"],
  "stargaze": ["elgafar-1"],
  "starknet": ["0x534e5f5345504f4c4941", "0x534e5f4d41494e"],
  "stride": ["stride-internal-1"],
  "sui": ["35834a8a", "4c78adac"],
  "union": ["union-testnet-8", "union-testnet-9", "union-testnet-10", "union-1"],
  "xion": ["xion-testnet-2", "xion-mainnet-1"]
}

从数据分布可以读出几条实际规律:

  1. EVM 系链以数字 ID 为主:如 ethereum: ["11155111", "560048", "1"]1 是主网、11155111 是 Sepolia 测试网,同一链族内主网与测试网并列存放,这正是 UCS04「防止环境歧义」的直观体现;
  2. Cosmos 系链使用语义化 IDunion 链族同时登记了 union-1 与三个 testnet(union-testnet-8/9/10),便于部署工具在同一文件里区分环境;
  3. 非数字、非 ASCII 字母的 ID 也被容纳starknet0x534e5f4d41494e 实为 "SN_MAIN" 的 ASCII 十六进制编码,sui 使用短十六进制字符串——Id 类型对此只需满足「非空、不含 .」即可。

此外,仓库根目录的 deployments/universal-chain-ids.json 维护了一份内容相同的映射,作为部署数据侧的参照。新增链时两份文件需要同步更新,这也是构建期从 well-known.json 生成代码(见下一节)的好处:数据即代码,编译期即可暴露不一致。

四、build.rs:把 JSON 编译成 Family 枚举、常量与查询函数

lib/ucs04/build.rs 是理解这个 crate 的关键。它通过 cargo:rerun-if-changed=well-known.jsonbuild.rs)声明数据源依赖,在每次 well-known.json 变动时重新执行,并用 serde_json 将其反序列化为 BTreeMap<String, Vec<String>>build.rs),随后生成三类代码写入 OUT_DIR/out.rs,再由 src/lib.rsinclude! 拉入编译。

4.1 Family 枚举与 Display / FromStr

构建脚本为每个链族生成一个 PascalCase 变体(heck::ToPascalCase),例如 ethereumFamily::Ethereumbuild.rs),并同步生成两个 trait 实现:

  • Display for Family:把 Family::Ethereum 格式化回字符串 "ethereum"build.rs);
  • FromStr for Family:把字符串 "ethereum" 解析回枚举,未命中时返回错误类型 UnknownFamilybuild.rs)。

由此,链族是一份封闭集合:任何不在 well-known.json 里的家族名都会在解析阶段被拒绝。例如 union2.a 这样的输入会命中 UnknownFamily("union2"),这一点被 src/lib.rs 的单元测试 parse_invalid 显式覆盖。

4.2 well_known 模块:每个 (家族, 链 ID) 一个常量

对 JSON 中每一个「链族 × 链 ID」组合,脚本生成一个 constbuild.rs):

/// ```txt
/// chain_family_name: ethereum
/// chain_id:          1
/// ```
pub const ETHEREUM_1: UniversalChainId = UniversalChainId::new(Ethereum, Id::new("1").unwrap());

命名规则为 家族名大写_链ID大写并将连字符替换为下划线,因此 babylonbbn-test-6 会生成 BABYLON_BBN_TEST_6。这些常量是零成本(const)且 'static 的,下游代码可以直接拿来与解析出的 UniversalChainId 做等值比较。

4.3 FAMILY_CHAIN_IDS 数组与 is_well_known 函数

脚本还为每个链族生成一个 const [UniversalChainId; N] 数组(build.rs),如 ETHEREUM_CHAIN_IDS: [UniversalChainId; 3],以及一个全局判定函数(build.rs):

/// Check whether the specified id is well-known.
#[must_use]
pub fn is_well_known(id: &UniversalChainId<'_>) -> bool {
    [/* 所有 well_known 常量 */].iter().any(|wk| wk == id)
}

is_well_known 让调用方可以判断「任意解析出来的链 ID 是否属于已登记的生态集合」,为跨链路由、白名单校验等逻辑提供了统一的入口。

五、运行时核心:UniversalChainId、Id 与解析错误

5.1 类型结构

src/lib.rs 中的核心类型是一个带生命周期的结构体:

pub struct UniversalChainId<'a> {
    family: Family,
    id: Cow<'a, Id>,
}
  • familyCopy 的枚举,直接内嵌;
  • idCow<'a, Id> 承载,支持「借用输入字符串」与「拥有堆分配数据」两种形态。构造函数相应分两类:
    • UniversalChainId::new(family, &'a Id):借用式构造,const fn,零分配(src/lib.rs);
    • UniversalChainId::new_owned(family, Box<Id>)'static 所有权版本(src/lib.rs);
    • into_owned() 可在两种形态间转换(src/lib.rs)。

启用 serde 特性时,该类型以「字符串」形式序列化/反序列化(serde(try_from = "&'de str", into = "String")src/lib.rs),即 JSON 中直接写作 "ethereum.1",反序列化时走解析路径并天然带有校验。

5.2 解析算法与合法性约束

UniversalChainId::parsesrc/lib.rs)的逻辑非常紧凑:

pub fn parse(s: &'a str) -> Result<UniversalChainId<'a>, UniversalChainIdParseError> {
    match s.split_once('.') {
        Some((family, chain_id)) => Ok(Self {
            family: family.parse()?,
            id: Cow::Borrowed(
                Id::new(chain_id)
                    .ok_or_else(|| UniversalChainIdParseError::Id(chain_id.to_owned()))?,
            ),
        }),
        None => Err(UniversalChainIdParseError::Invalid(s.to_owned())),
    }
}

规则可以总结为三条,全部有测试佐证(src/lib.rs):

  1. 必须有且只有一个作为分隔语义的首个 .split_once 找不到分隔符返回 Invalid
  2. 链族必须属于 well-known.json 登记的封闭集合:否则返回 Family(UnknownFamily)
  3. 链 ID 非空且不能包含 .:校验函数 is_valid_idsrc/lib.rs)逐字节扫描,出现 . 即非法。因此 union.(空 ID)、union..(ID 为 .)、union.a.(ID 为 a.)都会得到 Id 错误——测试用例把这三个形态逐一断言了具体错误值。

对应的错误枚举(src/lib.rs)基于 thiserror

变体 触发条件 错误文本
Invalid(String) 字符串中没有 . missing separator
Family(UnknownFamily) 链族不在 well-known 集合中 unknown universal chain id family {0:?}
Id(String) 链 ID 为空或含 . invalid chain id {0:?}

解析入口有三种等价形态,覆盖不同的生命周期需求:

  • UniversalChainId::parse(s):借用解析,输入 &'a str 得到 UniversalChainId<'a>src/lib.rs);
  • TryFrom<&'a str>:与 parse 等价,便于泛型场景(src/lib.rs);
  • FromStr:因为该 trait 无法返回借用结果,内部先 parseinto_owned,返回 'static 版本(src/lib.rs)。

文档注释中还内置了可执行示例(src/lib.rs):

assert_eq!(
    "ethereum.1".parse::<UniversalChainId>().unwrap(),
    well_known::ETHEREUM_1,
);

它同时演示了「解析结果 == 构建期生成的 well-known 常量」这一核心用法:字符串协议与编译期常量共享同一表示,== 比较即可消歧。

Display 实现(src/lib.rs)则按 {family}.{id} 原样回写,保证 parseto_string 互为逆操作,From<UniversalChainId> for Stringsrc/lib.rs)进一步简化了向下游输出字符串的场景。

5.3 Id 类型:repr(transparent) 的 str 新类型

Idsrc/lib.rs)是对 str 的零成本包装(#[repr(transparent)]),构造入口 Id::new(s) / Id::new_owned(s) 在入口执行 is_valid_id 校验后,用指针/盒子转置把底层 str 重解释为 Id。由于校验发生在类型边界上,一旦拿到 &Id,类型系统就承诺「非空且无 .」这一不变式——后文的解析、显示、比较都不需要重复检查。

六、在 Union 仓库中的真实应用

从源码结构看,ucs04 是多个上层模块的公共身份底座,FamilyUniversalChainIdwell_known 常量在仓库中被直接消费:

6.1 部署数据的索引键:lib/deployments

lib/deployments/src/lib.rs 中,部署表的键就是通用链 ID:

pub use ucs04::UniversalChainId;

pub struct Deployments<'a>(BTreeMap<UniversalChainId<'a>, Deployment>);

deployments/deployments.json 反序列化后以 UniversalChainId 为键组织各链部署,BTreeMap 配合 UniversalChainId 派生的 PartialOrd/Ordsrc/lib.rs)获得确定性的遍历顺序。

6.2 u CLI:按链 ID 查询部署

开发工具 u 的部署子命令直接把 UniversalChainId 用作命令行参数类型(tools/u/src/deployments.rs):

pub enum Cmd {
    Print {
        chain_id: Option<UniversalChainId<'static>>,
    },
}

运行 u deployments print ethereum.1 时,clap 通过 FromStr 完成「解析 + 校验」,命中 deployments.json 后输出对应部署;解析失败会返回带语义的 UniversalChainIdParseError。该文件还把整份 deployments/deployments.jsoninclude_bytes! 编进二进制(tools/u/src/deployments.rs),离线即可查询。

6.3 chain-kitchen:从链 ID 推导 RPC 域名

lib/chain-kitchen/src/lib.rs 提供 Endpoint::from_ucs04,把 UniversalChainId 拆成 idfamily 后构造 chain.kitchen 风格的端点描述,Displayhttps://<tags>.<id>.<family>.chain.kitchen 的形态拼出域名。换言之,UCS04 的两段结构可以直接映射成 DNS 的两级子域,通用链 ID 在这里成了服务发现的基础。

6.4 Voyager 轻客户端插件:well-known 常量作为证明端点集合

CometBLS 客户端更新插件在加载配置时,直接引用构建期生成的 Starknet 链 ID 数组(voyager/plugins/client-update/cometbls/src/main.rs):

cairo_chain_ids: ucs04::well_known::STARKNET_CHAIN_IDS
    .iter()
    .map(|id| ChainId::new(id.id().to_string()))
    .collect(),

这说明 well-known.json 不只是文档性质:它通过代码生成为 const 数组后,成为 Voyager 运行时判断「哪些 Starknet 网络受支持」的编译期事实来源。

七、小结

ucs04 crate 用极小的代码面实现了 Union 生态的链身份基石:

  1. 标准<chain_family_name>.<chain_id> 两段式标识(源自 lib/ucs04/README.md),链 ID 允许任意不含 . 的非空字符串;
  2. 数据即代码well-known.jsonbuild.rs 在编译期转换为 Family 封闭枚举、逐 ID 的 well_known::* 常量、*_CHAIN_IDS 数组和 is_well_known 判定函数,数据变更即触发重新编译并同步全部下游;
  3. 类型安全UniversalChainId 通过 Cow 兼顾借用与所有权,Id 在类型边界完成校验,serde 特性让 JSON 与 CLI 场景以字符串无缝互通;
  4. 工程落地:部署索引(lib/deployments)、u CLI 查询(tools/u/src/deployments.rs)、chain-kitchen 域名推导(lib/chain-kitchen/src/lib.rs)与 Voyager 插件(voyager/plugins/client-update/cometbls/src/main.rs)都以其为公共语言。

对于要在 Union 生态新增一条链的开发者,实操路径清晰:在 lib/ucs04/well-known.json(以及同步的 deployments/universal-chain-ids.json)中登记链族与链 ID,重新构建后即可获得对应的 well_known 常量与 Family 变体,再由各上层模块沿用现有解析与索引逻辑接入。

登录后查看全文
热门项目推荐
相关项目推荐