详解 Solana 交易结构

  • cig01
  • 发布于 2023-12-18 15:24
  • 阅读 9

本文详细分析了Solana区块链中交易的结构,并通过多个实例深入解析了不同类型的交易,包括一对一转账、多笔转账、Token 2022转账以及使用Address Lookup Tables的交易。文章还对比了Legacy Tx和V0 Tx的区别,并提供了Rust和JavaScript代码示例,有助于开发者理解和构建Solana交易。

Created: <2023-12-17 Sun> Last updated: <2025-04-19 Sat>

1. Solana 交易

在 Solana 区块链中,通过 RPC sendTransaction 可以把签名后的 Tx 提交到链上。提交 Tx 时,可以使用 base58(已经不推荐)或者 base64 编码的 Tx。

2. Tx 解析实例 1(一对一转账)

下面以 Tx tg7QzizXN6LpK2pEtzeKHG5DmUkgz7DiEsAmtPxvaYxEUEYNV2qKF6gaSEG4Qm5uZ7DeTT5F1CTmsEABN1DWnU7 为例介绍一下这个 Tx 的细节。

这个 Solana Devnet 上的 Tx 是通过 RPC sendTransaction 提交上链的,具体参数如下:

$ curl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d '
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "sendTransaction",
    "params": [\
      "ASyP3WwO1k00+UjmLS0RQ6I4A3SLmGagC49rPD8NqXXRezFZ9HH0QM5WYy0GRmzdmfM3Zr2r/kfY9Yzdj18D2AABAAEDjGW0vs4E7Y0JrlRS/FbfremUacCBVRSJl3WGWvd4P50G2indv8FqFG+2/Ieu9DifmgpE2oztoOfk3Y7YtJFgnQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK4RSMkONmm1NSkWHnNwtxr2vM5D5wXDojP+9vb4UJvIBAgIAAQwCAAAAQEIPAAAAAAA=",\
      {\
        "encoding": "base64"\
      }\
    ]
  }
'
{"jsonrpc":"2.0","result":"tg7QzizXN6LpK2pEtzeKHG5DmUkgz7DiEsAmtPxvaYxEUEYNV2qKF6gaSEG4Qm5uZ7DeTT5F1CTmsEABN1DWnU7","id":1}

这个 Tx 的功能是从地址 AT42Zu5kD6V27rgXdkoTpE4mqvsDxsitzWYDr1iStu3r 转移 0.001 SOL(即 1000000 Lamports) 到地址 TkPgkcDQYDUSDBfwTokwkZpn8spz94u4fvXii9U7CWY 中。

AT42Zu5kD6V27rgXdkoTpE4mqvsDxsitzWYDr1iStu3r   ------ 1000000 Lamports ------>   TkPgkcDQYDUSDBfwTokwkZpn8spz94u4fvXii9U7CWY
(8c65b4bece04ed8d09ae5452fc56dfade99469c0815514899775865af7783f9d)               (06da29ddbfc16a146fb6fc87aef4389f9a0a44da8ceda0e7e4dd8ed8b491609d)

为了便于分析,我们把 base64 编码的 Tx 转换为 Hex String 形式:

012c8fdd6c0ed64d34f948e62d2d1143a23803748b9866a00b8f6b3c3f0da975d17b3159f471f440ce56632d06466cdd99f33766bdabfe47d8f58cdd8f5f03d800010001038c65b4bece04ed8d09ae5452fc56dfade99469c0815514899775865af7783f9d06da29ddbfc16a146fb6fc87aef4389f9a0a44da8ceda0e7e4dd8ed8b491609d00000000000000000000000000000000000000000000000000000000000000002b845232438d9a6d4d4a45879cdc2dc6bdaf3390f9c170e88cffbdbdbe1426f201020200010c0200000040420f0000000000

Solana Tx 的格式如图 1 所示。

solana_tx.gif

Figure 1: Solana Legacy Tx

关于 Tx 的细节可参考: https://solana.com/docs/core/transactions 。按照格式定义,可把上面 Tx 的每个字节含义解释如下:

signatures signatures length 01
signature 0 2c8fdd6c0ed64d34f948e62d2d1143a23803748b9866a00b8f6b3c3f0da975d1 <br>  7b3159f471f440ce56632d06466cdd99f33766bdabfe47d8f58cdd8f5f03d800
message message header num required signatures 01
num readonly signed accounts 00
num readonly unsigned accounts 01
account addresses length 03
address 0 8c65b4bece04ed8d09ae5452fc56dfade99469c0815514899775865af7783f9d
address 1 06da29ddbfc16a146fb6fc87aef4389f9a0a44da8ceda0e7e4dd8ed8b491609d
address 2 0000000000000000000000000000000000000000000000000000000000000000
recent blockhash 2b845232438d9a6d4d4a45879cdc2dc6bdaf3390f9c170e88cffbdbdbe1426f2
instructions length 01
ins 0 program id index 02
account address  <br>      indexes length 02
index 0 00
index 1 01
instruction data length 0c
data 0200000040420f0000000000
  1. 上面的 signatures length 等表示长度的字段,并不是固定 1 字节大小,它可能是 1/2/3 字节大小,参考: https://github.com/solana-labs/solana/blob/a16f982169eb197fad0eb8c58c307fb069f69d8f/sdk/program/src/short_vec.rs

  2. 在 Solana 中,地址就是 Ed25519 公钥的 base58 编码。上面 Tx 中的 address 0 和 address 1 分别对应的地址就是 AT42Zu5kD6V27rgXdkoTpE4mqvsDxsitzWYDr1iStu3r 和 TkPgkcDQYDUSDBfwTokwkZpn8spz94u4fvXii9U7CWY。

  3. 上面 Tx 中 recent blockhash 为 2b845232438d9a6d4d4a45879cdc2dc6bdaf3390f9c170e88cffbdbdbe1426f2,其对应的 base58 编码为 3vsZGraLdzja3DUMfLr3NWPGeHcbCawGvCGj7nMn7tFT。

  4. Instructions 中 program id index 对应的是 account addresses 的元素下标,这个例子中是 02,即地址 0000000000000000000000000000000000000000000000000000000000000000,这个十六进制对应的 base58 地址就是 11111111111111111111111111111111,即 System Program 的地址。 在 Solana 中,System Program 是所有帐户钱包的 Owner,由于只有 Owner(对于帐户钱包来说 Owner 就是 System Program)才能减少帐户的余额,所以 Solana 中原生币转账其实就是调用 System Program 的方法。

  5. 0200000040420f0000000000 是 SystemInstruction Transfer 1000000 的编码(Solana 的 instruction data 采用 bincode 编码或者 borsh 编码,推导使用 borsh 编码)。其中 02000000 表示 Transfer,而 40420f0000000000 则是 1000000 的小端格式。

2.1. 签名

待签名数据就是图 1 中的 Message 部分(即去掉其中的 Signatures 部分)。它是 Ed25519 签名中 SHA512(R||A||M) 中的 M

具体到前面的例子,待签名数据就是:

010001038c65b4bece04ed8d09ae5452fc56dfade99469c0815514899775865af7783f9d06da29ddbfc16a146fb6fc87aef4389f9a0a44da8ceda0e7e4dd8ed8b491609d00000000000000000000000000000000000000000000000000000000000000002b845232438d9a6d4d4a45879cdc2dc6bdaf3390f9c170e88cffbdbdbe1426f201020200010c0200000040420f0000000000

前面交易的功能是从地址 AT42Zu5kD6V27rgXdkoTpE4mqvsDxsitzWYDr1iStu3r 往其它地址转移 0.001 SOL,所以需要 AT42Zu5kD6V27rgXdkoTpE4mqvsDxsitzWYDr1iStu3r 所对应的私钥进行签名。

AT42Zu5kD6V27rgXdkoTpE4mqvsDxsitzWYDr1iStu3r 的 Ed25519 私钥是

aa5e711d133aca45d6ec97116de54a24a421a823385fc42fd342d5ecb17ce7d0

使用这个私钥对前面的数据进行签名后,得到签名数据为:

2c8fdd6c0ed64d34f948e62d2d1143a23803748b9866a00b8f6b3c3f0da975d17b3159f471f440ce56632d06466cdd99f33766bdabfe47d8f58cdd8f5f03d800

标准的 Ed25519 签名是确定性,重复运行签名都会得到相同的签名结果。

上面的 Hex String 转换为 base58 编码就是: tg7QzizXN6LpK2pEtzeKHG5DmUkgz7DiEsAmtPxvaYxEUEYNV2qKF6gaSEG4Qm5uZ7DeTT5F1CTmsEABN1DWnU7。这恰好就是 Txid,这是因为 在 Solana 中,Txid 就是 Tx 中的首个签名。

3. Tx 解析实例 2(两笔一对一转账,手续费第三方支付)

下面考虑一个更加复杂的 Tx VGJxEj15eQaiQXwjvg7kYt3otikuZb4by2riKfFW8xoivAGn7tRDpAiWxppRbv8tvuTQ4FcWu5kmd9zdVeLjCjW,它同时有两笔转账:

7sCi23YDXsc3g4gRVMiaP2D2rFij4TQZV4T7xD2Jqh3o   ------ 1000000 Lamports ------>   TkPgkcDQYDUSDBfwTokwkZpn8spz94u4fvXii9U7CWY
(6602663e9bb4f10428464f45a81efe35c4a733dda01d9dbed6fc9f13d22eebf2)               (06da29ddbfc16a146fb6fc87aef4389f9a0a44da8ceda0e7e4dd8ed8b491609d)

4rRuNMdh8em2S5P8Ks9psJhCkKLhspDyFtfJoWxkpg8F   ------ 2000000 Lamports ------>   4HvKrJtm7nrsa66n639ooe6kcY1U4FRBpQvxz81pLhYA
(393cad9ffeafd731e020f5bb513046502fee78e262486d1c82f11748c1d0e7c8)               (30e8a603e3770346bfea245fb3f51f3cb7c6c29825e1ff12217d274fb980fad7)

而且 Fee 由另一个帐户 AT42Zu5kD6V27rgXdkoTpE4mqvsDxsitzWYDr1iStu3r 来支付。

这个 Solana Devnet 上的 Tx 是通过 RPC sendTransaction 提交上链的,具体参数如下:

$ curl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d '
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "sendTransaction",
    "params": [\
      "Axhfrv3KFApUmAuq4SvFaBXEnm5Qke8NsVgcky+iuKveD/JOmzkOJPO2+vwKoz1yAK1IpfjqvsMt5q5hji6Xrw2H1k+eOq+tKie0q9awu2sroNZ6iymYPQN9K8uk8DlBgoXii9jVY0lROTHXw05Xhs1qjNCYEHg5EGyQIOkVknYOe6PHszYnyA8W3vEYQLI3YlWbkuyH9o4at52/0tsC3f5NMyewl2wIAx9UK72+z4ofcv4svuEj7jr5R8VL1sCUCgMAAQaMZbS+zgTtjQmuVFL8Vt+t6ZRpwIFVFImXdYZa93g/nTk8rZ/+r9cx4CD1u1EwRlAv7njiYkhtHILxF0jB0OfIZgJmPpu08QQoRk9FqB7+NcSnM92gHZ2+1vyfE9Iu6/IG2indv8FqFG+2/Ieu9DifmgpE2oztoOfk3Y7YtJFgnTDopgPjdwNGv+okX7P1Hzy3xsKYJeH/EiF9J0+5gPrXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADdGkp+x27zNcira/9SmZHuA9x8HbaGHyK6YDwS0kdtUwIFAgIDDAIAAABAQg8AAAAAAAUCAQQMAgAAAICEHgAAAAAA",\
      {\
        "encoding": "base64"\
      }\
    ]
  }
'

{"jsonrpc":"2.0","result":"VGJxEj15eQaiQXwjvg7kYt3otikuZb4by2riKfFW8xoivAGn7tRDpAiWxppRbv8tvuTQ4FcWu5kmd9zdVeLjCjW","id":1}

为了便于分析,我们把 base64 编码的 Tx 转换为 Hex String 形式:

03185faefdca140a54980baae12bc56815c49e6e5091ef0db1581c932fa2b8abde0ff24e9b390e24f3b6fafc0aa33d7200ad48a5f8eabec32de6ae618e2e97af0d87d64f9e3aafad2a27b4abd6b0bb6b2ba0d67a8b29983d037d2bcba4f039418285e28bd8d56349513931d7c34e5786cd6a8cd098107839106c9020e91592760e7ba3c7b33627c80f16def11840b23762559b92ec87f68e1ab79dbfd2db02ddfe4d3327b0976c08031f542bbdbecf8a1f72fe2cbee123ee3af947c54bd6c0940a030001068c65b4bece04ed8d09ae5452fc56dfade99469c0815514899775865af7783f9d393cad9ffeafd731e020f5bb513046502fee78e262486d1c82f11748c1d0e7c86602663e9bb4f10428464f45a81efe35c4a733dda01d9dbed6fc9f13d22eebf206da29ddbfc16a146fb6fc87aef4389f9a0a44da8ceda0e7e4dd8ed8b491609d30e8a603e3770346bfea245fb3f51f3cb7c6c29825e1ff12217d274fb980fad70000000000000000000000000000000000000000000000000000000000000000dd1a4a7ec76ef335c8ab6bff529991ee03dc7c1db6861f22ba603c12d2476d5302050202030c0200000040420f0000000000050201040c0200000080841e0000000000

下面再把它解释为更方便阅读的形式:

signatures signatures length 03
signature 0 185faefdca140a54980baae12bc56815c49e6e5091ef0db1581c932fa2b8abde <br>  0ff24e9b390e24f3b6fafc0aa33d7200ad48a5f8eabec32de6ae618e2e97af0d
signature 1 87d64f9e3aafad2a27b4abd6b0bb6b2ba0d67a8b29983d037d2bcba4f0394182 <br>  85e28bd8d56349513931d7c34e5786cd6a8cd098107839106c9020e91592760e
signature 2 7ba3c7b33627c80f16def11840b23762559b92ec87f68e1ab79dbfd2db02ddfe <br>  4d3327b0976c08031f542bbdbecf8a1f72fe2cbee123ee3af947c54bd6c0940a
message message header num required signatures 03
num readonly signed accounts 00
num readonly unsigned accounts 01
account addresses length 06
address 0 8c65b4bece04ed8d09ae5452fc56dfade99469c0815514899775865af7783f9d
address 1 393cad9ffeafd731e020f5bb513046502fee78e262486d1c82f11748c1d0e7c8
address 2 6602663e9bb4f10428464f45a81efe35c4a733dda01d9dbed6fc9f13d22eebf2
address 3 06da29ddbfc16a146fb6fc87aef4389f9a0a44da8ceda0e7e4dd8ed8b491609d
address 4 30e8a603e3770346bfea245fb3f51f3cb7c6c29825e1ff12217d274fb980fad7
address 5 0000000000000000000000000000000000000000000000000000000000000000
recent blockhash dd1a4a7ec76ef335c8ab6bff529991ee03dc7c1db6861f22ba603c12d2476d53
instructions length 02
ins 0 program id index 05
account address  <br>      indexes length 02
index 0 02
index 1 03
instruction data length 0c
data 0200000040420f0000000000
ins 1 program id index 05
account address  <br>      indexes length 02
index 0 01
index 1 04
instruction data length 0c
data 0200000080841e0000000000

注:Instructions 的 program id index 对应的是 account addresses 的元素下标。上面例子中,两个 program id index 都是 05,即地址 0000000000000000000000000000000000000000000000000000000000000000,这个十六进制对应的 base58 地址就是 11111111111111111111111111111111,即 System Program 的地址。

3.1. 签名

待签名数据就是图 1 中的 Message 部分(即去掉其中的 Signatures 部分)。它是 Ed25519 签名中 SHA512(R||A||M) 中的 M

具体到前面的例子,待签名数据就是:

030001068c65b4bece04ed8d09ae5452fc56dfade99469c0815514899775865af7783f9d393cad9ffeafd731e020f5bb513046502fee78e262486d1c82f11748c1d0e7c86602663e9bb4f10428464f45a81efe35c4a733dda01d9dbed6fc9f13d22eebf206da29ddbfc16a146fb6fc87aef4389f9a0a44da8ceda0e7e4dd8ed8b491609d30e8a603e3770346bfea245fb3f51f3cb7c6c29825e1ff12217d274fb980fad70000000000000000000000000000000000000000000000000000000000000000dd1a4a7ec76ef335c8ab6bff529991ee03dc7c1db6861f22ba603c12d2476d5302050202030c0200000040420f0000000000050201040c0200000080841e0000000000

需要 3 个参与者对它进行签名:

  1. 支付交易 Fee 的 AT42Zu5kD6V27rgXdkoTpE4mqvsDxsitzWYDr1iStu3r

  2. 往其它地址转出 1000000 Lamports 的 7sCi23YDXsc3g4gRVMiaP2D2rFij4TQZV4T7xD2Jqh3o

  3. 往其它地址转出 2000000 Lamports 的 4rRuNMdh8em2S5P8Ks9psJhCkKLhspDyFtfJoWxkpg8F

下面只介绍首个签名。AT42Zu5kD6V27rgXdkoTpE4mqvsDxsitzWYDr1iStu3r 的 Ed25519 私钥是

aa5e711d133aca45d6ec97116de54a24a421a823385fc42fd342d5ecb17ce7d0

使用这个私钥对前面的数据进行签名后,得到签名数据为:

185faefdca140a54980baae12bc56815c49e6e5091ef0db1581c932fa2b8abde0ff24e9b390e24f3b6fafc0aa33d7200ad48a5f8eabec32de6ae618e2e97af0d

上面的 Hex String 转换为 base58 编码就是: VGJxEj15eQaiQXwjvg7kYt3otikuZb4by2riKfFW8xoivAGn7tRDpAiWxppRbv8tvuTQ4FcWu5kmd9zdVeLjCjW。这恰好就是 Txid,这是因为 在 Solana 中,Txid 就是 Tx 中的首个签名。

3.2. Rust 实例

打包上面交易的 Rust 程序如下:

// [dependencies]
// base64 = "0.22.1"
// bincode = "1.3.3"
// hex = "0.4.3"
// solana-program = "2.2.1"
// solana-sdk = "2.2.1"

use base64;
use bincode;
use hex;
use solana_program::hash::Hash;
use solana_program::system_instruction;
use solana_sdk::signature::{Keypair, Signer};
use solana_sdk::transaction::Transaction;
use std::str::FromStr;

fn main() {
    let payer_keypair_bytes: [u8; 64] = [170,94,113,29,19,58,202,69,214,236,151,17,109,229,74,36,164,33,168,35,56,95,196,47,211,66,213,236,177,124,231,208,140,101,180,190,206,4,237,141,9,174,84,82,252,86,223,173,233,148,105,192,129,85,20,137,151,117,134,90,247,120,63,157];
    let payer_keypair = Keypair::from_bytes(&payer_keypair_bytes).unwrap();
    println!("payer seckey {}", hex::encode(payer_keypair.secret().to_bytes()));
    let payer_pubkey = Signer::pubkey(&payer_keypair);
    println!("payer_pubkey {}", payer_pubkey.to_string()); // AT42Zu5kD6V27rgXdkoTpE4mqvsDxsitzWYDr1iStu3r

    let from_keypair_bytes: [u8; 64] = [10,178,29,4,179,245,97,5,183,90,15,188,22,32,116,195,134,44,159,5,226,5,98,67,98,110,240,79,52,106,175,221,102,2,102,62,155,180,241,4,40,70,79,69,168,30,254,53,196,167,51,221,160,29,157,190,214,252,159,19,210,46,235,242];
    let from_keypair = Keypair::from_bytes(&from_keypair_bytes).unwrap();
    println!("from seckey {}", hex::encode(from_keypair.secret().to_bytes()));
    let from_pubkey = Signer::pubkey(&from_keypair);
    println!("from_pubkey {}", from_pubkey.to_string()); // 7sCi23YDXsc3g4gRVMiaP2D2rFij4TQZV4T7xD2Jqh3o

    let from2_keypair_bytes: [u8; 64] = [38,101,100,119,58,71,219,79,52,219,72,137,101,25,58,229,156,215,79,222,39,131,151,220,160,191,119,117,229,89,87,8,57,60,173,159,254,175,215,49,224,32,245,187,81,48,70,80,47,238,120,226,98,72,109,28,130,241,23,72,193,208,231,200];
    let from2_keypair = Keypair::from_bytes(&from2_keypair_bytes).unwrap();
    println!("from2 seckey {}", hex::encode(from2_keypair.secret().to_bytes()));
    let from2_pubkey = Signer::pubkey(&from2_keypair);
    println!("from2_pubkey {}", from2_pubkey.to_string()); // 4rRuNMdh8em2S5P8Ks9psJhCkKLhspDyFtfJoWxkpg8F

    let to_keypair_bytes: [u8; 64] = [195,70,54,2,213,99,33,240,169,173,231,145,119,25,230,97,200,117,254,168,54,28,200,241,55,1,190,169,49,12,190,232,6,218,41,221,191,193,106,20,111,182,252,135,174,244,56,159,154,10,68,218,140,237,160,231,228,221,142,216,180,145,96,157];
    let to_keypair = Keypair::from_bytes(&to_keypair_bytes).unwrap();
    let to_pubkey = Signer::pubkey(&to_keypair);
    println!("to_pubkey {}", to_pubkey.to_string()); // TkPgkcDQYDUSDBfwTokwkZpn8spz94u4fvXii9U7CWY

    let to2_keypair_bytes: [u8; 64] = [90,49,225,138,1,223,59,108,196,75,251,191,171,182,89,126,28,184,47,27,226,140,180,230,206,219,137,165,17,31,98,157,48,232,166,3,227,119,3,70,191,234,36,95,179,245,31,60,183,198,194,152,37,225,255,18,33,125,39,79,185,128,250,215];
    let to2_keypair = Keypair::from_bytes(&to2_keypair_bytes).unwrap();
    let to2_pubkey = Signer::pubkey(&to2_keypair);
    println!("to2_pubkey {}", to2_pubkey.to_string()); // 4HvKrJtm7nrsa66n639ooe6kcY1U4FRBpQvxz81pLhYA

    // Creating the transfer sol instruction
    let ix0 = system_instruction::transfer(&from_pubkey, &to_pubkey, 1_000_000);
    let ix1 = system_instruction::transfer(&from2_pubkey, &to2_pubkey, 2_000_000);
    let ins = [ix0, ix1];

    // Putting the transfer sol instruction into a transaction
    // let rpc_url = String::from("https://api.devnet.solana.com");
    // let connection = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());
    // let recent_blockhash = connection.get_latest_blockhash().expect("Failed to get latest blockhash.");
    let recent_blockhash = Hash::from_str("Ft6MUpKNyyjyb1Trv1oXK75eAiGYfVN5VjUnGVbdXDkv").unwrap(); // dd1a4a7ec76ef335c8ab6bff529991ee03dc7c1db6861f22ba603c12d2476d53
    println!("recent_blockhash {}", recent_blockhash);
    println!("recent_blockhash {}", hex::encode(recent_blockhash.to_bytes()));

    let txn = Transaction::new_signed_with_payer(&ins, Some(&payer_pubkey), &[&payer_keypair, &from_keypair, &from2_keypair], recent_blockhash);

    let preimage = txn.message.serialize();
    println!("preimage = {}", hex::encode(preimage));

    let tx_vec = bincode::serialize(&txn).unwrap();
    println!("hex tx: {}", hex::encode(&tx_vec));
    println!("base64 tx: {}", base64::encode(&tx_vec));
}

4. Tx 解析实例 3(添加了小费的 Token 2022 转账)

下面以 Tx 3FLzsDWEyERGWyeQvX5CQgSuYAycErPyvvvEFHw9hn1hubySvVoix8C6LTjAveTP3e8LDCrWLZPpub4kyJsFAfJL 为例介绍一下这个 Tx 的细节。

这个 Solana Mainnet 上的 Tx 是通过 RPC sendTransaction 提交上链的,具体参数如下:

$ curl https://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" -d '
{"method":"sendTransaction","jsonrpc":"2.0","params":["AXBnhpJ4Y5GJ5uzEJkf4uWaIvpedL3cwyAOGE/De8ZSj1ac5PFDxdW1XnBSGOYeL74FminSVFIcuqzAA2WLpGg0BAAMGoyt1UmcbIpl8MwxTrJHqaNBHKdDL3XHkiTRBblLseNsUmkTrYwsPXTC7AzjBvZtaYJFJPNrtJLB67ToQRzcbTq+QnyOd/QYbgbjugrDHeLse9zlsmi5O2c7dpT8AbhBqqDijZLhcKuVLVBmL6Ve9S9DvSU7kn1XtkCnOtnDXGjADBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAAAbd9uHudY/eGEJdvORszdq2GvxNg7kNJ/69+SjYoYv8nXMENVJdGaxITQAKrAwgTRwOlBQHmWGi9xKCOKVr0WUDBAAJA7X0DAAAAAAABAAFAgEuAAAFBQEDAgAACgx7AAAAAAAAAAU=",{"encoding":"base64","preflightCommitment":"confirmed"}],"id":"a3c4f431-0f2a-4632-91ae-d3cafaaceb63"}
'
{"jsonrpc": "2.0", "result": "3FLzsDWEyERGWyeQvX5CQgSuYAycErPyvvvEFHw9hn1hubySvVoix8C6LTjAveTP3e8LDCrWLZPpub4kyJsFAfJL", "id": "a3c4f431-0f2a-4632-91ae-d3cafaaceb63"}

这个 Tx 的功能是从地址 BywtbATeVZvzjeiK9fuNtRwkKFzTxyL5nJVPNktRY3cn 转移 0.00123 BERN(属于 Token 2022 代币)到地址 J1nW8Vu6jPxYQMqZuHDGUPNaU3sNZvonXTTtDftFZxrM 中:

BywtbATeVZvzjeiK9fuNtRwkKFzTxyL5nJVPNktRY3cn                     ------ 0.00123 BERN ------>   J1nW8Vu6jPxYQMqZuHDGUPNaU3sNZvonXTTtDftFZxrM
(a32b7552671b22997c330c53ac91ea68d04729d0cbdd71e48934416e52ec78db)                             (fcca209592c3d537ecf153737a6d8e547b717783b5065715dd129521ca4d932a)
                 ^                                                                                   ^
                 |                                                                                   |
                 | owner                                                                             | owner
                 |                                                                                   |
                 |                                                                                   |
2PRbFDUUw8QXLHr4m5io6A3aU7bAGbsFiUTjuYTbDgyb (associated token account)                        CpLFa6WpP9nmmqYUtewthV4o3Lo8YWWoq6kdEqPvZep1 (associated token account)
(149a44eb630b0f5d30bb0338c1bd9b5a6091493cdaed24b07aed3a1047371b4e)                             (af909f239dfd061b81b8ee82b0c778bb1ef7396c9a2e4ed9cedda53f006e106a)

为了便于分析,我们把 base64 编码的 Tx 转换为 Hex String 形式:

017067869278639189e6ecc42647f8b96688be979d2f7730c8038613f0def194a3d5a7393c50f1756d579c148639878bef81668a749514872eab3000d962e91a0d01000306a32b7552671b22997c330c53ac91ea68d04729d0cbdd71e48934416e52ec78db149a44eb630b0f5d30bb0338c1bd9b5a6091493cdaed24b07aed3a1047371b4eaf909f239dfd061b81b8ee82b0c778bb1ef7396c9a2e4ed9cedda53f006e106aa838a364b85c2ae54b54198be957bd4bd0ef494ee49f55ed9029ceb670d71a300306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a4000000006ddf6e1ee758fde18425dbce46ccddab61afc4d83b90d27febdf928d8a18bfc9d730435525d19ac484d000aac0c204d1c0e9414079961a2f7128238a56bd1650304000903b5f40c000000000004000502012e0000050501030200000a0c7b0000000000000005

可把上面 Tx 的每个字节含义解释如下:

signa-  <br>  tures signatures length 01
signature 0 7067869278639189e6ecc42647f8b96688be979d2f7730c8038613f0def194a3 <br>  d5a7393c50f1756d579c148639878bef81668a749514872eab3000d962e91a0d
message message   <br>  header num required signatures 01
num readonly signed accounts 00
num readonly unsigned accounts 03
account   <br>  addresses length 06
address 0 a32b7552671b22997c330c53ac91ea68d04729d0cbdd71e48934416e52ec78db sender: BywtbATeVZvzjeiK9fuNtRwkKFzTxyL5nJVPNktRY3cn
address 1 149a44eb630b0f5d30bb0338c1bd9b5a6091493cdaed24b07aed3a1047371b4e sender token acnt: 2PRbFDUUw8QXLHr4m5io6A3aU7bAGbsFiUTjuYTbDgyb
address 2 af909f239dfd061b81b8ee82b0c778bb1ef7396c9a2e4ed9cedda53f006e106a recipient token acnt: CpLFa6WpP9nmmqYUtewthV4o3Lo8YWWoq6kdEqPvZep1
address 3 a838a364b85c2ae54b54198be957bd4bd0ef494ee49f55ed9029ceb670d71a30 Token 2022 mint addr: CKfatsPMUf8SkiURsDXs7eK6GWb4Jsd6UDbs7twMCWxo
address 4 0306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a40000000 ComputeBudget111111111111111111111111111111
address 5 06ddf6e1ee758fde18425dbce46ccddab61afc4d83b90d27febdf928d8a18bfc Token 2022 addr: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
recent blockhash 9d730435525d19ac484d000aac0c204d1c0e9414079961a2f7128238a56bd165
instruc-  <br>  tions length 03
ins 0 program id index 04 这是设置手续费相关的
account      <br>  address      <br>  indexes length 00
instruction  <br>  data length 09
data 03b5f40c0000000000
ins 1 program id index 04 这是设置手续费相关的
account      <br>  address      <br>  indexes length 00
instruction  <br>  data length 05
data 02012e0000
ins 2 program id index 05 这个索引对应的地址是 TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
account      <br>  address      <br>  indexes length 05 入口函数 process 的 accounts 参数
index 0 01 source_account_info
index 1 03 expected_mint_info
index 2 02 destination_account_info
index 3 00 authority_info
index 4 00
instruction  <br>  data length 0a 入口函数 process 的 input 参数
data 0c7b0000000000000005 0c(transferChecked), 7b00000000000000(amount), 05(decimals)

最后一行数据 0c7b0000000000000005 可以进一步分解如下(参考 PodTokenInstruction::TransferChecked ):

0c               // instruction type, transferChecked
7b00000000000000 // instruction data, 转帐数量 123 的小端格式
05               // instruction data, decimals。指定精度 5,它必须和所转帐 Token(这里是 BERN)的精度一致,否则报错

注 1:明明 Tx 中是转帐 0.00123 BERN,为什么在区块浏览器中查看帐户的 Token Balance Change 时,会显示目标地址只收到了 0.00117 BERN 呢?这是因为 Solana 2022 支持 Hook,代币 BERN 实现了 Hook,会在每次转帐时销毁一部分代币。

注 2:帐户的 Associated Token Account 合约是什么时候部署呢?Account1 给 Account2 转帐 TokenA 时,如果 Account2 对应的 TokenA 的 Associated Token Account 还没有部署(这往往是 Account2 从未收到过 TokenA 时),那么 Account1 会帮 Account2 部署这个合约(交易中会增加额外的 Instruction 来创建 Account2 的 Associated Token Account)。比如交易 25NhZZgELi5hi9xCvv6d1utXR7QQQKkc7b3bwDsa28wMtYLhSuxNy7Hw4VNwoxBiv9RveYDKT78xv1E3P3vd56uJ 就是转帐 Token 时还帮别人部署 Associated Token Account 的例子,这个交易会调用 Associated Token Account Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL 的 create 方法来为接收者创建 Associated Token Account 合约。

注 3:前两个指令(ins 0 和 ins 1)都是设置手续费相关的指令,它们的 data 的首个字节为 03/ 02 时分别表示 SetComputeUnitLimit/SetComputeUnitPrice。

5. Tx 解析实例 4(Address Lookup Tables 实例)

Tx 4q2w12CtjRxHtdfnWATLwtTbkngMwux8Wwr2rSwQib8tk1wsFqc4NhBU1t4Z7NBCtXnAAajxhvJz5iHJNxzT8J1K 是使用了 Address Lookup Tables 的交易实例。

下面我们通过 RPC getTransaction 得到这个 V0 Tx 的 base64 编码:

$ curl  https://api.devnet.solana.com  -X POST -H "Content-Type: application/json" -d '
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getTransaction",
    "params": [\
      "4q2w12CtjRxHtdfnWATLwtTbkngMwux8Wwr2rSwQib8tk1wsFqc4NhBU1t4Z7NBCtXnAAajxhvJz5iHJNxzT8J1K",\
      {\
        "maxSupportedTransactionVersion": 0,\
        "encoding": "base64"\
      }\
    ]
  }
'
{"jsonrpc":"2.0","result":{"blockTime":1730371045,"meta":{"computeUnitsConsumed":6349,"err":null,"fee":5000,"innerInstructions":[],"loadedAddresses":{"readonly":["4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"],"writable":["DUNbQThr4mNhhZBVqnWTSyy36rQE8dh8xEUDzJSq1aXa","mdeoks6fgy7muaZ2WtASZakNw6xjzdnzeZ2BfZfnkmR","E3eRXgQPpR27CaVt26rhWp2QEpaSgMMJqmcZCP7tHqvQ"]},"logMessages":["Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]","Program log: Instruction: TransferChecked","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6199 of 400000 compute units","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success","Program 11111111111111111111111111111111 invoke [1]","Program 11111111111111111111111111111111 success"],"postBalances":[4924289073,934087680,1,2039280,2039280,24715314,77166376524],"postTokenBalances":[{"accountIndex":3,"mint":"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU","owner":"8EhWjZGEt58UKzeiburZVx6QQF3rbayScpDjPNqCx62q","programId":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","uiTokenAmount":{"amount":"6500000","decimals":6,"uiAmount":6.5,"uiAmountString":"6.5"}},{"accountIndex":4,"mint":"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU","owner":"8NfYSmaYUFPPwa7YkJh98QSYWmw1A1CM98ZDQiXr8PMx","programId":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","uiTokenAmount":{"amount":"13500000","decimals":6,"uiAmount":13.5,"uiAmountString":"13.5"}}],"preBalances":[4924306418,934087680,1,2039280,2039280,24702969,77166376524],"preTokenBalances":[{"accountIndex":3,"mint":"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU","owner":"8EhWjZGEt58UKzeiburZVx6QQF3rbayScpDjPNqCx62q","programId":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","uiTokenAmount":{"amount":"8000000","decimals":6,"uiAmount":8.0,"uiAmountString":"8"}},{"accountIndex":4,"mint":"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU","owner":"8NfYSmaYUFPPwa7YkJh98QSYWmw1A1CM98ZDQiXr8PMx","programId":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","uiTokenAmount":{"amount":"12000000","decimals":6,"uiAmount":12.0,"uiAmountString":"12"}}],"rewards":[],"status":{"Ok":null}},"slot":336761912,"transaction":["Ab95HA/I96zVMhP6/DWWiWzFzz3S2qeMuq2suluw5Hc5R0WuPCuk8Hx0mdGvRUy59ym7dVmOCYgnNcIygUdBFA6AAQACA2uEKrOPvZNBtdUtSFXcg8+kj4O/Z1Ht/hwvnaqq5s6mBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN6DNWtaY/w2YHDuDsfkC/UJNp5p+8AzX0I+bY4pxjkwAgEEAwYEAAoMYOMWAAAAAAAGAgIABQwCAAAAOTAAAAAAAAAC/PSA7ptt/CTqicZK5tZVSI/C4I5cKeDMGZt+dba/3GUCAgQBAJDcz8dLPtTEIDIaAY0It5TpaUUrJIIy42+6pp9YVIqYAQEA","base64"],"version":0},"id":1}

为了便于分析,我们把 base64 编码的 Tx 转换为 Hex String 形式:

01bf791c0fc8f7acd53213fafc3596896cc5cf3dd2daa78cbaadacba5bb0e477394745ae3c2ba4f07c7499d1af454cb9f729bb75598e09882735c232814741140e80010002036b842ab38fbd9341b5d52d4855dc83cfa48f83bf6751edfe1c2f9daaaae6cea606ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90000000000000000000000000000000000000000000000000000000000000000de83356b5a63fc366070ee0ec7e40bf509369e69fbc0335f423e6d8e29c63930020104030604000a0c60e316000000000006020200050c02000000393000000000000002fcf480ee9b6dfc24ea89c64ae6d655488fc2e08e5c29e0cc199b7e75b6bfdc65020204010090dccfc74b3ed4c420321a018d08b794e969452b248232e36fbaa69f58548a98010100

可把上面 Tx 的每个字节含义解释如下:

signa-  <br>  tures signatures length 01
signature 0 bf791c0fc8f7acd53213fafc3596896cc5cf3dd2daa78cbaadacba5bb0e47739 <br>  4745ae3c2ba4f07c7499d1af454cb9f729bb75598e09882735c232814741140e
message version prefix 80
message   <br>  header num required signatures 01
num readonly signed accounts 00
num readonly unsigned accounts 02
account   <br>  addresses length 03
address 0 6b842ab38fbd9341b5d52d4855dc83cfa48f83bf6751edfe1c2f9daaaae6cea6 8EhWjZGEt58UKzeiburZVx6QQF3rbayScpDjPNqCx62q
address 1 06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
address 2 0000000000000000000000000000000000000000000000000000000000000000 11111111111111111111111111111111
recent blockhash de83356b5a63fc366070ee0ec7e40bf509369e69fbc0335f423e6d8e29c63930
instruc-  <br>  tions length 02
ins 0 program id index 01
account      <br>  address      <br>  indexes length 04
index 0 03 i.e. DUNbQThr4mNhhZBVqnWTSyy36rQE8dh8xEUDzJSq1aXa
index 1 06 i.e. 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU
index 2 04 i.e. mdeoks6fgy7muaZ2WtASZakNw6xjzdnzeZ2BfZfnkmR
index 3 00
instruction  <br>  data length 0a
data 0c60e316000000000006
ins 1 program id index 02
account      <br>  address      <br>  indexes length 02
index 0 00
index 1 05 i.e. E3eRXgQPpR27CaVt26rhWp2QEpaSgMMJqmcZCP7tHqvQ
instruction  <br>  data length 0c
data 020000003930000000000000
addr      <br>  lookup    <br>  tables length 02
addr0 lookup table account fcf480ee9b6dfc24ea89c64ae6d655488fc2e08e5c29e0cc199b7e75b6bfdc65 J2Ryr3MRadSJWvugVKnwU5reFmKSDDspsQW8HLeUHvyr
writable     <br>  indexes length 02
index 0 02
index 1 04
readonly     <br>  indexes length 01
index 0 00
addr1 lookup table account 90dccfc74b3ed4c420321a018d08b794e969452b248232e36fbaa69f58548a98 AkUzjYoaaTGthuEwCD8orpbkpN6iuqfhxDHvqPppkPcB
writable     <br>  indexes length 01
index 0 01
readonly     <br>  indexes length 00

这个例子中有两个 Address lookup table accounts,即 J2Ryr3MRadSJWvugVKnwU5reFmKSDDspsQW8HLeUHvyrAkUzjYoaaTGthuEwCD8orpbkpN6iuqfhxDHvqPppkPcB,它们分别保存了一些地址在链上(注:要在交易中使用它们时只需要在 writable indexes 和 readonly indexes 中指定它们在链上地址数组中的下标即可, Address lookup table 中的地址不能是交易的 Signer )。这些链上的地址,连同 Account Addresses(位于 Message Header 后面)一起,合并后可以得到指令中可以使用的最终帐户列表,如图 2 所示。最终帐户列表中的地址下标可以被 Instruction 引用。

solana_ALTs_merge.png

Figure 2: Solana 的 ALTs 和 Account Addresses 合并后得到最终帐户列表

由于 Solana 每个 Tx 有大小限制,Tx 的 Account Addresses(位于 Message Header 后面)中并不能保存太多的地址。 引入 Address lookup table 的主要目的就是缓解这个限制(因为地址是从链上读取的,不用直接打包在交易中,这可以显著减少交易大小), 交易 2Sca3UiTEaqQ7B9xPsL36gW8zRuBMRK5vDeUGG2Twe8zYfrVz1pwvAzbKHgRERsnDbVKQ6CUGCGJF1BiSQSafCCX 是另一个使用 Address lookup table 减少交易大小的例子。

5.1. Address lookup table 使用限制

Address lookup table 有下面限制:

  1. 如果地址是 signer,则不能放在 ALT 中。这是因为我们在使用 ALT 时,只有两个设置,即 writable indexes 和 readonly indexes,并没有 signer 相关的设置。

  2. program id 地址不能放在 ALT 中,program id 地址需要直接在 header 的 account addresses 中指定。只有 program id 的 accounts 地址才能放在 ALT 中。

参考: https://solana.stackexchange.com/questions/16122/using-program-ids-in-address-lookup-tables-missing-documentation-about-luts

5.2. Javascript 实例

打包上面交易的 Javascript 程序如下:

const web3 = require("@solana/web3.js");
const splToken = require("@solana/spl-token");

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function hexToUint8Array(hexString) {
    return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
}

const connection = new web3.Connection(web3.clusterApiUrl("devnet"), "confirmed");

let payerPrivateKey = "b44287f2f1d002e6a597bcc1d92cc939205e57efe06a9b7fcc683698c31335eb6b842ab38fbd9341b5d52d4855dc83cfa48f83bf6751edfe1c2f9daaaae6cea6";
let payer = web3.Keypair.fromSecretKey(hexToUint8Array(payerPrivateKey)); // payer addr: 8EhWjZGEt58UKzeiburZVx6QQF3rbayScpDjPNqCx62q

const usdcMint = new web3.PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");

async function createNewLookupTable() {
    // Why subtract 100 from the current slot?
    // See: https://solana.stackexchange.com/questions/4031/versioned-txs-157405384-is-not-a-recent-slot
    const slot = (await connection.getSlot()) - 100;

    const [lookupTableInstruction, lookupTableAddress] =
        web3.AddressLookupTableProgram.createLookupTable({
            authority: payer.publicKey,
            payer: payer.publicKey,
            recentSlot: slot,
        });
    console.log("lookup table address:", lookupTableAddress.toBase58()); // Example: QHKqp2HsUEqEcWxgYmemRVPAgDJs7BUCgekeiPmzufh

    const instructions = [\
        lookupTableInstruction,\
    ];

    // create v0 compatible message
    const messageV0 = new web3.TransactionMessage({
        payerKey: payer.publicKey,
        recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
        instructions,
    }).compileToV0Message();

    // create a v0 transaction from the v0 message
    const transactionV0 = new web3.VersionedTransaction(messageV0);
    // sign the v0 transaction
    transactionV0.sign([payer]);
    const txid = await web3.sendAndConfirmTransaction(connection, transactionV0);
    console.log(`Transaction (createNewLookupTable): https://explorer.solana.com/tx/${txid}?cluster=devnet`);

    return lookupTableAddress;
}

async function addAddressToLookupTable(lookupTableAddress, addresses) {
    // add addresses to the `lookupTableAddress` table via an `extend` instruction
    const extendInstruction = web3.AddressLookupTableProgram.extendLookupTable({
        payer: payer.publicKey,
        authority: payer.publicKey,
        lookupTable: lookupTableAddress,
        addresses: addresses,
    });

    const instructions = [\
        extendInstruction,\
    ];

    // create v0 compatible message
    const messageV0 = new web3.TransactionMessage({
        payerKey: payer.publicKey,
        recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
        instructions,
    }).compileToV0Message();

    // create a v0 transaction from the v0 message
    const transactionV0 = new web3.VersionedTransaction(messageV0);
    // sign the v0 transaction
    transactionV0.sign([payer]);
    const txid = await web3.sendAndConfirmTransaction(connection, transactionV0);
    console.log(`Transaction (addAddressToLookupTable): https://explorer.solana.com/tx/${txid}?cluster=devnet`);
}

async function txExampleWithLookupTableAddress(lookupTableAddresses, usdcSenderTokenAccount, usdcRecipientTokenAccount, solRecipient) {
    const addressLookupTableAccounts = [];
    for (let i = 0; i &lt; lookupTableAddresses.length; i++) {
        // get the table from the cluster
        const lookupTableAccount = (
            await connection.getAddressLookupTable(lookupTableAddresses[i])
        ).value;

        // loop through and parse all the addresses stored in the table
        for (let j = 0; j &lt; lookupTableAccount.state.addresses.length; j++) {
            const address = lookupTableAccount.state.addresses[j];
            console.log(`Table ${i}, Address ${j}:`, address.toBase58());
        }

        addressLookupTableAccounts.push(lookupTableAccount);
    }

    const instructions = [\
        splToken.createTransferCheckedInstruction(\
            usdcSenderTokenAccount, // source\
            usdcMint,\
            usdcRecipientTokenAccount, // destination\
            payer.publicKey, // owner\
            1500000, // 发送的 token 数量,单位是最小单位\
            6, // number of decimals\
            [],\
            splToken.TOKEN_PROGRAM_ID,\
        ),\
\
        web3.SystemProgram.transfer({\
            fromPubkey: payer.publicKey,\
            toPubkey: solRecipient,\
            lamports: 12345,\
        }),\
    ];

    // create v0 compatible message
    const messageV0 = new web3.TransactionMessage({
        payerKey: payer.publicKey,
        recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
        instructions,
    }).compileToV0Message(addressLookupTableAccounts);

    // create a v0 transaction from the v0 message
    const transactionV0 = new web3.VersionedTransaction(messageV0);
    // sign the v0 transaction
    transactionV0.sign([payer]);
    const txid = await web3.sendAndConfirmTransaction(connection, transactionV0);
    console.log(`Transaction (txExampleWithLookupTableAddress): https://explorer.solana.com/tx/${txid}?cluster=devnet`);
}

async function main() {
    // Step 1: Create a new lookup table 1
    const lookupTableAddress1 = await createNewLookupTable(); // J2Ryr3MRadSJWvugVKnwU5reFmKSDDspsQW8HLeUHvyr
    await sleep(3000);
    // https://explorer.solana.com/tx/56mj21K6MjPzrQLep2KR4ZqRN9dZojUQ6Xu7f5pMvcQjHMxmAsNSBaBtvXnwzLqFnj5mc42qPRTUoNNDaqnB9BEk?cluster=devnet

    const usdcSender = payer.publicKey;
    const usdcSenderTokenAccount = new web3.PublicKey("DUNbQThr4mNhhZBVqnWTSyy36rQE8dh8xEUDzJSq1aXa");

    const usdcRecipient = new web3.PublicKey("8NfYSmaYUFPPwa7YkJh98QSYWmw1A1CM98ZDQiXr8PMx");
    const usdcRecipientTokenAccount = new web3.PublicKey("mdeoks6fgy7muaZ2WtASZakNw6xjzdnzeZ2BfZfnkmR");

    const newAddressesAddToALTs1 = [\
        usdcMint,\
        usdcSender,\
        usdcSenderTokenAccount,\
        usdcRecipient,\
        usdcRecipientTokenAccount,\
    ];

    // Step 2: Add addresses to the lookup table 1
    await addAddressToLookupTable(lookupTableAddress1, newAddressesAddToALTs1);
    await sleep(3000);
    // https://explorer.solana.com/tx/24Bqs22G5sbYKEZ7sTfyZ3DYNM93fgazGtqBVjDWoG3T5Xq9qR73AuaBcq2TDnbf1ebgSWi6tqQWEqkSXjMYWTq4?cluster=devnet

    // Step 3: Create a new lookup table 2
    const lookupTableAddress2 = await createNewLookupTable(); // AkUzjYoaaTGthuEwCD8orpbkpN6iuqfhxDHvqPppkPcB
    await sleep(3000);
    // https://explorer.solana.com/tx/eXQZzfn1wvGCFY1ZD8wbPFEThEPkeizam9eFK15Woq5NNQzUQqZLFpr9TcY2417sT81TU1VDKXbsJ646fKA2BpA?cluster=devnet

    const solRecipient = new web3.PublicKey("E3eRXgQPpR27CaVt26rhWp2QEpaSgMMJqmcZCP7tHqvQ");
    const newAddressesAddToALTs2 = [\
        payer.publicKey, // 8EhWjZGEt58UKzeiburZVx6QQF3rbayScpDjPNqCx62q\
        solRecipient,\
    ];

    // Step 4: Add addresses to the lookup table 2
    await addAddressToLookupTable(lookupTableAddress2, newAddressesAddToALTs2);
    await sleep(3000);
    // https://explorer.solana.com/tx/2DYwAjn1aEW7gLXVUvW1tCiduB6iuZy5JcgF675c3tdFBYyxQK2RibMVgUsun5iQqeVFjd1e4EGwEpX5Xt4SiDfg?cluster=devnet

    // Step 3: Tx example with lookup table address
    await txExampleWithLookupTableAddress([lookupTableAddress1, lookupTableAddress2], usdcSenderTokenAccount, usdcRecipientTokenAccount, solRecipient);
    // https://explorer.solana.com/tx/4q2w12CtjRxHtdfnWATLwtTbkngMwux8Wwr2rSwQib8tk1wsFqc4NhBU1t4Z7NBCtXnAAajxhvJz5iHJNxzT8J1K?cluster=devnet
}

main().catch((error) => {
    console.error(error);
    process.exit(1);
});

6. 附录

6.1. Legacy Tx 和 V0 Tx 区别

目前 Solana 支持两种 Tx:

  • legacy - older transaction format with no additional benefit

  • 0 - added support for Address Lookup Tables

2, 3, 4 介绍的例子都是 Legacy Tx;Tx 5tbKLJCGpb2nCtTvgBdmT9d9gYt88CJARmdrGWafdN1T75GTxa562CB47wK87YrM4zeXk1ZSbSTbfFidYfibUii7 是 Version 0 Tx 的例子。

下面我们通过 RPC getTransaction 得到这个 V0 Tx 的 base64 编码:

$ curl https://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" -d '
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getTransaction",
    "params": [\
      "5tbKLJCGpb2nCtTvgBdmT9d9gYt88CJARmdrGWafdN1T75GTxa562CB47wK87YrM4zeXk1ZSbSTbfFidYfibUii7",\
      {\
        "maxSupportedTransactionVersion": 0,\
        "encoding": "base64"\
      }\
    ]
  }
'
{"jsonrpc":"2.0","result":{"blockTime":1728358950,"meta":{"computeUnitsConsumed":150,"err":null,"fee":5000,"innerInstructions":[],"loadedAddresses":{"readonly":[],"writable":[]},"logMessages":["Program 11111111111111111111111111111111 invoke [1]","Program 11111111111111111111111111111111 success"],"postBalances":[8995200,2308228622,1],"postTokenBalances":[],"preBalances":[77163960,2240064862,1],"preTokenBalances":[],"rewards":[],"status":{"Ok":null}},"slot":294356410,"transaction":["AfSPBaOhY49VsqQM8C+uvAn9vDcfJQRntic/YfvX2rCmQjMXK9Z4EeML3PP5gIsR2ZGlTICLATnakn6J+BIdIgyAAQABA3jdpBVkuQ1BY5Bz20KCVfbhJibMRd3dhqnUXp3AS0MV1B1bk/IiggjB55B2oFxQe+QpB4KjtY7cN1E/YatzHbEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFSZbNeMNJrG6mEsWMCSaQ0PRN1QFVxtA8D1CsxvEcXzAQICAAEMAgAAALAYEAQAAAAAAA==","base64"],"version":0},"id":1}

把 base64 编码转换为 hex 编码,得到:

01f48f05a3a1638f55b2a40cf02faebc09fdbc371f250467b6273f61fbd7dab0a64233172bd67811e30bdcf3f9808b11d991a54c808b0139da927e89f8121d220c800100010378dda41564b90d41639073db428255f6e12626cc45dddd86a9d45e9dc04b4315d41d5b93f2228208c1e79076a05c507be4290782a3b58edc37513f61ab731db1000000000000000000000000000000000000000000000000000000000000000054996cd78c349ac6ea612c58c092690d0f44dd50155c6d03c0f50acc6f11c5f301020200010c02000000b01810040000000000

可把上面 Tx 的每个字节含义解释如下:

signatures signatures length 01
signature 0 f48f05a3a1638f55b2a40cf02faebc09fdbc371f250467b6273f61fbd7dab0a6 <br>  4233172bd67811e30bdcf3f9808b11d991a54c808b0139da927e89f8121d220c
message version prefix 80
message header num required signatures 01
num readonly signed accounts 00
num readonly unsigned accounts 01
account addresses length 03
address 0 78dda41564b90d41639073db428255f6e12626cc45dddd86a9d45e9dc04b4315
address 1 d41d5b93f2228208c1e79076a05c507be4290782a3b58edc37513f61ab731db1
address 2 0000000000000000000000000000000000000000000000000000000000000000
recent blockhash 54996cd78c349ac6ea612c58c092690d0f44dd50155c6d03c0f50acc6f11c5f3
instructions length 01
ins 0 program id index 02
account address  <br>      indexes length 02
index 0 00
index 1 01
instruction data length 0c
data 02000000b018100400000000
addr lookup tables length 00

我们可以看到 V0 Tx 比 Legacy Tx 的序列化数据多了两部分内容:

  1. V0 Tx 多了 1 字节的 Version Prefix: 它的首个比特位如果是 1 则表示这个 Tx 是 Versioned Tx,后面 7 个比特位则是具体版本号(对于 V0 Tx 来说后面 7 个比特位全部是 0)。如果这个字节首个比特位是 0,则是 Legacy Tx。 参考: https://github.com/solana-labs/solana/blob/27eff8408b7223bb3c4ab70523f8a8dca3ca6645/sdk/program/src/message/versions/mod.rs#L30

  2. V0 Tx 最后部分多了 Address Look Tables 数组。不过,上面例子中它没有内容,所以只有一个 length 为 0x00 的数据。

7. 参考

  1. Solana Transactions: https://solana.com/docs/core/transactions

  2. bincode Serialization specification: https://github.com/bincode-org/bincode/blob/trunk/docs/spec.md

  3. The Solana host and client SDK: https://docs.rs/solana-sdk/latest/solana_sdk/index.html

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
cig01
cig01
https://aandds.com/