分类 Solana-新手教程 下的文章

SVM 私有链 Websocket延迟优化


背景

当交易上链后,需要websocket尽快订阅到新发起的交易信息,但已有逻辑有2s左右延迟

目前逻辑

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "blockSubscribe",
  "params": [
    { },
    { "commitment": "finalized" }
  ]
}

当前测试环境为私有网络,且仅有一个Validator

优化延迟

优化链路

  • 原有链路:监听服务 -> WAF -> RPC -> Validator
  • 压缩链路
    • 监听服务 -> RPC -> Validator
      • 去除WAF防护过程造成的延迟
      • 保留RPC对Validator压力防护
    • 监听服务 -> Validator
      • 在1的基础上去除RPC同步Validator区块的时间
      • 在链路路径上,为最短路径,只适合核心私有服务,时间优先

阶段测试结果

结果:链路优化后,延迟优化效果不明显

排除:排除WAF设置或防护逻辑,以及RPC同步相关对websocket的影响

修改参数

修改blockSubscribe.commitment 参数

测试结果

测试参数 最大延迟 概率 备注
blockSubscribe (confirmed) < 1s 95% 满足实时需求
blockSubscribe (finalized) ~ 2s 均值

RPC节点需开启 --rpc-pubsub-enable-block-subscription

总结:当blockSubscribe.commitment 为confirmed时,满足实时性需求

下面继续做具体分析,以及安全性确认

整体分析

commitment

Solana 分别引入了 processedconfirmedfinalized 三种 commitment

Commitment 等级 状态说明 是否可能回滚 用途
processed 当前 leader 节点已执行交易(未确认) 可能被 fork 快速响应
confirmed 超过 1 个 superminority(2/3 vote)节点已投票确认 小概率回滚(如果 fork 被重组) 较安全且快速
finalized 已不可逆地写入链上(root slot) 永不回滚 最终状态

安全分析

confirmed 还可能丢弃?

Solana 是基于 Turbine + Tower BFT 共识 的区块链,允许 fork 存在,直到达成“supermajority lockout”:

举个例子:

  1. Leader A 在 slot 100 产出 block1,包含你的交易 tx1
  2. 多数 validator 已投票确认 → tx1 达到 confirmed
  3. 但 Leader B 在 slot 100 提出了 fork block2(没有 tx1)
  4. 网络切换了分叉,block2 成为主链,block1 被丢弃
  5. 最终你会看到 tx1 消失

这种情况虽然极少发生(除非网络分叉非常激烈),但确实有可能,尤其在以下情况下:

  • 网络波动严重(多个 validator 未及时同步)
  • 特别是在 devnet/testnet 上更常见
  • 冲突交易竞争激烈(如套利机器人密集交易)

单一Validator

结论:如果整个网络只有一个 validator,交易一旦被执行(即 processed),就等同于 finalized,不会被回滚或丢弃

原因:Solana 的分叉和回滚机制依赖于 多个 validator 的投票(tower consensus),但如果你只有一个 validator 节点(如私链或测试环境):

  • 所有交易处理都只走 一个 chain path
  • 没有投票机制(自己 vote 自己)
  • 没有 fork、没有分歧
  • 所有被处理的 block/slot 会直接成为 root → 立即 finalized

延迟分析

问题:在只有一个 Validator 的 Solana 网络中,为什么交易已经 confirmed,但还没有立即 finalized?不是说没有其他节点就没有分叉吗?

原因:因为 finalized 状态取决于 root slot 的推进,而 root slot 的推进有“延迟”或“惰性机制”,即使只有一个 validator。

Solana 的 finalized 并不是单纯根据是否有分叉来判断,而是依赖一个特殊机制:

Tower BFT + Root Slot 推进规则:

  • 每个 validator 会有自己的 "lockout" 投票历史,形成“投票树”
  • 当 validator 在连续的 slot 上投票到一定深度,就能推进 root slot
  • 只有 root 之后的 slot 才会被标记为 finalized

即使只有一个 validator:

  1. 它仍然要投票 → 进入 lockout(自己 vote 自己)
  2. 它仍然需要满足 一定的 slot 深度 才能将旧 slot 设为 root
  3. root slot 推进后,对应的交易和 block 才被认为是 finalized

为什么存在“延迟 finalized”?

机制 描述
lockout period Tower BFT 规定,validator 必须投票到一定深度才可推进 root
skip slot 会阻碍 如果出现 skipped slot(空块),会延迟 root 推进
安全考虑 即使是单节点,仍沿用同样安全逻辑,避免不同部署模式产生不一致行为

举例

假设你只有一个 validator,slot 时间 400ms:

  1. 你在 slot 100 发出一笔交易
  2. 交易执行完成 → processed
  3. 自己投票确认该 block → confirmed
  4. 继续生产 slot 101、102...
  5. 到 slot 104 时,系统满足 root 推进条件,slot 100 被设为 root → 现在它才是 finalized

所以即使没有其他 validator,finalized 仍然比 confirmed 晚 2~4 个 slot(~1.5 秒)

最终总结

  1. 通过blockSubscribe (confirmed) 满足业务实时性要求
  2. 单一Validator,通过订阅confirmed,不存在安全性问题

TokenPocket 如何添加Solana私有网络


安装Chrome扩展

https://chromewebstore.google.com/detail/tokenpocket-web3-crypto-%E9%92%B1/mfgccjchihfkkindfppnaooecgfneiii?utm_source=ext_app_menu

打开资产展示

添加自定义网络

选择SVM


填入私有链对应的RPC,链ID 随意 (对于Solana链没有链ID,这里只是为了与EVM链类型共用布局)

例如

只做演示,BCSkill 技术社区未对外提供私有Solana链支持

添加新增网络

测试

然后回到资产展示,选择新添加的网络即可

注意

不要直接从 系统设置->节点列表->选择Solana->添加自定义节点
这里的添加只能添加Solana官方的节点,添加时会通过RPC 获取getGenesisHash,得到创世hash并与Solana 官方devnet testnet mainnet 进行比对,如果不一致则报错。

应该先添加完自定义网络后,再给自定义网络添加自定义节点


Solana RPC 返回 VersionedTransaction too large


Solana构建发起交易时,返回错误

(*jsonrpc.RPCError)(0xc00739b530)({
 Code: (int) -32602,
 Message: (string) (len=122) "base64 encoded solana_sdk::transaction::versioned::VersionedTransaction too large: 1668 bytes (max: encoded/raw 1644/1232)",
 Data: (interface {}) <nil>
})

错误分析

交易的大小超过1232 字节限制

static_assertions::const_assert_eq!(PACKET_DATA_SIZE, 1232);
/// Maximum over-the-wire size of a Transaction
/// 1280 是 IPv6 最小 MTU
/// 40 字节是 IPv6 报头的大小
/// 8 字节是分片报头的大小
pub const PACKET_DATA_SIZE: usize = 1280 - 40 - 8;

PACKET_DATA_SIZE 硬编码,不可配置,为保障节点间数据传输的稳定和安全性

解决

拆分交易,将多个逻辑拆分到多笔交易

参考

https://github.com/solana-foundation/anchor/issues/2051


Solana调整出块时间为50ms,并且保持recent_blockhash(120秒)过期时间不变


背景

比如想基于Solana运行一个Layer2网络或者应用链,需要尽可能的加快出块时间{从综合测试和其它类似项目综合分析,50ms是最快最稳定的},
如果单一的降低出块时间,会导致 recent_blockhash过期时间变短,从而可能导致交易发送时容易报 “Blockhash not found”
下面我们将给出如何实现50ms的slot,并保持recent_blockhash过期时间(120s)不变的方案

代码修改

agave\sdk\program\src\clock.rs

/// 每秒 tick 数:PoH 产生 tick 的速度(原为 160)
pub const DEFAULT_TICKS_PER_SECOND: u64 = 1000;  // 1000 ticks = 1秒

/// 每 slot 包含 tick 数(原为 64)
pub const DEFAULT_TICKS_PER_SLOT: u64 = 50;      // 50 ticks = 1 slot

/// blockhash 最大有效时间(保持主网一致)
pub const MAX_HASH_AGE_IN_SECONDS: u64 = 120;    // blockhash 有效时间 120秒

Solana 会自动计算:

pub const MAX_RECENT_BLOCKHASHES: usize =
    MAX_HASH_AGE_IN_SECONDS * DEFAULT_TICKS_PER_SECOND as usize / DEFAULT_TICKS_PER_SLOT as usize;
// => 2400

MAX_RECENT_BLOCKHASHES 为什么要 调整为 2400

这是因为你想要 每个 slot 时间为 50ms,而官方默认是 400ms,两者时间差 8 倍。

解释为什么需要把 MAX_RECENT_BLOCKHASHES 调整到 2400

1. 官方默认参数回顾:

参数 数值
MAX_HASH_AGE_IN_SECONDS 120 s
DEFAULT_TICKS_PER_SECOND 160
DEFAULT_TICKS_PER_SLOT 64
MAX_RECENT_BLOCKHASHES 300

计算:

意味着最近 120 秒内链上大约有 300 个 slot(因为每 slot 约 0.4 秒)。


2. 你想改成 50ms/slot

50ms = 0.05秒,等于官方 0.4秒的 18\frac{1}{8}81。

在 120 秒内,会有:


3. 为了保持 blockhash 有效时间不变

你必须保存最近 2400 个 blockhash(对应 2400 个 slot),才能覆盖 120 秒内所有 slot。


4. 计算验证

MAX_RECENT_BLOCKHASHES = MAX_HASH_AGE_IN_SECONDS * DEFAULT_TICKS_PER_SECOND / DEFAULT_TICKS_PER_SLOT

现在参数改成:

  • MAX_HASH_AGE_IN_SECONDS = 120
  • DEFAULT_TICKS_PER_SECOND = 1000 (tick 速度提高了)
  • DEFAULT_TICKS_PER_SLOT = 50

计算:


总结

每 slot 时间 slot 数(120秒内) 需保存最近 blockhash 数量 MAX_RECENT_BLOCKHASHES
400ms 300 300
50ms 2400 2400

所以,MAX_RECENT_BLOCKHASHES 调整为 2400 是为了匹配新的更快 slot 频率,保证 blockhash 在 120 秒内仍有效

solana-genesis 初始化

对于初始化时也需要对应的加入启动参数

--ticks-per-slot 50

测试脚本

#!/bin/bash

RPC_URL="https://api.mainnet-beta.solana.com"

# Step 1: 获取最新 blockhash
echo "Fetching latest blockhash..."
RESPONSE=$(curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"getLatestBlockhash"}')

BLOCKHASH=$(echo $RESPONSE | jq -r '.result.value.blockhash')

echo " Got blockhash: $BLOCKHASH"
echo " Starting to monitor validity..."

SECONDS_ELAPSED=0
INTERVAL=1  # 每次检测间隔,单位秒

while true; do
  VALID_RESPONSE=$(curl -s -X POST $RPC_URL \
    -H "Content-Type: application/json" \
    -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"isBlockhashValid\",\"params\":[\"$BLOCKHASH\"]}")

  IS_VALID=$(echo $VALID_RESPONSE | jq -r '.result.value')

  if [ "$IS_VALID" = "true" ]; then
    echo "[$SECONDS_ELAPSED s]  Blockhash still valid"
    sleep $INTERVAL
    SECONDS_ELAPSED=$((SECONDS_ELAPSED + INTERVAL))
  else
    echo "[$SECONDS_ELAPSED s]  Blockhash EXPIRED"
    break
  fi
done

echo " Total validity duration: $SECONDS_ELAPSED seconds"

Solana SPL 程序编译


代码Clone

git clone https://github.com/solana-program/token.git
cd token
git checkout program@v3.5.0

基础环境

# 安装nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
source ~/.bashrc

# 安装node
nvm install 20
node -v

# 安装pnpm
npm install -g pnpm

安装依赖

pnpm install

编译

cd program/
cargo build-sbf

默认编译可能出现以下错误

[2025-04-25T09:39:35.004769959Z ERROR cargo_build_sbf] Failed to obtain package metadata: `cargo metadata` exited with an error: error: current package believes it's in a workspace when it's not:
    current:   /mnt/d/github/token/program/Cargo.toml
    workspace: /mnt/d/github/token/Cargo.toml

    this may be fixable by adding `program` to the `workspace.members` array of the manifest located at: /mnt/d/github/token/Cargo.toml
    Alternatively, to keep it out of the workspace, add the package to the `workspace.exclude` array, or add an empty `[workspace]` table to the package's manifest.

解决

在 program 目录的 Cargo.toml 文件中添加空的 [workspace] 表

修改 program\Cargo.toml,在第一行增加

[workspace] // 增加此行
[package]
name = "spl-token"
version = "3.5.0"
...

执行成功后,程序编译出现在program\target\deploy\spl_token.so