mookim

mookim

mookim.eth

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 文檔,以及直接搜索代碼庫
# 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#

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 發送轉帳交易#

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

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 就是

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())
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。