在 Solidity 中,像读写存储、合约间调用或发送消息这样的底层操作,是通过使用 Yul 操作码(如 call、sload 和 sstore)的内联汇编直接执行的。这些操作码绕过了 Solidity 的高级抽象。
Cairo 通过系统调用(syscall)引入了类似的概念。Syscall 是从 Starknet 合约向 Starknet OS 发起的底层调用,用于执行普通 Cairo 代码无法独立完成的操作,例如调用其他合约、部署合约、触发事件、读取执行上下文或访问存储。
在本文中,我们将详细介绍可用的各种 syscall,以及它们在 Starknet 合约中的使用方法。
Syscall 及其在 Solidity 中的等效形式
以下列表包含了当前所有可用的 Starknet syscall 及其在 Solidity 中最接近的等效形式(如适用)。
storage_read_syscall和storage_write_syscall分别等同于 Solidity 汇编中的 sload 和 sstore。get_block_hash_syscall等同于 blockhash(),返回给定区块的哈希值。call_contract_syscall类似于用于合约调用的 address.call()。deploy_syscall类似于 create2,用于在可预测的地址部署合约。emit_event_syscall类似于 event + emit。两者都用于记录数据以供链下索引。keccak_syscall等同于 keccak256。get_class_hash_at_syscall类似于 address.codehash,它返回合约字节码的哈希值。library_call_syscall类似于 delegatecall,它在当前合约的执行上下文中执行另一个合约的代码。replace_class_syscall、get_execution_info_syscall、get_execution_info_v2_syscall、send_message_to_l1_syscall、sha256_process_block_syscall和meta_tx_v0_syscall在 Solidity 中没有直接的等效形式。
现在让我们看看上述 syscall 在实践中是如何使用的。
用于读写存储的 Syscall
与 Cairo 仅能读写声明在 Storage 结构体中的存储变量的高级方法 .read() 和 .write() 不同,storage_read_syscall 和 storage_write_syscall 直接对原始存储槽进行读写操作。它们不需要事先声明存储变量。只要知道某个槽的存储地址,你就可以直接对其进行读取或写入。
存储读取 syscall
storage_read_syscall 函数的签名如下:
fn storage_read_syscall(
address_domain: u32, address: StorageAddress,
) -> SyscallResult<felt252>;
这个 syscall 接受两个参数;
address_domain:决定存储操作使用哪种数据可用性模式。目前仅支持 domain0,因此现阶段始终传入0。address:要读取的存储地址(槽位),类型为StorageAddress。
storage_read_syscall 返回一个 felt252 值。SyscallResult 只是 Cairo 中用于 syscall 的 Result 类型,因此你可以通过 .unwrap_syscall() 将其解包(若失败则引发 panic 并回滚),或者显式地使用 match 处理错误,从而在不回滚的情况下自定义错误处理逻辑。
存储写入 syscall
storage_write_syscall 函数的签名如下:
fn storage_write_syscall(
address_domain: u32, address: StorageAddress, value: felt252,
) -> SyscallResult<()>;
这个 syscall 接受三个参数;address_domain、address 以及
value:要写入存储地址的值(felt252)。
它返回 SyscallResult<()>,其中 () 意味着成功时没有返回值,唯一重要的是它是否执行成功。
storage_read_syscall 和 storage_write_syscall 都围绕一个关键参数 address 展开。在我们能够有效使用它们之前,需要先了解 Cairo 是如何计算这些存储地址的。
计算存储变量的存储地址(槽位)
在 Solidity 中,每个存储变量在编译时都会被分配一个连续的槽号:第一个声明的变量获得槽位 0,第二个获得 1,依此类推。EVM 通过槽号来识别变量。在汇编中,我们可以直接使用 .slot 获取变量的槽位:
contract Hello {
uint256 public a; // slot 0
uint256 public b; // slot 1
function getSlot() public {
assembly {
let x := b.slot
}
}
}
在这里,b.slot 的解析结果为 1,因为 b 是第二个声明的变量。
在升级合约时,Solidity 的这种槽号分配模型会带来问题。如果新版本的合约删除或重新排序了存储变量,槽号就会发生偏移,导致新变量读取到属于旧版本中不同变量的数据。
在 Cairo 中,存储地址是由变量名推导而来的。Cairo 计算每个存储变量名称的 sn_keccak 哈希值,并将结果用作其存储地址。sn_keccak 是 Starknet 版本的以太坊 keccak256,截断为前 250 位。
下面是上述 Solidity 合约对应的 Cairo 等效实现。它使用 selector! 宏来计算变量的存储槽。后面的解释将分解代码的相关部分。
use starknet::storage_access::StorageAddress;
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn get_slot(self: @TContractState) -> StorageAddress;
}
#[starknet::contract]
mod HelloStarknet {
// *** IMPORTS *** //
use starknet::storage_access::{
StorageAddress,
storage_address_from_base,
storage_base_address_from_felt252,
};
#[storage]
struct Storage {
a: u256,
b: u256
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn get_slot(self: @ContractState) -> StorageAddress {
// The `selector!` macro can be used to compute {sn_keccak}.
let selector_in_felt = selector!("b");
// Convert `felt252` to `StorageBaseAddress` type
let slot_base = storage_base_address_from_felt252(selector_in_felt);
// Convert `StorageBaseAddress` to `StorageAddress` type
storage_address_from_base(slot_base)
}
}
}
以下是对上述代码的逐步解释:
-
我们引入所需的类型和辅助函数:
// *** IMPORTS *** // use starknet::storage_access::{ StorageAddress, storage_address_from_base, storage_base_address_from_felt252, };这些辅助函数允许我们将原始的
felt252转换为合法的StorageAddress类型,这正是存储读写 syscall 所期望的类型。 -
在
get_slot函数中,selector!("b")会计算出sn_keccak("b")的值,类型为felt252:// The `selector!` macro can be used to compute {sn_keccak}. let selector_in_felt = selector!("b"); -
结果使用
storage_base_address_from_felt252方法转换为StorageBaseAddress:// Convert `felt252` to `StorageBaseAddress` type let slot_base = storage_base_address_from_felt252(selector_in_felt);felt252只是一个数字,而 Cairo 不会自动将数字视为存储地址。将其转换为StorageBaseAddress并不会改变底层的值。相反,它改变了 Cairo 处理该值的方式,即它现在知道这个数字应该被用作存储地址,而不是普通的整数。换句话说,这种转换并没有修改数据本身,而是为它分配了一个存储地址类型,以便它能在存储操作中使用。 -
然后,使用
storage_address_from_base将该基地址转换为StorageAddress。StorageAddress是存储 syscall 期望的类型。// Convert `StorageBaseAddress` to `StorageAddress` type storage_address_from_base(slot_base)StorageAddress正是存储读写 syscall 所要求的确切格式。可以将其视为地址的最终、可用形态。这是 Cairo 明确区分“纯数字”与“准备好在 syscall 中使用的合法存储地址”的方式。
现在我们知道如何计算已声明变量的存储槽位了,接下来就是对某个槽位进行读写。
通过 Syscall 读写存储变量 b
下面的合约在前面的示例基础上增加了两个新函数:
- 一个用于写入变量
b; - 另一个用于读取该变量;
均直接使用 syscall:
use starknet::storage_access::StorageAddress;
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn get_slot(self: @TContractState) -> StorageAddress;
// *** NEWLY ADDED FUNCTIONS *** //
fn write_to_b_low_level(ref self: TContractState);
fn read_from_b_low_level(self: @TContractState) -> felt252;
}
#[starknet::contract]
mod HelloStarknet {
use starknet::storage_access::{
storage_address_from_base,
storage_base_address_from_felt252,
StorageAddress,
};
// *** NEWLY ADDED IMPORTS *** //
use starknet::syscalls::{
storage_read_syscall,
storage_write_syscall
};
use starknet::SyscallResultTrait;
#[storage]
struct Storage {
a: u256,
b: u256
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
// *** LOW-LEVEL WRITE TO VARIABLE `b` *** //
fn write_to_b_low_level(ref self: ContractState) {
// Get the slot for variable `b`
let slot = self.get_slot();
// Perform a syscall to write the value 4 into variable `b`
let _ = storage_write_syscall(0, slot, 4);
}
// *** LOW-LEVEL READ FROM VARIABLE `b` *** //
fn read_from_b_low_level(self: @ContractState) -> felt252 {
// Get the slot for variable `b`
let slot = self.get_slot();
// Perform a syscall to read from variable `b`
// `.unwrap_syscall()` unwraps the syscall result and panics if it failed
storage_read_syscall(0, slot).unwrap_syscall()
}
fn get_slot(self: @ContractState) -> StorageAddress {
let selector_in_felt = selector!("b");
let slot_base = storage_base_address_from_felt252(selector_in_felt);
storage_address_from_base(slot_base)
}
}
}
以下是上述代码相关部分的详细解释:
-
向
IHelloStarknet接口添加了两个函数:一个用于写入存储变量b,另一个用于读取。// *** NEWLY ADDED FUNCTIONS *** // fn write_to_b_low_level(ref self: TContractState); fn read_from_b_low_level(self: @TContractState) -> felt252; -
存储 syscall 与
SyscallResultTrait(用于解包 syscall 结果)。// *** NEWLY ADDED IMPORTS *** // use starknet::syscalls::{ storage_read_syscall, storage_write_syscall }; use starknet::SyscallResultTrait;添加了两个新的引入。
- 首先,从
starknet::syscalls引入storage_read_syscall和storage_write_syscall。这些是我们将用来直接读写存储的底层 syscall,以替代 Cairo 的高级.read()和.write()方法。 - 其次,从
starknet引入SyscallResultTrait,需要使用它对 syscall 返回的结果调用.unwrap_syscall()。
- 首先,从
-
在写入函数中,计算
b的存储槽并将其传给storage_write_syscall,向该槽位写入值4。// *** LOW-LEVEL WRITE TO VARIABLE `b` *** // fn write_to_b_low_level(ref self: ContractState) { // Get the slot for variable `b` let slot = self.get_slot(); // Perform a syscall to write the value 4 into variable `b` let _ = storage_write_syscall(0, slot, 4); } -
在读取函数中,计算出相同的槽位并传给
storage_read_syscall以读取数值,这会返回一个Result。对其调用.unwrap_syscall()会产生原始的felt252值。// *** LOW-LEVEL READ FROM VARIABLE `b` *** // fn read_from_b_low_level(self: @ContractState) -> felt252 { // Get the slot for variable `b` let slot = self.get_slot(); // Perform a syscall to read from variable `b` // `.unwrap_syscall()` unwraps the syscall result and panics if it failed storage_read_syscall(0, slot).unwrap_syscall() }
使用 Syscall 访问任意存储槽
到目前为止,我们只对在 Storage 结构体中显式声明过的存储变量进行了读写。但是,如果你需要访问从未声明过的存储槽(例如,为了调试而检查原始存储、构建代理合约或与其他合约的存储布局进行交互)怎么办?
这正是任意槽位访问派上用场的地方。我们不再通过 selector!() 从变量名派生槽位,而是直接将槽位值作为原始的 felt252 提供。
转换过程与我们前面介绍的完全相同:将原始的 felt252 转换为 StorageBaseAddress,再转换为 StorageAddress。唯一的区别在于槽位值的来源。
下面的合约直接读取并写入存储槽 1:
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn write_to_slot1_low_level(ref self: TContractState);
fn read_from_slot1_low_level(self: @TContractState) -> felt252;
}
#[starknet::contract]
mod HelloStarknet {
// IMPORTS
use starknet::syscalls::{
storage_read_syscall,
storage_write_syscall
};
use starknet::storage_access::{
storage_address_from_base,
storage_base_address_from_felt252
};
use starknet::SyscallResultTrait; // for .unwrap_syscall()
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
// LOW-LEVEL WRITE TO SLOT 1
fn write_to_slot1_low_level(ref self: ContractState) {
// Convert `felt252` to `StorageBaseAddress` type
let slot_base = storage_base_address_from_felt252(1);
// Convert `StorageBaseAddress` to `StorageAddress` type
let slot = storage_address_from_base(slot_base);
// Perform a syscall to write the value 4 into the slot 1
let _ = storage_write_syscall(0, slot, 4);
}
// LOW-LEVEL READ FROM SLOT 1
fn read_from_slot1_low_level(self: @ContractState) -> felt252 {
// Convert `felt252` to `StorageBaseAddress` type
let slot_base = storage_base_address_from_felt252(1);
// Convert `StorageBaseAddress` to `StorageAddress` type
let slot = storage_address_from_base(slot_base);
// Perform a syscall to read from the slot 1
// `.unwrap_syscall()` unwraps the syscall result and panics if it failed
storage_read_syscall(0, slot).unwrap_syscall()
}
}
}
在这个示例中,在读写存储地址(槽位)1 之前,我们将值 1 从 felt252 转换为 StorageBaseAddress 类型,然后再转换为 StorageAddress,这是这两种 syscall 要求的存储地址类型。
跨合约调用 Syscall
call_contract_syscall 用于执行底层的跨合约调用,类似于 Solidity 中的 address.call()。它的函数签名如下:
fn call_contract_syscall(
address: ContractAddress,
entry_point_selector: felt252,
calldata: Span<felt252>,
) -> SyscallResult<Span<felt252>>;
它接受三个参数;
address:被调用合约的地址。entry_point_selector:被调用函数的选择器,通过selector!()宏从函数名推导得出。calldata:包含传给函数的参数的Span<felt252>。
该 syscall 返回一个 Span<felt252>,因为在 syscall 层面,Cairo 并不知道被调用函数的返回类型,它可能返回单个值、元组、结构体,或者什么都不返回。
为了统一处理所有这些情况,返回数据被序列化为一个展平的 felt252 序列。例如,如果被调用函数返回一个 u256(它在内部表示为两个 u128 值 —— 低位和高位),结果将作为 Span 中的两个 felt252 元素返回。然后由调用方负责将返回的 Span 反序列化回预期的类型。
let result: u256 = u256 {
low: ret_data_low_felt252,
high: ret_data_high_felt252,
};
下面的代码演示了在实践中如何使用 call_contract_syscall 来调用另一个合约的 transfer 函数。请注意如何使用 selector!() 宏从名称派生函数选择器,以及如何将 calldata 作为 Span<felt252> 传递:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn call_something(ref self: TContractState, target: ContractAddress, calldata: Span<felt252>);
}
#[starknet::contract]
mod HelloStarknet {
// IMPORTS
use starknet::syscalls::call_contract_syscall;
use starknet::SyscallResultTrait; // for .unwrap_syscall()
use starknet::ContractAddress;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn call_something(
ref self: ContractState,
target: ContractAddress,
calldata: Span<felt252>,
) {
// selector for `transfer(felt252,u128)`
let selector = selector!("transfer");
// CALL `target` CONTRACT
let _ = call_contract_syscall(target, selector, calldata).unwrap_syscall();
}
}
}
在 call_something 函数中,selector! 宏将函数名 "transfer" 哈希成一个 felt252 值,该值唯一标识目标合约中的入口点(与 Solidity 中函数选择器的概念相同,keccak256("transfer(address,uint256)") 会生成一个 4 字节的选择器)。当我们准备好选择器和 calldata 后,便将它们与目标地址一起传递给 call_contract_syscall 以执行实际的调用。
如果调用失败会发生什么?
与 Solidity 的 address.call() 返回一个布尔值指示成功或失败并允许调用方优雅处理不同,call_contract_syscall 不提供这种选项。如果被调用的合约因任何原因回滚(revert),该失败会立即向上传播并导致整个交易回滚,没有办法在链上捕获或恢复。在设计依赖跨合约调用的合约时,要牢记这一重要区别。
部署新合约
deploy_syscall 是 Cairo 创建合约的底层方式,类似于 Solidity 的 create2。它在可预测的地址部署合约。
deploy_syscall 的函数签名如下:
fn deploy_syscall(
class_hash: ClassHash,
contract_address_salt: felt252,
calldata: Span<felt252>,
deploy_from_zero: bool,
) -> SyscallResult<(ContractAddress, Span<felt252>)>;
此 syscall 接受以下参数:
class_hash:要部署的合约类标识符,类型为ClassHash。contract_address_salt:用于确定性地计算已部署合约地址的盐值 (salt)。类型为felt252。calldata:包含构造函数参数的Span<felt252>。deploy_from_zero:一个布尔值 (bool),指示合约地址是通过调用者的地址计算,还是使用0作为部署者地址来计算。
deploy_syscall 返回一个元组 (ContractAddress, Span<felt252>)。第一个元素是新部署合约的地址。第二个元素是一个 felt252 的 span,表示合约构造函数的返回数据。正如 call_contract_syscall 一样,构造函数返回值被展平为连续的 felt 序列,这使得处理多种不同类型的返回值成为可能。
以下代码部署了一个示例计数器合约,其构造函数接受一个初始值:
use starknet::ClassHash;
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn deploy_counter(ref self: TContractState, class_hash: ClassHash, initial: felt252);
}
#[starknet::contract]
mod HelloStarknet {
// IMPORTS
use starknet::ClassHash;
use starknet::SyscallResultTrait; // for .unwrap_syscall()
use starknet::syscalls::deploy_syscall;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn deploy_counter(ref self: ContractState, class_hash: ClassHash, initial: felt252) {
// Prepare constructor calldata as Span<felt252>
let mut calldata: Array<felt252> = ArrayTrait::new();
calldata.append(initial);
let calldata_span = calldata.span();
// Salt for deterministic address
let salt: felt252 = 123;
// Deploy the contract
let (_contract_address, _) = deploy_syscall(class_hash, salt, calldata_span, false)
.unwrap_syscall();
}
}
}
deploy_counter 函数将构造函数参数序列化为 Span<felt252>,然后使用 class hash、盐值和构造函数 calldata 调用 deploy_syscall。
用于触发事件的 Syscall
emit_event_syscall 是在 Starknet 上记录事件的底层原语。它的用途与底层 Solidity 中的 logN() 相同,其中 N 是主题 (topics) 的数量。但与 EVM 的 log 操作码最多支持 4 个索引主题不同,Starknet 支持多达 50 个键 (keys/topics)。
emit_event_syscall 的函数签名如下:
fn emit_event_syscall(
keys: Span<felt252>, data: Span<felt252>,
) -> SyscallResult<()>;
它接受两个参数:
keys:包含用于过滤和搜索事件的索引主题(相当于 Solidity 事件中的indexed参数)的Span<felt252>。data:包含非索引事件数据的Span<felt252>。
emit_event_syscall 返回 SyscallResult<()>:成功时值为 (),失败时返回错误而不会中止执行,允许优雅地处理错误。
试看下面这个包含 2 个主题记录事件的 Solidity 合约。不要过于纠结代码在做什么,直接跳转到 FOCUS HERE 注释即可:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Hello {
// Event to emit
event MyEvent(address indexed from, uint256 amount);
function emitMyEvent(bytes32 t0, bytes32 t1) external {
assembly {
// Write the non-indexed data (amount = 4) to memory ptr 0x00
mstore(0x00, 4)
// **** FOCUS HERE ****
// _____data_____ _____keys_____
// | | | |
// log2(memPtr, memSize, topic0, topic1)
log2(0x00, 0x20, t0, t1)
}
}
}
上述代码中的 log2 操作码:
- 从内存位置
0x00(memPtr)开始读取0x20字节(memSize)的数据:这是事件数据(非索引的)。 - 使用
t0和t1作为两个主题(带索引的)。
从而将非索引数据与主题分离开来。
以下是使用 emit_event_syscall 的 Cairo 等效实现:
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn emit_my_event(ref self: TContractState, amount: felt252);
}
#[starknet::contract]
mod HelloStarknet {
use starknet::get_caller_address;
use starknet::SyscallResultTrait; // for .unwrap_syscall()
use starknet::syscalls::emit_event_syscall;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn emit_my_event(ref self: ContractState, amount: felt252) {
// event MyEvent(address indexed from, uint256 amount);
let t0: felt252 = selector!("MyEvent");
let t1: felt252 = get_caller_address().into(); // like msg.sender
// ---- keys (topics) ----
let mut keys: Array<felt252> = ArrayTrait::new();
keys.append(t0); // topic0 = event selector
keys.append(t1); // topic1 = indexed "from"
// ---- data (non-indexed) ----
let mut data: Array<felt252> = ArrayTrait::new();
data.append(amount);
// low-level syscall (assembly-like)
// __keys___ ___data___
// | | | |
emit_event_syscall(keys.span(), data.span()).unwrap_syscall();
}
}
}
注意 emit_event_syscall 是如何明确地将 keys 和 data 作为两个独立的 Span<felt252> 参数分离的,这与 log2 将用于数据的内存指针和大小与用于主题的参数区分开来的方式类似。
获取区块哈希
在 Solidity 中,使用内置的 blockhash(blockNumber) 获取区块哈希非常简单(仅对最近的 256 个区块有效)。Cairo 通过 get_block_hash_syscall 提供了类似的功能,但具有不同的可用性限制。
此 syscall 可用于 first_v0_12_0_block(网络升级到 Starknet v0.12.0 后生成的第一个 Starknet 区块号)到 current_block - 10 范围内的区块号。存在 10 个区块的延迟是因为新区块在安全暴露其哈希之前必须被最终确认 (finalized),这意味着你不能查询最近 10 个区块的哈希。请求该范围之外的任何内容都会抛出 BLOCK_NUMBER_OUT_OF_RANGE 错误。
get_block_hash_syscall 函数签名如下:
fn get_block_hash_syscall(
block_number: u64,
) -> SyscallResult<felt252>;
它接受一个 u64 类型的参数 block_number,并返回一个代表被请求区块哈希的 felt252。
这是一个简单的合约,用于检索给定区块号的区块哈希:
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn get_block_hash(self: @TContractState, block_number: u64) -> felt252;
}
#[starknet::contract]
mod HelloStarknet {
// IMPORTS
use starknet::SyscallResultTrait; // for .unwrap_syscall()
use starknet::syscalls::get_block_hash_syscall;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn get_block_hash(self: @ContractState, block_number: u64) -> felt252 {
// Call the syscall and unwrap the result
// Will throw if the `block_number` is outside the valid range
let block_hash: felt252 = get_block_hash_syscall(block_number).unwrap_syscall();
// Return the block hash as a felt252
block_hash
}
}
}
获取执行上下文 (Execution Context)
在 Solidity 中,像 block.timestamp、msg.sender、tx.origin 这样的值可以作为内置全局变量来使用。
Cairo 不为这些值提供内置的全局变量。相反,你必须通过调用 get_execution_info_syscall 来获取它们。
get_execution_info_syscall 函数签名如下:
fn get_execution_info_syscall() ->
SyscallResult<Box<starknet::info::ExecutionInfo>>
它不接受任何参数,并返回包装在 SyscallResult 中的 Box<ExecutionInfo> 结构体。该结构体包含调用者地址、区块信息、交易信息以及其他执行上下文数据的字段,如下所示:

返回的结构体 (Box<ExecutionInfo>) 随后可以被解构,以提取感兴趣的特定字段,例如调用者地址、区块信息、交易信息等。
以下代码示例展示了如何从执行上下文中检索调用者地址和区块时间戳:
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn context_info(self: @TContractState);
}
#[starknet::contract]
mod HelloStarknet {
// IMPORTS
use starknet::SyscallResultTrait; // for .unwrap_syscall()
use starknet::syscalls::get_execution_info_syscall;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn context_info(self: @ContractState) {
// Call syscall
let info = get_execution_info_syscall().unwrap_syscall();
// Extract sender and timestamp
let (_sender, _timestamp) = (info.caller_address, info.block_info.block_timestamp);
}
}
}
context_info 函数调用 get_execution_info_syscall() 来检索执行上下文。它直接从返回的结构体中访问 caller_address (info.caller_address),并从嵌套的 block_info 字段中访问 block_timestamp (info.block_info.block_timestamp)。
注意:为简单起见,本例仅提取而未返回或使用这些值。在实际合约中,通常会返回这些值或将它们用于访问控制、时序逻辑或其他逻辑。
获取执行上下文 v2
这类似于获取执行上下文,区别在于 TxInfo 字段被替换为 v2::TxInfo,它包含了原始的交易字段以及较新交易版本中引入的额外字段。
下图展示了包含新 v2::TxInfo 的 ExecutionInfo 结构体:

v2::TxInfo 中的较新字段(以红色高亮显示)被添加以支持 V3 交易。它们描述了交易被允许使用多少网络资源、费用支付方式、特定交易数据的存储位置,以及交易是否包含账户部署数据。
resource_bounds:设置交易允许消耗的资源上限。你可以把它看作是预算:交易声明了它愿意为多少网络工作量买单。在 V3 中,这是描述交易费用机制的一部分。tip:附加到交易中的额外金额,通过使交易优先于其他交易来激励更快的处理。paymaster_data:包含 paymaster(支付交易费用的发送方以外的账户)地址的数据以及发送给 paymaster 的额外数据。该额外数据并非由协议固定,它的存在是为了让 paymaster 可以强制执行自己的逻辑来赞助交易。nonce_data_availability_mode:指定交易 nonce 所使用的数据可用性模式。换句话说,它指示交易的 nonce 数据应发布在 L1(以太坊)还是 L2(Starknet)。- L1 数据可用性模式为 0(默认)。
- L2 数据可用性模式为 1。
fee_data_availability_mode:指定用于支付交易费用的账户余额的数据可用性模式。与上面的 nonce 字段类似,它决定与费用相关的账户数据是发布在 L1 还是 L2。account_deployment_data:当你想将账户合约部署和交易执行合并到一个操作中时使用该字段。它包含部署参数(class hash、地址盐值和构造函数 calldata)。当被填充时,它会在执行交易前部署账户合约。当为空时,它跳过部署并从现有账户合约执行。
以下代码示例演示了如何从执行上下文 (v2) 中检索交易的 tip:
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn context_info(self: @TContractState);
}
#[starknet::contract]
mod HelloStarknet {
// IMPORTS
use starknet::syscalls::{get_execution_info_v2_syscall};
use starknet::SyscallResultTrait; // for .unwrap_syscall()
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn context_info(self: @ContractState) {
// Call syscall
let info = get_execution_info_v2_syscall().unwrap_syscall();
// Extract tip
let _tip = info.tx_info.tip;
}
}
}
获取 Class Hash - get_class_hash_at_syscall
在 Starknet 中,每个已部署的合约都与一个 class hash 相关联,这是代表其编译合约类 (contract class) 的唯一标识符(类似于 EVM 中的字节码哈希)。
get_class_hash_at_syscall 用于检索给定地址的任何合约的 class hash。这类似于 Solidity 中的 address.codehash,后者返回地址处的字节码哈希。
get_class_hash_at_syscall 函数签名如下:
fn get_class_hash_at_syscall(contract_address: ContractAddress) ->
SyscallResult<ClassHash>
它接受一个参数 contract_address(已部署合约的地址),并返回一个表示该地址合约类哈希的 ClassHash。
以下代码示例使用 get_class_hash_at_syscall 检索并返回一个合约的 class hash:
use starknet::{ContractAddress, ClassHash};
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn get_class_hash(self: @TContractState, target: ContractAddress) -> ClassHash;
}
#[starknet::contract]
mod HelloStarknet {
// IMPORTS
use starknet::{ContractAddress, ClassHash};
use starknet::SyscallResultTrait; // for .unwrap_syscall()
use starknet::syscalls::get_class_hash_at_syscall;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn get_class_hash(self: @ContractState, target: ContractAddress) -> ClassHash {
// Retrieve and return the class hash of the contract at `target`.
get_class_hash_at_syscall(target).unwrap_syscall()
}
}
}
Library_call syscall
在 Solidity 中,在调用者存储上下文中执行另一个合约的代码是通过 delegatecall 操作码完成的。Cairo 通过 library_call syscall 提供类似的机制,该机制调用在不同 class hash(合约编译代码的标识符)中定义的函数,同时保持调用者的存储上下文。
这意味着,尽管执行的代码来自目标合约,但对存储的任何读写都会影响调用合约的存储,而不是目标合约本身的存储。
library_call_syscall 函数签名如下:
fn library_call_syscall(
class_hash: ClassHash, function_selector: felt252, calldata: Span<felt252>,
) -> SyscallResult<Span<felt252>>;
它接受以下参数:
class_hash:包含要调用函数的类哈希(合约代码哈希)。类型为ClassHash。function_selector:要执行的函数选择器。类型为felt252。calldata:传给该函数的参数数组。类型为Span<felt252>。
它以 Span<felt252> 形式返回被调用函数的返回数据,并包装在 SyscallResult 中以处理潜在的错误。
下面的代码示例演示了 library_call_syscall 的使用方法:
use starknet::ClassHash;
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn lib_call(
ref self: TContractState,
class_hash: ClassHash,
selector: felt252,
calldata: Span<felt252>
);
}
#[starknet::contract]
mod HelloStarknet {
// IMPORTS
use starknet::ClassHash;
use starknet::SyscallResultTrait; // for .unwrap_syscall()
use starknet::syscalls::library_call_syscall;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn lib_call(ref self: ContractState, class_hash: ClassHash, selector: felt252, calldata: Span<felt252>) {
// Call syscall
let _ = library_call_syscall(class_hash, selector, calldata).unwrap_syscall();
}
}
}
与 call_contract_syscall 一样,如果 library_call_syscall 失败(例如,目标函数发生 revert 或 class hash 无效),整个交易将被回滚。
library_call_syscall 存储布局
就像 Solidity 中的 delegatecall 一样,Cairo 中的 library_call_syscall 在调用合约的存储上下文中执行目标合约里的函数。区别在于这些存储位置的寻址方式。
在 Solidity 中,存储是基于槽位的:
- 变量被分配连续的槽号。
- 变量名在运行时无关紧要。
- 只要槽位布局匹配,不同的变量名也是无害的。
在 Cairo 中,存储是基于名称的:
- 存储地址通过
sn_keccak("variable_name")派生。 - 变量名称就是地址。
- 如果名称不同,地址也不同。
这意味着使用 library_call_syscall 时,对于任何共享状态,调用合约和目标合约必须使用完全相同的存储变量名。如果名称不同,目标合约将对不同名称进行哈希处理,计算出不同的地址,最终读取或写入错误的存储位置。
升级合约 - replace_class syscall
在以太坊中,合约升级通常通过代理模式完成。代理持有所有的存储,而实现合约 (implementation contract) 包含逻辑。要升级时,代理更新它委托调用的实现合约地址即可。
Starknet 采取了不同的方式。合约不需要通过代理进行路由,而是可以通过 replace_class_syscall 原生升级自己。
这个 syscall 用于将其当前的 class hash(运行时代码)替换为新的 class hash,同时保持其存储不变。合约的“身体”被替换,而它的“记忆”(存储)保持原样。
replace_class_syscall 的函数签名如下:
fn replace_class_syscall(
class_hash: ClassHash,
) -> SyscallResult<()>;
它接受一个参数 class_hash(用于替换当前实现的新实现的 class hash,类型为 ClassHash),成功时不返回任何内容。
以下代码示例展示了如何使用它:
use starknet::ClassHash;
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn upgrade(ref self: TContractState, class_hash: ClassHash);
}
#[starknet::contract]
mod HelloStarknet {
// IMPORTS
use starknet::ClassHash;
use starknet::SyscallResultTrait; // for .unwrap_syscall()
use starknet::syscalls::replace_class_syscall;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn upgrade(ref self: ContractState, class_hash: ClassHash) {
// access control logic so only authorised can upgrade
// Call syscall
let _ = replace_class_syscall(class_hash).unwrap_syscall();
}
}
}
在实践中,没有任何安全防护直接调用 replace_class_syscall 是不安全的。升级改变了合约的逻辑,因此应当通过某种方式加以保护。一种方法是确保只有所有者才能升级合约。
在升级合约前需要注意的事项:
- 存储被保留,只有逻辑被替换。
- 在以太坊中,升级需要在各个版本之间保持相同的连续存储槽位布局。在 Cairo 中,由于存储槽位是从变量名派生的,新类必须对其存储变量使用相同的变量名,以避免读取或写入错误的存储位置。
- 调用
replace_class_syscall后,当前从旧类中正在执行的代码将完成运行。新类代码将从调用该合约的下一个交易开始生效;如果在完成替换之后、仍在同一交易内通过call_contract_syscall再次调用该合约,也会使用新代码。
Keccak-256 Syscall
keccak_syscall 在 Cairo 中提供了直接访问 keccak 哈希函数的途径。它的函数签名如下:
fn keccak_syscall(
input: Span<u64>,
) -> SyscallResult<u256>;
它接受一个参数 input(包含以小端格式的 64 位字 (words) 作为哈希数据的 Span<u64>),并返回一个小端格式的 u256 哈希值。
小端和大端格式示例:假设你有一个 4 字节的数字
0x12345678。
- 大端(高位字节在前):
12 34 56 78- 小端(低位字节在前):
78 56 34 12因此在小端格式中,“最后”的字节排在最前面。
输入必须根据 Cairo 版的 keccak “pad10*1” 规则进行预填充:
- 起始标记:在原像 (preimage) 之后紧接着追加一个单字节
0x01。 - 补零:添加尽可能多的
0x00字节以达到所需的块大小。 - 结束标记:追加最后一个字节
0x80来表示填充的结束。
填充后,总长度必须是 1088 位(即 17 个 u64 字)的倍数,否则我们会遇到类似于“Invalid input length”(输入长度无效)的运行时错误:

为了说明输入数据构造方式的差异,我们首先看看 Solidity 版本,然后再与 Cairo 的方法进行比较。
下面的底层 Solidity 合约返回了原像 "hello" 的 Keccak 哈希:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract KeccakExample {
function hashHello() external pure returns (bytes32 hash) {
assembly {
// "hello" in ASCII is 5 bytes long (0x68656c6c6f)
// Store bytes in memory
mstore(0x00, 0x68656c6c6f)
// Compute keccak256 over the last 5 bytes in memory
// keccak256(memory_ptr, length)
hash := keccak256(0x1b, 0x05)
}
}
}
在汇编块中,mstore 将输入值 hello 写入一个完整的 32 字节内存字中,右对齐。这意味着前 27 个字节是全零,最后 5 个字节包含 "hello",如下所示:
// ______________first 27 bytes are zeros______________ _hello__
// | | | |
0x 000000000000000000000000000000000000000000000000000000 68656c6c6f
因为这种布局,对 keccak256(0x1b, 0x05) 的调用从偏移量 0x1b (27) 处开始计算哈希,并且刚好哈希 5 个字节,仅针对 "hello" 字节,忽略前面的零填充。非常直接,对吧?
而等效的 Cairo 实现则需要更深入的研究,因为 keccak_syscall 期望其输入预先填充到 1088 位(17 × 64 位字)的倍数,严格遵循 Keccak pad10*1 规则。
在计算哈希之前,"hello" 的 ASCII 字节(0x68656c6c6f)会按如下方式进行转换:
-
追加起始位——在消息后紧接着添加一个单字节
0x01。0x68 65 6c 6c 6f 01 -
补零——用
0x00字节填充,直到总长度为 136 字节(1088 位 ÷ 8)。 -
结束位——追加一个单字节
0x80以标记填充的结束。0x68 65 6c 6c 6f 01 ...00... 80
绿色部分代表实际消息(即要哈希的原像)。红色部分标记了 Keccak
pad10*1规则要求的起始0x01和结束0x80填充字节。蓝色部分显示了填充空隙的零字节,以使总长度达到完整的 1088 位块。
填充后,总长度必须是 1088 位(136 字节)的倍数。
在本例中,消息只有 5 字节长,因此填充块会扩展为完整的 136 字节。如果原像更大(例如 154 字节),你将需要两个 136 字节块(总共 272 字节),因为 154 字节无法容纳在一个 136 字节块中。
现在消息 (hello) 已经填充到 136 字节,下一步是将其拆分为 17 个小端 u64 字(17 × 64 位 = 1088 位),这随后将作为 keccak_syscall 的输入 span。
这是一个演示如何将填充后的 136 字节块转换为 17 个小端 u64 块的动画:
上述 Solidity 示例对应的 Cairo 实现如下:
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn hasher(ref self: TContractState) -> u256;
}
#[starknet::contract]
mod HelloStarknet {
// IMPORTS
use starknet::SyscallResultTrait; // for .unwrap_syscall()
use starknet::syscalls::keccak_syscall;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn hasher(ref self: ContractState) -> u256 {
// Encode the input as a span<u64>
let mut input: Array<u64> = ArrayTrait::new();
input.append(0x0000016f6c6c6568);
input.append(0x0000000000000000);
input.append(0x0000000000000000);
input.append(0x0000000000000000);
input.append(0x0000000000000000);
input.append(0x0000000000000000);
input.append(0x0000000000000000);
input.append(0x0000000000000000);
input.append(0x0000000000000000);
input.append(0x0000000000000000);
input.append(0x0000000000000000);
input.append(0x0000000000000000);
input.append(0x0000000000000000);
input.append(0x0000000000000000);
input.append(0x0000000000000000);
input.append(0x0000000000000000);
input.append(0x8000000000000000);
// Call syscall
keccak_syscall(input.span()).unwrap_syscall()
}
}
}
keccak_syscall 返回一个小端 u256。
要与 Solidity 的 bytes32 结果进行直接比较,我们必须将其转换为大端十六进制格式,这将在后面的章节(哈希函数)中进行介绍。
总结
在 Cairo 中使用 keccak_syscall 时:
- 根据
pad10*1规则手动将原像填充至 1088 位块的倍数。 - 以 17 个小端
u64字的形式提供填充后的消息。 - 如果需要兼容 Solidity 的
bytes32,则需将结果字节反转为大端序。
Sha-256 Syscall
就像 Solidity 通过预编译提供 SHA-256 哈希算法一样,Starknet 也通过 syscall 提供该功能。两者都能访问相同的哈希函数,但在抽象层次上有所不同。
- 在 Solidity 中,
sha256暴露为 EVM 预编译合约,它接受任意长度的输入,并由函数在底层自动处理正确的填充和所需的块。结果以大端格式的bytes32值返回。 - 在 Cairo 中,SHA-256 哈希通过
sha256_process_block_syscall提供,它的操作层级更低:- 它一次只对单个 512 位(64 字节)的块进行操作。
- 它接收一个初始 SHA-256 状态和提供的输入来计算下一个 SHA-256 状态。
- 输出返回传入输入的下一个 SHA-256 状态,以小端的
Sha256StateHandle形式表现,底层是一个装箱的 8 个 32 位字组成的数组。
sha256_process_block_syscall 函数签名如下:
fn sha256_process_block_syscall(
state: Sha256StateHandle,
input: Box<[u32; 16]>
) -> SyscallResult<core::sha256::Sha256StateHandle>;
它接受两个参数:
-
state:表示内部哈希状态(八个 32 位字,[u32; 8])的Sha256StateHandle。对于第一个块,该状态初始化为 SHA-256 的初始化常数。每个块处理完后,更新后的状态将成为新的内部状态并传递到后续调用中。 -
input:一个包含 16 个字([u32; 16])的 Box 数组,每个字为 32 位。它们加在一起代表要哈希消息的单个 512 位块(64 字节)。短于 64 字节的输入必须按照 sha256 标准手动填充:- 在消息后紧接着追加
0x80, - 追加零字节(
0x00),并且 - 在最后字节中追加(消息长度 * 8)以补齐完整的 512 位块。
例如,消息
hello填充后的输入将是:
消息
cat填充后的输入将是:
- 在消息后紧接着追加
它的返回值:
SyscallResult<Sha256StateHandle>:处理该块后更新的内部状态。如果还有更多的数据块,此新状态应输入到下一轮计算中,否则(必要时应用 SHA-256 填充后)将被视为最终的摘要。
为什么不能直接调用 sha256_process_block
截至撰写本文时,用户合约无法直接调用 sha256_process_block syscall,因为其状态句柄的声明具有受限的可见性:
/// A handle to the state of a SHA-256 hash.
pub(crate) extern type Sha256StateHandle;
pub(crate) 可见性意味着 Sha256StateHandle 只能在核心库的 crate 内部访问,对用户合约不可见。换句话说,即使 syscall 存在,其状态句柄类型也没有公开暴露,从而阻止了我们直接与其交互。
话虽如此,核心库确实提供了在内部处理此问题的高级函数:
compute_sha256_byte_arraycompute_sha256_u32_array
但是,本文不会介绍它们,因为我们的重点主要在于 syscall。
向以太坊 L1 发送消息的 Syscall
到目前为止,我们只关注了 Starknet 内部使用的 syscall。然而,Starknet 合约也可以与以太坊 (L1) 进行通信。这可以通过 send_message_to_l1_syscall 完成,它允许 Starknet (L2) 合约向 L1 合约发送消息或传递数据。
实际应用场景包括向以太坊桥接代币、通知 L1 合约在 Starknet 上发生的事件,或最终确认跨链操作。
send_message_to_l1_syscall 的函数签名如下:
fn send_message_to_l1_syscall(
to_address: felt252, payload: Span<felt252>,
) -> SyscallResult<()>;
它接受两个参数:
to_address:目标 L1 合约的 20 字节以太坊地址(作为felt252)。payload:代表消息内容的Span<felt252>。这可以包含 L1 合约能理解的任意编码数据(函数选择器、参数等)。
它不返回任何内容。
需要注意的是,syscall 本身并不会直接调用以太坊合约上的函数。相反,该流程是通过部署在 L1 上的 Starknet Core 合约完成的:
-
L2 上的 Cairo 合约调用
send_message_to_l1_syscall,将 payload 推送到消息桥。 -
在 L1 上,目标 Solidity 合约(你在
to_address中指定的地址)通过调用 Starknet Core 合约中的consumeMessageFromL2函数来消费该消息(Starknet Core 合约是部署在以太坊上的 Solidity 合约。它验证 Starknet 状态更新,维护 L2 ↔ L1 的消息队列,并暴露供 L1 合约发送和消费 Starknet 消息的函数):consumeMessageFromL2(l2_sender, payload)l2_sender:发送消息的合约的 Starknet L2 地址(作为uint256)。- 这必须是执行
send_message_to_l1_syscall的那个确切的 L2 合约地址。 - 在 L1 上将其表示为与 L2 felt 地址数值相同的
uint256(无需额外编码)。 - Starknet Core 合约会验证
msg.sender是否匹配目标地址。因此,你必须从to_address中指定的那个确切 L1 合约地址调用consumeMessageFromL2。从 EOA 账户或其他不同合约调用会导致交易回滚。
- 这必须是执行
payload:L2 合约发送的完全相同的字数组(作为uint256[])——顺序相同,值相同,长度相同。
-
consumeMessageFromL2函数随后将该消息标记为已消费,防止它被多次处理(重放保护)。 -
随后,Solidity 合约就可以触发任何后续逻辑——例如更新状态、铸造代币或调用其他函数。
示例
让我们创建一个向 L1 发送消息的简单 Cairo 合约,然后测试它确实发送了消息至 L1。
我们首先创建一个新的 Scarb 项目,我们叫它 l1_msg:
scarb new l1_msg
然后用下面的代码替换 lib.cairo 中生成的代码。该合约定义了一个 greetings_to_l1 函数,用于将消息 Hello from RareSkills! 发送到 L1。
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn greetings_to_l1(ref self: TContractState);
}
#[starknet::contract]
mod HelloStarknet {
// IMPORTS
use starknet::syscalls::send_message_to_l1_syscall;
use starknet::SyscallResultTrait; // for .unwrap_syscall()
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn greetings_to_l1(ref self: ContractState) {
// payload must be Span<felt252>
let mut payload: Array<felt252> = ArrayTrait::new();
payload.append('Hello from RareSkills!'); // any data you want to send
// call the syscall
send_message_to_l1_syscall(0x1234, payload.span()).unwrap_syscall();
}
}
}
在这里,我们将消息 Hello from RareSkills! 发送给接收方的 L1 地址 0x1234。
现在为了测试 Cairo 合约能否正确向 L1 发送消息,我们将使用 snforge_std 库中的辅助函数。导航到 tests/test_contract.cairo 并将代码替换为以下内容:
use snforge_std::{
ContractClassTrait, DeclareResultTrait, declare,
// ======== 3 NEW HELPERS =========
spy_messages_to_l1,
MessageToL1,
MessageToL1SpyAssertionsTrait,
};
use starknet::{ContractAddress, EthAddress};
use l1_msg::{IHelloStarknetDispatcher, IHelloStarknetDispatcherTrait};
fn deploy_contract(name: ByteArray) -> ContractAddress {
let contract = declare(name).unwrap().contract_class();
let (address, _) = contract.deploy(@ArrayTrait::new()).unwrap();
address
}
#[test]
fn test_send_message_to_l1() {
let main_contract_addr = deploy_contract("HelloStarknet");
let main_dispatcher = IHelloStarknetDispatcher { contract_address: main_contract_addr };
// Start spying L2->L1 messages
let mut spy = spy_messages_to_l1();
// Call the Cairo function
let _ = main_dispatcher.greetings_to_l1();
// Choose any L1 address (EthAddress wraps a felt252 under-the-hood)
let to: EthAddress = 0x1234.try_into().unwrap();
// Build our expected message
let expected_payload = array!['Hello from RareSkills!'];
// Assert the message was sent (from our contract to the expected L1 address with payload)
spy.assert_sent(
@array![
(main_contract_addr, MessageToL1 { to_address: to, payload: expected_payload }),
],
);
}
该测试使用 spy_messages_to_l1() 充当 L2 → L1 消息队列的观察者,调用 Cairo 合约函数 greetings_to_l1,然后断言消息已带着正确的 payload 从该合约成功发送到了预期的 L1 地址。
练习:
- 使用
scarb test运行测试并确认其通过。 - 将测试中的
expected_payload修改为Hi from RareSkills!。 - 重新运行测试,并观察当 payload 不匹配时断言是如何失败的。
Meta_tx_v0 Syscall
meta_tx_v0_syscall 是 Starknet v0.14.0 中引入的用于向后兼容的 syscall。
Starknet 交易经历过多个版本,v0 是最初版,v3 是当前版。当废弃 v0、v1 和 v2 交易以支持 v3 时,原本为 v0 交易格式编写的账户合约便无法再被直接调用了。
这个 syscall 弥合了这一差距,允许 v3 交易调用那些预期接收 v0 交易格式而编写的账户合约。
meta_tx_v0_syscall 的函数签名如下:
fn meta_tx_v0_syscall(
address: ContractAddress,
entry_point_selector: felt252,
calldata: Span<felt252>,
signature: Span<felt252>,
) -> SyscallResult<Span<felt252>>;
此 syscall 接受以下参数:
address:要调用的目标 v0 版本账户的合约地址。entry_point_selector:要在目标账户上调用的函数选择器。在 v0 中,它同样计算为sn_keccak(function_name)。calldata:传给该函数的参数,作为felt252值的 span。signature:作为felt252值 span 的交易签名。与 v3 交易自动从交易上下文中派生签名不同,这里必须显式地计算和传递它。要生成它:
它以 Span<felt252> 形式返回被调用函数的返回数据。
此 syscall 在底层如何工作
当你调用 meta_tx_v0_syscall 时,Starknet 会修改执行上下文,使其看起来像一个旧式的 v0 交易:
- 签名被替换为你通过
signature参数传入的签名 - 调用者变为地址 0(OS / 协议本身)
- 交易版本更改为 0(假装它是一个旧式交易)
- 交易哈希被重新计算为 v0 版本的交易哈希
下图准确地展示了调用此 syscall 前后各字段的情况:

这些更改不仅适用于被调用的合约,还适用于它所调用的任何内部合约。这确保了整个调用链都在“v0 兼容模式”下运行。
注意:该 syscall 文档明确指出它“应仅用于支持旧版本 0 账户合约,不应用于其他目的。”如果你需要调用较新版本的账户合约,请始终使用
call_contract_syscall。