高级
Day 9
55 分钟
高级交易与错误处理
深入理解 TTL、Nonce、Gas 管理机制,并学习健壮的错误处理和重试策略。
学习目标
- 理解 TTL(交易有效期)机制
- 掌握 Nonce 管理与并发问题
- 调整 Gas 限制和价格
- 处理节点连接错误
- 诊断交易失败原因
- 实现交易确认重试逻辑
第一部分:高级交易参数
理解控制交易行为的关键参数:TTL、Nonce 和 Gas。
1. Time To Live (TTL)
TTL 定义了交易的有效区块高度。超过此高度后,未被打包的交易将从内存池中移除。
TTL 工作原理
| 配置 | 行为 | 适用场景 |
|---|---|---|
| 默认 (current + 500) | 约 25 小时有效期 | 普通交易 |
| 短 TTL (current + 50) | 约 2.5 小时有效期 | 时间敏感交易 |
| 长 TTL (current + 1000) | 约 50 小时有效期 | 网络拥堵时期 |
自定义 TTL
package main
import (
"log"
"math/big"
"github.com/aeternity/aepp-sdk-go/v9/naet"
"github.com/aeternity/aepp-sdk-go/v9/transactions"
)
func main() {
node := naet.NewNode("https://testnet.aeternity.io", false)
// 获取当前区块高度
height, err := node.GetHeight()
if err != nil {
log.Fatal(err)
}
// 使用 TTLNoncer 创建交易
ttlnoncer := transactions.NewTTLNoncer(node)
spendTx, _ := transactions.NewSpendTx(
alice.Address, recipientAddress,
big.NewInt(1e18), []byte{}, ttlnoncer,
)
// 修改 TTL(交易对象字段是导出的)
spendTx.TTL = height + 100 // 短 TTL:约 5 小时
// 或者设置更长的 TTL
// spendTx.TTL = height + 2000 // 约 100 小时
log.Printf("Current height: %d, TX TTL: %d\n", height, spendTx.TTL)
}
注意:如果交易在 TTL 前未被打包,需要创建新交易。不要重复广播过期交易。
2. Nonce 管理
Nonce 是账户的交易序号,必须严格递增。它防止重放攻击并确保交易顺序。
Nonce 规则
| 情况 | 结果 |
|---|---|
| nonce = expected | 交易有效 |
| nonce < expected | 拒绝(Nonce too low) |
| nonce > expected + 1 | 等待前序交易(可能超时) |
并发交易的 Nonce 问题
// 问题:快速发送多笔交易时的 Nonce 冲突
// TTLNoncer 每次都从节点查询 nonce
// 如果前一笔交易还未确认,会获取到相同的 nonce
// ❌ 错误方式
for i := 0; i < 5; i++ {
ttlnoncer := transactions.NewTTLNoncer(node)
tx, _ := transactions.NewSpendTx(..., ttlnoncer)
// 可能所有交易都使用相同 nonce,只有第一笔成功
}
// ✅ 正确方式:手动管理 nonce
account, _ := node.GetAccount(alice.Address)
startNonce := account.Nonce + 1
for i := 0; i < 5; i++ {
tx, _ := transactions.NewSpendTx(...)
tx.Nonce = startNonce + uint64(i) // 递增 nonce
// 签名并广播
signedTx, txHash, _, _ := transactions.SignHashTx(alice, tx, networkID)
node.PostTransaction(signedTx, txHash)
log.Printf("TX %d sent with nonce %d\n", i, tx.Nonce)
}
批量交易的最佳实践
type NonceManager struct {
currentNonce uint64
mu sync.Mutex
}
func NewNonceManager(node *naet.Node, address string) (*NonceManager, error) {
acc, err := node.GetAccount(address)
if err != nil {
return nil, err
}
return &NonceManager{currentNonce: acc.Nonce}, nil
}
func (nm *NonceManager) Next() uint64 {
nm.mu.Lock()
defer nm.mu.Unlock()
nm.currentNonce++
return nm.currentNonce
}
// 使用示例
nm, _ := NewNonceManager(node, alice.Address)
for i := 0; i < 10; i++ {
tx, _ := transactions.NewSpendTx(...)
tx.Nonce = nm.Next()
// 并发安全地发送交易
}
3. Gas 计算与调整
每笔交易都需要 Gas 作为执行费用。Gas = GasLimit × GasPrice。
Gas 参数说明
| 参数 | 单位 | 说明 | 推荐值 |
|---|---|---|---|
| GasPrice | aettos/gas | 每单位 gas 的价格 | ≥ 1,000,000,000 (1 Gwei) |
| GasLimit | gas units | 最大允许消耗量 | 交易类型相关 |
| Fee | aettos | 实际支付费用 | GasUsed × GasPrice |
不同交易类型的 Gas 估算
| 交易类型 | 典型 GasLimit | 备注 |
|---|---|---|
| SpendTx | 16,740 | 基础转账 |
| NamePreclaimTx | 16,740 | 域名预注册 |
| ContractCreateTx | 100,000 - 500,000 | 合约复杂度相关 |
| ContractCallTx | 50,000 - 200,000 | 函数复杂度相关 |
调整 Gas 参数
import "math/big"
// 创建合约调用交易
callTx, _ := transactions.NewContractCallTx(
alice.Address,
contractID,
big.NewInt(0), // amount
big.NewInt(100000), // gasLimit (默认)
big.NewInt(1000000000), // gasPrice (1 Gwei)
uint16(3),
calldata,
ttlnoncer,
)
// 如果遇到 "Out of Gas" 错误,增加 GasLimit
callTx.GasLimit = big.NewInt(300000)
// 在网络拥堵时提高 GasPrice 以优先打包
callTx.GasPrice = big.NewInt(2000000000) // 2 Gwei
// 计算预估费用
estimatedFee := new(big.Int).Mul(callTx.GasLimit, callTx.GasPrice)
log.Printf("Max fee: %s aettos (%f AE)\n",
estimatedFee.String(),
new(big.Float).Quo(
new(big.Float).SetInt(estimatedFee),
big.NewFloat(1e18),
),
)
第二部分:错误处理
学习识别和处理各类常见错误,构建健壮的应用程序。
4. 节点连接错误
naet.NewNode() 不会立即验证连接,第一次实际调用时才会发现连接问题。
package main
import (
"log"
"time"
"github.com/aeternity/aepp-sdk-go/v9/naet"
)
func main() {
node := naet.NewNode("https://testnet.aeternity.io", false)
// 验证连接的最佳实践
if err := validateConnection(node); err != nil {
log.Fatalf("Node connection failed: %v", err)
}
log.Println("Connected to node successfully")
}
func validateConnection(node *naet.Node) error {
// 尝试获取区块高度
height, err := node.GetHeight()
if err != nil {
return fmt.Errorf("cannot reach node: %w", err)
}
log.Printf("Current block height: %d\n", height)
// 可选:获取节点版本
status, err := node.GetStatus()
if err != nil {
return fmt.Errorf("cannot get node status: %w", err)
}
log.Printf("Node version: %s, Network: %s\n",
status.NodeVersion, status.NetworkID)
return nil
}
// 带重试的连接
func connectWithRetry(url string, maxRetries int) (*naet.Node, error) {
var lastErr error
for i := 0; i < maxRetries; i++ {
node := naet.NewNode(url, false)
if _, err := node.GetHeight(); err == nil {
return node, nil
} else {
lastErr = err
log.Printf("Connection attempt %d failed: %v\n", i+1, err)
time.Sleep(time.Duration(i+1) * time.Second)
}
}
return nil, fmt.Errorf("failed after %d retries: %w", maxRetries, lastErr)
}
5. 交易广播错误
常见交易错误
| 错误类型 | 关键字 | 原因 | 解决方案 |
|---|---|---|---|
| 签名无效 | invalid_signature |
Network ID 不匹配 | 检查使用正确的 NetworkID |
| Nonce 过低 | nonce_too_low |
账户已使用该 nonce | 查询最新 nonce 重新创建交易 |
| Nonce 过高 | nonce_too_high |
跳过了中间 nonce | 等待前序交易或重置 nonce |
| 余额不足 | insufficient_funds |
fee + amount > balance | 减少金额或充值账户 |
| Gas 不足 | out_of_gas |
GasLimit 设置过低 | 增加 GasLimit |
| TTL 过期 | ttl_expired |
交易有效期已过 | 创建新交易 |
错误检测与处理
import "strings"
func broadcastWithErrorHandling(
node *naet.Node,
signedTx string,
txHash string,
) error {
err := node.PostTransaction(signedTx, txHash)
if err == nil {
return nil
}
errStr := err.Error()
// 分析错误类型
switch {
case strings.Contains(errStr, "nonce_too_low"):
return fmt.Errorf("nonce already used, fetch latest nonce: %w", err)
case strings.Contains(errStr, "nonce_too_high"):
return fmt.Errorf("missing pending transactions, check mempool: %w", err)
case strings.Contains(errStr, "insufficient_funds"):
return fmt.Errorf("not enough balance for fee + amount: %w", err)
case strings.Contains(errStr, "invalid_signature"):
return fmt.Errorf("wrong network ID or corrupted signature: %w", err)
case strings.Contains(errStr, "out_of_gas"):
return fmt.Errorf("increase GasLimit and retry: %w", err)
case strings.Contains(errStr, "ttl_expired"):
return fmt.Errorf("transaction expired, create new tx: %w", err)
default:
return fmt.Errorf("unknown error: %w", err)
}
}
6. 交易确认与重试
广播后需要轮询检查交易是否被打包。交易可能在内存池等待或因竞争失败。
基础轮询模式
import "time"
func waitForConfirmation(node *naet.Node, txHash string, maxWait time.Duration) error {
deadline := time.Now().Add(maxWait)
interval := 3 * time.Second
for time.Now().Before(deadline) {
tx, err := node.GetTransactionByHash(txHash)
if err != nil {
// 交易可能还在内存池
log.Printf("Waiting for tx %s...\n", txHash[:16])
time.Sleep(interval)
continue
}
// 检查是否已被打包到区块
if tx.BlockHeight > 0 {
log.Printf("Transaction confirmed at block %d\n", tx.BlockHeight)
return nil
}
// 在内存池中
log.Println("Transaction in mempool, waiting...")
time.Sleep(interval)
}
return fmt.Errorf("timeout waiting for confirmation")
}
带指数退避的重试
func waitWithBackoff(
node *naet.Node,
txHash string,
maxRetries int,
) (*TxInfo, error) {
baseDelay := 2 * time.Second
for i := 0; i < maxRetries; i++ {
tx, err := node.GetTransactionByHash(txHash)
if err == nil && tx.BlockHeight > 0 {
return tx, nil
}
// 指数退避:2s, 4s, 8s, 16s...
delay := baseDelay * time.Duration(1< 30*time.Second {
delay = 30 * time.Second // 最大延迟 30 秒
}
log.Printf("Retry %d/%d, waiting %v...\n", i+1, maxRetries, delay)
time.Sleep(delay)
}
return nil, fmt.Errorf("max retries exceeded")
}
// 使用示例
txInfo, err := waitWithBackoff(node, txHash, 10)
if err != nil {
log.Fatal("Transaction not confirmed:", err)
}
log.Printf("Confirmed in block: %s\n", txInfo.BlockHash)
健壮交易发送示例
package main
import (
"fmt"
"log"
"math/big"
"strings"
"time"
"github.com/aeternity/aepp-sdk-go/v9/naet"
"github.com/aeternity/aepp-sdk-go/v9/account"
"github.com/aeternity/aepp-sdk-go/v9/config"
"github.com/aeternity/aepp-sdk-go/v9/transactions"
)
type TxResult struct {
Hash string
BlockHeight uint64
GasUsed uint64
}
func sendWithRetry(
node *naet.Node,
alice *account.Account,
recipient string,
amount *big.Int,
maxRetries int,
) (*TxResult, error) {
for attempt := 0; attempt < maxRetries; attempt++ {
// 1. 获取最新 nonce
acc, err := node.GetAccount(alice.Address)
if err != nil {
time.Sleep(2 * time.Second)
continue
}
// 2. 获取当前高度设置 TTL
height, _ := node.GetHeight()
// 3. 创建交易
tx := &transactions.SpendTx{
SenderID: alice.Address,
RecipientID: recipient,
Amount: amount,
Fee: big.NewInt(20000000000000), // 0.00002 AE
Nonce: acc.Nonce + 1,
TTL: height + 200,
Payload: []byte{},
}
// 4. 签名
signedTx, txHash, _, err := transactions.SignHashTx(
alice, tx, config.NetworkIDTestnet,
)
if err != nil {
return nil, fmt.Errorf("sign failed: %w", err)
}
// 5. 广播
err = node.PostTransaction(signedTx, txHash)
if err != nil {
errStr := err.Error()
if strings.Contains(errStr, "nonce_too_low") {
log.Println("Nonce conflict, retrying...")
continue
}
return nil, err
}
// 6. 等待确认
for i := 0; i < 20; i++ {
time.Sleep(3 * time.Second)
txInfo, err := node.GetTransactionByHash(txHash)
if err == nil && txInfo.BlockHeight > 0 {
return &TxResult{
Hash: txHash,
BlockHeight: txInfo.BlockHeight,
}, nil
}
}
log.Printf("Attempt %d failed, tx not confirmed\n", attempt+1)
}
return nil, fmt.Errorf("all retry attempts failed")
}
func main() {
node := naet.NewNode("https://testnet.aeternity.io", false)
alice, _ := account.FromHexString("YOUR_PRIVATE_KEY")
result, err := sendWithRetry(
node, alice,
"ak_recipient_address",
big.NewInt(1e18), // 1 AE
3, // 最多重试 3 次
)
if err != nil {
log.Fatal("Transfer failed:", err)
}
fmt.Printf("Transfer successful!\n")
fmt.Printf("TX Hash: %s\n", result.Hash)
fmt.Printf("Block: %d\n", result.BlockHeight)
}
知识检查
- TTL 设置为 current + 100 意味着交易有效期约多长时间?
- 快速发送多笔交易时,为什么直接使用 TTLNoncer 可能导致问题?
- 遇到 "nonce_too_low" 错误应该如何处理?
- 为什么合约调用需要比普通转账更高的 GasLimit?
- 指数退避重试策略的优势是什么?
本页目录