この記事は「自分の得意なプログラミング言語でSymbolブロックチェーンを動かす方法」をJavaで実践したものです。
ほぼすべてのロジックを @Toshi_ma さん(Twitter:toshiya_ma)に作成していただきました。ありがとうございます。
pom.xml
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
import
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.nio.ByteBuffer;
import java.text.ParseException;
import java.time.Instant;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.binary.Base32;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator;
import static java.lang.System.arraycopy;
アカウント生成
Security.addProvider(new BouncyCastleProvider());
Ed25519KeyPairGenerator keyPairGenerator = new Ed25519KeyPairGenerator();
SecureRandom RANDOM = new SecureRandom();
keyPairGenerator.init(new Ed25519KeyGenerationParameters(RANDOM));
AsymmetricCipherKeyPair asymmetricCipherKeyPair = keyPairGenerator.generateKeyPair();
Ed25519PrivateKeyParameters privateKey = (Ed25519PrivateKeyParameters) asymmetricCipherKeyPair.getPrivate();
Ed25519PublicKeyParameters publicKey = (Ed25519PublicKeyParameters) asymmetricCipherKeyPair.getPublic();
System.out.println(toHex(privateKey.getEncoded()));
System.out.println(toHex(publicKey.getEncoded()));
アカウント復元
Ed25519PrivateKeyParameters alicePrivateKey = new Ed25519PrivateKeyParameters(getBytes("94ee0f4d7fe388ac4b04a6a6ae2ba969617879b83616e4d25710d688a89d80c7"), 0);
Ed25519PublicKeyParameters alicePublicKey = alicePrivateKey.generatePublicKey();
System.out.println(toHex(alicePrivateKey.getEncoded()));
System.out.println(toHex(alicePublicKey.getEncoded()));
MessageDigest addressHasher = MessageDigest.getInstance("SHA3-256", "BC");
addressHasher.update(alicePublicKey.getEncoded());
byte[] publicKeyHash = addressHasher.digest();
MessageDigest addressBodyHasher = MessageDigest.getInstance("RIPEMD160", "BC");
addressBodyHasher.update(publicKeyHash);
byte[] addressBody = addressBodyHasher.digest();
MessageDigest sumHasher = MessageDigest.getInstance("SHA3-256", "BC");
sumHasher.update(getBytes("98" + toHex(addressBody)));
byte[] preSumHash = sumHasher.digest();
byte[] sumHash = new byte[3];
arraycopy(preSumHash, 0, sumHash, 0, 3);
String aliceAddress = new String(new Base32().encode(getBytes("98" + toHex(addressBody) + toHex(sumHash))), StandardCharsets.UTF_8).toUpperCase();
System.out.println(aliceAddress);
トランザクション構築
byte[] version = new byte[] { 1 };
byte[] networkType = new byte[] { (byte)152 };
byte[] transactionType = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort((short)16724).array();
byte[] fee = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(1000000).array();
long secondLater7200 = (Instant.now().getEpochSecond() + 7200 - 1637848847) * 1000;
byte[] deadline = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(secondLater7200).array();
byte[] recipientAddress = new Base32().decode("TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ");
byte[] mosaicCount = new byte[] { 1 };
byte[] mosaicId = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(Long.decode("0x3A8416DB2D53B6C8")).array();
byte[] mosaicAmount = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(100).array();
String messageStr = "Hello Java! Welcome back to Symbol world!!";
byte[] message = messageStr.getBytes(StandardCharsets.UTF_8);
byte[] messageSize = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort((short)(messageStr.getBytes(StandardCharsets.UTF_8).length + 1)).array();
トランザクション署名
String verifiableBody = toHex(version)
+ toHex(networkType)
+ toHex(transactionType)
+ toHex(fee)
+ toHex(deadline)
+ toHex(recipientAddress)
+ toHex(messageSize)
+ toHex(mosaicCount)
+ "00" + "00000000"
+ toHex(mosaicId)
+ toHex(mosaicAmount)
+ "00" + toHex(message);
String verifiableString = "7fccd304802016bebbcd342a332f91ff1f3bb5e902988b352697be245f48e836"
+ verifiableBody;
byte[] verifiableBuffer = getBytes(verifiableString);
Ed25519Signer signer = new Ed25519Signer();
signer.init(true, alicePrivateKey);
signer.update(verifiableBuffer, 0, verifiableBuffer.length);
byte[] signature = signer.generateSignature();
トランザクション通知
byte[] transactionSize = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(getBytes(verifiableBody).length + 108).array();
String payloadString = toHex(transactionSize)
+ "00000000"
+ toHex(signature)
+ toHex(alicePublicKey.getEncoded())
+ "00000000"
+ verifiableBody;
String payload = "{ \"payload\" : \"" + payloadString + "\"}";
URL url = new URL("https://sym-test-02.opening-line.jp:3001/transactions");
HttpURLConnection httpCon = (HttpURLConnection) url.openConnection();
httpCon.setDoOutput(true);
httpCon.setRequestMethod("PUT");
httpCon.setDoInput(true);
httpCon.setDoOutput(true);
httpCon.setRequestProperty("Content-Type", "application/json; charset=utf-8");
httpCon.connect();
PrintStream ps = new PrintStream(httpCon.getOutputStream());
ps.print(payload);
ps.close();
httpCon.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(httpCon.getInputStream(), "UTF-8"));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
br.close();
System.out.println(sb);
確認
byte[] hashableBuffer = getBytes(
toHex(signature)
+ toHex(alicePublicKey.getEncoded())
+ verifiableString
);
MessageDigest hasher = MessageDigest.getInstance("SHA3-256", "BC");
hasher.update(hashableBuffer);
byte[] transactionHash = hasher.digest();
System.out.println("transactionStatus: https://sym-test-02.opening-line.jp:3001/transactionStatus/" + toHex(transactionHash));
System.out.println("confirmed: https://sym-test-02.opening-line.jp:3001/transactions/confirmed/" + toHex(transactionHash));
System.out.println("explorer: https://testnet.symbol.fyi/transactions/" + toHex(transactionHash));
検証プログラム
Appendix
public static String toHex(byte[] bytes) {
Hex codec = new Hex();
byte[] decodedBytes = codec.encode(bytes);
return new String(decodedBytes, StandardCharsets.UTF_8).toUpperCase();
}
private static byte[] getBytes(String hexString) throws DecoderException {
Hex codec = new Hex();
String paddedHexString = 0 == hexString.length() % 2 ? hexString : "0" + hexString;
byte[] encodedBytes = paddedHexString.getBytes(StandardCharsets.UTF_8);
return codec.decode(encodedBytes);
}