深入剖析Solana程序中的Borsh序列化:从TypeScript到Rust再返回

本文深入讲解了Solana区块链开发中Borsh序列化机制,解释了数据如何在TypeScript客户端和Rust程序之间进行编码和解码。通过一个计数器程序的例子,详细展示了Borsh将结构体和枚举转换为字节以及字节反序列化为Rust类型,强调了其确定性、小端序以及对区块链应用的重要性。

客户端应用程序和Solana程序之间的数据传输方式,以及理解序列化对区块链开发至关重要的原因

如果你正在Solana上进行开发,你可能已经遇到过Borsh序列化,但没有完全理解其底层原理。你从JavaScript客户端发送一些数据,它神奇地出现在你的Rust程序中,经过处理,并以可读数据的形式返回。但是,在这个过程中究竟发生了什么?

今天,我们将通过构建一个简单的计数器程序,并探索数据如何在Solana生态系统的TypeScript和Rust之间流动,来揭开Borsh序列化的神秘面纱。

什么是Borsh?

Borsh(二进制对象表示序列化器用于哈希)是一种为安全关键项目设计的序列化格式。与JSON或其他基于文本的格式不同,Borsh生成紧凑、确定性的二进制输出,这对于区块链应用程序来说非常完美,因为在区块链中,每个字节都很重要,并且一致性至关重要。

将序列化视为语言之间的翻译:

  • 序列化:将Rust结构体/枚举 → 字节
  • 反序列化:将字节 → Rust结构体/枚举

示例:

让我们从一个简单的Solana程序开始,该程序维护一个计数器并支持各种操作:

use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
    entrypoint,
    account_info::{next_account_info, AccountInfo},
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    program_error::ProgramError,
};
#[derive(BorshSerialize, BorshDeserialize)]
struct Number {
    count: u32
}#[derive(BorshSerialize, BorshDeserialize)]
enum Instruction {
    Init,
    Double,
    Half,
    Add { val: u32 },
    Subtract { val: u32 },
}entrypoint!(process_instruction);fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8]  // ← 魔法发生的地方
) -> ProgramResult {
    let mut iter = accounts.iter();
    let data_account = next_account_info(&mut iter)?;

    // Security check
    if !data_account.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }    // Deserialize instruction from bytes
    let instruction = Instruction::try_from_slice(instruction_data)?;
    let mut data = Number::try_from_slice(&data_account.data.borrow())?;

    // Process the instruction
    match instruction {
        Instruction::Init => data.count = 1,
        Instruction::Double => data.count = data.count.saturating_mul(2),
        Instruction::Half => data.count = data.count / 2,
        Instruction::Add { val } => data.count = data.count.saturating_add(val),
        Instruction::Subtract { val } => data.count = data.count.saturating_sub(val),
    }

    // Serialize back to account storage
    data.serialize(&mut *data_account.data.borrow_mut())?;
    Ok(())
}

数据之旅:从客户端到程序

现在,让我们看看数据如何从我们的TypeScript客户端传输到这个Rust程序:

步骤1:客户端创建指令

import { TransactionInstruction, PublicKey } from "@solana/web3.js";
// 我们想给计数器加5
const addInstruction = {
    type: "Add",
    value: 5
};

步骤2:序列化以进行传输

这里变得有趣了。我们需要将指令转换为与Rust程序期望的字节匹配的格式:

enum InstructionType {
  Init = 0,
  Double = 1,
  Half = 2,
  Add = 3,
  Subtract = 4,
}
function createInstructionData(instructionType: InstructionType, value?: number): Buffer {
  switch (instructionType) {
    case InstructionType.Add:
      // 创建: [指令变体, 值(4字节)]
      const buffer = Buffer.alloc(5);
      buffer[0] = instructionType;  // Add的枚举值为3
      buffer.writeUInt32LE(value!, 1);  // 5,以little-endian存储
      return buffer;
    // ... 其他情况
  }
}// 创建: [3, 5, 0, 0, 0]
const instructionData = createInstructionData(InstructionType.Add, 5);

步骤3:发送交易

const instruction = new TransactionInstruction({
    programId: myProgramId,
    keys: [\
        { pubkey: dataAccount.publicKey, isSigner: true, isWritable: true }\
    ],
    data: instructionData  // [3, 5, 0, 0, 0]
});
const transaction = new Transaction().add(instruction);
// ... 签名并发送

步骤4:Solana调用你的程序

当Solana处理交易时,它会调用你的程序并提供:

  • program_id:你的程序地址
  • accounts:你指定的账户
  • instruction_data:原始字节 [3, 5, 0, 0, 0]

Borsh反序列化的魔法

Borsh在这里施展魔法。当你的程序调用:

let instruction = Instruction::try_from_slice(instruction_data)?;

Borsh会执行以下步骤:

1. 读取变体号

[3, 5, 0, 0, 0]
 ↑
第一个字节 = 3 → 这是"Add"变体

2. 检查枚举定义

enum Instruction {
    Init,        // 0
    Double,      // 1
    Half,        // 2
    Add { val: u32 },      // 3 ← 找到了!
    Subtract { val: u32 }, // 4
}

3. 读取字段数据

[3, 5, 0, 0, 0]
    ↑────────↑
    4个字节 = u32值

4. 解析Little-Endian

[5, 0, 0, 0] → u32值为5

5. 构建最终对象

Instruction::Add { val: 5 }

理解Little-Endian

你可能想知道为什么 5 会变成 [5, 0, 0, 0] 而不是 [0, 0, 0, 5]。这是由于little-endian字节序,其中最低有效字节排在前面。

可以这样理解:

  • 人类直觉:先写重要数字 → 1234
  • Little-endian:先存储易于访问的部分 → 字节顺序针对处理进行了优化

对于像我们计数器值这样的小数字,实际的数字最终会出现在第一个字节中,使得调试时易于读取。

读回状态

读取状态时也会发生相同的反向序列化:

// 获取账户数据
const accountInfo = await connection.getAccountInfo(dataAccount.publicKey);
// accountInfo.data 包含: [5, 0, 0, 0] (count = 5)
const count = accountInfo.data.readUInt32LE(0); // 读取5
console.log(`计数器值: ${count}`);

测试我们的理解

这是一个完整的测试,展示了整个流程:

test("向计数器添加值", () => {
    // 发送值为5的Add指令
    const instruction = new TransactionInstruction({
        programId,
        keys: [{ pubkey: dataAccount.publicKey, isSigner: true, isWritable: true }],
        data: createInstructionData(InstructionType.Add, 5) // [3, 5, 0, 0, 0]
    });
    const transaction = new Transaction().add(instruction);
    transaction.recentBlockhash = svm.latestBlockhash();
    transaction.feePayer = userAccount.publicKey;
    transaction.sign(dataAccount, userAccount);
    svm.sendTransaction(transaction);    // 检查结果
    const updatedAccountData = svm.getAccount(dataAccount.publicKey);

    // 计数器值以little-endian u32存储
    expect(updatedAccountData.data[0]).toBe(5);  // 我们的值
    expect(updatedAccountData.data[1]).toBe(0);  // 高位字节
    expect(updatedAccountData.data[2]).toBe(0);
    expect(updatedAccountData.data[3]).toBe(0);
});

关键要点

  1. Borsh是确定性的:相同的数据总是产生相同的字节
  2. 枚举变体自动编号:0, 1, 2, 3, 4…
  3. Little-endian很重要:数字以最低有效字节优先存储
  4. 类型安全:数据不匹配会导致反序列化失败
  5. 效率:二进制格式紧凑且解析速度快

常见的误区

1. 变体顺序改变

// 不要改变现有变体的顺序
enum Instruction {
    Init,     // 0
    Add,      // 1 ← 位置改变了!
    Double,   // 2
    // 这会破坏现有的客户端代码!
}

2. 忘记错误处理

// 始终处理反序列化错误
let instruction = Instruction::try_from_slice(instruction_data)?;
//                                                            ↑
//                                                    不要忘记这个!

3. 字节序混淆

// 错误: 手动字节操作
const wrongData = Buffer.from([3, 0, 0, 0, 5]);
// 正确: 使用正确的字节序函数
buffer.writeUInt32LE(5, 1);

结论

理解Borsh序列化对于Solana开发至关重要。它是客户端应用程序和链上程序之间的桥梁,确保了数据完整性并实现了高效通信。

下次你在Solana程序中看到 instruction_data: &[u8] 时,你就会确切地知道这些字节是如何到达那里,以及它们如何转换回有意义的Rust类型。这种理解将使调试更容易,帮助你设计更好的API,并在构建复杂的Solana应用程序时充满信心。

请记住:指令数据中的每个字节都具有意义,Borsh是解读这一切的翻译器。

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

0 条评论

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