LoginSignup
9
6

More than 5 years have passed since last update.

go-ethereumを用いて作成したTransactionをEthereumネットワークに送る方法

Last updated at Posted at 2018-09-06

はじめに

TruffleやOpenZeppelinを利用して発行したERC20ベースのTokenを送金するのに、MistやMetamaskを使うことがあると思います。

本記事ではこれらを用いず、コードベースで作成したトランザクションをネットワークに送る方法を行います。

EthereumのTransactionについて

Ethereum yellow paper の4-2に詳細が書いてありますが、トランザクションは、

  • nonce: 送信者が今までに送信したTransactionの数
  • gasPrice:Transactionが実行された結果として消費されるgasの価格 (wei/gas)
  • gasLimit:Transactionが実行される前に支払われるgas. このgasLimitを超えてTransactionが実行されるということはない
  • to:送信先。今回の場合だとERC20ベースTokenのコントラクトアドレス
  • value:Transactionと同時に送るETH
  • data:コントラクトを実行する内容が入っている
  • v, r, s :Transactionの署名と対応しているデータ

で基本構成されています。このデータの形式によって、コントラクト作成なのか、アカウント作成なのか、コントラクト内のメソッドを呼び出すのか、変わってきます。

Transactionを作成するにはこの仕様に沿う必要があります。go-ethereum上では、transaction.goにstructとして定義されています。

transaction.go#L46
type txdata struct {
    AccountNonce uint64          `json:"nonce"    gencodec:"required"`
    Price        *big.Int        `json:"gasPrice" gencodec:"required"`
    GasLimit     uint64          `json:"gas"      gencodec:"required"`
    Recipient    *common.Address `json:"to"       rlp:"nil"` // nil means contract creation
    Amount       *big.Int        `json:"value"    gencodec:"required"`
    Payload      []byte          `json:"input"    gencodec:"required"`

    // Signature values
    V *big.Int `json:"v" gencodec:"required"`
    R *big.Int `json:"r" gencodec:"required"`
    S *big.Int `json:"s" gencodec:"required"`

    // This is only used when marshaling to JSON.
    Hash *common.Hash `json:"hash" rlp:"-"`
}

このTransactionを作るには、このtransaction.go内のfunc NewTransaction() を用いるのですが、ここでポイントとなるのは、dataの準備の仕方です。

transaction.go#L74
func NewTransaction(nonce uint64, to common.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte) *Transaction { |
 return newTransaction(nonce, &to, amount, gasLimit, gasPrice, data) |
}

TransactionのDataについて

Dataには、コントラクトで実行するメソッドについてのデータを入れる必要があります。Ethereumで実行される為には、The Contract Application Binary Interface (ABI)という形式にエンコードしなければなりません。

ABIの仕様の詳細は、Contract ABI Specificationに書かれています。

このdataは、

  1. コントラクトで呼び出したいメソッド(Method ID)
  2. メソッド引数をそれぞれハッシュ化して32byteにしたもの

をつなぎ合わせた形式です。(例としては、Contract ABI Specificationのexampleが分かりやすいです。)

Method IDとは、コントラクトのメソッドをbyte形式にしてKECCAK-256でハッシュかし、先頭から4バイト取ってきたものです。

今回は、発行したトークンを送金したいので、ERC20で定義されている、transfer(address,uint256) を使います。

コードでは下記のようになります。

transferFnSignature := []byte("transfer(address,uint256)") 
hash := sha3.NewKeccak256()
hash.Write(transferFnSignature)
methodID := hash.Sum(nil)[:4]

引数も同様に、バイト形式にしてハッシュ化します。ハッシュ化したのち、32バイトになるように0埋めを行います。この0埋めもgo-ethereumでは準備されています。LeftPadBytesメソッドを使います。

toAddress := common.HexToAddress(receiveAddress)
paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)
pIntAmount := big.NewInt(int64(amount))
paddedAmount := common.LeftPadBytes(pIntAmount.Bytes(), 32)

以上のmethodId、oaddedAddress、paddedAmountを、appendを使って結合させれば、dataの作成が完了です。

//トランザクションで送るデータを作成
var data []byte
data = append(data, methodID...)
data = append(data, paddedAddress...)
data = append(data, paddedAmount...)

Transactionの送信

作成したトランザクションを自分で署名して、ネットワークに送れば完了です。


signedTx, err := types.SignTx(tx, types.HomesteadSigner{}, privateKey)
if err != nil {
    log.Fatal(err)
}
//サインしたトランザクションをRopstenNetworkに送る。
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
    log.Fatal(err)
}

実行環境

今回のGo, go-ethereumの環境は下記の通りとなっています。

$ geth version
Geth
Version: 1.8.13-stable
Architecture: amd64
Protocol Versions: [63 62]
Network Id: 1
Go Version: go1.10.3
Operating System: darwin
GOPATH=/Users/akitk/go_workspace
GOROOT=/usr/local/opt/go/libexec

実際に作成したコード

今回、Ropstenネットワークに接続する為に、infuraを利用しました。ご自身で準備した、Ropstenネットワークに繋がっているethereumクライアントに接続しても良いと思います。(下記のprivateKey、contractAddressなど、ダミーデータを入れています。)

package main

import (
    "context"
    "crypto/ecdsa"
    "fmt"
    "log"
    "math/big"

    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/crypto/sha3"
    "github.com/ethereum/go-ethereum/ethclient"
)

func sendExternalRawTransaction(receiveAddress string, amount float64) (transaction string) {
    //Ropstenネットワークに接続
    client, err := ethclient.Dial("https://ropsten.infura.io")
    if err != nil {
        log.Fatal(err)
    }

    //PrivateKeyを読み込む
    privateKey, err := crypto.HexToECDSA("f47321dc18a9bcafa063d0980cbcdcb1ff19e7430bf501612c21da0648cf2f8")
    if err != nil {
        log.Fatal(err)
    }

    // PrivateKeyからPublickeyを生成
    publicKey := privateKey.Public()
    publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
    if !ok {
        log.Fatal("Error casting public key to ECDSA")
    }

    //PublicKeyから、送金主アドレスを生成
    fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)

    //Ropstenネットワークから、Nonce情報を読み取る
    nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
    if err != nil {
        log.Fatal(err)
    }

    //トークン送金Transactionをテストネット送るためのgasLimit、
    value := big.NewInt(0) //(オプション)後で使用する関数NewTransactionの引数で必要になるため設定。Transactionと同時に送るETHの量を設定できます。
    gasLimit := uint64(2000000)

    //ロプステンネットワークから、現在のgasPriceを取得。トランザクションがマイニングされずに放置されることを防ぐ。
    gasPrice, err := client.SuggestGasPrice(context.Background())
    if err != nil {
        log.Fatal(err)
    }

    //送金先を指定
    toAddress := common.HexToAddress(receiveAddress)
    //トークンコントラクトアドレスを指定
    tokenAddress := common.HexToAddress("0x43aff81f88c18bc85281039df68002872b233afc")
    //ERC20のどの関数を使用するか指定。https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sendtransaction
    transferFnSignature := []byte("transfer(address,uint256)")
    //hash化し、先頭から4バイトまで取得。これで使用する関数を指定したことになる。
    hash := sha3.NewKeccak256()
    hash.Write(transferFnSignature)
    methodID := hash.Sum(nil)[:4]

    //0埋め
    paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)
    //送金額を設定
    pIntAmount := big.NewInt(int64(amount))
    //0埋め
    paddedAmount := common.LeftPadBytes(pIntAmount.Bytes(), 32)

    //トランザクションで送るデータを作成
    var data []byte
    data = append(data, methodID...)
    data = append(data, paddedAddress...)
    data = append(data, paddedAmount...)

    /***** Preparing signed transaction *****/
    tx := types.NewTransaction(nonce, tokenAddress, value, gasLimit, gasPrice, data)
    signedTx, err := types.SignTx(tx, types.HomesteadSigner{}, privateKey)
    if err != nil {
        log.Fatal(err)
    }

    //サインしたトランザクションをRopstenNetworkに送る。
    err = client.SendTransaction(context.Background(), signedTx)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Signed tx sent: %s", signedTx.Hash().Hex())

    return signedTx.Hash().Hex()
}

上記の関数で、ご自身で発行したERC20トークンコントラクトのアドレス、ご自身のPrivateKey、送金先、金額を変更すれば、Tokenの送金ができます。また、ERC20の他のメソッドを実行するトランザクションを作成されたい場合は、dataをそのメソッドに合わせて変更すれば、作成が可能です。

以上、ご参考になれば幸いです。

9
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6