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 文檔,以及直接搜索代碼庫
# 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))
如何創建 Nonce Account#
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))
其中 solana 創建一個新的地址需要餘額大於 rent exemption 的需要,nonce account 佔用 80 字節,使用 get_minimum_balance_for_rent_exemption 來查詢需要的數量
如何使用 Durable Nonce 發送轉帳交易#
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))
為什麼 nonce 值是 account info 裡面的[40:72]
?#
可以看出 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 指令在這裡處理
let next_durable_nonce = DurableNonce::from_blockhash(&invoke_context.blockhash);
而 DurableNonce::from_blockhash 就是
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
import base58
h=hashlib.sha256()
h.update(b"DURABLE_NONCE" + base58.b58decode(parentBlockHash))
print(h.digest())