Solana Durable Nonce介绍与python使用

Solana Durable Nonce 是什么？

solana 发交易需要对recent_blockhash 做签名，超过 2 分钟之后这个交易就不再被承认，从而避免双花

但这个设计无法支持多签场景（多个人没办法保证 2 分钟之内干完所有签名并广播），Solana 就引入了 Nonce Account 来支持离线签名

效果：

一笔交易可以不再依赖最近的区块 hash，而是改用 Nonce Account 存储的一个值，从而可以任意时间内完成离线签名；

交易的第一条指令需要是 AdvanceNonce，使得存储的 Nonce 值发生变化

无法提前预测 Nonce 的变化（不是 EVM 连续整数而是 Hash）

没有限制一个地址能拥有多少个 Nonce Account，所以如果需要离线签名多笔交易就需要多个 Nonce Account

如何用 Python 发送简单的转账交易

基础知识：

lamport 是 SOL 的最小单位 1 lamport = 1e-9 SOL

测试网可以通过 request_airdrop 来获取测试币，对 IP 限制 1 次 / 24 小时，最多请求 5SOL

Solana 地址，BlockHash 都是用 Base58 编码表示 本质上也是一个 uint256

虽然有 solathon 这个 python 包 但功能支持很不完备 甚至 solana-py 也没有暴露 Nonce 相关的接口 只能用更底层的 solders

solana-py 网络请求用的是 httpx，并不尊重 https_proxy 环境变量，需要用 proxychains 走代理

solana-py 和 solders 的文档都很不完善 建议参考solana web3.js 文档，以及直接搜索代码库

Copy
# pip install solana==0.32.0
import base58
from solana.rpc.api import Client
from solders.keypair import Keypair
from solders.system_program import transfer, TransferParams
from solana.transaction import Transaction
client = Client("https://api.devnet.solana.com")

sender = Keypair()
print("save this privatekey sender:", base58.b58encode(bytes(sender.to_bytes_array())).decode())
#sender = Keypair.from_base58_string(pk1)
print("testnet request airdrop:", client.request_airdrop(sender.pubkey(), 5*10**9))
from time import sleep
sleep(10) #wait for airdrop finish

receiver = Keypair().pubkey() #change to your transfer to

amt = 1238888 # must be greater than client.get_minimum_balance_for_rent_exemption(0).value
instruction = transfer(TransferParams(
from_pubkey=sender.pubkey(),
to_pubkey=receiver,
lamports=amt
))
instructions=[instruction]

bh = client.get_latest_blockhash().value.blockhash
transaction = Transaction(recent_blockhash=bh, instructions=instructions, fee_payer=sender.pubkey())
transaction.sign(sender)
stx = transaction.serialize()
result = client.send_raw_transaction(stx)
print("transfer tx:", str(result.value))

交易示例：https://explorer.solana.com/tx/3w3Fj2wYHNxr49umWf2p94RHxfEMWwk2JCQp1U1btniW9eLugVT5ZWiiJ2pf1a1gJdpZsBhombdHmnyQSbWUQfKn?cluster=devnet

如何创建 Nonce Account

Copy
from solana.rpc.api import Client
from solders.keypair import Keypair
from solders.system_program import create_nonce_account
from solana.transaction import Transaction
client = Client("https://api.devnet.solana.com")

pk1 = "..."
sender = Keypair.from_base58_string(pk1)
pk2 = "..."
nonceacc = Keypair.from_base58_string(pk2)

rent_value = client.get_minimum_balance_for_rent_exemption(80).value
instructions = create_nonce_account(
from_pubkey=sender.pubkey(),
nonce_pubkey=nonceacc.pubkey(),
authority=sender.pubkey(),
lamports=rent_value
)

bh = client.get_latest_blockhash().value.blockhash
transaction = Transaction(recent_blockhash=bh, instructions=instructions, fee_payer=sender.pubkey())
transaction.sign(sender, nonceacc)
stx = transaction.serialize()
result = client.send_raw_transaction(stx)
print("create nonce account tx:", str(result.value))

交易示例：https://explorer.solana.com/tx/4GqHiidSi1s5bzNijKyn5yoweEAu97W26524je62iY5nrAVEFDF62sW869mJbFBHBXsLWkFHsZoPne1xbmGCQboi?cluster=devnet

其中 solana 创建一个新的地址需要余额大于 rent exemption 的需要，nonce account 占用 80 字节，使用 get_minimum_balance_for_rent_exemption 来查询需要的数量

如何使用 Durable Nonce 发送转账交易

Copy
from solana.rpc.api import Client
from solders.hash import Hash
from solders.keypair import Keypair
from solders.pubkey import Pubkey
from solders.system_program import transfer, TransferParams, create_nonce_account, AdvanceNonceAccountParams, advance_nonce_account
from solana.transaction import Transaction

pk1 = "..."
sender = Keypair.from_base58_string(pk1)
pk2 = "..."
nonceacc = Keypair.from_base58_string(pk2)
receiver = "..."
receiver = Pubkey.from_string(receiver)

advance = advance_nonce_account(AdvanceNonceAccountParams(
nonce_pubkey=nonceacc.pubkey(),
authorized_pubkey=sender.pubkey()
))
amt = 1238888
instruction = transfer(TransferParams(
from_pubkey=sender.pubkey(),
to_pubkey=receiver, lamports=amt
))
instructions=[advance, instruction]

nonceinfo = client.get_account_info(nonceacc.pubkey())
bh = Hash.from_bytes(nonceinfo.value.data[40:72])
transaction = Transaction(recent_blockhash=bh, instructions=instructions, fee_payer=sender.pubkey())
transaction.sign(sender)
stx = transaction.serialize()
result = client.send_raw_transaction(stx)
print("transfer with nonce tx:", str(result.value))

交易示例：https://explorer.solana.com/tx/5nHUFP6K8px6L7N7k7qsCcJiyigGj11KuzPW7AZT5YZ3j2SZqcnQFdvjK1gJa4kyM48Ev4YxuTbWczNj5kyz22Bk?cluster=devnet

为什么 nonce 值是 account info 里面的[40:72]?

根据 https://github.com/solana-labs/solana-web3.js/blob/b7d1c26/packages/library-legacy/src/nonce-account.ts#L11

可以看出 Nonce 账号存储的数据是：

I uint32 version=1

I uint32 state=1

32s publicKey authorizedPubkey=sender 地址

32s publicKey nonce = 当前 nonce 值 就是[40:72]

Q uint64 feeCalculator=5000 每个签名需要多少 lamports

你可以这样解码 Nonce Account Data: struct.unpack("II32s32sQ", nonceinfo.value.data)

AdvanceNonce 之后 nonce 到底怎么变化的呢？

AdvanceNonce 指令在这里处理

https://github.com/solana-labs/solana/blob/cdb0d15283bbad0724a98d58458684468921c66c/programs/system/src/system_instruction.rs#L45

Copy
let next_durable_nonce = DurableNonce::from_blockhash(&invoke_context.blockhash);

https://github.com/solana-labs/solana/blob/cdb0d15283bbad0724a98d58458684468921c66c/sdk/program/src/nonce/state/current.rs#L56

而 DurableNonce::from_blockhash 就是

Copy
const DURABLE_NONCE_HASH_PREFIX: &[u8] = "DURABLE_NONCE".as_bytes();

pub fn from_blockhash(blockhash: &Hash) -> Self {
Self(hashv(&[DURABLE_NONCE_HASH_PREFIX, blockhash.as_ref()]))
}

这里的 hashv 是 sha256，所以新的 nonce = sha256 ("DURABLE_NONCE" + parentBlockHash)，就是当前交易所在区块的上一个区块 hash 加个前缀DURABLE_NONCE再做 hash

Copy
import base58
h=hashlib.sha256()
h.update(b"DURABLE_NONCE" + base58.b58decode(parentBlockHash))
print(h.digest())