理解传统以太坊交易(类型 0x0)

这篇文章介绍了以太坊的传统交易格式(类型0x0),即EIP-2718之前的交易结构。它通过使用Go语言和go-ethereum库,详细演示了如何在Polygon Amoy测试网上创建、签名和广播一个传统以太坊交易,并解释了其中的关键字段和EIP-155兼容性。

在深入了解 EIP-2930 和 EIP-1559 等现代交易类型之前,重要的是要理解一切的起点——传统交易格式

传统交易(类型 0x0)是原始的以太坊交易结构,在 EIP-2718 引入类型化交易之前使用。它们今天仍然有效,并构成了后来所有类型都以此为基础进行扩展的基础格式。

在这篇文章中,我们将:

1. 在 Go 中设置两个测试账户,用于概念验证。

2. 使用 Polygon Amoy 测试网为它们充值,该网络提供快速出块和近乎零的费用。

使用 go-ethereum 发送一个 传统交易,以了解 noncegasPricev/r/s 签名等字段如何协同工作。

最后,你将拥有一个完整的可运行示例,展示传统以太坊交易是如何在一个实时 EVM 网络上创建、签名和广播的。

在我们深入了解交易类型和示例之前:

对于所有的区块链交互,我们将使用 Polygon Amoy 测试网,它具有低费用和快速出块的特点。确保你的 MetaMask 已在 Polygon Amoy 区块链上设置好,并有一些资金,你可以从这个水龙头获取。

让我们编写一些代码来为 POC 目的设置我们的钱包。(请勿将此用于真实的钱包生成,这仅用于示例目的)

注意 : 我将省略导入以节省空间。 完整的 GitHub 代码参考在文章底部

package account

const (
 projectMarker = "transaction-types"
 key1FilePath  = "account1.key"
 key2FilePath  = "account2.key"
)

// getPath returns the absolute path to the directory.
func getPath() string {
   // Get the path of the current file
   _, filename, _, _ := runtime.Caller(0)

   // Find index of your project root folder name
   idx := strings.Index(strings.ToLower(filename), strings.ToLower(projectMarker))

   if idx == -1 {
    panic("project root folder not found in path")
   }

   // Cut path up to the project root
   rootPath := filename[:idx+len(projectMarker)]

   // Append relative path to root
   return filepath.Join(rootPath, "account")
}

// Generate new account if not exist, otherwise load saved account
func GetAccount(accNum int) (*common.Address, *ecdsa.PrivateKey) {
   var priv *ecdsa.PrivateKey

   path := getPath()
   var keyFilePath string
   switch accNum {
   case 1:
    keyFilePath = filepath.Join(path, key1FilePath)
   case 2:
    keyFilePath = filepath.Join(path, key2FilePath)
   default:
    log.Fatal("Invalid account number. Use 1 or 2.")
   }

   if _, err := os.Stat(keyFilePath); os.IsNotExist(err) {
    fmt.Println("Key file not found. Generating new Ethereum key...")

    priv, err = crypto.GenerateKey()
    if err != nil {
     log.Fatal("Failed to generate key:", err)
    }
    privBytes := crypto.FromECDSA(priv)

    err = os.WriteFile(keyFilePath, []byte(hex.EncodeToString(privBytes)), 0600)
    if err != nil {
     log.Fatal("Failed to write key file:", err)
    }

    fmt.Println("New key saved to", keyFilePath)
   } else {
    // Load existing key
    fmt.Println("Loading existing key from", keyFilePath)
    keyHex, err := os.ReadFile(keyFilePath)
    if err != nil {
     log.Fatal("Failed to read key file:", err)
    }

    privBytes, err := hex.DecodeString(string(keyHex))
    if err != nil {
     log.Fatal("Invalid hex in key file:", err)
    }

    priv, err = crypto.ToECDSA(privBytes)
    if err != nil {
     log.Fatal("Invalid private key:", err)
    }
   }
   // Print address and keys
   address := crypto.PubkeyToAddress(priv.PublicKey)
   pubBytes := crypto.FromECDSAPub(&priv.PublicKey)
   fmt.Println("Address:    ", address.Hex())
   fmt.Println("Public Key: ", hex.EncodeToString(pubBytes))
   fmt.Println("Private Key:", hex.EncodeToString(crypto.FromECDSA(priv)))
   return &address, priv
}

在 MetaMask 钱包设置好并用 POL 原生货币充值后,让我们通过 MetaMask 为其中一个生成的地址充值。(只需复制打印的地址并将其粘贴到 MetaMask 的发送接收人字段中)

传统交易

传统交易是原始的以太坊交易格式,在 EIP-2718 引入类型化交易之前使用。它们没有类型前缀,但按照惯例被视为 0x0 类型。

记住我以便更快登录

这些交易包括基本参数:

  • noncegasPricegasLimittovaluedata
  • vrs(签名)

传统交易的 RLP 编码如下:

rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s])
{
  nonce:    "0x0",            // 发送者在此交易之前进行的交易数量。
  gasPrice: "0x01284a32b000", // 发送者提供的 Gas 价格,单位 wei。
  gasLimit: "0x21000",         // 发送者提供的最大 Gas。
  to:       "0x...",          // 接收者的地址。在合约创建交易中不使用。
  value:    "0x0",            // 转移的值,单位 wei。
  data:     "0x...",          // 用于定义合约创建和交互。
  v:        "0x1",            // ECDSA 恢复 ID。
  r:        "0x...",          // ECDSA 签名 r。
  s:        "0x...",          // ECDSA 签名 s。
}

示例

package main

import (
 "context"
 "fmt"
 "log"
 "math/big"
 "time"
 "transactiontypes/account"
 "github.com/ethereum/go-ethereum/core/types"
 "github.com/ethereum/go-ethereum/ethclient"
 "github.com/samber/lo"
)

const (
   // Polygon Amoy 测试网的公共 RPC URL
   NodeRPCURL  = "https://polygon-amoy.drpc.org"
   GasLimit    = 21000   // 简单 ETH 转账的标准 Gas 上限
   AmoyChainID = 80002   // Polygon Amoy 测试网链 ID
   ValueToSend = 0.01e18 // 0.01 ETH
)

func main() {
   acc1Addr, acc1Priv := account.GetAccount(1)
   acc2Addr, _ := account.GetAccount(2)
   ctx := context.Background()
   client, err := ethclient.Dial(NodeRPCURL)
   if err != nil {
    log.Fatal("Failed to connect to Ethereum node:", err)
   }
   nonce, err := client.PendingNonceAt(ctx, lo.FromPtr(acc1Addr))
   if err != nil {
    log.Fatal("Failed to fetch nonce:", err)
   }
   // Get suggested gas price
   gasPrice, err := client.SuggestGasPrice(ctx)
   if err != nil {
    log.Fatal("Failed to fetch gas price:", err)
   }
   // Create a types.LegacyTx
   tx := types.LegacyTx{
    Nonce:    nonce,
    GasPrice: gasPrice,
    Gas:      GasLimit,
    To:       acc2Addr, // 发送到账户 2
    Value:    big.NewInt(ValueToSend),
    Data:     []byte{},
   }
   // 将其转换为完整的 types.Transaction 对象
   legacyTx := types.NewTx(&tx)

   chainID := big.NewInt(AmoyChainID) // 使用你的链 ID(80002 = Polygon Mumbai 测试网)
   // 注意:尽管这是一个传统交易,但我们仍需要使用链 ID 进行签名,以兼容 EIP-155。
   // 因为大多数节点通过要求签名中包含链 ID 来防止重放攻击。
   // 不防范此攻击的节点将能够使用以下方式获取已签名的交易:
   // types.SignTx(tx, types.HomesteadSigner{}, priv)
   signedTx, err := types.SignTx(legacyTx, types.NewEIP155Signer(chainID), acc1Priv)
   if err != nil {
    log.Fatal("Failed to sign transaction:", err)
   }

   // 广播交易
   err = client.SendTransaction(ctx, signedTx)
   if err != nil {
    log.Fatal("Broadcast failed:", err)
   }

   fmt.Println("Transaction sent!")
   fmt.Println("Tx hash:", signedTx.Hash().Hex())

   // 可选地等待交易被打包
   time.Sleep(10 * time.Second)
   receipt, err := client.TransactionReceipt(ctx, signedTx.Hash())
   if err != nil {
    fmt.Println("Tx not mined yet.") // 交易尚未被挖出。
   } else {
    fmt.Println("Tx mined in block:", receipt.BlockNumber) // 交易在以下区块中被挖出:
   }
 }

注意 : 在交易签名中,我们仍然使用 types.NewEIP155Signer,因为大多数节点会防止 重放攻击 。不防范此攻击的节点将允许使用 types.SignTx(tx, types.HomesteadSigner{}, priv) 进行签名。

交易被挖出后,你可以在区块浏览器中查看它,并看到类似以下内容:

按回车或点击以查看完整大小的图片

总结

传统交易(type 0x0)是所有以太坊交易类型的基础。

它们使用简单的 RLP 编码,并依赖 vrs 值进行签名验证。

尽管像 EIP-2930EIP-1559 这样的新交易类型扩展了这种格式,但它们仍然遵循此处介绍的相同核心原则。

了解传统交易有助于你读取原始 calldata、调试低级别客户端跟踪,并理解以太坊在协议演进的同时如何保持向后兼容性。

资源:

  • 交易类型代码可以在这里找到 这里
  • 原文链接: medium.com/@andrey_obruc...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Andrey Obruchkov
Andrey Obruchkov
江湖只有他的大名,没有他的介绍。