Help us understand the problem. What is going on with this article?

Web3jを使って署名付きトランザクションを送信する

記事の内容

EthereumのJavaライブラリ「Web3j」を使って署名付きトランザクションを送信する方法

環境

  • geth:1.9.0-stable
  • web3j:4.5.0

実装

自分の学習用に書いたコードなのでかなり雑ですが。。
このサンプルソースではトランザクションの送信メソッドを署名なしと署名ありでそれぞれ作成しており、mainメソッドから両方を順番に呼び出しているだけになります。

SendTransaction.java
package jp.ethereum.transaction;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Optional;

import org.web3j.crypto.CipherException;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.RawTransaction;
import org.web3j.crypto.TransactionEncoder;
import org.web3j.crypto.WalletUtils;
import org.web3j.protocol.admin.Admin;
import org.web3j.protocol.admin.methods.response.PersonalUnlockAccount;
import org.web3j.protocol.core.methods.response.EthGetTransactionReceipt;
import org.web3j.protocol.core.methods.response.EthSendTransaction;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.protocol.exceptions.TransactionException;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.Transfer;
import org.web3j.utils.Convert.Unit;
import org.web3j.utils.Numeric;

public class SendTransaction {

  // ①
  public static final Admin web3j = Admin.build(new HttpService("http://127.0.0.1:8545"));

  public static void main(String args[]) {
    try {
      // ②
      // トランザクション送信者の資格情報を取得する
      // 資格情報に公開鍵、秘密鍵が含まれている
      String password = "password";
      Credentials credentials = WalletUtils.loadCredentials(password, "秘密鍵ファイルのフルパス");

      String toAddress = "0x66b4e7be902300f9a15d900822bbd8803be87391";

      SendTransaction tx= new SendTransaction();

      // トランザクションの送信
      tx.sendTransaction(credentials, password, toAddress, 10);

      // 署名付きトランザクションの送信
      TransactionReceipt receipt = tx.sendSignedTransaction(credentials, password, toAddress);

      if (receipt != null) System.out.println(receipt.getTransactionHash()) ;

    }catch(IOException | CipherException ex) {
      ex.printStackTrace();
    }
  }

  public TransactionReceipt sendTransaction(Credentials credentials, String password, String toAddress, long value) {

    TransactionReceipt receipt = null;
      try {
        // トランザクションの生成
        // "personal_unlockAccount"のリクエストを送信し、レスポンスを受信する
        PersonalUnlockAccount unlockAccountResponse = web3j.personalUnlockAccount(
            credentials.getAddress(), // アドレス
            password  // パスワード
            ).send();

        // アンロックが成功していたら、Etherを送金する
        if (unlockAccountResponse.getResult()) {
          // Transactionを送信する。Blockに取り込まれるまで応答が返ってこない
          receipt = Transfer.sendFunds(web3j, credentials, toAddress, BigDecimal.valueOf(value), Unit.ETHER).send();
        }
      }catch(IOException | TransactionException ex) {
        ex.printStackTrace();
      }catch(Exception ex) {
        ex.printStackTrace();
      }
    return receipt;
  }
  public TransactionReceipt sendSignedTransaction(Credentials credentials, String password, String toAddress) {

      TransactionReceipt receipt = null;
      try {
        // トランザクションの生成
        // "personal_unlockAccount"のリクエストを送信し、レスポンスを受信する
        PersonalUnlockAccount unlockAccountResponse = web3j.personalUnlockAccount(
            credentials.getAddress(), // アドレス
            password  // パスワード
            ).send();

        // アンロックが成功していたら、Etherを送金する
        if (unlockAccountResponse.getResult()) {
            // "eth_sendTransaction"の引数に渡すオブジェクトを作成
          RawTransaction rawTransaction = RawTransaction.createEtherTransaction(
            BigInteger.valueOf(10),     // nonce
            BigInteger.valueOf(700),     // gasPrice
            BigInteger.valueOf(4712388),   // gasLimit
            toAddress,             // to
            BigInteger.valueOf(101)     // value
            );

          // トランザクションに署名
          byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
          String hexValue = Numeric.toHexString(signedMessage);

          // トランザクションの送信
          EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(hexValue).send();
          String response = ethSendTransaction.getRawResponse();
          String transactionHash = ethSendTransaction.getTransactionHash();

          Optional<TransactionReceipt> transactionReceipt = null;
          int retry = 0;

          // トランザクションの監視
          if(transactionHash != null) {
            do {
            System.out.printf("%3d checking if transaction " + transactionHash + " is mined....\n" ,retry);
              EthGetTransactionReceipt ethGetTransactionReceiptResp = 
                  web3j.ethGetTransactionReceipt(transactionHash).send();
                transactionReceipt = ethGetTransactionReceiptResp.getTransactionReceipt();

                Thread.sleep(3000);
                retry++;
            }while(!transactionReceipt.isPresent() && retry < 100);
          } else {
            System.out.println("Transaction Send failed...");
            System.out.println("Message:" + ethSendTransaction.getError().getMessage());
            System.out.println("Data   :" + ethSendTransaction.getError().getData());
          }
        }
      }catch(IOException | InterruptedException ex) {
        ex.printStackTrace();
      }catch(Exception ex) {
        ex.printStackTrace();
      }
    return receipt;
  }
}

ポイントとなりそうなところを補足していきます。

Ethereumに接続する

Ethereumに接続する

SendTransaction.java
// ①
public static final Admin admin = Admin.build(new HttpService("http://127.0.0.1:8545"));

ここではAdminクラスのインスタンスを作成しています。
AdminクラスはWeb3jクラスを継承したクラスであり、Web3jクラスには実装されていないpersonal情報を使ったメソッドを使用することができます。
トランザクション送信ではアカウントのアンロックが必要になるので、Web3jではなくAdminクラスを使用します。

引数に指定するURLはgeth起動時に「--rpcaddr」オプションで指定したアドレスになります。
ポートは指定しなければ「8545」になります。

秘密鍵を取得する

SendTransaction.java
// トランザクション送信者の資格情報を取得する
// 資格情報に公開鍵、秘密鍵が含まれている
String password = "password";
Credentials credentials = WalletUtils.loadCredentials(password, "秘密鍵ファイルのフルパス");

WalletUtils.loadCredentialsメソッドで秘密鍵の情報を取得します。
第一引数にはユーザーアカウントのパスワード
第二引数には秘密鍵ファイル(UTCから始まるファイル)のパス(ファイル名を含む)を指定

トランザクションの送信

やっていることは単純でアカウントをアンロックして、トランザクションを送信するだけです。

SendTransaction.java
  public TransactionReceipt sendTransaction(Credentials credentials, String password, String toAddress, long value) {

    TransactionReceipt receipt = null;
      try {
        // トランザクションの生成
        // "personal_unlockAccount"のリクエストを送信し、レスポンスを受信する
        PersonalUnlockAccount unlockAccountResponse = web3j.personalUnlockAccount(
            credentials.getAddress(), // アドレス
            password  // パスワード
            ).send();

        // アンロックが成功していたら、Etherを送金する
        if (unlockAccountResponse.getResult()) {
          // Transactionを送信する。Blockに取り込まれるまで応答が返ってこない
          receipt = Transfer.sendFunds(web3j, credentials, toAddress, BigDecimal.valueOf(value), Unit.ETHER).send();
        }
      }catch(IOException | TransactionException ex) {
        ex.printStackTrace();
      }catch(Exception ex) {
        ex.printStackTrace();
      }
    return receipt;
  }

ポイントとなる点は、この処理がトランザクションがブロックに取り込まれるまでレスポンスが返ってこなかったことです。
Bitcoinよりはブロック生成の間隔が短いとはいえ、実際のアプリで使う場面があれば、考慮が必要になりそうです。

receipt = Transfer.sendFunds(web3j, credentials, toAddress, BigDecimal.valueOf(value), Unit.ETHER).send();

トランザクション送信の実行結果

まずはトランザクションが生成されたタイミングで「eth.getTransaction」で確認してみます。

INFO [09-15|23:28:07.213] Submitted transaction                    fullhash=0xfa7ab0924c82b9e45a10acd5c6b72136a088b4dee0d4d4810a2d4f4408c3ee97 recipient=0x66B4e7bE902300F9a15D900822Bbd8803Be87391

> eth.getTransaction("0xfa7ab0924c82b9e45a10acd5c6b72136a088b4dee0d4d4810a2d4f4408c3ee97")
{
  blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  blockNumber: null,
  from: "0x945cd603a6754cb13c3d61d8fe240990f86f9f8a",
  gas: 21000,
  gasPrice: 1000000000,
  hash: "0xfa7ab0924c82b9e45a10acd5c6b72136a088b4dee0d4d4810a2d4f4408c3ee97",
  input: "0x",
  nonce: 7,
  r: "0x3ca5a820995553d30656f2218dc10729d3e0f660c35817bbd69845ac96dc6279",
  s: "0x2e915ae47699771108f65881273f464d70db0229ad12a94ac38746f498ea7ed3",
  to: "0x66b4e7be902300f9a15d900822bbd8803be87391",
  transactionIndex: 0,
  v: "0x1c",
  value: 10000000000000000000
}

トランザクションが作成されました。マイニングしてブロックに取り込まれるか確認します。

> eth.getTransaction("0xfa7ab0924c82b9e45a10acd5c6b72136a088b4dee0d4d4810a2d4f4408c3ee97")
{
  blockHash: "0xc9634aec9670d312759a0e12ea5fee54948688c88e4d45a5b9cdbeef3c44c681",
  blockNumber: 2792,
  from: "0x945cd603a6754cb13c3d61d8fe240990f86f9f8a",
  gas: 21000,
  gasPrice: 1000000000,
  hash: "0xfa7ab0924c82b9e45a10acd5c6b72136a088b4dee0d4d4810a2d4f4408c3ee97",
  input: "0x",
  nonce: 7,
  r: "0x3ca5a820995553d30656f2218dc10729d3e0f660c35817bbd69845ac96dc6279",
  s: "0x2e915ae47699771108f65881273f464d70db0229ad12a94ac38746f498ea7ed3",
  to: "0x66b4e7be902300f9a15d900822bbd8803be87391",
  transactionIndex: 0,
  v: "0x1c",
  value: 10000000000000000000
}

無事取り込まれました。

署名付きトランザクションの送信

SendTransaction.java
  public TransactionReceipt sendSignedTransaction(Credentials credentials, String password, String toAddress) {

      TransactionReceipt receipt = null;
      try {
        // トランザクションの生成
        // "personal_unlockAccount"のリクエストを送信し、レスポンスを受信する
        PersonalUnlockAccount unlockAccountResponse = web3j.personalUnlockAccount(
            credentials.getAddress(), // アドレス
            password  // パスワード
            ).send();

        // アンロックが成功していたら、Etherを送金する
        if (unlockAccountResponse.getResult()) {
            // "eth_sendTransaction"の引数に渡すオブジェクトを作成
          RawTransaction rawTransaction = RawTransaction.createEtherTransaction(
            BigInteger.valueOf(10),     // nonce
            BigInteger.valueOf(700),     // gasPrice
            BigInteger.valueOf(4712388),   // gasLimit
            toAddress,             // to
            BigInteger.valueOf(101)     // value
            );

          // トランザクションに署名
          byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
          String hexValue = Numeric.toHexString(signedMessage);

          // トランザクションの送信
          EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(hexValue).send();
          String response = ethSendTransaction.getRawResponse();
          String transactionHash = ethSendTransaction.getTransactionHash();

          Optional<TransactionReceipt> transactionReceipt = null;
          int retry = 0;

          // トランザクションの監視
          if(transactionHash != null) {
            do {
            System.out.printf("%3d checking if transaction " + transactionHash + " is mined....\n" ,retry);
              EthGetTransactionReceipt ethGetTransactionReceiptResp = 
                  web3j.ethGetTransactionReceipt(transactionHash).send();
                transactionReceipt = ethGetTransactionReceiptResp.getTransactionReceipt();

                Thread.sleep(3000);
                retry++;
            }while(!transactionReceipt.isPresent() && retry < 100);
          } else {
            System.out.println("Transaction Send failed...");
            System.out.println("Message:" + ethSendTransaction.getError().getMessage());
            System.out.println("Data   :" + ethSendTransaction.getError().getData());
          }
        }
      }catch(IOException | InterruptedException ex) {
        ex.printStackTrace();
      }catch(Exception ex) {
        ex.printStackTrace();
      }
    return receipt;
  }
}

やっていることは
1. アカウントのアンロック
2. トランザクションオブジェクトの生成
3. トランザクションへ署名
4. トランザクションの送信
5. トランザクションの監視

という流れになります。
トランザクションの監視は指定したトランザクションのハッシュ値がブロックに取り込まれたかどうかを確認しています。

実行結果

トランザクションが作成されました。

INFO [09-15|23:29:23.752] Submitted transaction                    fullhash=0x533ff5d2635284d01d8e85015da00d8962de8b98bf5efaa6d9ceca3200243a88 recipient=0x66B4e7bE902300F9a15D900822Bbd8803Be87391

> eth.getTransaction("0x533ff5d2635284d01d8e85015da00d8962de8b98bf5efaa6d9ceca3200243a88")
{
  blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  blockNumber: null,
  from: "0x945cd603a6754cb13c3d61d8fe240990f86f9f8a",
  gas: 4712388,
  gasPrice: 800,
  hash: "0x533ff5d2635284d01d8e85015da00d8962de8b98bf5efaa6d9ceca3200243a88",
  input: "0x",
  nonce: 10,
  r: "0xc8022528f46078b3e9a8f2a3174d147b85c23af6794fc8b07d58651931b7556f",
  s: "0x1ab35d15cf3afa6990bc6e96a593e6c2cba6432f769d0de919c770cec4a3f2c7",
  to: "0x66b4e7be902300f9a15d900822bbd8803be87391",
  transactionIndex: 0,
  v: "0x1b",
  value: 101
}

ただ、自分の理解が追い付いていないのか、どれだけマイニングをしてもこのトランザクションがブロックに取り込まれません。
何かトランザクションに指定した値に不備があるのか?
それとも、署名したトランザクションを検証しないとブロックに取り込まれないのか?

このあたりを引き続き学習していきます。

最後に

書籍に書いてある内容を漠然とインプットしているだけの学習をしていましたが、Javaという一番得意な言語でどうやって使えるのかを調べると今まで点だったものがどんどん繋がってきました。

まだまだ情報が少ない分野なので積極的に学習した内容をアウトプットしていけたらなと思います。

2019/09/17 加筆

署名付きトランザクションがブロックに取り込まれないという旨を記載していますが、解決策が分かった(先輩に聞いたら1分で分かった。。)ので以下の記事に記載しました。

Ethereum トランザクションがブロックに取り込まれない場合の確認

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away