这篇文章介绍了以太坊的传统交易格式(类型0x0),即EIP-2718之前的交易结构。它通过使用Go语言和go-ethereum库,详细演示了如何在Polygon Amoy测试网上创建、签名和广播一个传统以太坊交易,并解释了其中的关键字段和EIP-155兼容性。
在深入了解 EIP-2930 和 EIP-1559 等现代交易类型之前,重要的是要理解一切的起点——传统交易格式。
传统交易(类型 0x0)是原始的以太坊交易结构,在 EIP-2718 引入类型化交易之前使用。它们今天仍然有效,并构成了后来所有类型都以此为基础进行扩展的基础格式。
在这篇文章中,我们将:
1. 在 Go 中设置两个测试账户,用于概念验证。
2. 使用 Polygon Amoy 测试网为它们充值,该网络提供快速出块和近乎零的费用。
使用 go-ethereum 发送一个 传统交易,以了解 nonce、gasPrice 和 v/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 类型。
记住我以便更快登录
这些交易包括基本参数:
nonce、gasPrice、gasLimit、to、value、datav、r、s(签名)传统交易的 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 编码,并依赖 v、r、s 值进行签名验证。
尽管像 EIP-2930 和 EIP-1559 这样的新交易类型扩展了这种格式,但它们仍然遵循此处介绍的相同核心原则。
了解传统交易有助于你读取原始 calldata、调试低级别客户端跟踪,并理解以太坊在协议演进的同时如何保持向后兼容性。
资源:
- 原文链接: medium.com/@andrey_obruc...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!