記事の内容
EthereumのJavaライブラリ「Web3j」を使って署名付きトランザクションを送信する方法
環境
- geth:1.9.0-stable
- web3j:4.5.0
#実装
自分の学習用に書いたコードなのでかなり雑ですが。。
このサンプルソースではトランザクションの送信メソッドを署名なしと署名ありでそれぞれ作成しており、mainメソッドから両方を順番に呼び出しているだけになります。
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に接続する
// ①
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」になります。
秘密鍵を取得する
// トランザクション送信者の資格情報を取得する
// 資格情報に公開鍵、秘密鍵が含まれている
String password = "password";
Credentials credentials = WalletUtils.loadCredentials(password, "秘密鍵ファイルのフルパス");
WalletUtils.loadCredentialsメソッドで秘密鍵の情報を取得します。
第一引数にはユーザーアカウントのパスワード
第二引数には秘密鍵ファイル(UTCから始まるファイル)のパス(ファイル名を含む)を指定
トランザクションの送信
やっていることは単純でアカウントをアンロックして、トランザクションを送信するだけです。
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
}
無事取り込まれました。
署名付きトランザクションの送信
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;
}
}
やっていることは
- アカウントのアンロック
- トランザクションオブジェクトの生成
- トランザクションへ署名
- トランザクションの送信
- トランザクションの監視
という流れになります。
トランザクションの監視は指定したトランザクションのハッシュ値がブロックに取り込まれたかどうかを確認しています。
実行結果
トランザクションが作成されました。
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分で分かった。。)ので以下の記事に記載しました。