2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

nemAdvent Calendar 2021

Day 1

JavaでSymbolブロックチェーンのトランザクションを送信する

Posted at

この記事は「自分の得意なプログラミング言語で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);
}
2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?