# 用golang开发ethereum

By [rbtree](https://paragraph.com/@rbtree) · 2022-11-12

---

之前看到一个用golang开发以太坊的教程

[https://goethereumbook.org/zh/](https://goethereumbook.org/zh/)

这个教程非常详细，然而它太陈旧了，目前很多go-ethereum函数接口已经有所修改。最明显的是，EIP1559之后，交易的格式的已经大不一样。因此，我基于上述教程，依据最新的go-ethereum(v1.10.26)对代码demo进行了修改。

用golang开发以太坊的一个好处是，可以很方便的查看和调试geth的源码，可以帮助我们更深入地理解以太坊的底层实现。

代码库为

[https://github.com/CryptoRbtree/goeth-client](https://github.com/CryptoRbtree/goeth-client)

下面对主要功能做简单介绍。

1 账户
----

首先需要调用ethclient.DialContext连接一个rpc，这个rpc可以是外部服务商提供的公链rpc，也可以是本地区块链的。

    var (
            ctx         = context.Background()
            url         = "https://eth-mainnet.g.alchemy.com/v2/" + os.Getenv("ALCHEMY_ID")
            client, err = ethclient.DialContext(ctx, url)
        )
    

BalanceAt获取某账户的eth余额

    account := common.HexToAddress("0xab5801a7d398351b8be11c439e05c5b3259aec9b") // vitalik
    balance, err := client.BalanceAt(context.Background(), account, nil)
    

CodeAt获取某账户的code（EOA账户code为空）

    contractAccount := common.HexToAddress("0x220866B1A2219f40e72f5c628B65D54268cA3A9D") // vitalik multisig
    code, err := client.CodeAt(context.Background(), contractAccount, nil)
    

2 区块
----

获取当前区块/指定编号的区块

    header, err := client.HeaderByNumber(context.Background(), nil)
    blockNumber := big.NewInt(15500000)
    block, err := client.BlockByNumber(context.Background(), blockNumber)
    
    fmt.Println("block number =", block.Number().Uint64())
    fmt.Println("block timestamp =", block.Time())
    fmt.Println("block difficulty =", block.Difficulty().Uint64())
    fmt.Println("block hash =", block.Hash().Hex())
    fmt.Println("block transaction count =", len(block.Transactions()))
    

对于区块，可以实现订阅功能。不过如果想使用订阅，不能使用https的rpc，而要用wss，因为订阅之后，服务器需要主动向客户端发消息。

    var (
            ctx         = context.Background()
            url         = "wss://eth-mainnet.g.alchemy.com/v2/" + os.Getenv("ALCHEMY_ID")
            client, err = ethclient.DialContext(ctx, url)
    )
    

订阅函数为SubscribeNewHead

    headers := make(chan *types.Header)
    sub, err := client.SubscribeNewHead(context.Background(), headers)
    if err != nil {
          log.Fatal(err)
    }
    

利用下面的代码可以监控最新block

    for {
            select {
            case err := <-sub.Err():
                  log.Fatal(err)
            case header := <-headers:
                    fmt.Println("----new block mined----")
                    block, err := client.BlockByHash(context.Background(), header.Hash())
                    if err != nil {
                          log.Fatal(err)
                    }
                    fmt.Println("block number:", block.Number().Uint64())
                    fmt.Println("block hash:", block.Hash().Hex())
                    fmt.Println("block timestamp:", block.Time())
            }
    }
    

3 交易
----

可以直接从区块获得交易

    blockNumber := big.NewInt(15000023)
    block, err := client.BlockByNumber(context.Background(), blockNumber)
    
    transactions := block.Transactions()
    fmt.Println("total transactions count:", len(transactions))
    for ind, tx := range transactions {
        fmt.Println("----", ind, "----")
        fmt.Println("transaction hash:", tx.Hash().Hex())
        fmt.Println("transaction value:", tx.Value().String())
        fmt.Println("transaction gas limit:", tx.Gas())
    }
    

也可以从交易哈希获得交易

    txHash := common.HexToHash("0xec9db5bfbcd30ad2e3070b626ed4f78abce88687c5d1eb23464242be5edcb537")
    tx, isPending, err := client.TransactionByHash(context.Background(), txHash)
    
    fmt.Println("transaction hash:", tx.Hash().Hex())
    fmt.Println("transaction gas limit:", tx.Gas())
    fmt.Println("isPending:", isPending)
    

4 发送交易
------

发送交易需要私钥，这里ACCOUNT\_KEY是环境变量里保存的私钥（不要用主钱包！）

    privateKey, err := crypto.HexToECDSA(os.Getenv("ACCOUNT_KEY"))
    publicKey := privateKey.Public()
    publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
    if !ok {
        log.Fatal("error casting public key to ECDSA")
    }
    fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
    

获取账户nonce、链上gas等信息

    nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
    value := big.NewInt(1) // 1 wei
    gasLimit := uint64(21000)
    gasFeeCap, err := client.SuggestGasPrice(context.Background())
    gasTipCap, err := client.SuggestGasTipCap(context.Background())
    toAddress := fromAddress // send eth to self
    var data []byte
    chainID, err := client.NetworkID(context.Background())
    

构造交易，这里我们使用的是EIP1559之后的新交易类型。

    tx := types.NewTx(&types.DynamicFeeTx{
            ChainID:   chainID,
            Nonce:     nonce,
            GasFeeCap: gasFeeCap,
            GasTipCap: gasTipCap,
            Gas:       gasLimit,
            To:        &toAddress,
            Value:     value,
            Data:      data,
    })
    

私钥签名，发送交易

    signedTx, err := types.SignTx(tx, types.NewLondonSigner(chainID), privateKey)
    err = client.SendTransaction(context.Background(), signedTx)
    

5 用交易签名恢复出地址
------------

随便找一个链上transaction作为例子，找出消息签名和消息hash

    txHash := common.HexToHash("0xec9db5bfbcd30ad2e3070b626ed4f78abce88687c5d1eb23464242be5edcb537")
    tx, _, err := client.TransactionByHash(context.Background(), txHash)
    
    // 消息签名sig
    v, r, s := tx.RawSignatureValues()
    R := r.Bytes()
    S := s.Bytes()
    V := byte(v.Uint64())
    sig := make([]byte, 65)
    copy(sig[32-len(R):32], R)
    copy(sig[64-len(S):64], S)
    sig[64] = V
    
    // 消息hash
    signer := types.NewLondonSigner(tx.ChainId())
    hash := signer.Hash(tx)
    

用Ecrecover计算地址，实际上是先计算公钥，再计算地址。

    publicKeyBytes, err := crypto.Ecrecover(hash.Bytes(), sig)
    publicKeyECDSA, err := crypto.UnmarshalPubkey(publicKeyBytes)
    address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
    

6 rlp对消息进行打包和解包
---------------

打包

    txHash := common.HexToHash("0xec9db5bfbcd30ad2e3070b626ed4f78abce88687c5d1eb23464242be5edcb537")
    tx, _, err := client.TransactionByHash(context.Background(), txHash)
    rawTx, err := rlp.EncodeToBytes(tx)
    

解包

    tx = new(types.Transaction)
    rawTxBytes, err := hex.DecodeString(hex.EncodeToString(rawTx))
    rlp.DecodeBytes(rawTxBytes, &tx)
    

可以打印出解包得到的交易和原始交易进行对比，验证正确性。

不过，需要指出一点，这里的rlp得到的rawTx并不符合以太坊标准。以太坊交易type并没有参与rlp，而是被简单放在了其他参数的前面，可能是因为这样比较容易兼容老类型的交易。

EIP1559标准，

[https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md)

We introduce a new [EIP-2718](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2718.md)  transaction type, with the format `0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list, signature_y_parity, signature_r, signature_s])`

7 rpc调用
-------

连接rpc

    var (
        url         = "https://eth-mainnet.g.alchemy.com/v2/" + os.Getenv("ALCHEMY_ID")
        client, err = rpc.DialHTTP(url)
    )
    

利用rpc的eth\_call方法，查询usdt的totalSupply

    usdt := "0xdAC17F958D2ee523a2206206994597C13D831ec7"
    functionHash := crypto.Keccak256([]byte(string("totalSupply()")))
    data := "0x" + hex.EncodeToString(functionHash[0:4])
    req := request{usdt, data}
    
    var result string
    if err := client.Call(&result, "eth_call", req, "latest"); err != nil {
        log.Fatal(err)
    }
    

8 合约部署
------

demo合约，Store.sol

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    contract Store {
      event ItemSet(bytes32 key, bytes32 value);
    
      string public version;
      mapping (bytes32 => bytes32) public items;
    
      constructor(string memory _version) {
        version = _version;
      }
    
      function setItem(bytes32 key, bytes32 value) external {
        items[key] = value;
        emit ItemSet(key, value);
      }
    }
    

需要安装solidity编译器，solc。

顺便提一句，有时候我们可能需要安装不能版本的编译器，以方便编译不同时期的solidity文件，可以安装solc-select方便版本管理。

例如solc-select use 0.8.0，可指定当前版本为0.8.0

首先，我们需要用solc生成abi文件和deploy字节码文件

    solc --abi Store.sol -o .
    solc --bin Store.sol -o .
    

安装abigen工具

    go get -u github.com/ethereum/go-ethereum
    cd $GOPATH/src/github.com/ethereum/go-ethereum/
    make
    make devtools
    

利用abigen生成go文件，pkg为生成的go包名。后面，我们的所有操作都是基于这个生成go文件。

    abigen --bin=Store.bin --abi=Store.abi --pkg=store --out=Store.go
    

部署合约

    auth, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID)
    auth.Nonce = big.NewInt(int64(nonce))
    auth.Value = big.NewInt(0)     // in wei
    auth.GasLimit = uint64(500000) // in units
    //auth.GasPrice = big.NewInt(1e9) // only for legacy transactions
    auth.GasFeeCap = gasPrice
    auth.GasTipCap = gasTipCap
    
    input := "1.0"
    address, tx, instance, err := store.DeployStore(auth, client, input)
    

9 读/写合约
-------

读合约比较简单，不需要上链。

    // 获取合约instance
    address := common.HexToAddress("0xd3047d5bbcbcfe4256f9c1668c57b3d875c4adea")
    instance, err := store.NewStore(address, client)
    
    // 读合约
    version, err := instance.Version(nil)
    

写合约比较复杂，因为需要发交易，因此需要构造transaction

    auth, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID)
    auth.Nonce = big.NewInt(int64(nonce))
    auth.Value = big.NewInt(0) // in wei
    auth.GasFeeCap = gasPrice
    auth.GasTipCap = gasTipCap
    auth.GasLimit = 500000
    
    tx, err := instance.SetItem(auth, key, value)
    

此外，gasLimit可以通过调用EstimateGas来进行估计。

    parsed, err := abi.JSON(strings.NewReader(store.StoreABI))
    encodedData, err := parsed.Pack("setItem", key, value)
    estimatedGas, err := client.EstimateGas(context.Background(), ethereum.CallMsg{
        From:      fromAddress,
        To:        &address,
        Data:      encodedData,
        GasFeeCap: gasPrice,
        GasTipCap: gasTipCap,
    })
    auth.GasLimit = estimatedGas // in units
    

10 交互Erc20合约
------------

这一节本来是没必要写的，因为和第9节基本一致。

不同的是，因为只需要交互，不需要部署合约，所以并不需要全部的solidity文件源码，也不需要deploy字节码，只需要接口文件即可。

    solc --abi IERC20.sol -o .
    abigen --abi=IERC20.abi --pkg=token --out=IERC20.go
    

交互的代码和上节基本一样。

    tokenAddress := common.HexToAddress("0x6B175474E89094C44Da98b954EedeAC495271d0F") // DAI Address
    instance, err := token.NewToken(tokenAddress, client)
    address := common.HexToAddress("0x245cc372c84b3645bf0ffe6538620b04a217988b") // Olympus funds
    bal, err := instance.BalanceOf(&bind.CallOpts{}, address)
    

11 事件
-----

我们以ERC20的transfer为例，因此定义如下结构。

    type LogTransfer struct {
        From   common.Address
        To     common.Address
        Tokens *big.Int
    }
    

获取事件，指定合约地址和区块范围

    tokenAddress := common.HexToAddress("0x6B175474E89094C44Da98b954EedeAC495271d0F") // DAI Address
    query := ethereum.FilterQuery{
        FromBlock: big.NewInt(15500000),
        ToBlock:   big.NewInt(15500005),
        Addresses: []common.Address{
            tokenAddress,
        },
    }
    logs, err := client.FilterLogs(context.Background(), query)
    

过滤并打印出所有的Transfer事件

    contractAbi, err := abi.JSON(strings.NewReader(string(token.TokenABI)))
    
    logTransferSig := []byte("Transfer(address,address,uint256)")
    logTransferSigHash := crypto.Keccak256Hash(logTransferSig)
    
    for _, vLog := range logs {
        fmt.Printf("Log Block Number: %d\n", vLog.BlockNumber)
        fmt.Printf("Log Index: %d\n", vLog.Index)
    
        switch vLog.Topics[0].Hex() {
        case logTransferSigHash.Hex():
            fmt.Printf("Log Name: Transfer\n")
    
            var transferEvent LogTransfer
            err := contractAbi.UnpackIntoInterface(&transferEvent, "Transfer", vLog.Data)
            if err != nil {
                log.Fatal(err)
            }
    
            transferEvent.From = common.HexToAddress(vLog.Topics[1].Hex())
            transferEvent.To = common.HexToAddress(vLog.Topics[2].Hex())
            fmt.Printf("From: %s\n", transferEvent.From.Hex())
            fmt.Printf("To: %s\n", transferEvent.To.Hex())
            fmt.Printf("Value: %s\n", transferEvent.Tokens.String())
        }
    
        fmt.Printf("\n")
    }
    

和区块一样，事件也是支持订阅的（记得订阅时rpc不能用https，要用wss）

如下例子，可以用来监控所有DAI合约的事件。

    contractAddress := common.HexToAddress("0x6B175474E89094C44Da98b954EedeAC495271d0F")
    query := ethereum.FilterQuery{
        Addresses: []common.Address{contractAddress},
    }
    
    logs := make(chan types.Log)
    sub, err := client.SubscribeFilterLogs(context.Background(), query, logs)
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("----subscribe DAI events----\n\n")
    for {
        select {
        case err := <-sub.Err():
            log.Fatal(err)
        case vLog := <-logs:
            fmt.Printf("%+v\n\n", vLog) // pointer to log event
        }
    }
    

12 钱包生成
-------

之前的例子，我们用了自己本来就有的私钥生成的账户，实际上，geth提供了生成私钥的函数，有两种方式。

第一种方式，keystore.NewKeyStore。

    ks := keystore.NewKeyStore("./wallet", keystore.StandardScryptN, keystore.StandardScryptP)
    password := "secret"
    account, err := ks.NewAccount(password)
    fmt.Println("create Account", account.Address.Hex())
    

这种方式的好处是，私钥可以被加密保存下来，以后可以从文件导出再次使用。

    file := "./wallet/UTC--2022-11-12T10-43-05.221514000Z--94e920211dd7f5ee2b3ab69ed2a491284a0690de"
    ks := keystore.NewKeyStore("./tmp", keystore.StandardScryptN, keystore.StandardScryptP)
    jsonBytes, err := ioutil.ReadFile(file)
    
    password := "secret"
    account, err := ks.Import(jsonBytes, password, password)
    fmt.Println("import Address", account.Address.Hex())
    

第二种方式，crypto.GenerateKey。生成私钥之后，可以再算出公钥和地址。

    privateKey, err := crypto.GenerateKey()
    privateKeyBytes := crypto.FromECDSA(privateKey)
    fmt.Println("privateKey:", hexutil.Encode(privateKeyBytes)[2:])
    
    publicKey := privateKey.Public()
    publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
    publicKeyBytes := crypto.FromECDSAPub(publicKeyECDSA)
    fmt.Println("publicKey:", hexutil.Encode(publicKeyBytes)[4:])
    
    address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
    fmt.Println("address:", address)
    hash := sha3.NewLegacyKeccak256()
    hash.Write(publicKeyBytes[1:])
    fmt.Println("address:", hexutil.Encode(hash.Sum(nil)[12:]))
    

13 本地模拟区块链

backends.NewSimulatedBackend可在本地模拟，我们可以编辑创世区块的一些属性，例如给我们的地址赋予初始eth。

    genesisAlloc := map[common.Address]core.GenesisAccount{
        address: {
            Balance: balance,
        },
    }
    blockGasLimit := uint64(4712388)
    client := backends.NewSimulatedBackend(genesisAlloc, blockGasLimit)
    

发送交易和远端区块链一样，不同的是，我们可以通过Commit函数来控制矿工何时打包新区块。

    err = client.SendTransaction(context.Background(), signedTx)
    client.Commit() // mine
    

我觉得模拟区块链在学习区块链的底层知识时，是一个很有用的功能。我们可以通过dlv调试研究evm的执行细节，如果我们使用公链或者本地用hardhat启一个区块链，是没有办法进行这样的源码调试的。

![源码调试EVM](https://storage.googleapis.com/papyrus_images/4cd33b2aa9a2e1d634dc2bed2439f56297eb9ea98c32afbc3bcf4b4895a89c93.png)

源码调试EVM

---

*Originally published on [rbtree](https://paragraph.com/@rbtree/golang-ethereum)*
