Solana SBF 程序返回数据(Return Data)机制设计与实现全解析 📅 发布时间:2026/9/14 6:49:47 👁 浏览次数: Solana SBF 程序返回数据Return Data机制设计与实现全解析【免费下载链接】solanaWeb-Scale Blockchain for fast, secure, scalable, decentralized apps and marketplaces.项目地址: https://gitcode.com/GitHub_Trending/so/solana本文是 Solana 代码仓库中 return-data 设计提案 的深度解读与技术实现指南。该提案为 SBFSolana Bytecode Format程序设计了sol_set_return_data/sol_get_return_data两个新系统调用解决了 Solidity 合约风格的多值返回、跨程序调用CPI返回值传递以及 RPC 场景下返回值获取的问题。读完本文你将理解 Solana 返回数据的完整语义1024 字节上限、CPI 调用栈自动向上传播、invoke 前自动清除掌握 Rust 与 C 两套 SDK 的实际编程用法并能通过源码与测试验证每一项设计决策。问题背景Solidity 的返回值能力与 Solana 的空白在 Solidity 语言中函数可以返回任意数量的值包括变长字符串、数组、结构体等例如function foo1() public returns (string) { return Hello, world!\n; }struct S { int f1; bool f2 }; function foo2() public returns (string, int[], S) { return (a, b, c); }所有返回值都会被 Ethereum ABI 编码成变长字节数组。此外以太坊还允许返回错误信息function withdraw() public { require(msg.sender owner, Permission denied); } function failure() public { revert(I afraid I cant do that dave); }这些错误信息能帮助开发者调试问题并且可以在 Solidity 的try..catch块中被捕获而在try..catch块之外任何此类错误都会导致交易或 RPC 调用失败。而 Solana 的 SBF 程序此前缺少等价的返回值机制——程序入口只返回成功或失败的 exit code没有向调用方返回任意数据的能力。这正是本提案要解决的问题。现有解决方案及其缺陷在sol_set_return_data出现之前SolangSolana 上的 Solidity 编译器采用的方案是把返回数据写入被调用的callee账户数据。这个方案有两处天然的限制调用者账户不能使用因为 callee 可能不是同一个 SBF 程序它没有权限写入 caller 的账户数据单账户方案同样不可行有人提出维护一个单一的“返回数据账户”并在 CPI 过程中传递但同样面临 callee 无写入权限的问题。该方案的根本缺陷体现在两方面提案原文不适用于 RPC 调用RPC 无法通过写入账户数据来返回任意数量的值存在严重竞态客户端必须先提交交易Tx再读取账户数据这个过程不是原子的返回数据可能被其他交易覆盖。正是这些缺陷推动了专用返回数据缓冲区的设计。解决方案的需求定义提案明确了新机制必须同时满足三种场景场景要求RPCRPC 应能返回任意数量的值且无需写入账户数据交易Transaction一笔交易应能返回任意数量的值且无需写入账户数据CPIcallee 必须能设置返回值caller 必须能取回它其他链的方案对比以太坊EVMEVM 的RETURN操作码允许合约把一个内存缓冲区设置为 returndata该操作码接收内存指针和大小两个参数。REVERT操作码工作方式类似但语义是宣告调用失败并且所有账户数据变更都必须回滚。对于 CPI 场景调用者可以通过RETURNDATASIZE操作码获取被调用合约返回数据的长度通过RETURNDATACOPY操作码拷贝返回数据参数为内存目标指针、returndata 中的偏移量、长度。以太坊将 returndata 存储在区块中。Parity SubstrateParity Substrate 通过seal_return(u32 flags, u32 pointer, u32 size)系统调用设置返回数据flags 为 1 表示 revert0 表示成功没有定义其他值该函数不返回。CPI 场景下seal_call()系统调用接收一个缓冲区指针和缓冲区长度指针返回数据写入其中并且有32KB 的返回数据大小限制。Parity Substrate 不把返回数据写入区块。对 Solana 设计的启示对比可见Solana 采纳了专用缓冲区 系统调用读写的通用思路但把大小限制收紧为 1024 字节并在 CPI 语义上做了更精细的设计见下文。被拒绝的方案临时账户Ephemeral Accounts社区曾提出用临时账户ephemeral accounts来解决返回值问题。提案明确否决了这一方向临时账户对 CPI 场景确实可行但对 RPC 场景和交易场景不可行无法覆盖需求定义中的全部三种场景。提议方案两个新的系统调用提案的核心是新增两个系统调用配套一个**每交易唯一per-transaction**的返回数据缓冲区sol_set_return_data设置返回数据void sol_set_return_data(buf: *const u8, length: u64);callee 通过该系统调用设置返回数据返回数据上限为1024 字节超过会触发ReturnDataTooLarge错误该函数可以多次调用后一次调用会覆盖前一次写入的内容。sol_get_return_data读取返回数据u64 sol_get_return_data(buf: *mut u8, length: u64, program_id: *mut Pubkey) - u64;拷贝返回缓冲区同时返回设置该返回数据的program_id返回值是返回数据的实际长度如果没有任何返回数据被设置则返回0此时program_id不会被写入。源码中的系统调用定义与 Rust 封装两个系统调用的签名定义在 sdk/program/src/syscalls/definitions.rsdefine_syscall!(fn sol_set_return_data(data: *const u8, length: u64)); define_syscall!(fn sol_get_return_data(data: *mut u8, length: u64, program_id: *mut Pubkey) - u64);面向 Rust 合约开发者的安全封装位于 sdk/program/src/program.rs常量MAX_RETURN_DATA 1024定义了大小上限set_return_data(data: [u8])内部调用sol_set_return_data(data.as_ptr(), data.len() as u64)get_return_data() - Option(Pubkey, Vecu8)内部先申请[0u8; MAX_RETURN_DATA]缓冲区调用sol_get_return_data当返回0时返回None否则返回(program_id, 截断到实际长度的数据)。面向 C 合约开发者的头文件位于 sdk/bpf/c/inc/sol/return_data.h 与 sdk/sbf/c/inc/sol/return_data.h其中sol_get_return_data的注释特别说明返回值可能超过传入的bytes_len当返回数据更长时调用方应据此判断缓冲区是否够用。底层运行时实现系统调用的运行时实现位于 programs/bpf_loader/src/syscalls/mod.rsSyscallSetReturnData先校验len MAX_RETURN_DATA此时返回SyscallError::ReturnDataTooLarge然后通过translate_slice把 SBF 虚拟机内存中的字节安全拷贝到 Rust 侧再取得当前指令上下文中的program_id最后调用transaction_context.set_return_data(program_id, return_data)SyscallGetReturnData从transaction_context.get_return_data()取得(program_id, data)计算length.min(return_data.len())后经translate_slice_mut把数据拷回 VM 内存并把program_id写入调用方提供的 Pubkey 指针返回值始终是返回数据的真实长度而非本次拷贝的长度若长度为 0 则不写program_id。返回数据最终存储在TransactionContext的return_data: TransactionReturnData字段中读写逻辑见 sdk/src/transaction_context.rs。整个交易生命周期内只有一份缓冲区因此它在提案与 SDK 文档中被明确称为全局资源global resource需要调用方谨慎对待其内容归属。CPI 调用栈的传播与清除语义提案定义了关键的一条规则当一条指令调用sol_invoke()时被调用方callee的返回数据会被拷贝到当前指令的返回数据中。这意味着任何返回数据都会自动沿调用栈向上传递直到当前指令的调用方或 RPC 调用方。同时sol_invoke()在调用 callee 之前会先清除返回数据这样如果被调用方没有设置返回数据就不会复用上一次 invoke 遗留的旧数据。这一点在 program-runtime/src/invoke_context.rs 中有明确的源码证据每次指令执行前都会调用transaction_context.set_return_data(program_id, Vec::new())即用空数据覆盖。场景一清除语义防止数据复用A 调用 B进入 B 之前返回数据被清除B 设置了一些返回数据后返回A 调用 C进入 C 之前返回数据再次被清除C 没有设置返回数据就返回A 检查返回数据发现它是空的。场景二返回数据沿调用栈向上传播A 调用 BB 调用 CC 设置返回数据并返回B 没有触碰返回数据就返回A 获取到的返回数据来自 CA 也没有触碰返回数据最终整笔交易对外暴露的返回数据就是 C 设置的那份。program.rs中get_return_data的文档也印证了这一语义返回数据不会在 CPI 返回后被清除——一个调用过其他程序的程序可能取到并非由直接 callee 设置、而是由调用栈更深处程序设置的数据甚至递归调用自身时返回数据可能来自后续某层递归调用而非最近一次直接调用。同理外部 RPC 调用方看到的数据也可能不是它直接调用的那个程序设置的。计算成本获取和设置返回数据的计算成本按照系统调用的通用规则计费实现于 programs/bpf_loader/src/syscalls/mod.rs 与 L1281-L1290sol_set_return_data成本 len / cpi_bytes_per_unit syscall_base_cost先按字节数折算再叠加系统调用基础成本ReturnDataTooLarge校验在计费之后sol_get_return_data基础成本为syscall_base_cost若实际拷贝长度不为 0额外增加(length size_of::Pubkey()) / cpi_bytes_per_unit的字节成本。RPC 与交易场景base64 编码进入稳定日志对于普通 RPC 调用或交易返回数据会base64 编码后与sol_log字符串一起写入稳定日志stable log。该机制的实现在 program-runtime/src/stable_log.rsProgram return: program-id program-generated-data-in-base64该函数注释明确了细节若没有设置返回数据或返回数据长度为零则这一行日志不会出现。客户端可以通过解析日志中的Program return:行来获得程序返回的任意字节数据无需写入任何账户。关于错误返回的说明Solidity 在以太坊上允许合约在返回数据中返回错误信息此时该账户的所有数据变更应当回滚。Solana 的语义与之不同SBF 程序的任何非零退出码都意味着整笔交易失败。提案明确表示我们不希望通过返回成功 在返回数据中携带错误的方式支持错误返回。因为那意味着我们必须支持回滚账户数据变更这在 VM 侧和 SBF 合约侧都代价过高。因此错误将统一通过sol_log报告而不是以成功但携带错误的形式返回。测试与验证仓库提供了完整的测试覆盖可作为理解与验证该机制的活教材Program Test 集成测试program-test/tests/return_data.rsreturn_data测试一个读取方程序通过 CPI 调用写入方程序写入方set_return_data(input)回显指令数据读取方get_return_data()取回并断言与输入一致simulation_return_data测试程序设置返回数据后返回InvalidInstructionData错误通过模拟simulation路径断言SimulationError中携带了正确的program_id与返回数据——这正是错误场景下返回数据仍可被 RPC 模拟调用获取的证据。C 语言 SBF 测试programs/sbf/c/src/return_data/return_data.c 验证了关键语义程序入口处sol_get_return_data(NULL, 0, NULL)返回 0证明进入时无返回数据设置返回数据后用NULL缓冲区长调用获取真实长度只请求 4 字节子集时返回值仍是完整数据长度印证返回值是真实长度而非拷贝长度完整取出并与原文比对一致。此外 programs/sbf/c/src/invoke/invoke.c、programs/sbf/c/src/invoked/invoked.c 与 programs/sbf/rust/invoke/src/processor.rs 中均有结合 CPI 使用返回数据的示例。总结Solana 的返回数据机制最终以两个系统调用 每交易唯一缓冲区 稳定日志输出的形态落地sol_set_return_data负责写入1024 字节上限、后写覆盖sol_get_return_data负责读取返回真实长度、可选择性带回设置者program_idCPI 场景下返回数据在sol_invoke()调用前被清除、调用栈向上自动传播RPC 与交易场景则通过Program return:稳定日志以 base64 形式暴露给客户端。这一设计在保留非零退出码即整笔失败的 Solana 执行模型的同时为 SBF 合约提供了高效、原子、无竞态的任意数据返回能力。【免费下载链接】solanaWeb-Scale Blockchain for fast, secure, scalable, decentralized apps and marketplaces.项目地址: https://gitcode.com/GitHub_Trending/so/solana创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考