内部约束(Internal Constraint)
除「转移限制」等与 key / store 相关的规则外,Sui 字节码验证器还对一类 API 施加内部约束(internal constraint):调用带有泛型参数 T 的某些函数时,T 必须由当前调用模块定义——即该类型对调用方而言是「内部的」。这与 Move 类型系统在字面上的规则并不完全等同,容易在第一次遇到时报错却不解其意,因此单独说明。
规则在说什么
对受内部约束的函数,验证器要求:
泛型实参
T的「定义模块」必须与发出调用的模块相同。
换句话说,你不能在模块 A 里调用 foo<T>(),却传入仅在模块 B 中定义的类型(除非 foo 的签名与验证器规则另有规定)。该检查在发布包时完成,无法靠运行时技巧绕过。
这一机制与「转移限制」等规则目的一致:让关键操作(事件形态、部分存储相关行为等)所涉及的类型由发布该逻辑的模块掌控,降低任意模块冒充或滥用类型的风险。
典型例子:sui::event::emit
sui::event 中的 emit 在链上事件流里会标注完全限定类型名;若允许任意模块对任意类型调用 emit,语义与安全模型都会变得模糊。因此验证器要求 T 对调用模块为内部类型:
module sui::event;
/// 若调用方模块未定义 `T`,验证器会拒绝发布。
public native fun emit<T: copy + drop>(event: T);
合法:类型定义与 emit 调用在同一模块内。
module examples::exercise_internal;
use sui::event;
public struct A has copy, drop {}
public fun call_internal() {
event::emit(A {});
}
不合法:试图用其他模块定义的值类型作为事件负载(此处借标准库类型说明「非本模块定义」):
public fun call_foreign_fail() {
use std::type_name;
event::emit(type_name::get<A>());
// 验证器报错:`sui::event::emit` 必须使用当前模块定义的类型。
}
实际开发中更常见的是误用「别的包里的结构体」当事件体——同样会触发内部约束。
与「转移限制」的关系
- 转移限制主要围绕
transfer/public_transfer/share_object/freeze_object等,并与key、store能力组合相关。 - 内部约束是验证器对一类泛型 API 的横向规则;
emit<T>是其中最常踩坑的例子。
二者都体现「关键操作所操作的类型应由谁定义」这一思路,但检查的 API 集合与错误信息不同,需要对照文档与报错分别理解。
还有哪些 API 会涉及
内部约束并非对所有泛型函数生效,而是 Sui Framework(及链上相关规则)为特定函数单独声明的验证条件。除 emit 外,文档与源码中会逐步列出;编写代码时以编译器 / 验证器报错为准。
若在实现「可插拔类型」或跨模块组合时遇到「类型必须在本模块定义」类错误,应先检查是否触发了某条内部约束,再考虑调整类型定义位置或使用 public 包装、能力模式等设计。
小结
| 要点 | 说明 |
|---|---|
| 定义 | 调用方模块必须是泛型 T 的定义模块 |
| 目的 | 保证事件等关键路径上的类型由可信模块声明 |
| 与 Move 语言 | 规则由 Sui 验证器 施加,而非一般 Move 语义中的可见性 |
| 排查 | 将事件类型、相关结构体挪到发出调用的同一模块内定义,或改用允许的设计模式 |
更多关于事件与类型的实践说明,见事件系统。
关于仅定义类型所在模块可调用的 transfer / public_transfer 等与存储相关的规则,见转移限制(与 emit 的约束相关但检查点不同)。