news 2026/9/19 5:16:18

Aptos Move 中的 Friend Visibility(好友可见性):从 public/private 到细粒度模块信任

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Aptos Move 中的 Friend Visibility(好友可见性):从 public/private 到细粒度模块信任

Aptos Move 中的 Friend Visibility(好友可见性):从 public/private 到细粒度模块信任

【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core

Friend visibility 是 Move 语言 1.2 版本引入的可见性机制,为模块函数提供介于 private 与 public 之间的"白名单式"访问控制。本文以 friend visibility 变更设计文档 为核心,结合 Aptos 仓库中字节码格式、字节码验证器、编译器 v2 及 Aptos 框架的源码与真实用例,完整讲解public(friend)修饰符、friend 列表声明规则、字节码层面的实现以及设计权衡,帮助你理解并正确使用这一细粒度模块信任机制。

一、背景:传统 public/private 可见性模型的局限

在 Move 1.2 之前,一个函数只有两种可见性:public或 private(不加修饰符)。public函数可以在任何地方被调用,private 函数只能在其定义模块内被调用。这种简单模型在实际开发中暴露出三个问题。

1. 过于宽松的可见性模型

对于"仅限已知的特定模块集合访问"的函数(即白名单式访问),开发者只能将其声明为public,这违背了设计初衷。文档以 Diem 框架中的initialize函数为例:理论上这些初始化函数只应被Genesis模块调用,绝不应暴露给其他模块或脚本。但由于可见性模型的限制,它们不得不被声明为public,同时依靠运行时能力检查和静态验证来保证这些函数在非 genesis 状态下调用时会 abort——这是一种"事后补救"式的安全策略。

2. 模块后续升级不灵活

public函数是面向全世界的契约:它不能被删除、重命名,函数签名也不能被修改。原因在于,要评估修改public函数 API 的影响,必须全局扫描链上已发布的所有代码调用点——在开放发布模型的区块链网络中,这既不可行也不可扩展。

而 friend 函数只对模块的 friend 列表(白名单)构成契约,且模块所有者完全控制 friend 列表的成员。因此更新 friend 函数要容易得多:只需协调 friend 列表中的模块即可,尤其当 friend 函数与所有 friend 模块都由同一所有者维护时。

3. 规范编写与验证的简化机会

Friend visibility 能简化 Move Prover 的规范编写与验证:给定一个 friend 函数及其宿主模块的 friend 列表,可以轻松且穷尽地找到该 friend 函数的所有调用点。基于此,甚至可以完全跳过 friend 函数的规范,将其实现内联到调用方,从而简化验证技术并证明更强的性质。相比之下,public函数必须编写完整且准确的规范。

二、核心概念:四种可见性级别与 friend 列表

Friend visibility 将可见性级别从两种扩展为四种:

可见性源码写法字节码文件格式中的表示
private(无修饰符)Private
friendpublic(friend)Friend
scriptpublic(script)Script
publicpublicPublic

其中 Script 可见性解决的是 Diem 框架中一个正交的问题,详见 Script Visibility 变更文档。

从字节码层面看,Visibility枚举定义在 move-binary-format 的 file_format.rs 中:

#[repr(u8)] pub enum Visibility { /// Accessible within its defining module only. #[default] Private = 0x0, /// Accessible by any module or script outside of its declaring module. Public = 0x1, // DEPRECATED for separate entry modifier // Script = 0x2, /// Accessible by this module as well as modules declared in the friend list. Friend = 0x3, }

注意两点:

  • Friend = 0x3是一个独立的字节码值,0x2(Script)已标记为 DEPRECATED,改为独立的 entry 修饰符;
  • 工具方法is_public()Friend返回false,而is_public_or_friend()Friend返回true,说明在需要"对外可见"的判断语境中,friend 函数被归类为受限可见。

除新的public(friend)修饰符外,每个模块允许拥有一个 friend 列表,通过零个或多个friend <address::name>语句声明。friend 列表中的模块可以调用宿主模块中定义的public(friend)函数,而非 friend 模块则被禁止访问public(friend)函数。在字节码文件格式中,friend 列表是一个新的 section,由friend_decls: Vec<ModuleHandle>字段承载(见 file_format.rs)。

三、public(friend)新可见性修饰符

public(friend)可以应用于模块中的任何函数定义。一个public(friend)函数可以被以下两种函数调用:

  1. 同一模块(模块M)中的任何其他函数;
  2. 模块M的 friend 列表中任一模块所定义的任何函数。

除此之外,public(friend)函数与其他模块内函数遵循相同规则:它们可以调用同一模块中的其他函数(public(script)函数除外)、创建新的结构体实例、访问(该模块声明的类型的)全局存储等。

Aptos 框架中大量使用该特性。例如 aptos_account.move 中的register_aptburn_from_fungible_store_for_gasmint_to_fungible_store_for_gas等均为public(friend)函数;再如 aptos_coin.move 中的initialize函数:

public(friend) fun initialize(aptos_framework: &signer): (BurnCapability<AptosCoin>, MintCapability<AptosCoin>) {

这与变更文档中"initialize这类函数应只被特定模块使用"的动机完全对应——在 Aptos 框架中,aptos_coin::initialize正是通过 friend 机制仅向genesistransaction_fee等受信模块开放。

四、friend 列表声明语法

一个模块可以通过 friend 声明语句声明其他模块为好友,支持两种形式:

  • friend <address::name>— 使用完全限定模块名的 friend 声明;
  • friend <module-name-alias>— 使用模块别名(通过use语句引入)的 friend 声明。

一个模块可以有多个 friend 声明,所有 friend 模块的并集被记录在 friend 列表中。为了可读性,friend 声明通常应放在模块定义的开头附近。需要注意:Move 脚本不能声明 friend 模块,因为 friend 函数的概念在脚本中根本不存在。

以 coin.move 的实际声明为例,其模块定义开头即是三个 friend 声明:

module aptos_framework::coin { use std::error; use std::option::{Self, Option}; ... friend aptos_framework::aptos_coin; friend aptos_framework::genesis; friend aptos_framework::transaction_fee;

同样的模式还出现在 aptos_account.move,它声明了genesisresource_accounttransaction_feetransaction_validation四个 friend。在 aptos-framework 的全部源码中,friend声明的使用数量达上百处,可见该机制已是框架内部模块协作的标准手段。

五、friend 声明的规则约束

Friend 声明受以下规则约束,且这些规则并非仅停留在文档层面,而是由字节码验证器强制实施:

  1. 模块不能将自己声明为 friend,例如0x2::M不能声明0x2::M为 friend;
  2. friend 模块必须在同一账户地址下,例如0x2::M不能声明0x3::N为 friend;
  3. friend 关系不能形成循环模块依赖,例如0x2::Afriend0x2::Bfriend0x2::Cfriend0x2::A是不允许的;更一般地,声明 friend 会给 friend 模块增加对当前模块的依赖,若该 friend 模块已被直接或间接使用,就会产生依赖环;
  4. friend 在模块发布时必须存在,例如0x2::M不能声明一个加载器无法解析的0x2::X为 friend;
  5. friend 列表不能包含重复项

其中规则 1、2、5 的强制性可以在源码中找到直接证据:

  • move-bytecode-verifier/src/friends.rs 是专门的验证 pass:若friend_decls包含模块自身,返回INVALID_FRIEND_DECL_WITH_SELF;若存在地址与自身不同的 friend 模块,返回INVALID_FRIEND_DECL_WITH_MODULES_OUTSIDE_ACCOUNT_ADDRESS。源码注释明确说明:同账户地址限制是策略决定而非技术要求——VM 和字节码验证器的其他 pass 并不依赖"friend 模块必须同地址"这一假设,只是因为缺乏跨账户 friend 的明确用例、且为最小化发布流程改动,才暂时强制此约束,未来可能放开;
  • 重复项检查由 check_duplication.rs 中的check_module_handles(module.friend_decls())完成;
  • 对应的 VM 状态码定义在 move-core 的 vm_status.rs:INVALID_FRIEND_DECL_WITH_SELF = 1104INVALID_FRIEND_DECL_WITH_MODULES_OUTSIDE_ACCOUNT_ADDRESS = 1105INVALID_FRIEND_DECL_WITH_MODULES_IN_DEPENDENCIES = 1106

从编译器侧看,module_generator.rs 在生成字节码时遍历module_env.get_friend_modules(),为每个 friend 模块生成ModuleHandlepushmodule.friend_decls,从而把源码中的friend声明物化为字节码中的 friend 列表 section。

六、完整示例:模块 A 与其 friend 模块

变更文档给出了一个典型示例,展示public(friend)函数与 friend 模块的完整协作方式:

address 0x2 { module A { // friend declaration via fully qualified module name friend 0x2::B; // friend declaration via module alias use 0x2::C; friend C; public(friend) fun foo() { // a friend function can call other non-script functions in the same module i_am_private(); i_am_public(); bar(); } public(friend) fun bar() {} fun i_am_private() { // other functions in the same module can also call friend functions bar(); } public fun i_am_public() { // other functions in the same module can also call friend functions bar(); } } module B { use 0x2::A; public fun foo() { // as a friend of 0x2::A, functions in B can call friend functions in A A::foo(); } public fun bar() { 0x2::A::bar(); } } }

该示例完整展示了 friend 可见性的三条访问路径:

  • 模块A通过完全限定名friend 0x2::B;和模块别名use 0x2::C; friend C;两种方式声明 friend;
  • public(friend)函数foo可以调用同模块的 private、public 以及其他 friend 函数;
  • 同模块的 private 函数i_am_private和 public 函数i_am_public也可以调用 friend 函数bar
  • friend 模块B中的函数既可以通过use引入的别名(A::foo())调用,也可以通过完全限定路径(0x2::A::bar())调用 friend 函数。

七、设计权衡:为何选择"模块到模块"粒度

变更文档详细记录了设计过程中对 friend 粒度、声明位置和发布顺序的权衡,理解这些有助于正确运用该特性。

1. 粒度选择:Module-to-Module(已采纳)

四种候选方案中,最终选择了模块到模块的粒度——模块B是模块A的 friend,模块B中任何函数都能访问模块A中的任何 friend 函数。理由如下:

  • 模块一直是 Move 语言的信任边界:现有可见性模型以宿主模块定义 public/private;Struct/Resource 类型只有定义模块才能访问其内部结构。因此以模块作为 friend 访问的信任边界最自然;
  • 与其他语言(如 C++)的 friend 粒度呼应

未被采纳的方案及其原因:

  • Module-to-Function(模块 B 是函数 foo 的 friend):粒度过细,破坏了"模块是边界"的心智模型,且可能导致一个模块对每个 friend 函数都要重复写friend A
  • Function-to-Module(函数 foo 是模块 A 的 friend):表达上很奇怪——信任0x3::B::foo()却不信任同一模块中的0x3::B::bar(),难以设想有效用例;
  • Function-to-Function(函数 foo 是函数 bar 的 friend):除了"信任同一模块的一个函数而非另一个"的怪异感外,还会导致开发不灵活:若B::foo是 private 函数,重命名它居然需要同步更新模块 A 中的内容,private 函数将不再真正"私有"。

2. 声明位置:Callee-side(已采纳)

Friend 列表由被调用方(模块所有者)在源码中声明。优点是 friend 列表与模块源码同文件,开发者查看同一文件即可知道谁能调用 friend 函数、friend 函数应如何加固;后期增删 friend 只需更新列表并重新发布模块(受可升级性与兼容性检查约束)。

备选的 Caller-side(调用方申请友谊)方案则存在明显缺陷:代码所有者无法直观掌握 friend 关系全貌,来源真相需要额外存储(VM 可更新的字节码 section 或账户中的FriendList实体),且查看模块源码时无法获知谁可访问 friend 函数。

3. 发布顺序与跨模块引用

Friend 机制引入了跨模块引用,使发布流程复杂化。考虑如下模块:

address 0x2 { module M { friend 0x2::N; public(friend) fun foo() {} } module N { use 0x2::M; fun bar() { M::foo(); } } }

模块Nuse 0x2::M依赖M,而Mfriend 0x2::N反向引用N。在逐个模块发布的模型下:

  • 显然必须先发布M(先发布N会使N::bar()调用失败;先发布M无副作用,因为无人能调用M::foo());
  • 发布M时,字节码验证器看到指向尚不存在的N::bar()的可见性约束(前向声明),不会尝试解析该函数句柄,必须容忍这种前向引用;
  • 风险在于竞态条件:若 Alice 和 Eve 都能在0x2发布,当M发布后,Eve 可能抢在 Alice 之前发布一个恶意bar()N,滥用M开发者对 Alice 的信任。

文档给出的缓解方案有两种:

  • 仍用单模块发布模型的安全三步流程:先发布一个空的占位模块N,再发布模块M,最后发布使用 friend 函数的更新版N
  • 未来的多签名 + 多模块原子发布模型:允许一批模块(即使位于不同账户)在单笔交易中原子发布/更新,从根本上消除竞态风险,也无需三步流程。

4. 其他"共享可见性"方案的对比

  • Address visibility(地址可见性):类似 Java 的 package 概念,用地址充当命名空间,实现public(address)(即internal)。问题在于无法控制后续向该地址的发布,可能违反 Move"发布时所有绑定已知且不可更改"的原则——若同地址发布即获得他人模块内部状态访问权,就可能读写原本仅限一组模块私有的状态;
  • Package visibility(包可见性):即 .NET CLR 的internal模型,一起编译(在 Move 中即一起发布)的模块可互访内部状态。问题在于:发布后(如加载时)难以验证可见性/可访问性;版本升级可能遗留"保留权限但非本意"的模块;且 Move 尚无多模块包概念。相比而言,针对选定模块的 friend visibility 提供比包可见性更细粒度的访问控制,因此是更优的选择。

八、总结

Friend visibility 为 Move 提供了面向模块白名单的细粒度访问控制,从语言层面解决了"受限访问函数被迫 public"的历史问题:

  • 语言层面:新增public(friend)修饰符与friend <address::name>声明语句,四种可见性级别完整覆盖私有实现、好友协作、脚本入口与全局公开四种场景;
  • 字节码层面Visibility::Friend = 0x3与模块的friend_decls列表成为字节码格式的一部分(见 file_format.rs);
  • 验证层面:字节码验证器以独立 pass 强制实施"不能自我 friend、必须同账户地址、不能重复"等规则(见 friends.rs 与 check_duplication.rs);
  • 实践层面:Aptos 框架(coin.move、aptos_account.move、aptos_coin.move 等)已将 friend 机制作为模块间受信协作的标准工具,尤其用于initialize、gas 结算、代币铸造/销毁等敏感操作。

对开发者而言,在设计模块 API 时:面向全局的稳定契约用public;仅供内部实现用 private;只允许同地址下特定模块访问的敏感能力,则应优先考虑public(friend)+ friend 列表,它既提供了比public更严格的安全边界,又保留了比 private 更灵活的跨模块协作能力,同时显著降低了未来升级维护的成本。

【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

OpenHarmony Flutter插件开发:基于Plugin Platform Interface的鸿蒙适配实践

1. 为什么 OpenHarmony 插件开发不能照搬 Android 那套写法接触过 Flutter 插件开发的同行应该都有印象&#xff0c;以前写一个插件&#xff0c;主要工作就是写个 MethodChannel 的封装&#xff0c;把 Dart 侧的方法调用映射到 Android 的 Kotlin 或者 iOS 的 Swift 上。这样做…

作者头像 李华
网站建设 2026/9/19 5:18:37

WebGoat 多语言完整指南:3 种方法切换语言并解决中文界面问题

WebGoat 多语言完整指南&#xff1a;3 种方法切换语言并解决中文界面问题 【免费下载链接】WebGoat WebGoat is a deliberately insecure application 项目地址: https://gitcode.com/GitHub_Trending/we/WebGoat 打开 WebGoat&#xff0c;满屏英文的课程名和任务描述&a…

作者头像 李华
网站建设 2026/9/19 5:13:03

Flutter应用在OpenHarmony上的隐私保护适配:secure_application移植实践

团队接到适配任务那天&#xff0c;我第一反应是去Flutter社区翻了一圈&#xff0c;发现secure_application这个包在GitHub上维护比较活跃&#xff0c;覆盖Android、iOS、Web、Windows等多个平台&#xff0c;唯独没有OpenHarmony的官方支持。这个场景很典型&#xff1a;银行App要…

作者头像 李华
网站建设 2026/9/19 5:22:37

从CRUD工具人到AI狱警:内容安全工程师的转型实战指南

从什么时候开始&#xff0c;“CRUD工具人”成了我们这行最扎心的自嘲&#xff1f;天天写增删改查&#xff0c;接需求、改接口、调样式&#xff0c;忙到晚上十点&#xff0c;回头一看&#xff0c;简历上能写的东西还是那几行业务逻辑。但这两年AI起来之后&#xff0c;我发现身边…

作者头像 李华