关于 Anchor 账户大小的所有信息

  • Sec3dev
  • 发布于 2023-04-07 18:28
  • 阅读 23

本文对 Rust 中 Anchor 智能合约的账户大小计算进行了比较,分析了使用 _std::mem::sizeof 与 Anchor 官方空间参考之间的异同。尽管 _std::mem::sizeof 在大多数情况下准确,但在处理某些数据类型,如 Vec 和 Enum 时可能导致不一致的结果,因此建议开发人员根据 Anchor 文档手动计算账户大小。

0. 摘要

使用 Anchor 的智能合约要求开发者为新账户分配空间并指定账户大小。Anchor 提供了基于账户结构计算大小的指导,但许多开发者选择使用 std::mem::size_of,因为在更改账户结构时,他们不必手动更新大小。它们是否等价?在本文中,我们对 std::mem::size_of 产生的结果与 Anchor 空间参考 进行了系统比较。

虽然 std::mem::size_of 通常工作良好,并分配的空间甚至超出所需,但在某些情况下它可能返回一个较小的大小。因此,开发者在选择适当的方法之前应理解这两种方法之间的差异。

1. 比较

在 Anchor 中,开发者普遍使用 std::mem::size_of<T>() 来确定 Account 结构的大小。然而,该函数产生的大小与经过 BorshSerialize 序列化后存储在 Account 中的数据大小可能并不完全匹配。

下表总结了使用两种方法计算不同数据类型的大小。虽然大多数类型给出的结果相同,但某些数据类型,如 VectorEnum 可能会出现差异,此时 std::mem::size_of<T>() 可能返回一个较小的值。

注意:本文中的所有实验都在 64 位机器上进行

类型 std::mem::size_of<T>() Anchor 空间参考
bool 1 1
u8/i8 1 1
u16/i16 2 2
u32/i32 4 4
u64/i64 8 8
u128/i128 16 16
[T;amount] space(T) * amount space(T) * amount
Pubkey 32 32
Vec<T> 24 4 + space(T) * amount
String 24 4 + string length
Option<T> pad(space(T)),当 T 是 String、Vec<...>、Box<...><br>pad(1 + space(T)),否则 1 + space(T)
Enum 0,零变体枚举<br>space(variant),单变体枚举<br>pad(space(max discriminant) + space(max variant)),否则 1 + largest variant size

2. 差异

Rust 中的 size_of 函数返回固定值,基于类型的大小信息,可能无法准确计算某些对象占用的内存。

2.1. Vec 和 String


         指针    容量    长度
       +----------+----------+----------+
       | 0x0123   |       4  |        2 |
       +----------+----------+----------+
            |
            v
堆   +--------+--------+--------+--------+
       |    'a' |    'b' | uninit | uninit |
       +--------+--------+--------+--------+
  • Vector: 当计算 Vector 的大小时,使用 size_of 函数返回值为 24,因为在内存中,Vector 包含三个字段:指向 data 的指针、capacitylength,每个字段大小为 8 字节。
  • String: 类似地,对于字符串,size_of 函数返回值为 24,这是因为字符串在内存中也由三个字段组成:指向 data 的指针、capacitylength,每个字段大小为 8 字节。
示例

pub struct Example {
  pub example: Vec&lt;T>,
}

当计算包含类型为 T 的向量的 Example 账户的大小时,一些开发者可能使用公式 std::mem::size_of::<Example>() + (std::mem::size_of::<T>() * example.len())

这种方法是有效的,甚至可能导致账户大小超过原来的 20 字节,因为经过 BorshSerialize 序列化后,向量的内容变为长度(4 字节)加上实际内容的大小。

请注意,在计算总账户大小时,需要考虑存储向量长度所需的额外字节。

2.2. Option<T>

Option<T> 有两个变体:NoneSome。在内存中,None 变体不存储任何值,仅存储一个“标签” 0,而 Some 变体存储值以及 “标签” 1

然而,当 TBox 或其他智能指针类型时,不需要 0/1 标签。由于 Rust 中的智能指针不能为 0,因此 None 可以用 0 表示,而 Some 则可以通过指针本身的值直接表示。

因此,当使用 size_of 函数计算 Option<T> 的大小时,结果将取决于类型 T

  • 如果是 Box<...>Vec<...>String,结果为 T 的大小加上正确对齐填充后。
  • 否则,结果是 T 的大小加上 1,经过对齐填充。

相比之下,经过序列化后,实际占用的空间为 1 + space(T),因为 Option 使用 1 字节来表示 SomeNone

示例

println!("u8 = {}",                std::mem::size_of::&lt;u8>());                // 1
println!("Option&lt;u8> = {}",        std::mem::size_of::&lt;Option&lt;u8>>());        // 2

println!("u16 = {}",               std::mem::size_of::&lt;u16>());               // 2
println!("Option&lt;u16> = {}",       std::mem::size_of::&lt;Option&lt;u16>>());       // 4

println!("u32 = {}",               std::mem::size_of::&lt;u32>());               // 4
println!("Option&lt;u32> = {}",       std::mem::size_of::&lt;Option&lt;u32>>());       // 8

println!("u64 = {}",               std::mem::size_of::&lt;u64>());               // 8
println!("Option&lt;u64> = {}",       std::mem::size_of::&lt;Option&lt;u64>>());       // 16

println!("u128 = {}",              std::mem::size_of::&lt;u128>());              // 16
println!("Option&lt;u128> = {}",      std::mem::size_of::&lt;Option&lt;u128>>());      // 24

println!("Box&lt;u8> = {}",           std::mem::size_of::&lt;Box&lt;u8>>());           // 8
println!("Option&lt;Box&lt;u8>> = {}",   std::mem::size_of::&lt;Option&lt;Box&lt;u8>>>());   // 8

println!("Box&lt;u16> = {}",          std::mem::size_of::&lt;Box&lt;u16>>());          // 8
println!("Option&lt;Box&lt;u16>> = {}",  std::mem::size_of::&lt;Option&lt;Box&lt;u16>>>());  // 8

println!("Box&lt;u32> = {}",          std::mem::size_of::&lt;Box&lt;u32>>());          // 8
println!("Option&lt;Box&lt;u32>> = {}",  std::mem::size_of::&lt;Option&lt;Box&lt;u32>>>());  // 8

println!("Box&lt;u64> = {}",          std::mem::size_of::&lt;Box&lt;u64>>());          // 8
println!("Option&lt;Box&lt;u64>> = {}",  std::mem::size_of::&lt;Option&lt;Box&lt;u64>>>());  // 8

println!("Box&lt;u128> = {}",         std::mem::size_of::&lt;Box&lt;u128>>());         // 8
println!("Option&lt;Box&lt;u128>> = {}", std::mem::size_of::&lt;Option&lt;Box&lt;u128>>>()); // 8

println!("Vec&lt;u8> = {}",           std::mem::size_of::&lt;Vec&lt;u8>>());           // 24
println!("Option&lt;Vec&lt;u8>> = {}",   std::mem::size_of::&lt;Option&lt;Vec&lt;u8>>>());   // 24

println!("Vec&lt;u16> = {}",          std::mem::size_of::&lt;Vec&lt;u16>>());          // 24
println!("Option&lt;Vec&lt;u16>> = {}",  std::mem::size_of::&lt;Option&lt;Vec&lt;u16>>>());  // 24

println!("Vec&lt;u32> = {}",          std::mem::size_of::&lt;Vec&lt;u32>>());          // 24
println!("Option&lt;Vec&lt;u32>> = {}",  std::mem::size_of::&lt;Option&lt;Vec&lt;u32>>>());  // 24

println!("Vec&lt;u64> = {}",          std::mem::size_of::&lt;Vec&lt;u64>>());          // 24
println!("Option&lt;Vec&lt;u64>> = {}",  std::mem::size_of::&lt;Option&lt;Vec&lt;u64>>>());  // 24

println!("Vec&lt;u128> = {}",         std::mem::size_of::&lt;Vec&lt;u128>>());         // 24
println!("Option&lt;Vec&lt;u128>> = {}", std::mem::size_of::&lt;Option&lt;Vec&lt;u128>>>()); // 24

println!("Vec&lt;String> = {}",       std::mem::size_of::&lt;Vec&lt;String>>());       // 24
println!("Option&lt;Stirng> = {}",    std::mem::size_of::&lt;Option&lt;String>>());    // 24

2.3. Enum

通常,当对一个枚举应用 size_of 函数时,结果等于需要的判别字段大小加上最大变体的大小,同时考虑到合适的内存对齐填充。

  • 判别字段空间。对于 零变体枚举单变体枚举,不需要判别字段,因此该空间为 0。对于其他情况,它是最大判别值所需的空间或“ #[repr(inttype)]” 指定的空间。
  • 变体空间。对于不携带数据的枚举,其空间为 0。否则,它是变体字段所需的相同空间。有关详细信息,请参见 可视化 Rust 数据类型的内存布局 教程。
示例

// 零变体枚举
pub enum Foo {}

// 单变体枚举,变体大小 = 0
pub enum Bar {
    A(())
}

// 单变体枚举,变体大小 = 8
pub enum Baz {
    A(u64)
}

// 最大判别 = 1 (u8,1 字节) + 最大变体大小 = 16
pub enum Qux {
    A(u8),
    B(u128)
}

// 判别 (u16,2 字节) + 最大变体大小 = 4
#[repr(u16)]
pub enum Fred {
    A(bool),
    B(u32) = 200,
    C(u8) = 404
}

// 判别 (u64,8 字节) + 最大变体大小 = 4
#[repr(u64)]
pub enum Thud {
    A(bool),
    B(u32) = 200,
    C(u8) = 404
}

fn main() {
    println!("Foo : {}", std::mem::size_of::&lt;Foo>());  // 0
    println!("Bar : {}", std::mem::size_of::&lt;Bar>());  // 0
    println!("Baz : {}", std::mem::size_of::&lt;Baz>());  // 8
    println!("Qux : {}", std::mem::size_of::&lt;Qux>());  // 24
    println!("Fred: {}", std::mem::size_of::&lt;Fred>()); // 8
    println!("Thud: {}", std::mem::size_of::&lt;Thud>()); // 16
}

3. 结论

在 Rust 中,size_of 函数返回固定值,基于给定类型的大小信息,但可能不会准确计算对象占用的内存。

因此,在计算账户大小时,重要的是要注意使用 size_of 函数可能导致计算出的大小与实际账户大小之间的不一致。为了确保准确性,建议根据 官方 Anchor 文档 的相应值手动计算账户大小。


关于 sec3(前称 Soteria)

sec3 是一家安全研究公司,旨在为数百万用户准备 Web3 项目。启动审计是一种严格的、研究人员主导的代码审查,调查并认证主网级的智能合约;sec3 的持续审计软件平台 X-ray 与 Github 集成,逐步扫描拉取请求,帮助项目在部署之前加固代码;sec3 的后部署安全解决方案 WatchTower 确保资金安全。sec3 正在为 Web3 项目构建基于技术的可扩展解决方案,以确保协议在扩展时保持安全。

要了解有关 sec3 的更多信息,请访问 https://www.sec3.dev

  • 原文链接: sec3.dev/blog/all-about-...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Sec3dev
Sec3dev
https://www.sec3.dev/