Transaction / Sign and Instruction
简单了解 Transaction 能帮助我们理解 solana 的设计原理:一切都是 instruction invoking。
Compute Budget
CU 类似 evm中 gas,也是以native sol 计价的。
Compute Budget 就是对tx中的每条 ix 设置一个 CU 上限,类似 gasLimit。
在 solana的tx中,设置 CU 大小和 CU price 同样也是靠调用 native program`ComputeBudget111111111111111111111111111111` 的 ix完成。主要用到两个 ix:
ComputeBudgetInstruction::set_compute_unit_limit
设置这个 tx 中最大消耗多少 CU 的总量,按照 ix 的顺序消耗,默认值为 200k,最大为 1_400k 。
ComputeBudgetInstruction::set_compute_unit_price
用于设置 prioritization fee。
单位是 MICRO_LAMPORTS:There are 10^6 micro-lamports in one lamport
注意,1 SOL = 1e9 lamports
CU Price
默认情况下 CU Price 为0(当前版本1.18.15,以后必然会更改,注意更新)。提高 CU Price将提高 tx 打包的优先级,在网络拥堵时,将提升。
Prioritization fee
类似 EVM 中的 Priority Fee,决定了在一个 slot 中 tx 的排序顺序。
同时设置CU Limit和CU Price两者相乘作为优先费用,默认为 0,目前(当前版本1.18.15) prioritization fee将全部支付给validator,用于提升tx打包的优先级。
设置方式
-
CU price 和 limit set ix 是如何被遍历和设定的
solana/program-runtime/src/compute_budget_processor.rs at master · solana-labs/solana (github.com)
从代码来看是在类似解析tx的阶段就计算好了CU相关的budget, 因此在目前版本(1.18.15)
set_compute_unit_price和set_compute_unit_limit在tx中的位置是任意的,顺序不会影响ix的实际消耗,并且,由于在tx解析过程就完成了设置,因此不可能在 program runtime中设置。 -
CU pirce / limit这两个都不设置的时候默认CU Price=0,CU Limit每一条指令200k CU,收取基础5k Lamports签名费
-
设置CU Limit最大的有效值不超过1.4M,设置的时候超过也不会报错,使用超过会报错
-
同时设置CU Limit和CU Price两者相乘作为优先费用,无论指定的 CU limit 是否用完都会收取全部的优先费,不会退还
[TODO] TX 序列化反序列化
TX 结构: https://solana.com/docs/core/transactions
还包括CPI中;
tx如何序列化/反序列化的?
Instruction 中的 AccountMeta 是什么,与 AccountInfo 有什么关系?是否要求顺序?
[TODO] ix接收到的 account 属性以 account meta 中的为准,忽略传入的 account info的原本的属性(如果他们不一致的话)
例子:
/// spl-token
/// Creates a `Transfer` instruction.
pub fn transfer(
token_program_id: &Pubkey,
source_pubkey: &Pubkey,
destination_pubkey: &Pubkey,
authority_pubkey: &Pubkey,
signer_pubkeys: &[&Pubkey],
amount: u64,
) -> Result<Instruction, ProgramError> {
check_program_account(token_program_id)?;
let data = TokenInstruction::Transfer { amount }.pack();
let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len());
accounts.push(AccountMeta::new(*source_pubkey, false));
accounts.push(AccountMeta::new(*destination_pubkey, false));
accounts.push(AccountMeta::new_readonly(
*authority_pubkey,
signer_pubkeys.is_empty(),
));
for signer_pubkey in signer_pubkeys.iter() {
accounts.push(AccountMeta::new_readonly(**signer_pubkey, true));
}
Ok(Instruction {
program_id: *token_program_id,
accounts,
data,
})
}
CPI transfer
solana_program::program::invoke(
&spl_token::instruction::transfer(token_program_info.key, user_token0_account.key, pair_token0_account.key, user_info.key, &[], token0_amount).unwrap(),
&[
user_token0_account.clone(),
pair_token0_account.clone(),
user_info.clone(),
token_program_info.clone()
])?;
常见的Solana Explorer/Cli 工具对tx反序列化的支持
demo tx 基础信息列举
const web3 = require("@solana/web3.js");
(async () => {
const solana = new web3.Connection("https://docs-demo.solana-mainnet.quiknode.pro/");
const tx = await solana.getParsedTransaction(
"D13jTJYXoQBcRY9AfT5xRtsew7ENgCkNs6mwwwAcUCp4ZZCEM7YwZ7en4tVsoDa7Gu75Jjj2FgLXNUz8Zmgedff",
{ maxSupportedTransactionVersion: 0 }
);
const a = tx.transaction//.message.instructions[0];
console.log(a);
})();
一笔tx中包含如下字段:
-
message
- accountKeys tx中输入的所有account集合
- instructions 两条指令
-
signatures
{
message: {
accountKeys: [
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object]
],
instructions: [ [Object], [Object] ],
recentBlockhash: 'ErCR1EJmwxjjtizWH7BUNJD76HS2WjykpHuRsx5FZ9TR',
addressTableLookups: undefined
},
signatures: [
'D13jTJYXoQBcRY9AfT5xRtsew7ENgCkNs6mwwwAcUCp4ZZCEM7YwZ7en4tVsoDa7Gu75Jjj2FgLXNUz8Zmgedff',
'ujqg9dXH1mSyfaJbPLBU5trkjLXSR41RWmkXuQoKcNhyZVQ5uQvyhCKz9CmJkndJebm8o7XfpiUskN3wmDvHENZ'
]
}
其中accountKeys形如:
[
{
pubkey: PublicKey [PublicKey(vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg)] {
_bn: <BN: dc3081f221bfaa1d9f6a9352efe03a2b03b5a7825b5aa5c16fe0c7771caffe3>
},
signer: true,
source: 'transaction',
writable: true
},
... x10个
]
instructions形如:
(有idl):
{
parsed: {
info: {
account: 'FWENiRdx6WGGGdrKR7KAobz7XAtDfXaeCvisjGVj1DAz',
mint: '6rkUDKQHgq65LFtXKNd7BMU11kghsT3g12G9SSZrV8JK',
source: 'vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg',
systemProgram: '11111111111111111111111111111111',
tokenProgram: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
wallet: '8iiuNM8WDy8hoouZ9K34vVhKsnFWYc1TnpxcaFeCifqe'
},
type: 'create'
},
program: 'spl-associated-token-account',
programId: PublicKey [PublicKey(ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL)] {
_bn: <BN: 8c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f859>
},
stackHeight: null
}
(无idl):
{
accounts: [
PublicKey [PublicKey(Ftsh6f3K4izWzV2X7Uc4GBs9UHoz225fCuYKtUx85Rc8)] {
_bn: <BN: dd4d8e1b9959365a8c98e44f420b19a9acf588b3364f86982359fe8efe992d05>
}
],
data: '',
programId: PublicKey [PublicKey(Ftsh6f3K4izWzV2X7Uc4GBs9UHoz225fCuYKtUx85Rc8)] {
_bn: <BN: dd4d8e1b9959365a8c98e44f420b19a9acf588b3364f86982359fe8efe992d05>
},
stackHeight: null
}
主要包含:
- programId:要调用的program地址
- accounts:输入的account,metadata属性应该是不超过message.accountKeys中定义的属性
- data:输出的ix参数,选择调用program的函数
demo tx输入10个Accounts,metadata信息(是否program/是否Signer/是否Writable/是否Fee Payer)如下

tx中包含的指令概览:

其中含有2条指令,第一条指令ATA Program中进行了两次额外的外部invoke
Compressing on chain addresses & Address Lookup Table Program
Versioned tx 和 Legacy tx
versioned tx 是为了支持 Address Lookup Table Account (ALT)特性的最新版本的tx格式
从tx格式上做了修改,用来判断tx 的版本Messages transmitted to Solana validators must not exceed the IPv6 MTU size to ensure fast and reliable network transmission of cluster info over UDP. Solana's networking stack uses a conservative MTU size of 1280 bytes which, after accounting for headers, leaves 1232 bytes for packet data like serialized transactions. (header 48 bytes)
Therefore the current cap is about 35 accounts after accounting for signatures and other transaction metadata.
Proposed Solution
- Introduce a new program which manages on-chain address lookup tables
- Add a new transaction format which can make use of on-chain address lookup tables to efficiently load more accounts in a single transaction.
After addresses are stored on-chain in an address lookup table account, they may be succinctly referenced in a transaction using a 1-byte u8 index rather than a full 32-byte address. This will require a new transaction format to make use of these succinct references as well as runtime handling for looking up and loading addresses from the on-chain lookup tables.
Newly appended addresses require one slot to warmup before being available to transactions for lookups.
Since transactions use a
u8index to look up addresses, address tables can store up to 256 addresses each. In addition to stored addresses, address table accounts also tracks various metadata explained below.Versioned Transactions
In order to support address table lookups, the structure of serialized transactions must be modified. The new transaction format should not affect transaction processing in the Solana program runtime beyond the increased capacity for accounts and program invocations. Invoked programs will be unaware of which transaction format was used.
The new transaction format must be distinguished from the current transaction format. Current transactions can fit at most 19 signatures (64-bytes each) but the message header encodes
num_required_signaturesas au8. Since the upper bit of theu8will never be set for a valid transaction, we can enable it to denote whether a transaction should be decoded with the versioned format or not.The original transaction format will be referred to as the legacy transaction version and the first versioned transaction format will start at version 0.
Versioned tx format
#[derive(Serialize, Deserialize)]
pub struct VersionedTransaction {
/// List of signatures
#[serde(with = "short_vec")]
pub signatures: Vec<Signature>,
/// Message to sign.
pub message: VersionedMessage,
}
// Uses custom serialization. If the first bit is set, the remaining bits
// in the first byte will encode a version number. If the first bit is not
// set, the first byte will be treated as the first byte of an encoded
// legacy message.
pub enum VersionedMessage {
Legacy(LegacyMessage),
V0(v0::Message),
}
// The structure of the new v0 Message
#[derive(Serialize, Deserialize)]
pub struct Message {
// unchanged
pub header: MessageHeader,
// unchanged
#[serde(with = "short_vec")]
pub account_keys: Vec<Pubkey>,
// unchanged
pub recent_blockhash: Hash,
// unchanged
//
// # Notes
//
// Account and program indexes will index into the list of addresses
// constructed from the concatenation of three key lists:
// 1) message `account_keys`
// 2) ordered list of keys loaded from address table `writable_indexes`
// 3) ordered list of keys loaded from address table `readonly_indexes`
#[serde(with = "short_vec")]
pub instructions: Vec<CompiledInstruction>,
/// List of address table lookups used to load additional accounts
/// for this transaction.
#[serde(with = "short_vec")]
pub address_table_lookups: Vec<MessageAddressTableLookup>,
}
/// Address table lookups describe an on-chain address lookup table to use
/// for loading more readonly and writable accounts in a single tx.
#[derive(Serialize, Deserialize)]
pub struct MessageAddressTableLookup {
/// Address lookup table account key
pub account_key: Pubkey,
/// List of indexes used to load writable account addresses
#[serde(with = "short_vec")]
pub writable_indexes: Vec<u8>,
/// List of indexes used to load readonly account addresses
#[serde(with = "short_vec")]
pub readonly_indexes: Vec<u8>,
}
部分签名/链下签名
https://solanacookbook.com/references/offline-transactions.html#sign-transaction
签名算法 TweetNaCl
https://solana.com/developers/cookbook/wallets/sign-message
交易确认机制
31 个 slots 达到 finalized 状态,
确保blockhash不过期需要保证 RecentBlockhash 在 151 个块以内,注意是最新的,正在生成的块(可以理解为 processed 状态的slot + 1)
commitments状态说明: https://docs.solanalabs.com/consensus/commitments
https://www.helius.dev/blog/how-to-deal-with-blockhash-errors-on-solana
https://solana.com/docs/advanced/confirmation
ref
Overview of the Solana Runtime | Solana
Address Lookup Tables | Solana
Priority Fees: Understanding Solana's Transaction Fee Mechanics