Solana系统调用与SIMD-0512哈希机制

本文深入探讨了 Solana 程序的系统调用机制,特别是针对 SIMD-0512 提案中新增 sol_sha512 系统调用的讨论。文章解释了 SVM 如何通过稳定的 C ABI 实现合约与宿主机环境的通信,并分析了哈希函数在底层是如何通过内存指针进行数据传递与处理的。

Image

最近我研究了 Dean Little 提出的 SIMD 提案,特别是 SIMD-0512,该提案建议增加 sol_sha512 系统调用(syscall)。这让我深入探索了 Solana 虚拟机(SVM)的实际运作方式,特别是因为我最近在与团队合作的 CLOB 项目中使用了 sol_sha256

sol_sha256 的底层实现与调用

以下是 sol_sha256 在代码中的样子:

use core::any::type_name;
use pinocchio::{error::ProgramError, Address};

// 成本约为 140 CUs
// 直接调用 Solana 原生的 SHA256 实现
#[cfg(target_os = "solana")]
unsafe extern "C" {
    unsafe fn sol_sha256(vals: *const u8, val_len: u64, hash_result: *mut u8) -> u64;
}

#[cfg(not(target_os = "solana"))]
pub unsafe fn sol_sha256(vals: *const u8, val_len: u64, hash_result: *mut u8) {
    #[cfg(test)]
    {
        use sha2::{Digest, Sha256};
        let mut hasher = Sha256::new();

        // 将指针转换回我们的 SolBytes 结构体数组
        let slices = core::slice::from_raw_parts(vals as *const SolBytes, val_len as usize);

        for slice in slices {
            // 对切片指向的实际数据进行哈希
            let data = core::slice::from_raw_parts(slice.ptr, slice.len as usize);
            hasher.update(data);
        }

        let hash = hasher.finalize();
        core::ptr::copy_nonoverlapping(hash.as_ptr(), hash_result, 32);
    }
}

这里发生的事情本质上是我在调用一个内置的虚拟机函数,它将字符串哈希成一个 256 位的值(即 sol_sha256[u8; 32])。其工作原理如下:

  1. 传递一个指向要哈希的值的指针 [vals]
  2. 传递数据的字节长度 [val_len]
  3. 传递一个指向哈希结果存放位置的可变指针 [hash_result]
  4. 获取一个 u64 返回类型,代表指令状态(0 = 成功,非零 = 错误)。

之所以有两种 sol_sha256 的实现,是因为我的 Linux 电脑上没有原生的 sol_sha256,我需要在 Solana 虚拟机之外模拟相同的行为。

使用示例

#[repr(C)]
struct SolBytes {
    ptr: *const u8,
    len: u64,
}

#[cfg(target_os = "solana")]
unsafe extern "C" {
    fn sol_sha256(vals: *const u8, val_len: u64, hash_result: *mut u8) -> u64;
}

pub fn hash_a_string() {
    let message = "Hello, Solana!";  // 你的字符串

    // 步骤 1 - 将输入描述为系统调用能理解的切片
    // 我们不复制字符串,只是告诉系统调用:
    // “字节存在这个地址,长度是这么多”
    let slices = [
        SolBytes {
            ptr: message.as_ptr(),       // 指向 BPF 内存中字符串的指针
            len: message.len() as u64,   // 14 字节
        }
    ];

    // 步骤 2 - 分配输出缓冲区
    // 你创建这个缓冲区,以便系统调用将结果写入其中
    let mut hash_result = [0u8; 32];

    // 步骤 3 - 执行系统调用
    let status = unsafe {
        sol_sha256(
            slices.as_ptr() as *const u8,  // 指向 SolBytes 数组的指针
            slices.len() as u64,           // 有多少个切片(这里只有 1 个)
            hash_result.as_mut_ptr(),      // 写入 32 字节结果的位置
        )
    };

    // 步骤 4 - 检查是否成功
    assert_eq!(status, 0, "syscall failed");

    // hash_result 现在填充了 32 字节的哈希值
    msg!("hash bytes: {:?}", hash_result);
}

什么是 Solana Syscall?

当你在 Solana 上编写程序时,它并不会直接在验证节点 CPU 的裸机上运行。相反,它运行在 Solana 虚拟机 (SVM) 内部,使用一种称为 sBPF (Solana Berkeley Packet Filter) 的格式。这种沙盒机制对于安全性至关重要,它可以防止你的代码破坏验证节点。

你可以将系统调用(syscall)看作是一个“作弊码”或是一个通往受限沙盒环境之外的传送门。在 SIMD-0512 中,Dean Little 提议包含一个新的系统调用 sol_sha512,它产生一个 512 位的哈希值 [u8; 64],其接口与 sol_sha256 完全一致。

SHA-512 是 Ed25519 签名验证的核心原语,并且已经作为内部依赖存在于 Agave 和 Firedancer 验证节点客户端中。然而,目前它尚未作为系统调用开放给链上程序。

为什么使用 C ABI 而不是 Rust ABI?

当你下次在 Anchor、Pinocchio 或 Quasar 中执行 Rent::get()? 时,你本质上是在执行一个系统调用,可能是 sol_get_rent_sysvarsol_get_sysvar

这里有一个关键问题:如果 SVM 是用 Rust 编写的,为什么我们需要使用 "C"(系统调用 ABI)来进行系统调用?

原因在于,系统调用 ABI(应用程序二进制接口) 定义为 C 是经过深思熟虑的:

  • Rust 的 ABI 是不稳定的:如果验证节点是用一个版本的 Rust 编译的,而你的程序是用另一个版本编译的,直接的 Rust 到 Rust 调用可能会因为内存布局不同而崩溃。
  • C ABI 是稳定的、简单的且与语言无关的:它被认为是跨语言(在这种情况下是虚拟机到宿主机)通信的通用标准。
  • 这意味着用 C、C++、Rust 或任何能以 sBPF 为目标的语言编写的程序,都能以相同的方式调用相同的接口。

因此,extern "C" 并不意味着验证节点是用 C 编写的,它意味着你的程序正在跨越 ABI 边界,需要使用约定的标准来执行此操作。

深入探讨:内存与堆栈

如果我的数据存在于堆栈(stack)上,并且虚拟机接收到了指向它的指针,例如:

let slices = [
    SolBytes {
        ptr: message.as_ptr(),       // 指向 BPF 内存中数据的指针
        len: message.len() as u64,   
    }
];

那么这个指针指向的是谁的堆栈?

  1. 验证节点的原生堆栈?
  2. 还是虚拟机自己管理的某个堆栈?

这是否意味着虚拟机只能看到该指令执行时内存中的内容,从而使其在本质上是无状态的?

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

0 条评论

请先 登录 后评论
inspiration_gx
inspiration_gx
江湖只有他的大名,没有他的介绍。