高级 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 参数说明
参数单位说明推荐值
GasPriceaettos/gas每单位 gas 的价格≥ 1,000,000,000 (1 Gwei)
GasLimitgas units最大允许消耗量交易类型相关
Feeaettos实际支付费用GasUsed × GasPrice
不同交易类型的 Gas 估算
交易类型典型 GasLimit备注
SpendTx16,740基础转账
NamePreclaimTx16,740域名预注册
ContractCreateTx100,000 - 500,000合约复杂度相关
ContractCallTx50,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)
}
知识检查
  1. TTL 设置为 current + 100 意味着交易有效期约多长时间?
  2. 快速发送多笔交易时,为什么直接使用 TTLNoncer 可能导致问题?
  3. 遇到 "nonce_too_low" 错误应该如何处理?
  4. 为什么合约调用需要比普通转账更高的 GasLimit?
  5. 指数退避重试策略的优势是什么?