diff --git a/README.md b/bytom-sdk/README.md similarity index 100% rename from README.md rename to bytom-sdk/README.md diff --git a/doc/index.md b/bytom-sdk/doc/index.md similarity index 100% rename from doc/index.md rename to bytom-sdk/doc/index.md diff --git a/doc/transactions.md b/bytom-sdk/doc/transactions.md similarity index 100% rename from doc/transactions.md rename to bytom-sdk/doc/transactions.md diff --git a/bytom-sdk/pom.xml b/bytom-sdk/pom.xml new file mode 100644 index 0000000..72536c5 --- /dev/null +++ b/bytom-sdk/pom.xml @@ -0,0 +1,180 @@ + + + + 4.0.0 + + + io.bytom + bytom-java-sdk + 1.0.0 + + + io.bytom + bytom-sdk + 1.0.0 + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 7 + 7 + + + + + jar + + bytom-sdk-java + + http://www.example.com + SDK for Bytom project in github. + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + https://github.com/Bytom/bytom-java-sdk/issues + GitHub Issues + + + + + successli + successli@outlook.com + https://github.com/successli + + + + + scm:git:git@github.com:Bytom/bytom-java-sdk.git + scm:git:git@github.com:Bytom/bytom-java-sdk.git + git@github.com:Bytom/bytom-java-sdk.git + + + + UTF-8 + + + + + junit + junit + 4.12 + test + + + log4j + log4j + 1.2.17 + + + com.squareup.okhttp + okhttp + 2.5.0 + + + com.squareup.okhttp + mockwebserver + 2.5.0 + test + + + com.google.code.gson + gson + 2.8.0 + + + org.bouncycastle + bcpkix-jdk15on + 1.56 + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + + + maven-assembly-plugin + 2.3 + + + jar-with-dependencies + + + + + package + + single + + + + + + + + + + + + oss + https://oss.sonatype.org/content/repositories/snapshots + + + oss + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + diff --git a/src/main/java/io/bytom/api/AccessToken.java b/bytom-sdk/src/main/java/io/bytom/api/AccessToken.java similarity index 100% rename from src/main/java/io/bytom/api/AccessToken.java rename to bytom-sdk/src/main/java/io/bytom/api/AccessToken.java diff --git a/src/main/java/io/bytom/api/Account.java b/bytom-sdk/src/main/java/io/bytom/api/Account.java similarity index 100% rename from src/main/java/io/bytom/api/Account.java rename to bytom-sdk/src/main/java/io/bytom/api/Account.java diff --git a/src/main/java/io/bytom/api/Asset.java b/bytom-sdk/src/main/java/io/bytom/api/Asset.java similarity index 100% rename from src/main/java/io/bytom/api/Asset.java rename to bytom-sdk/src/main/java/io/bytom/api/Asset.java diff --git a/src/main/java/io/bytom/api/Balance.java b/bytom-sdk/src/main/java/io/bytom/api/Balance.java similarity index 100% rename from src/main/java/io/bytom/api/Balance.java rename to bytom-sdk/src/main/java/io/bytom/api/Balance.java diff --git a/src/main/java/io/bytom/api/Block.java b/bytom-sdk/src/main/java/io/bytom/api/Block.java similarity index 100% rename from src/main/java/io/bytom/api/Block.java rename to bytom-sdk/src/main/java/io/bytom/api/Block.java diff --git a/src/main/java/io/bytom/api/CoreConfig.java b/bytom-sdk/src/main/java/io/bytom/api/CoreConfig.java similarity index 100% rename from src/main/java/io/bytom/api/CoreConfig.java rename to bytom-sdk/src/main/java/io/bytom/api/CoreConfig.java diff --git a/src/main/java/io/bytom/api/Key.java b/bytom-sdk/src/main/java/io/bytom/api/Key.java similarity index 100% rename from src/main/java/io/bytom/api/Key.java rename to bytom-sdk/src/main/java/io/bytom/api/Key.java diff --git a/src/main/java/io/bytom/api/Message.java b/bytom-sdk/src/main/java/io/bytom/api/Message.java similarity index 100% rename from src/main/java/io/bytom/api/Message.java rename to bytom-sdk/src/main/java/io/bytom/api/Message.java diff --git a/src/main/java/io/bytom/api/RawTransaction.java b/bytom-sdk/src/main/java/io/bytom/api/RawTransaction.java similarity index 100% rename from src/main/java/io/bytom/api/RawTransaction.java rename to bytom-sdk/src/main/java/io/bytom/api/RawTransaction.java diff --git a/src/main/java/io/bytom/api/Receiver.java b/bytom-sdk/src/main/java/io/bytom/api/Receiver.java similarity index 100% rename from src/main/java/io/bytom/api/Receiver.java rename to bytom-sdk/src/main/java/io/bytom/api/Receiver.java diff --git a/src/main/java/io/bytom/api/Transaction.java b/bytom-sdk/src/main/java/io/bytom/api/Transaction.java similarity index 100% rename from src/main/java/io/bytom/api/Transaction.java rename to bytom-sdk/src/main/java/io/bytom/api/Transaction.java diff --git a/src/main/java/io/bytom/api/UnconfirmedTransaction.java b/bytom-sdk/src/main/java/io/bytom/api/UnconfirmedTransaction.java similarity index 100% rename from src/main/java/io/bytom/api/UnconfirmedTransaction.java rename to bytom-sdk/src/main/java/io/bytom/api/UnconfirmedTransaction.java diff --git a/src/main/java/io/bytom/api/UnspentOutput.java b/bytom-sdk/src/main/java/io/bytom/api/UnspentOutput.java similarity index 100% rename from src/main/java/io/bytom/api/UnspentOutput.java rename to bytom-sdk/src/main/java/io/bytom/api/UnspentOutput.java diff --git a/src/main/java/io/bytom/api/Wallet.java b/bytom-sdk/src/main/java/io/bytom/api/Wallet.java similarity index 100% rename from src/main/java/io/bytom/api/Wallet.java rename to bytom-sdk/src/main/java/io/bytom/api/Wallet.java diff --git a/src/main/java/io/bytom/common/Configuration.java b/bytom-sdk/src/main/java/io/bytom/common/Configuration.java similarity index 100% rename from src/main/java/io/bytom/common/Configuration.java rename to bytom-sdk/src/main/java/io/bytom/common/Configuration.java diff --git a/src/main/java/io/bytom/common/ParameterizedTypeImpl.java b/bytom-sdk/src/main/java/io/bytom/common/ParameterizedTypeImpl.java similarity index 100% rename from src/main/java/io/bytom/common/ParameterizedTypeImpl.java rename to bytom-sdk/src/main/java/io/bytom/common/ParameterizedTypeImpl.java diff --git a/src/main/java/io/bytom/common/Utils.java b/bytom-sdk/src/main/java/io/bytom/common/Utils.java similarity index 100% rename from src/main/java/io/bytom/common/Utils.java rename to bytom-sdk/src/main/java/io/bytom/common/Utils.java diff --git a/src/main/java/io/bytom/exception/APIException.java b/bytom-sdk/src/main/java/io/bytom/exception/APIException.java similarity index 100% rename from src/main/java/io/bytom/exception/APIException.java rename to bytom-sdk/src/main/java/io/bytom/exception/APIException.java diff --git a/src/main/java/io/bytom/exception/BadURLException.java b/bytom-sdk/src/main/java/io/bytom/exception/BadURLException.java similarity index 100% rename from src/main/java/io/bytom/exception/BadURLException.java rename to bytom-sdk/src/main/java/io/bytom/exception/BadURLException.java diff --git a/src/main/java/io/bytom/exception/BuildException.java b/bytom-sdk/src/main/java/io/bytom/exception/BuildException.java similarity index 100% rename from src/main/java/io/bytom/exception/BuildException.java rename to bytom-sdk/src/main/java/io/bytom/exception/BuildException.java diff --git a/src/main/java/io/bytom/exception/BytomException.java b/bytom-sdk/src/main/java/io/bytom/exception/BytomException.java similarity index 100% rename from src/main/java/io/bytom/exception/BytomException.java rename to bytom-sdk/src/main/java/io/bytom/exception/BytomException.java diff --git a/src/main/java/io/bytom/exception/ConfigurationException.java b/bytom-sdk/src/main/java/io/bytom/exception/ConfigurationException.java similarity index 100% rename from src/main/java/io/bytom/exception/ConfigurationException.java rename to bytom-sdk/src/main/java/io/bytom/exception/ConfigurationException.java diff --git a/src/main/java/io/bytom/exception/ConnectivityException.java b/bytom-sdk/src/main/java/io/bytom/exception/ConnectivityException.java similarity index 100% rename from src/main/java/io/bytom/exception/ConnectivityException.java rename to bytom-sdk/src/main/java/io/bytom/exception/ConnectivityException.java diff --git a/src/main/java/io/bytom/exception/HTTPException.java b/bytom-sdk/src/main/java/io/bytom/exception/HTTPException.java similarity index 100% rename from src/main/java/io/bytom/exception/HTTPException.java rename to bytom-sdk/src/main/java/io/bytom/exception/HTTPException.java diff --git a/src/main/java/io/bytom/exception/JSONException.java b/bytom-sdk/src/main/java/io/bytom/exception/JSONException.java similarity index 100% rename from src/main/java/io/bytom/exception/JSONException.java rename to bytom-sdk/src/main/java/io/bytom/exception/JSONException.java diff --git a/src/main/java/io/bytom/http/BatchResponse.java b/bytom-sdk/src/main/java/io/bytom/http/BatchResponse.java similarity index 100% rename from src/main/java/io/bytom/http/BatchResponse.java rename to bytom-sdk/src/main/java/io/bytom/http/BatchResponse.java diff --git a/src/main/java/io/bytom/http/Client.java b/bytom-sdk/src/main/java/io/bytom/http/Client.java similarity index 100% rename from src/main/java/io/bytom/http/Client.java rename to bytom-sdk/src/main/java/io/bytom/http/Client.java diff --git a/src/main/java/io/bytom/http/SuccessMessage.java b/bytom-sdk/src/main/java/io/bytom/http/SuccessMessage.java similarity index 100% rename from src/main/java/io/bytom/http/SuccessMessage.java rename to bytom-sdk/src/main/java/io/bytom/http/SuccessMessage.java diff --git a/src/main/resources/config.properties b/bytom-sdk/src/main/resources/config.properties similarity index 100% rename from src/main/resources/config.properties rename to bytom-sdk/src/main/resources/config.properties diff --git a/src/main/resources/log4j.properties b/bytom-sdk/src/main/resources/log4j.properties similarity index 100% rename from src/main/resources/log4j.properties rename to bytom-sdk/src/main/resources/log4j.properties diff --git a/src/test/java/io/bytom/AppTest.java b/bytom-sdk/src/test/java/io/bytom/AppTest.java similarity index 100% rename from src/test/java/io/bytom/AppTest.java rename to bytom-sdk/src/test/java/io/bytom/AppTest.java diff --git a/src/test/java/io/bytom/TestUtils.java b/bytom-sdk/src/test/java/io/bytom/TestUtils.java similarity index 100% rename from src/test/java/io/bytom/TestUtils.java rename to bytom-sdk/src/test/java/io/bytom/TestUtils.java diff --git a/src/test/java/io/bytom/integration/AccessTokenTest.java b/bytom-sdk/src/test/java/io/bytom/integration/AccessTokenTest.java similarity index 100% rename from src/test/java/io/bytom/integration/AccessTokenTest.java rename to bytom-sdk/src/test/java/io/bytom/integration/AccessTokenTest.java diff --git a/src/test/java/io/bytom/integration/AccountTest.java b/bytom-sdk/src/test/java/io/bytom/integration/AccountTest.java similarity index 100% rename from src/test/java/io/bytom/integration/AccountTest.java rename to bytom-sdk/src/test/java/io/bytom/integration/AccountTest.java diff --git a/src/test/java/io/bytom/integration/AssetTest.java b/bytom-sdk/src/test/java/io/bytom/integration/AssetTest.java similarity index 100% rename from src/test/java/io/bytom/integration/AssetTest.java rename to bytom-sdk/src/test/java/io/bytom/integration/AssetTest.java diff --git a/src/test/java/io/bytom/integration/BalanceTest.java b/bytom-sdk/src/test/java/io/bytom/integration/BalanceTest.java similarity index 100% rename from src/test/java/io/bytom/integration/BalanceTest.java rename to bytom-sdk/src/test/java/io/bytom/integration/BalanceTest.java diff --git a/src/test/java/io/bytom/integration/BlockTest.java b/bytom-sdk/src/test/java/io/bytom/integration/BlockTest.java similarity index 100% rename from src/test/java/io/bytom/integration/BlockTest.java rename to bytom-sdk/src/test/java/io/bytom/integration/BlockTest.java diff --git a/src/test/java/io/bytom/integration/CoreConfigTest.java b/bytom-sdk/src/test/java/io/bytom/integration/CoreConfigTest.java similarity index 100% rename from src/test/java/io/bytom/integration/CoreConfigTest.java rename to bytom-sdk/src/test/java/io/bytom/integration/CoreConfigTest.java diff --git a/src/test/java/io/bytom/integration/KeyTest.java b/bytom-sdk/src/test/java/io/bytom/integration/KeyTest.java similarity index 100% rename from src/test/java/io/bytom/integration/KeyTest.java rename to bytom-sdk/src/test/java/io/bytom/integration/KeyTest.java diff --git a/src/test/java/io/bytom/integration/MessageTest.java b/bytom-sdk/src/test/java/io/bytom/integration/MessageTest.java similarity index 100% rename from src/test/java/io/bytom/integration/MessageTest.java rename to bytom-sdk/src/test/java/io/bytom/integration/MessageTest.java diff --git a/src/test/java/io/bytom/integration/RawTransactionTest.java b/bytom-sdk/src/test/java/io/bytom/integration/RawTransactionTest.java similarity index 100% rename from src/test/java/io/bytom/integration/RawTransactionTest.java rename to bytom-sdk/src/test/java/io/bytom/integration/RawTransactionTest.java diff --git a/src/test/java/io/bytom/integration/TransactionTest.java b/bytom-sdk/src/test/java/io/bytom/integration/TransactionTest.java similarity index 100% rename from src/test/java/io/bytom/integration/TransactionTest.java rename to bytom-sdk/src/test/java/io/bytom/integration/TransactionTest.java diff --git a/src/test/java/io/bytom/integration/UTXOTest.java b/bytom-sdk/src/test/java/io/bytom/integration/UTXOTest.java similarity index 100% rename from src/test/java/io/bytom/integration/UTXOTest.java rename to bytom-sdk/src/test/java/io/bytom/integration/UTXOTest.java diff --git a/src/test/java/io/bytom/integration/UnconfirmedTransactionTest.java b/bytom-sdk/src/test/java/io/bytom/integration/UnconfirmedTransactionTest.java similarity index 100% rename from src/test/java/io/bytom/integration/UnconfirmedTransactionTest.java rename to bytom-sdk/src/test/java/io/bytom/integration/UnconfirmedTransactionTest.java diff --git a/src/test/java/io/bytom/integration/WalletTest.java b/bytom-sdk/src/test/java/io/bytom/integration/WalletTest.java similarity index 100% rename from src/test/java/io/bytom/integration/WalletTest.java rename to bytom-sdk/src/test/java/io/bytom/integration/WalletTest.java diff --git a/pom.xml b/pom.xml index 7425c0c..3586f95 100644 --- a/pom.xml +++ b/pom.xml @@ -1,12 +1,19 @@ - - 4.0.0 io.bytom - bytom-sdk + bytom-java-sdk 1.0.0 + pom + + + bytom-sdk + tx-signer + + @@ -19,12 +26,6 @@ - jar - - bytom-sdk-java - - http://www.example.com - SDK for Bytom project in github. @@ -58,108 +59,6 @@ UTF-8 - - - junit - junit - 4.12 - test - - - log4j - log4j - 1.2.17 - - - com.squareup.okhttp - okhttp - 2.5.0 - - - com.squareup.okhttp - mockwebserver - 2.5.0 - test - - - com.google.code.gson - gson - 2.8.0 - - - org.bouncycastle - bcpkix-jdk15on - 1.56 - - - - - - release - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.5 - - - sign-artifacts - verify - - sign - - - - - - maven-assembly-plugin - 2.3 - - - jar-with-dependencies - - - - - package - - single - - - - - - - - - oss @@ -170,4 +69,4 @@ https://oss.sonatype.org/service/local/staging/deploy/maven2/ - + \ No newline at end of file diff --git a/tx-signer/.DS_Store b/tx-signer/.DS_Store new file mode 100644 index 0000000..8478ae5 Binary files /dev/null and b/tx-signer/.DS_Store differ diff --git a/tx-signer/README.md b/tx-signer/README.md new file mode 100644 index 0000000..d1ebb51 --- /dev/null +++ b/tx-signer/README.md @@ -0,0 +1,254 @@ +# tx_signer + +Java implementation of signing transaction offline to bytomd. + +## Pre + +#### Get the source code + +``` +$ git clone https://github.com/Bytom/bytom.git $GOPATH/src/github.com/bytom +``` + +#### git checkout + +``` +$ git checkout dev +``` + +**Why need dev branch? Because you could call decode transaction api from dev branch and obtain tx_id and some inputs ids.** + +#### Build + +``` +$ cd $GOPATH/src/github.com/bytom +$ make bytomd # build bytomd +$ make bytomcli # build bytomcli +``` + +When successfully building the project, the `bytom` and `bytomcli` binary should be present in `cmd/bytomd` and `cmd/bytomcli` directory, respectively. + +#### Initialize + +First of all, initialize the node: + +``` +$ cd ./cmd/bytomd +$ ./bytomd init --chain_id solonet +``` + +#### launch + +``` +$ ./bytomd node --mining +``` + +## Usage + +#### Build jar + +1. first get source code + + ``` + git clone https://github.com/successli/tx_signer.git + ``` + +2. get jar package + + ``` + $ mvn assembly:assembly -Dmaven.test.skip=true + ``` + + You can get a jar with dependencies, and you can use it in your project. + +#### Test cases + +Need 3 Parameters: + +- Private Keys Array +- Template Object + - After call build transaction api return a Template json object. [build transaction api](https://github.com/Bytom/bytom/wiki/API-Reference#build-transaction) + - use bytom java sdk return a Template object. +- Raw Transaction + - call decode raw-transaction api from dev branch. [decode raw-transaction api](https://github.com/Bytom/bytom/wiki/API-Reference#decode-raw-transaction) + +Call method: + +```java +// return a Template object signed offline basically. +Template result = signatures.generateSignatures(privates, template, rawTransaction); +// use result's raw_transaction call sign transaction api to build another data but not need password or private key. +``` + +Single-key Example: + +```java +@Test +// 使用 SDK 来构造 Template 对象参数, 单签 +public void testSignSingleKey() throws BytomException { + Client client = Client.generateClient(); + + String asset_id = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + String address = "sm1qvyus3s5d7jv782syuqe3qrh65fx23lgpzf33em"; + // build transaction obtain a Template object + Template template = new Transaction.Builder() + .addAction( + new Transaction.Action.SpendFromAccount() + .setAccountId("0G0NLBNU00A02") + .setAssetId(asset_id) + .setAmount(40000000) + ) + .addAction( + new Transaction.Action.SpendFromAccount() + .setAccountId("0G0NLBNU00A02") + .setAssetId(asset_id) + .setAmount(300000000) + ) + .addAction( + new Transaction.Action.ControlWithAddress() + .setAddress(address) + .setAssetId(asset_id) + .setAmount(30000000) + ).build(client); + logger.info("template: " + template.toJson()); + // use Template object's raw_transaction id to decode raw_transaction obtain a RawTransaction object + RawTransaction decodedTx = RawTransaction.decode(client, template.rawTransaction); + logger.info("decodeTx: " + decodedTx.toJson()); + // need a private key array + String[] privateKeys = new String[]{"10fdbc41a4d3b8e5a0f50dd3905c1660e7476d4db3dbd9454fa4347500a633531c487e8174ffc0cfa76c3be6833111a9b8cd94446e37a76ee18bb21a7d6ea66b"}; + logger.info("private key:" + privateKeys[0]); + // call offline sign method to obtain a basic offline signed template + Signatures signatures = new SignaturesImpl(); + Template basicSigned = signatures.generateSignatures(privateKeys, template, decodedTx); + logger.info("basic signed raw: " + basicSigned.toJson()); + // call sign transaction api to calculate whole raw_transaction id + // sign password is None or another random String + Template result = new Transaction.SignerBuilder().sign(client, + basicSigned, ""); + logger.info("result raw_transaction: " + result.toJson()); + // success to submit transaction +} +``` + +Multi-keys Example: + +> Need an account has two or more keys. + +```java +@Test +// 使用 SDK 来构造 Template 对象参数, 多签 +public void testSignMultiKeys() throws BytomException { + Client client = Client.generateClient(); + + String asset_id = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + String address = "sm1qvyus3s5d7jv782syuqe3qrh65fx23lgpzf33em"; + // build transaction obtain a Template object + // account 0G1RPP6OG0A06 has two keys + Template template = new Transaction.Builder() + .setTtl(10) + .addAction( + new Transaction.Action.SpendFromAccount() + .setAccountId("0G1RPP6OG0A06") + .setAssetId(asset_id) + .setAmount(40000000) + ) + .addAction( + new Transaction.Action.SpendFromAccount() + .setAccountId("0G1RPP6OG0A06") + .setAssetId(asset_id) + .setAmount(300000000) + ) + .addAction( + new Transaction.Action.ControlWithAddress() + .setAddress(address) + .setAssetId(asset_id) + .setAmount(30000000) + ).build(client); + logger.info("template: " + template.toJson()); + // use Template object's raw_transaction id to decode raw_transaction obtain a RawTransaction object + RawTransaction decodedTx = RawTransaction.decode(client, template.rawTransaction); + logger.info("decodeTx: " + decodedTx.toJson()); + // need a private key array + String[] privateKeys = new String[]{"08bdbd6c22856c5747c930f64d0e5d58ded17c4473910c6c0c3f94e485833a436247976253c8e29e961041ad8dfad9309744255364323163837cbef2483b4f67", + "40c821f736f60805ad59b1fea158762fa6355e258601dfb49dda6f672092ae5adf072d5cab2ceaaa0d68dd3fe7fa04869d95afed8c20069f446a338576901e1b"}; + logger.info("private key 1:" + privateKeys[0]); + logger.info("private key 2:" + privateKeys[1]); + // call offline sign method to obtain a basic offline signed template + Signatures signatures = new SignaturesImpl(); + Template basicSigned = signatures.generateSignatures(privateKeys, template, decodedTx); + logger.info("basic signed raw: " + basicSigned.toJson()); + // call sign transaction api to calculate whole raw_transaction id + // sign password is None or another random String + Template result = new Transaction.SignerBuilder().sign(client, + basicSigned, ""); + logger.info("result raw_transaction: " + result.toJson()); + // success to submit transaction +} +``` +Multi-keys and Multi-inputs Example: + +```java +@Test +// 使用 SDK 来构造 Template 对象参数, 多签, 多输入 +public void testSignMultiKeysMultiInputs() throws BytomException { + Client client = Client.generateClient(); + + String asset_id = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + String address = "sm1qvyus3s5d7jv782syuqe3qrh65fx23lgpzf33em"; + // build transaction obtain a Template object + Template template = new Transaction.Builder() + .setTtl(10) + // 1 input + .addAction( + new Transaction.Action.SpendFromAccount() + .setAccountId("0G1RPP6OG0A06") // Multi-keys account + .setAssetId(asset_id) + .setAmount(40000000) + ) + .addAction( + new Transaction.Action.SpendFromAccount() + .setAccountId("0G1RPP6OG0A06") + .setAssetId(asset_id) + .setAmount(300000000) + ) // 2 input + .addAction( + new Transaction.Action.SpendFromAccount() + .setAccountId("0G1Q6V1P00A02") // Multi-keys account + .setAssetId(asset_id) + .setAmount(40000000) + ) + .addAction( + new Transaction.Action.SpendFromAccount() + .setAccountId("0G1Q6V1P00A02") + .setAssetId(asset_id) + .setAmount(300000000) + ) + .addAction( + new Transaction.Action.ControlWithAddress() + .setAddress(address) + .setAssetId(asset_id) + .setAmount(60000000) + ).build(client); + logger.info("template: " + template.toJson()); + // use Template object's raw_transaction id to decode raw_transaction obtain a RawTransaction object + RawTransaction decodedTx = RawTransaction.decode(client, template.rawTransaction); + logger.info("decodeTx: " + decodedTx.toJson()); + // need a private key array + String[] privateKeys = new String[]{"08bdbd6c22856c5747c930f64d0e5d58ded17c4473910c6c0c3f94e485833a436247976253c8e29e961041ad8dfad9309744255364323163837cbef2483b4f67", + "40c821f736f60805ad59b1fea158762fa6355e258601dfb49dda6f672092ae5adf072d5cab2ceaaa0d68dd3fe7fa04869d95afed8c20069f446a338576901e1b", + "08bdbd6c22856c5747c930f64d0e5d58ded17c4473910c6c0c3f94e485833a436247976253c8e29e961041ad8dfad9309744255364323163837cbef2483b4f67"}; + logger.info("private key 1:" + privateKeys[0]); + logger.info("private key 2:" + privateKeys[1]); + // call offline sign method to obtain a basic offline signed template + Signatures signatures = new SignaturesImpl(); + Template basicSigned = signatures.generateSignatures(privateKeys, template, decodedTx); + logger.info("basic signed raw: " + basicSigned.toJson()); + // call sign transaction api to calculate whole raw_transaction id + // sign password is None or another random String + Template result = new Transaction.SignerBuilder().sign(client, + basicSigned, ""); + logger.info("result raw_transaction: " + result.toJson()); + // success to submit transaction +} +``` + diff --git a/tx-signer/pom.xml b/tx-signer/pom.xml new file mode 100644 index 0000000..b9284bb --- /dev/null +++ b/tx-signer/pom.xml @@ -0,0 +1,137 @@ + + + + 4.0.0 + + + io.bytom + bytom-java-sdk + 1.0.0 + + + io.bytom + tx-signer + 1.0.0-SNAPSHOT + + tx-signer + + http://www.example.com + + + UTF-8 + 1.7 + 1.7 + + + + + junit + junit + 4.12 + test + + + + com.google.crypto.tink + tink + 1.1.1 + + + com.google.protobuf + protobuf-java + + + + + + com.google.protobuf + protobuf-java + 3.6.1 + + + + org.bouncycastle + bcprov-ext-jdk15on + 1.55 + + + + + net.i2p.crypto + eddsa + 0.3.0 + + + log4j + log4j + 1.2.17 + + + com.squareup.okhttp + okhttp + 2.5.0 + + + com.squareup.okhttp + mockwebserver + 2.5.0 + test + + + com.google.code.gson + gson + 2.8.0 + + + org.bouncycastle + bcprov-jdk15on + 1.59 + + + + + + + + maven-clean-plugin + 3.0.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.7.0 + + + maven-surefire-plugin + 2.20.1 + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + maven-assembly-plugin + + + jar-with-dependencies + + + + + + + jar + diff --git a/tx-signer/src/.DS_Store b/tx-signer/src/.DS_Store new file mode 100644 index 0000000..170e467 Binary files /dev/null and b/tx-signer/src/.DS_Store differ diff --git a/tx-signer/src/main/java/com/google/crypto/tink/subtle/Bytes.java b/tx-signer/src/main/java/com/google/crypto/tink/subtle/Bytes.java new file mode 100644 index 0000000..af98103 --- /dev/null +++ b/tx-signer/src/main/java/com/google/crypto/tink/subtle/Bytes.java @@ -0,0 +1,182 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.crypto.tink.subtle; + +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.util.Arrays; + +/** + * Helper methods that deal with byte arrays. + * + * @since 1.0.0 + */ +public final class Bytes { + /** + * Best effort fix-timing array comparison. + * + * @return true if two arrays are equal. + */ + public static final boolean equal(final byte[] x, final byte[] y) { + if (x == null || y == null) { + return false; + } + if (x.length != y.length) { + return false; + } + int res = 0; + for (int i = 0; i < x.length; i++) { + res |= x[i] ^ y[i]; + } + return res == 0; + } + + /** + * Returns the concatenation of the input arrays in a single array. For example, {@code concat(new + * byte[] {a, b}, new byte[] {}, new byte[] {c}} returns the array {@code {a, b, c}}. + * + * @return a single array containing all the values from the source arrays, in order + */ + public static byte[] concat(byte[]... chunks) throws GeneralSecurityException { + int length = 0; + for (byte[] chunk : chunks) { + if (length > Integer.MAX_VALUE - chunk.length) { + throw new GeneralSecurityException("exceeded size limit"); + } + length += chunk.length; + } + byte[] res = new byte[length]; + int pos = 0; + for (byte[] chunk : chunks) { + System.arraycopy(chunk, 0, res, pos, chunk.length); + pos += chunk.length; + } + return res; + } + + /** + * Computes the xor of two byte arrays, specifying offsets and the length to xor. + * + * @return a new byte[] of length len. + */ + public static final byte[] xor( + final byte[] x, int offsetX, final byte[] y, int offsetY, int len) { + if (len < 0 || x.length - len < offsetX || y.length - len < offsetY) { + throw new IllegalArgumentException( + "That combination of buffers, offsets and length to xor result in out-of-bond accesses."); + } + byte[] res = new byte[len]; + for (int i = 0; i < len; i++) { + res[i] = (byte) (x[i + offsetX] ^ y[i + offsetY]); + } + return res; + } + + /** + * Computes the xor of two byte buffers, specifying the length to xor, and + * stores the result to {@code output}. + * + * @return a new byte[] of length len. + */ + public static final void xor(ByteBuffer output, ByteBuffer x, ByteBuffer y, int len) { + if (len < 0 || x.remaining() < len || y.remaining() < len || output.remaining() < len) { + throw new IllegalArgumentException( + "That combination of buffers, offsets and length to xor result in out-of-bond accesses."); + } + for (int i = 0; i < len; i++) { + output.put((byte) (x.get() ^ y.get())); + } + } + + /** + * Computes the xor of two byte arrays of equal size. + * + * @return a new byte[] of length x.length. + */ + public static final byte[] xor(final byte[] x, final byte[] y) { + if (x.length != y.length) { + throw new IllegalArgumentException("The lengths of x and y should match."); + } + return xor(x, 0, y, 0, x.length); + } + + /** + * xors b to the end of a. + * + * @return a new byte[] of length x.length. + */ + public static final byte[] xorEnd(final byte[] a, final byte[] b) { + if (a.length < b.length) { + throw new IllegalArgumentException("xorEnd requires a.length >= b.length"); + } + int paddingLength = a.length - b.length; + byte[] res = Arrays.copyOf(a, a.length); + for (int i = 0; i < b.length; i++) { + res[paddingLength + i] ^= b[i]; + } + return res; + } + + // TODO(thaidn): add checks for boundary conditions/overflows. + /** + * Transforms a passed value to a LSB first byte array with the size of the specified capacity + * + * @param capacity size of the resulting byte array + * @param value that should be represented as a byte array + */ + public static byte[] intToByteArray(int capacity, int value) { + final byte[] result = new byte[capacity]; + for (int i = 0; i < capacity; i++) { + result[i] = (byte) ((value >> (8 * i)) & 0xFF); + } + return result; + } + + /** + * Transforms a passed LSB first byte array to an int + * + * @param bytes that should be transformed to a byte array + */ + public static int byteArrayToInt(byte[] bytes) { + return byteArrayToInt(bytes, bytes.length); + } + + /** + * Transforms a passed LSB first byte array to an int + * + * @param bytes that should be transformed to a byte array + * @param length amount of the passed {@code bytes} that should be transformed + */ + public static int byteArrayToInt(byte[] bytes, int length) { + return byteArrayToInt(bytes, 0, length); + } + + /** + * Transforms a passed LSB first byte array to an int + * + * @param bytes that should be transformed to a byte array + * @param offset start index to start the transformation + * @param length amount of the passed {@code bytes} that should be transformed + */ + public static int byteArrayToInt(byte[] bytes, int offset, int length) { + int value = 0; + for (int i = 0; i < length; i++) { + value += (bytes[i + offset] & 0xFF) << (i * 8); + } + return value; + } +} diff --git a/tx-signer/src/main/java/com/google/crypto/tink/subtle/Curve25519.java b/tx-signer/src/main/java/com/google/crypto/tink/subtle/Curve25519.java new file mode 100644 index 0000000..7e22ef4 --- /dev/null +++ b/tx-signer/src/main/java/com/google/crypto/tink/subtle/Curve25519.java @@ -0,0 +1,429 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.crypto.tink.subtle; + +import com.google.crypto.tink.annotations.Alpha; +import java.security.InvalidKeyException; +import java.util.Arrays; + +/** + * This class implements point arithmetic on the elliptic curve Curve25519. + * + *

This class only implements point arithmetic, if you want to use the ECDH Curve25519 function, + * please checkout {@link X25519}. + * + *

This implementation is based on curve255-donna C + * implementation. + */ +@Alpha +final class Curve25519 { + // https://cr.yp.to/ecdh.html#validate doesn't recommend validating peer's public key. However, + // validating public key doesn't harm security and in certain cases, prevents unwanted edge + // cases. + // As we clear the most significant bit of peer's public key, we don't have to include public keys + // that are larger than 2^255. + static final byte[][] BANNED_PUBLIC_KEYS = + new byte[][] { + // 0 + new byte[] { + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 + }, + // 1 + new byte[] { + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }, + // 325606250916557431795983626356110631294008115727848805560023387167927233504 + new byte[] { + (byte) 0xe0, (byte) 0xeb, (byte) 0x7a, (byte) 0x7c, + (byte) 0x3b, (byte) 0x41, (byte) 0xb8, (byte) 0xae, + (byte) 0x16, (byte) 0x56, (byte) 0xe3, (byte) 0xfa, + (byte) 0xf1, (byte) 0x9f, (byte) 0xc4, (byte) 0x6a, + (byte) 0xda, (byte) 0x09, (byte) 0x8d, (byte) 0xeb, + (byte) 0x9c, (byte) 0x32, (byte) 0xb1, (byte) 0xfd, + (byte) 0x86, (byte) 0x62, (byte) 0x05, (byte) 0x16, + (byte) 0x5f, (byte) 0x49, (byte) 0xb8, (byte) 0x00, + }, + // 39382357235489614581723060781553021112529911719440698176882885853963445705823 + new byte[] { + (byte) 0x5f, (byte) 0x9c, (byte) 0x95, (byte) 0xbc, + (byte) 0xa3, (byte) 0x50, (byte) 0x8c, (byte) 0x24, + (byte) 0xb1, (byte) 0xd0, (byte) 0xb1, (byte) 0x55, + (byte) 0x9c, (byte) 0x83, (byte) 0xef, (byte) 0x5b, + (byte) 0x04, (byte) 0x44, (byte) 0x5c, (byte) 0xc4, + (byte) 0x58, (byte) 0x1c, (byte) 0x8e, (byte) 0x86, + (byte) 0xd8, (byte) 0x22, (byte) 0x4e, (byte) 0xdd, + (byte) 0xd0, (byte) 0x9f, (byte) 0x11, (byte) 0x57 + }, + // 2^255 - 19 - 1 + new byte[] { + (byte) 0xec, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }, + // 2^255 - 19 + new byte[] { + (byte) 0xed, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f + }, + // 2^255 - 19 + 1 + new byte[] { + (byte) 0xee, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f + } + }; + /** + * Computes Montgomery's double-and-add formulas. + * + *

On entry and exit, the absolute value of the limbs of all inputs and outputs are < 2^26. + * + * @param x2 x projective coordinate of output 2Q, long form + * @param z2 z projective coordinate of output 2Q, long form + * @param x3 x projective coordinate of output Q + Q', long form + * @param z3 z projective coordinate of output Q + Q', long form + * @param x x projective coordinate of input Q, short form, destroyed + * @param z z projective coordinate of input Q, short form, destroyed + * @param xprime x projective coordinate of input Q', short form, destroyed + * @param zprime z projective coordinate of input Q', short form, destroyed + * @param qmqp input Q - Q', short form, preserved + */ + private static void monty( + long[] x2, + long[] z2, + long[] x3, + long[] z3, + long[] x, + long[] z, + long[] xprime, + long[] zprime, + long[] qmqp) { + long[] origx = Arrays.copyOf(x, Field25519.LIMB_CNT); + long[] zzz = new long[19]; + long[] xx = new long[19]; + long[] zz = new long[19]; + long[] xxprime = new long[19]; + long[] zzprime = new long[19]; + long[] zzzprime = new long[19]; + long[] xxxprime = new long[19]; + + Field25519.sum(x, z); + // |x[i]| < 2^27 + Field25519.sub(z, origx); // does x - z + // |z[i]| < 2^27 + + long[] origxprime = Arrays.copyOf(xprime, Field25519.LIMB_CNT); + Field25519.sum(xprime, zprime); + // |xprime[i]| < 2^27 + Field25519.sub(zprime, origxprime); + // |zprime[i]| < 2^27 + Field25519.product(xxprime, xprime, z); + // |xxprime[i]| < 14*2^54: the largest product of two limbs will be < 2^(27+27) and {@ref + // Field25519#product} adds together, at most, 14 of those products. (Approximating that to + // 2^58 doesn't work out.) + Field25519.product(zzprime, x, zprime); + // |zzprime[i]| < 14*2^54 + Field25519.reduceSizeByModularReduction(xxprime); + Field25519.reduceCoefficients(xxprime); + // |xxprime[i]| < 2^26 + Field25519.reduceSizeByModularReduction(zzprime); + Field25519.reduceCoefficients(zzprime); + // |zzprime[i]| < 2^26 + System.arraycopy(xxprime, 0, origxprime, 0, Field25519.LIMB_CNT); + Field25519.sum(xxprime, zzprime); + // |xxprime[i]| < 2^27 + Field25519.sub(zzprime, origxprime); + // |zzprime[i]| < 2^27 + Field25519.square(xxxprime, xxprime); + // |xxxprime[i]| < 2^26 + Field25519.square(zzzprime, zzprime); + // |zzzprime[i]| < 2^26 + Field25519.product(zzprime, zzzprime, qmqp); + // |zzprime[i]| < 14*2^52 + Field25519.reduceSizeByModularReduction(zzprime); + Field25519.reduceCoefficients(zzprime); + // |zzprime[i]| < 2^26 + System.arraycopy(xxxprime, 0, x3, 0, Field25519.LIMB_CNT); + System.arraycopy(zzprime, 0, z3, 0, Field25519.LIMB_CNT); + + Field25519.square(xx, x); + // |xx[i]| < 2^26 + Field25519.square(zz, z); + // |zz[i]| < 2^26 + Field25519.product(x2, xx, zz); + // |x2[i]| < 14*2^52 + Field25519.reduceSizeByModularReduction(x2); + Field25519.reduceCoefficients(x2); + // |x2[i]| < 2^26 + Field25519.sub(zz, xx); // does zz = xx - zz + // |zz[i]| < 2^27 + Arrays.fill(zzz, Field25519.LIMB_CNT, zzz.length - 1, 0); + Field25519.scalarProduct(zzz, zz, 121665); + // |zzz[i]| < 2^(27+17) + // No need to call reduceSizeByModularReduction here: scalarProduct doesn't increase the degree + // of its input. + Field25519.reduceCoefficients(zzz); + // |zzz[i]| < 2^26 + Field25519.sum(zzz, xx); + // |zzz[i]| < 2^27 + Field25519.product(z2, zz, zzz); + // |z2[i]| < 14*2^(26+27) + Field25519.reduceSizeByModularReduction(z2); + Field25519.reduceCoefficients(z2); + // |z2|i| < 2^26 + } + + /** + * Conditionally swap two reduced-form limb arrays if {@code iswap} is 1, but leave them unchanged + * if {@code iswap} is 0. Runs in data-invariant time to avoid side-channel attacks. + * + *

NOTE that this function requires that {@code iswap} be 1 or 0; other values give wrong + * results. Also, the two limb arrays must be in reduced-coefficient, reduced-degree form: the + * values in a[10..19] or b[10..19] aren't swapped, and all all values in a[0..9],b[0..9] must + * have magnitude less than Integer.MAX_VALUE. + */ + static void swapConditional(long[] a, long[] b, int iswap) { + int swap = -iswap; + for (int i = 0; i < Field25519.LIMB_CNT; i++) { + int x = swap & (((int) a[i]) ^ ((int) b[i])); + a[i] = ((int) a[i]) ^ x; + b[i] = ((int) b[i]) ^ x; + } + } + + /** + * Conditionally copies a reduced-form limb arrays {@code b} into {@code a} if {@code icopy} is 1, + * but leave {@code a} unchanged if 'iswap' is 0. Runs in data-invariant time to avoid + * side-channel attacks. + * + *

NOTE that this function requires that {@code icopy} be 1 or 0; other values give wrong + * results. Also, the two limb arrays must be in reduced-coefficient, reduced-degree form: the + * values in a[10..19] or b[10..19] aren't swapped, and all all values in a[0..9],b[0..9] must + * have magnitude less than Integer.MAX_VALUE. + */ + static void copyConditional(long[] a, long[] b, int icopy) { + int copy = -icopy; + for (int i = 0; i < Field25519.LIMB_CNT; i++) { + int x = copy & (((int) a[i]) ^ ((int) b[i])); + a[i] = ((int) a[i]) ^ x; + } + } + + /** + * Calculates nQ where Q is the x-coordinate of a point on the curve. + * + * @param resultx the x projective coordinate of the resulting curve point (short form). + * @param n a little endian, 32-byte number. + * @param qBytes a little endian, 32-byte number representing the public point' x coordinate. + * @throws InvalidKeyException iff the public key is in the banned list or its length is not + * 32-byte. + * @throws IllegalStateException iff there is arithmetic error. + */ + static void curveMult(long[] resultx, byte[] n, byte[] qBytes) throws InvalidKeyException { + validatePubKeyAndClearMsb(qBytes); + + long[] q = Field25519.expand(qBytes); + long[] nqpqx = new long[19]; + long[] nqpqz = new long[19]; + nqpqz[0] = 1; + long[] nqx = new long[19]; + nqx[0] = 1; + long[] nqz = new long[19]; + long[] nqpqx2 = new long[19]; + long[] nqpqz2 = new long[19]; + nqpqz2[0] = 1; + long[] nqx2 = new long[19]; + long[] nqz2 = new long[19]; + nqz2[0] = 1; + long[] t = new long[19]; + + System.arraycopy(q, 0, nqpqx, 0, Field25519.LIMB_CNT); + + for (int i = 0; i < Field25519.FIELD_LEN; i++) { + int b = n[Field25519.FIELD_LEN - i - 1] & 0xff; + for (int j = 0; j < 8; j++) { + int bit = (b >> (7 - j)) & 1; + + swapConditional(nqx, nqpqx, bit); + swapConditional(nqz, nqpqz, bit); + monty(nqx2, nqz2, nqpqx2, nqpqz2, nqx, nqz, nqpqx, nqpqz, q); + swapConditional(nqx2, nqpqx2, bit); + swapConditional(nqz2, nqpqz2, bit); + + t = nqx; + nqx = nqx2; + nqx2 = t; + t = nqz; + nqz = nqz2; + nqz2 = t; + t = nqpqx; + nqpqx = nqpqx2; + nqpqx2 = t; + t = nqpqz; + nqpqz = nqpqz2; + nqpqz2 = t; + } + } + + // Computes nqx/nqz. + long[] zmone = new long[Field25519.LIMB_CNT]; + Field25519.inverse(zmone, nqz); + Field25519.mult(resultx, nqx, zmone); + + // Nowadays it should be standard to protect public key crypto against flaws. I.e. if there is a + // computation error through a faulty CPU or if the implementation contains a bug, then if + // possible this should be detected at run time. + // + // The situation is a bit more tricky for X25519 where for example the implementation + // proposed in https://tools.ietf.org/html/rfc7748 only uses the x-coordinate. However, a + // verification is still possible, but depends on the actual computation. + // + // Tink's Java implementation is equivalent to RFC7748. We will use the loop invariant in the + // Montgomery ladder to detect fault computation. In particular, we use the following invariant: + // q, resultx, nqpqx/nqpqx are x coordinates of 3 collinear points q, n*q, (n + 1)*q. + if (!isCollinear(q, resultx, nqpqx, nqpqz)) { + throw new IllegalStateException( + "Arithmetic error in curve multiplication with the public key: " + + Hex.encode(qBytes)); + } + } + + /** + * Validates public key and clear its most significant bit. + * + * @throws InvalidKeyException iff the {@code pubKey} is in the banned list or its length is not + * 32-byte. + */ + private static void validatePubKeyAndClearMsb(byte[] pubKey) throws InvalidKeyException { + if (pubKey.length != 32) { + throw new InvalidKeyException("Public key length is not 32-byte"); + } + // Clears the most significant bit as in the method decodeUCoordinate() of RFC7748. + pubKey[31] &= (byte) 0x7f; + + for (int i = 0; i < BANNED_PUBLIC_KEYS.length; i++) { + if (Bytes.equal(BANNED_PUBLIC_KEYS[i], pubKey)) { + throw new InvalidKeyException("Banned public key: " + Hex.encode(BANNED_PUBLIC_KEYS[i])); + } + } + } + + /** + * Checks whether there are three collinear points with x coordinate x1, x2, x3/z3. + * + * @return true if three collinear points with x coordianate x1, x2, x3/z3 are collinear. + */ + private static boolean isCollinear(long[] x1, long[] x2, long[] x3, long[] z3) { + // If x1, x2, x3 (in this method x3 is represented as x3/z3) are the x-coordinates of three + // collinear points on a curve, then they satisfy the equation + // y^2 = x^3 + ax^2 + x + // They also satisfy the equation + // 0 = (x - x1)(x - x2)(x - x3) + // = x^3 + Ax^2 + Bx + C + // where + // A = - x1 - x2 - x3 + // B = x1*x2 + x2*x3 + x3*x1 + // C = - x1*x2*x3 + // Hence, the three points also satisfy + // y^2 = (a - A)x^2 + (1 - B)x - C + // This is a quadratic curve. Three distinct collinear points can only be on a quadratic + // curve if the quadratic curve has a line as component. And if a quadratic curve has a line + // as component then its discriminant is 0. + // Therefore, discriminant((a - A)x^2 + (1-B)x - C) = 0. + // In particular: + // a = 486662 + // lhs = 4 * ((x1 + x2 + a) * z3 + x3) * (x1 * x2 * x3) + // rhs = ((x1 * x2 - 1) * z3 + x3 * (x1 + x2))**2 + // assert (lhs - rhs) == 0 + // + // There are 2 cases that we haven't discussed: + // + // * If x1 and x2 are both points with y-coordinate 0 then the argument doesn't hold. + // However, our ECDH computation doesn't allow points of low order (see {@code + // validatePublicKey}). Therefore, this edge case never happen. + // + // * x1, x2 or x3/y3 may be points on the twist. If so, they satisfy the equation + // 2y^2 = x^3 + ax^2 + x + // Hence, the three points also satisfy + // 2y^2 = (a - A)x^2 + (1 - B)x - C + // Thus, this collinear check should work for this case too. + long[] x1multx2 = new long[Field25519.LIMB_CNT]; + long[] x1addx2 = new long[Field25519.LIMB_CNT]; + long[] lhs = new long[Field25519.LIMB_CNT + 1]; + long[] t = new long[Field25519.LIMB_CNT + 1]; + long[] t2 = new long[Field25519.LIMB_CNT + 1]; + Field25519.mult(x1multx2, x1, x2); + Field25519.sum(x1addx2, x1, x2); + long[] a = new long[Field25519.LIMB_CNT]; + a[0] = 486662; + // t = x1 + x2 + a + Field25519.sum(t, x1addx2, a); + // t = (x1 + x2 + a) * z3 + Field25519.mult(t, t, z3); + // t = (x1 + x2 + a) * z3 + x3 + Field25519.sum(t, x3); + // t = ((x1 + x2 + a) * z3 + x3) * x1 * x2 + Field25519.mult(t, t, x1multx2); + // t = ((x1 + x2 + a) * z3 + x3) * (x1 * x2 * x3) + Field25519.mult(t, t, x3); + Field25519.scalarProduct(lhs, t, 4); + Field25519.reduceCoefficients(lhs); + + // t = x1 * x2 * z3 + Field25519.mult(t, x1multx2, z3); + // t = x1 * x2 * z3 - z3 + Field25519.sub(t, t, z3); + // t2 = (x1 + x2) * x3 + Field25519.mult(t2, x1addx2, x3); + // t = x1 * x2 * z3 - z3 + (x1 + x2) * x3 + Field25519.sum(t, t, t2); + // t = (x1 * x2 * z3 - z3 + (x1 + x2) * x3)^2 + Field25519.square(t, t); + return Bytes.equal(Field25519.contract(lhs), Field25519.contract(t)); + } +} diff --git a/tx-signer/src/main/java/com/google/crypto/tink/subtle/Ed25519.java b/tx-signer/src/main/java/com/google/crypto/tink/subtle/Ed25519.java new file mode 100644 index 0000000..b864ea8 --- /dev/null +++ b/tx-signer/src/main/java/com/google/crypto/tink/subtle/Ed25519.java @@ -0,0 +1,1612 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.crypto.tink.subtle; + +import static com.google.crypto.tink.subtle.Ed25519Constants.B2; +import static com.google.crypto.tink.subtle.Ed25519Constants.B_TABLE; +import static com.google.crypto.tink.subtle.Ed25519Constants.D; +import static com.google.crypto.tink.subtle.Ed25519Constants.D2; +import static com.google.crypto.tink.subtle.Ed25519Constants.SQRTM1; +import static com.google.crypto.tink.subtle.Field25519.FIELD_LEN; +import static com.google.crypto.tink.subtle.Field25519.LIMB_CNT; + +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.util.Arrays; + +/** + * This implementation is based on the ed25519/ref10 implementation in NaCl. + * + *

It implements this twisted Edwards curve: + * + *

+ * -x^2 + y^2 = 1 + (-121665 / 121666 mod 2^255-19)*x^2*y^2
+ * 
+ * + * @see Bernstein D.J., Birkner P., Joye M., Lange + * T., Peters C. (2008) Twisted Edwards Curves + * @see Hisil H., Wong K.KH., Carter G., Dawson E. + * (2008) Twisted Edwards Curves Revisited + */ +public final class Ed25519 { + + public static final int SECRET_KEY_LEN = FIELD_LEN; + public static final int PUBLIC_KEY_LEN = FIELD_LEN; + public static final int SIGNATURE_LEN = FIELD_LEN * 2; + + // (x = 0, y = 1) point + private static final CachedXYT CACHED_NEUTRAL = new CachedXYT( + new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + private static final PartialXYZT NEUTRAL = new PartialXYZT( + new XYZ(new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}), + new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + + /** + * Projective point representation (X:Y:Z) satisfying x = X/Z, y = Y/Z + * + * Note that this is referred as ge_p2 in ref10 impl. + * Also note that x = X, y = Y and z = Z below following Java coding style. + * + * See + * Koyama K., Tsuruoka Y. (1993) Speeding up Elliptic Cryptosystems by Using a Signed Binary + * Window Method. + * + * https://hyperelliptic.org/EFD/g1p/auto-twisted-projective.html + */ + private static class XYZ { + + final long[] x; + final long[] y; + final long[] z; + + XYZ() { + this(new long[LIMB_CNT], new long[LIMB_CNT], new long[LIMB_CNT]); + } + + XYZ(long[] x, long[] y, long[] z) { + this.x = x; + this.y = y; + this.z = z; + } + + XYZ(XYZ xyz) { + x = Arrays.copyOf(xyz.x, LIMB_CNT); + y = Arrays.copyOf(xyz.y, LIMB_CNT); + z = Arrays.copyOf(xyz.z, LIMB_CNT); + } + + XYZ(PartialXYZT partialXYZT) { + this(); + fromPartialXYZT(this, partialXYZT); + } + + /** + * ge_p1p1_to_p2.c + */ + static XYZ fromPartialXYZT(XYZ out, PartialXYZT in) { + Field25519.mult(out.x, in.xyz.x, in.t); + Field25519.mult(out.y, in.xyz.y, in.xyz.z); + Field25519.mult(out.z, in.xyz.z, in.t); + return out; + } + + /** + * Encodes this point to bytes. + */ + byte[] toBytes() { + long[] recip = new long[LIMB_CNT]; + long[] x = new long[LIMB_CNT]; + long[] y = new long[LIMB_CNT]; + Field25519.inverse(recip, z); + Field25519.mult(x, this.x, recip); + Field25519.mult(y, this.y, recip); + byte[] s = Field25519.contract(y); + s[31] = (byte) (s[31] ^ (getLsb(x) << 7)); + return s; + } + + /** Checks that the point is on curve */ + boolean isOnCurve() { + long[] x2 = new long[LIMB_CNT]; + Field25519.square(x2, x); + long[] y2 = new long[LIMB_CNT]; + Field25519.square(y2, y); + long[] z2 = new long[LIMB_CNT]; + Field25519.square(z2, z); + long[] z4 = new long[LIMB_CNT]; + Field25519.square(z4, z2); + long[] lhs = new long[LIMB_CNT]; + // lhs = y^2 - x^2 + Field25519.sub(lhs, y2, x2); + // lhs = z^2 * (y2 - x2) + Field25519.mult(lhs, lhs, z2); + long[] rhs = new long[LIMB_CNT]; + // rhs = x^2 * y^2 + Field25519.mult(rhs, x2, y2); + // rhs = D * x^2 * y^2 + Field25519.mult(rhs, rhs, D); + // rhs = z^4 + D * x^2 * y^2 + Field25519.sum(rhs, z4); + // z^2 (y^2 - x^2) == z^4 + D * x^2 * y^2 + return Bytes.equal(Field25519.contract(lhs), Field25519.contract(rhs)); + } + } + + /** + * Represents extended projective point representation (X:Y:Z:T) satisfying x = X/Z, y = Y/Z, + * XY = ZT + * + * Note that this is referred as ge_p3 in ref10 impl. + * Also note that t = T below following Java coding style. + * + * See + * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited. + * + * https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html + */ + private static class XYZT { + + final XYZ xyz; + final long[] t; + + XYZT() { + this(new XYZ(), new long[LIMB_CNT]); + } + + XYZT(XYZ xyz, long[] t) { + this.xyz = xyz; + this.t = t; + } + + XYZT(PartialXYZT partialXYZT) { + this(); + fromPartialXYZT(this, partialXYZT); + } + + /** + * ge_p1p1_to_p2.c + */ + private static XYZT fromPartialXYZT(XYZT out, PartialXYZT in) { + Field25519.mult(out.xyz.x, in.xyz.x, in.t); + Field25519.mult(out.xyz.y, in.xyz.y, in.xyz.z); + Field25519.mult(out.xyz.z, in.xyz.z, in.t); + Field25519.mult(out.t, in.xyz.x, in.xyz.y); + return out; + } + + /** + * Decodes {@code s} into an extented projective point. + * See Section 5.1.3 Decoding in https://tools.ietf.org/html/rfc8032#section-5.1.3 + */ + private static XYZT fromBytesNegateVarTime(byte[] s) throws GeneralSecurityException { + long[] x = new long[LIMB_CNT]; + long[] y = Field25519.expand(s); + long[] z = new long[LIMB_CNT]; z[0] = 1; + long[] t = new long[LIMB_CNT]; + long[] u = new long[LIMB_CNT]; + long[] v = new long[LIMB_CNT]; + long[] vxx = new long[LIMB_CNT]; + long[] check = new long[LIMB_CNT]; + Field25519.square(u, y); + Field25519.mult(v, u, D); + Field25519.sub(u, u, z); // u = y^2 - 1 + Field25519.sum(v, v, z); // v = dy^2 + 1 + + long[] v3 = new long[LIMB_CNT]; + Field25519.square(v3, v); + Field25519.mult(v3, v3, v); // v3 = v^3 + Field25519.square(x, v3); + Field25519.mult(x, x, v); + Field25519.mult(x, x, u); // x = uv^7 + + pow2252m3(x, x); // x = (uv^7)^((q-5)/8) + Field25519.mult(x, x, v3); + Field25519.mult(x, x, u); // x = uv^3(uv^7)^((q-5)/8) + + Field25519.square(vxx, x); + Field25519.mult(vxx, vxx, v); + Field25519.sub(check, vxx, u); // vx^2-u + if (isNonZeroVarTime(check)) { + Field25519.sum(check, vxx, u); // vx^2+u + if (isNonZeroVarTime(check)) { + throw new GeneralSecurityException("Cannot convert given bytes to extended projective " + + "coordinates. No square root exists for modulo 2^255-19"); + } + Field25519.mult(x, x, SQRTM1); + } + + if (!isNonZeroVarTime(x) && (s[31] & 0xff) >> 7 != 0) { + throw new GeneralSecurityException("Cannot convert given bytes to extended projective " + + "coordinates. Computed x is zero and encoded x's least significant bit is not zero"); + } + if (getLsb(x) == ((s[31] & 0xff) >> 7)) { + neg(x, x); + } + + Field25519.mult(t, x, y); + return new XYZT(new XYZ(x, y, z), t); + } + } + + /** + * Partial projective point representation ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T + * + * Note that this is referred as complete form in the original ref10 impl (ge_p1p1). + * Also note that t = T below following Java coding style. + * + * Although this has the same types as XYZT, it is redefined to have its own type so that it is + * readable and 1:1 corresponds to ref10 impl. + * + * Can be converted to XYZT as follows: + * X1 = X * T = x * Z * T = x * Z1 + * Y1 = Y * Z = y * T * Z = y * Z1 + * Z1 = Z * T = Z * T + * T1 = X * Y = x * Z * y * T = x * y * Z1 = X1Y1 / Z1 + */ + private static class PartialXYZT { + + final XYZ xyz; + final long[] t; + + PartialXYZT() { + this(new XYZ(), new long[LIMB_CNT]); + } + + PartialXYZT(XYZ xyz, long[] t) { + this.xyz = xyz; + this.t = t; + } + + PartialXYZT(PartialXYZT other) { + xyz = new XYZ(other.xyz); + t = Arrays.copyOf(other.t, LIMB_CNT); + } + } + + /** + * Corresponds to the caching mentioned in the last paragraph of Section 3.1 of + * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited. + * with Z = 1. + */ + static class CachedXYT { + + final long[] yPlusX; + final long[] yMinusX; + final long[] t2d; + + CachedXYT() { + this(new long[LIMB_CNT], new long[LIMB_CNT], new long[LIMB_CNT]); + } + + /** + * Creates a cached XYZT with Z = 1 + * + * @param yPlusX y + x + * @param yMinusX y - x + * @param t2d 2d * xy + */ + CachedXYT(long[] yPlusX, long[] yMinusX, long[] t2d) { + this.yPlusX = yPlusX; + this.yMinusX = yMinusX; + this.t2d = t2d; + } + + CachedXYT(CachedXYT other) { + yPlusX = Arrays.copyOf(other.yPlusX, LIMB_CNT); + yMinusX = Arrays.copyOf(other.yMinusX, LIMB_CNT); + t2d = Arrays.copyOf(other.t2d, LIMB_CNT); + } + + // z is one implicitly, so this just copies {@code in} to {@code output}. + void multByZ(long[] output, long[] in) { + System.arraycopy(in, 0, output, 0, LIMB_CNT); + } + + /** + * If icopy is 1, copies {@code other} into this point. Time invariant wrt to icopy value. + */ + void copyConditional(CachedXYT other, int icopy) { + Curve25519.copyConditional(yPlusX, other.yPlusX, icopy); + Curve25519.copyConditional(yMinusX, other.yMinusX, icopy); + Curve25519.copyConditional(t2d, other.t2d, icopy); + } + } + + private static class CachedXYZT extends CachedXYT { + + private final long[] z; + + CachedXYZT() { + this(new long[LIMB_CNT], new long[LIMB_CNT], new long[LIMB_CNT], new long[LIMB_CNT]); + } + + /** + * ge_p3_to_cached.c + */ + CachedXYZT(XYZT xyzt) { + this(); + Field25519.sum(yPlusX, xyzt.xyz.y, xyzt.xyz.x); + Field25519.sub(yMinusX, xyzt.xyz.y, xyzt.xyz.x); + System.arraycopy(xyzt.xyz.z, 0, z, 0, LIMB_CNT); + Field25519.mult(t2d, xyzt.t, D2); + } + + /** + * Creates a cached XYZT + * + * @param yPlusX Y + X + * @param yMinusX Y - X + * @param z Z + * @param t2d 2d * (XY/Z) + */ + CachedXYZT(long[] yPlusX, long[] yMinusX, long[] z, long[] t2d) { + super(yPlusX, yMinusX, t2d); + this.z = z; + } + + @Override + public void multByZ(long[] output, long[] in) { + Field25519.mult(output, in, z); + } + } + + /** + * Addition defined in Section 3.1 of + * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited. + * + * Please note that this is a partial of the operation listed there leaving out the final + * conversion from PartialXYZT to XYZT. + * + * @param extended extended projective point input + * @param cached cached projective point input + */ + private static void add(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) { + long[] t = new long[LIMB_CNT]; + + // Y1 + X1 + Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x); + + // Y1 - X1 + Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x); + + // A = (Y1 - X1) * (Y2 - X2) + Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yMinusX); + + // B = (Y1 + X1) * (Y2 + X2) + Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yPlusX); + + // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper) + Field25519.mult(partialXYZT.t, extended.t, cached.t2d); + + // Z1 * Z2 + cached.multByZ(partialXYZT.xyz.x, extended.xyz.z); + + // D = 2 * Z1 * Z2 + Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x); + + // X3 = B - A + Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y); + + // Y3 = B + A + Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y); + + // Z3 = D + C + Field25519.sum(partialXYZT.xyz.z, t, partialXYZT.t); + + // T3 = D - C + Field25519.sub(partialXYZT.t, t, partialXYZT.t); + } + + /** + * Based on the addition defined in Section 3.1 of + * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited. + * + * Please note that this is a partial of the operation listed there leaving out the final + * conversion from PartialXYZT to XYZT. + * + * @param extended extended projective point input + * @param cached cached projective point input + */ + private static void sub(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) { + long[] t = new long[LIMB_CNT]; + + // Y1 + X1 + Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x); + + // Y1 - X1 + Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x); + + // A = (Y1 - X1) * (Y2 + X2) + Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yPlusX); + + // B = (Y1 + X1) * (Y2 - X2) + Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yMinusX); + + // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper) + Field25519.mult(partialXYZT.t, extended.t, cached.t2d); + + // Z1 * Z2 + cached.multByZ(partialXYZT.xyz.x, extended.xyz.z); + + // D = 2 * Z1 * Z2 + Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x); + + // X3 = B - A + Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y); + + // Y3 = B + A + Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y); + + // Z3 = D - C + Field25519.sub(partialXYZT.xyz.z, t, partialXYZT.t); + + // T3 = D + C + Field25519.sum(partialXYZT.t, t, partialXYZT.t); + } + + /** + * Doubles {@code p} and puts the result into this PartialXYZT. + * + * This is based on the addition defined in formula 7 in Section 3.3 of + * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited. + * + * Please note that this is a partial of the operation listed there leaving out the final + * conversion from PartialXYZT to XYZT and also this fixes a typo in calculation of Y3 and T3 in + * the paper, H should be replaced with A+B. + */ + private static void doubleXYZ(PartialXYZT partialXYZT, XYZ p) { + long[] t0 = new long[LIMB_CNT]; + + // XX = X1^2 + Field25519.square(partialXYZT.xyz.x, p.x); + + // YY = Y1^2 + Field25519.square(partialXYZT.xyz.z, p.y); + + // B' = Z1^2 + Field25519.square(partialXYZT.t, p.z); + + // B = 2 * B' + Field25519.sum(partialXYZT.t, partialXYZT.t, partialXYZT.t); + + // A = X1 + Y1 + Field25519.sum(partialXYZT.xyz.y, p.x, p.y); + + // AA = A^2 + Field25519.square(t0, partialXYZT.xyz.y); + + // Y3 = YY + XX + Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.x); + + // Z3 = YY - XX + Field25519.sub(partialXYZT.xyz.z, partialXYZT.xyz.z, partialXYZT.xyz.x); + + // X3 = AA - Y3 + Field25519.sub(partialXYZT.xyz.x, t0, partialXYZT.xyz.y); + + // T3 = B - Z3 + Field25519.sub(partialXYZT.t, partialXYZT.t, partialXYZT.xyz.z); + } + + /** + * Doubles {@code p} and puts the result into this PartialXYZT. + */ + private static void doubleXYZT(PartialXYZT partialXYZT, XYZT p) { + doubleXYZ(partialXYZT, p.xyz); + } + + /** + * Compares two byte values in constant time. + * + * Please note that this doesn't reuse {@link Curve25519#eq} method since the below inputs are + * byte values. + */ + private static int eq(int a, int b) { + int r = ~(a ^ b) & 0xff; + r &= r << 4; + r &= r << 2; + r &= r << 1; + return (r >> 7) & 1; + } + + /** + * This is a constant time operation where point b*B*256^pos is stored in {@code t}. + * When b is 0, t remains the same (i.e., neutral point). + * + * Although B_TABLE[32][8] (B_TABLE[i][j] = (j+1)*B*256^i) has j values in [0, 7], the select + * method negates the corresponding point if b is negative (which is straight forward in elliptic + * curves by just negating y coordinate). Therefore we can get multiples of B with the half of + * memory requirements. + * + * @param t neutral element (i.e., point 0), also serves as output. + * @param pos in B[pos][j] = (j+1)*B*256^pos + * @param b value in [-8, 8] range. + */ + private static void select(CachedXYT t, int pos, byte b) { + int bnegative = (b & 0xff) >> 7; + int babs = b - (((-bnegative) & b) << 1); + + t.copyConditional(B_TABLE[pos][0], eq(babs, 1)); + t.copyConditional(B_TABLE[pos][1], eq(babs, 2)); + t.copyConditional(B_TABLE[pos][2], eq(babs, 3)); + t.copyConditional(B_TABLE[pos][3], eq(babs, 4)); + t.copyConditional(B_TABLE[pos][4], eq(babs, 5)); + t.copyConditional(B_TABLE[pos][5], eq(babs, 6)); + t.copyConditional(B_TABLE[pos][6], eq(babs, 7)); + t.copyConditional(B_TABLE[pos][7], eq(babs, 8)); + + long[] yPlusX = Arrays.copyOf(t.yMinusX, LIMB_CNT); + long[] yMinusX = Arrays.copyOf(t.yPlusX, LIMB_CNT); + long[] t2d = Arrays.copyOf(t.t2d, LIMB_CNT); + neg(t2d, t2d); + CachedXYT minust = new CachedXYT(yPlusX, yMinusX, t2d); + t.copyConditional(minust, bnegative); + } + + /** + * Computes {@code a}*B + * where a = a[0]+256*a[1]+...+256^31 a[31] and + * B is the Ed25519 base point (x,4/5) with x positive. + * + * Preconditions: + * a[31] <= 127 + * @throws IllegalStateException iff there is arithmetic error. + */ + @SuppressWarnings("NarrowingCompoundAssignment") + private static XYZ scalarMultWithBase(byte[] a) { + byte[] e = new byte[2 * FIELD_LEN]; + for (int i = 0; i < FIELD_LEN; i++) { + e[2 * i + 0] = (byte) (((a[i] & 0xff) >> 0) & 0xf); + e[2 * i + 1] = (byte) (((a[i] & 0xff) >> 4) & 0xf); + } + // each e[i] is between 0 and 15 + // e[63] is between 0 and 7 + + // Rewrite e in a way that each e[i] is in [-8, 8]. + // This can be done since a[63] is in [0, 7], the carry-over onto the most significant byte + // a[63] can be at most 1. + int carry = 0; + for (int i = 0; i < e.length - 1; i++) { + e[i] += carry; + carry = e[i] + 8; + carry >>= 4; + e[i] -= carry << 4; + } + e[e.length - 1] += carry; + + PartialXYZT ret = new PartialXYZT(NEUTRAL); + XYZT xyzt = new XYZT(); + // Although B_TABLE's i can be at most 31 (stores only 32 4bit multiples of B) and we have 64 + // 4bit values in e array, the below for loop adds cached values by iterating e by two in odd + // indices. After the result, we can double the result point 4 times to shift the multiplication + // scalar by 4 bits. + for (int i = 1; i < e.length; i += 2) { + CachedXYT t = new CachedXYT(CACHED_NEUTRAL); + select(t, i / 2, e[i]); + add(ret, XYZT.fromPartialXYZT(xyzt, ret), t); + } + + // Doubles the result 4 times to shift the multiplication scalar 4 bits to get the actual result + // for the odd indices in e. + XYZ xyz = new XYZ(); + doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret)); + doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret)); + doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret)); + doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret)); + + // Add multiples of B for even indices of e. + for (int i = 0; i < e.length; i += 2) { + CachedXYT t = new CachedXYT(CACHED_NEUTRAL); + select(t, i / 2, e[i]); + add(ret, XYZT.fromPartialXYZT(xyzt, ret), t); + } + + // This check is to protect against flaws, i.e. if there is a computation error through a + // faulty CPU or if the implementation contains a bug. + XYZ result = new XYZ(ret); + if (!result.isOnCurve()) { + throw new IllegalStateException("arithmetic error in scalar multiplication"); + } + return result; + } + + /** + * Computes {@code a}*B + * where a = a[0]+256*a[1]+...+256^31 a[31] and + * B is the Ed25519 base point (x,4/5) with x positive. + * + * Preconditions: + * a[31] <= 127 + */ + public static byte[] scalarMultWithBaseToBytes(byte[] a) { + return scalarMultWithBase(a).toBytes(); + } + + @SuppressWarnings("NarrowingCompoundAssignment") + private static byte[] slide(byte[] a) { + byte[] r = new byte[256]; + // Writes each bit in a[0..31] into r[0..255]: + // a = a[0]+256*a[1]+...+256^31*a[31] is equal to + // r = r[0]+2*r[1]+...+2^255*r[255] + for (int i = 0; i < 256; i++) { + r[i] = (byte) (1 & ((a[i >> 3] & 0xff) >> (i & 7))); + } + + // Transforms r[i] as odd values in [-15, 15] + for (int i = 0; i < 256; i++) { + if (r[i] != 0) { + for (int b = 1; b <= 6 && i + b < 256; b++) { + if (r[i + b] != 0) { + if (r[i] + (r[i + b] << b) <= 15) { + r[i] += r[i + b] << b; + r[i + b] = 0; + } else if (r[i] - (r[i + b] << b) >= -15) { + r[i] -= r[i + b] << b; + for (int k = i + b; k < 256; k++) { + if (r[k] == 0) { + r[k] = 1; + break; + } + r[k] = 0; + } + } else { + break; + } + } + } + } + } + return r; + } + + /** + * Computes {@code a}*{@code pointA}+{@code b}*B + * where a = a[0]+256*a[1]+...+256^31*a[31]. + * and b = b[0]+256*b[1]+...+256^31*b[31]. + * B is the Ed25519 base point (x,4/5) with x positive. + * + * Note that execution time varies based on the input since this will only be used in verification + * of signatures. + */ + private static XYZ doubleScalarMultVarTime(byte[] a, XYZT pointA, byte[] b) { + // pointA, 3*pointA, 5*pointA, 7*pointA, 9*pointA, 11*pointA, 13*pointA, 15*pointA + CachedXYZT[] pointAArray = new CachedXYZT[8]; + pointAArray[0] = new CachedXYZT(pointA); + PartialXYZT t = new PartialXYZT(); + doubleXYZT(t, pointA); + XYZT doubleA = new XYZT(t); + for (int i = 1; i < pointAArray.length; i++) { + add(t, doubleA, pointAArray[i - 1]); + pointAArray[i] = new CachedXYZT(new XYZT(t)); + } + + byte[] aSlide = slide(a); + byte[] bSlide = slide(b); + t = new PartialXYZT(NEUTRAL); + XYZT u = new XYZT(); + int i = 255; + for (; i >= 0; i--) { + if (aSlide[i] != 0 || bSlide[i] != 0) { + break; + } + } + for (; i >= 0; i--) { + doubleXYZ(t, new XYZ(t)); + if (aSlide[i] > 0) { + add(t, XYZT.fromPartialXYZT(u, t), pointAArray[aSlide[i] / 2]); + } else if (aSlide[i] < 0) { + sub(t, XYZT.fromPartialXYZT(u, t), pointAArray[-aSlide[i] / 2]); + } + if (bSlide[i] > 0) { + add(t, XYZT.fromPartialXYZT(u, t), B2[bSlide[i] / 2]); + } else if (bSlide[i] < 0) { + sub(t, XYZT.fromPartialXYZT(u, t), B2[-bSlide[i] / 2]); + } + } + + return new XYZ(t); + } + + /** + * Returns true if {@code in} is nonzero. + * + * Note that execution time might depend on the input {@code in}. + */ + private static boolean isNonZeroVarTime(long[] in) { + long[] inCopy = new long[in.length + 1]; + System.arraycopy(in, 0, inCopy, 0, in.length); + Field25519.reduceCoefficients(inCopy); + byte[] bytes = Field25519.contract(inCopy); + for (byte b : bytes) { + if (b != 0) { + return true; + } + } + return false; + } + + /** + * Returns the least significant bit of {@code in}. + */ + private static int getLsb(long[] in) { + return Field25519.contract(in)[0] & 1; + } + + /** + * Negates all values in {@code in} and store it in {@code out}. + */ + private static void neg(long[] out, long[] in) { + for (int i = 0; i < in.length; i++) { + out[i] = -in[i]; + } + } + + /** + * Computes {@code in}^(2^252-3) mod 2^255-19 and puts the result in {@code out}. + */ + private static void pow2252m3(long[] out, long[] in) { + long[] t0 = new long[LIMB_CNT]; + long[] t1 = new long[LIMB_CNT]; + long[] t2 = new long[LIMB_CNT]; + + // z2 = z1^2^1 + Field25519.square(t0, in); + + // z8 = z2^2^2 + Field25519.square(t1, t0); + for (int i = 1; i < 2; i++) { + Field25519.square(t1, t1); + } + + // z9 = z1*z8 + Field25519.mult(t1, in, t1); + + // z11 = z2*z9 + Field25519.mult(t0, t0, t1); + + // z22 = z11^2^1 + Field25519.square(t0, t0); + + // z_5_0 = z9*z22 + Field25519.mult(t0, t1, t0); + + // z_10_5 = z_5_0^2^5 + Field25519.square(t1, t0); + for (int i = 1; i < 5; i++) { + Field25519.square(t1, t1); + } + + // z_10_0 = z_10_5*z_5_0 + Field25519.mult(t0, t1, t0); + + // z_20_10 = z_10_0^2^10 + Field25519.square(t1, t0); + for (int i = 1; i < 10; i++) { + Field25519.square(t1, t1); + } + + // z_20_0 = z_20_10*z_10_0 + Field25519.mult(t1, t1, t0); + + // z_40_20 = z_20_0^2^20 + Field25519.square(t2, t1); + for (int i = 1; i < 20; i++) { + Field25519.square(t2, t2); + } + + // z_40_0 = z_40_20*z_20_0 + Field25519.mult(t1, t2, t1); + + // z_50_10 = z_40_0^2^10 + Field25519.square(t1, t1); + for (int i = 1; i < 10; i++) { + Field25519.square(t1, t1); + } + + // z_50_0 = z_50_10*z_10_0 + Field25519.mult(t0, t1, t0); + + // z_100_50 = z_50_0^2^50 + Field25519.square(t1, t0); + for (int i = 1; i < 50; i++) { + Field25519.square(t1, t1); + } + + // z_100_0 = z_100_50*z_50_0 + Field25519.mult(t1, t1, t0); + + // z_200_100 = z_100_0^2^100 + Field25519.square(t2, t1); + for (int i = 1; i < 100; i++) { + Field25519.square(t2, t2); + } + + // z_200_0 = z_200_100*z_100_0 + Field25519.mult(t1, t2, t1); + + // z_250_50 = z_200_0^2^50 + Field25519.square(t1, t1); + for (int i = 1; i < 50; i++) { + Field25519.square(t1, t1); + } + + // z_250_0 = z_250_50*z_50_0 + Field25519.mult(t0, t1, t0); + + // z_252_2 = z_250_0^2^2 + Field25519.square(t0, t0); + for (int i = 1; i < 2; i++) { + Field25519.square(t0, t0); + } + + // z_252_3 = z_252_2*z1 + Field25519.mult(out, t0, in); + } + + /** + * Returns 3 bytes of {@code in} starting from {@code idx} in Little-Endian format. + */ + private static long load3(byte[] in, int idx) { + long result; + result = (long) in[idx] & 0xff; + result |= (long) (in[idx + 1] & 0xff) << 8; + result |= (long) (in[idx + 2] & 0xff) << 16; + return result; + } + + /** + * Returns 4 bytes of {@code in} starting from {@code idx} in Little-Endian format. + */ + private static long load4(byte[] in, int idx) { + long result = load3(in, idx); + result |= (long) (in[idx + 3] & 0xff) << 24; + return result; + } + + /** + * Input: + * s[0]+256*s[1]+...+256^63*s[63] = s + * + * Output: + * s[0]+256*s[1]+...+256^31*s[31] = s mod l + * where l = 2^252 + 27742317777372353535851937790883648493. + * Overwrites s in place. + */ + public static void reduce(byte[] s) { + // Observation: + // 2^252 mod l is equivalent to -27742317777372353535851937790883648493 mod l + // Let m = -27742317777372353535851937790883648493 + // Thus a*2^252+b mod l is equivalent to a*m+b mod l + // + // First s is divided into chunks of 21 bits as follows: + // s0+2^21*s1+2^42*s3+...+2^462*s23 = s[0]+256*s[1]+...+256^63*s[63] + long s0 = 2097151 & load3(s, 0); + long s1 = 2097151 & (load4(s, 2) >> 5); + long s2 = 2097151 & (load3(s, 5) >> 2); + long s3 = 2097151 & (load4(s, 7) >> 7); + long s4 = 2097151 & (load4(s, 10) >> 4); + long s5 = 2097151 & (load3(s, 13) >> 1); + long s6 = 2097151 & (load4(s, 15) >> 6); + long s7 = 2097151 & (load3(s, 18) >> 3); + long s8 = 2097151 & load3(s, 21); + long s9 = 2097151 & (load4(s, 23) >> 5); + long s10 = 2097151 & (load3(s, 26) >> 2); + long s11 = 2097151 & (load4(s, 28) >> 7); + long s12 = 2097151 & (load4(s, 31) >> 4); + long s13 = 2097151 & (load3(s, 34) >> 1); + long s14 = 2097151 & (load4(s, 36) >> 6); + long s15 = 2097151 & (load3(s, 39) >> 3); + long s16 = 2097151 & load3(s, 42); + long s17 = 2097151 & (load4(s, 44) >> 5); + long s18 = 2097151 & (load3(s, 47) >> 2); + long s19 = 2097151 & (load4(s, 49) >> 7); + long s20 = 2097151 & (load4(s, 52) >> 4); + long s21 = 2097151 & (load3(s, 55) >> 1); + long s22 = 2097151 & (load4(s, 57) >> 6); + long s23 = (load4(s, 60) >> 3); + long carry0; + long carry1; + long carry2; + long carry3; + long carry4; + long carry5; + long carry6; + long carry7; + long carry8; + long carry9; + long carry10; + long carry11; + long carry12; + long carry13; + long carry14; + long carry15; + long carry16; + + // s23*2^462 = s23*2^210*2^252 is equivalent to s23*2^210*m in mod l + // As m is a 125 bit number, the result needs to scattered to 6 limbs (125/21 ceil is 6) + // starting from s11 (s11*2^210) + // m = [666643, 470296, 654183, -997805, 136657, -683901] in 21-bit limbs + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + // s23 = 0; + + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + // s22 = 0; + + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + // s21 = 0; + + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + // s20 = 0; + + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + // s19 = 0; + + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + // s18 = 0; + + // Reduce the bit length of limbs from s6 to s15 to 21-bits. + carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; s13 += carry12; s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; s15 += carry14; s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; s17 += carry16; s16 -= carry16 << 21; + + carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; s14 += carry13; s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; s16 += carry15; s15 -= carry15 << 21; + + // Resume reduction where we left off. + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + // s17 = 0; + + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + // s16 = 0; + + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + // s15 = 0; + + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + // s14 = 0; + + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + // s13 = 0; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + // Reduce the range of limbs from s0 to s11 to 21-bits. + carry0 = (s0 + (1 << 20)) >> 21; s1 += carry0; s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; s3 += carry2; s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; s5 += carry4; s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21; + + carry1 = (s1 + (1 << 20)) >> 21; s2 += carry1; s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; s4 += carry3; s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; s6 += carry5; s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + // Carry chain reduction to propagate excess bits from s0 to s5 to the most significant limbs. + carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21; + carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21; + carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21; + carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21; + carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21; + carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21; + carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21; + carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21; + carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21; + carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21; + carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21; + carry11 = s11 >> 21; s12 += carry11; s11 -= carry11 << 21; + + // Do one last reduction as s12 might be 1. + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + // s12 = 0; + + carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21; + carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21; + carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21; + carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21; + carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21; + carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21; + carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21; + carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21; + carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21; + carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21; + carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21; + + // Serialize the result into the s. + s[0] = (byte) s0; + s[1] = (byte) (s0 >> 8); + s[2] = (byte) ((s0 >> 16) | (s1 << 5)); + s[3] = (byte) (s1 >> 3); + s[4] = (byte) (s1 >> 11); + s[5] = (byte) ((s1 >> 19) | (s2 << 2)); + s[6] = (byte) (s2 >> 6); + s[7] = (byte) ((s2 >> 14) | (s3 << 7)); + s[8] = (byte) (s3 >> 1); + s[9] = (byte) (s3 >> 9); + s[10] = (byte) ((s3 >> 17) | (s4 << 4)); + s[11] = (byte) (s4 >> 4); + s[12] = (byte) (s4 >> 12); + s[13] = (byte) ((s4 >> 20) | (s5 << 1)); + s[14] = (byte) (s5 >> 7); + s[15] = (byte) ((s5 >> 15) | (s6 << 6)); + s[16] = (byte) (s6 >> 2); + s[17] = (byte) (s6 >> 10); + s[18] = (byte) ((s6 >> 18) | (s7 << 3)); + s[19] = (byte) (s7 >> 5); + s[20] = (byte) (s7 >> 13); + s[21] = (byte) s8; + s[22] = (byte) (s8 >> 8); + s[23] = (byte) ((s8 >> 16) | (s9 << 5)); + s[24] = (byte) (s9 >> 3); + s[25] = (byte) (s9 >> 11); + s[26] = (byte) ((s9 >> 19) | (s10 << 2)); + s[27] = (byte) (s10 >> 6); + s[28] = (byte) ((s10 >> 14) | (s11 << 7)); + s[29] = (byte) (s11 >> 1); + s[30] = (byte) (s11 >> 9); + s[31] = (byte) (s11 >> 17); + } + + /** + * Input: + * a[0]+256*a[1]+...+256^31*a[31] = a + * b[0]+256*b[1]+...+256^31*b[31] = b + * c[0]+256*c[1]+...+256^31*c[31] = c + * + * Output: + * s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l + * where l = 2^252 + 27742317777372353535851937790883648493. + */ + public static void mulAdd(byte[] s, byte[] a, byte[] b, byte[] c) { + // This is very similar to Ed25519.reduce, the difference in here is that it computes ab+c + // See Ed25519.reduce for related comments. + long a0 = 2097151 & load3(a, 0); + long a1 = 2097151 & (load4(a, 2) >> 5); + long a2 = 2097151 & (load3(a, 5) >> 2); + long a3 = 2097151 & (load4(a, 7) >> 7); + long a4 = 2097151 & (load4(a, 10) >> 4); + long a5 = 2097151 & (load3(a, 13) >> 1); + long a6 = 2097151 & (load4(a, 15) >> 6); + long a7 = 2097151 & (load3(a, 18) >> 3); + long a8 = 2097151 & load3(a, 21); + long a9 = 2097151 & (load4(a, 23) >> 5); + long a10 = 2097151 & (load3(a, 26) >> 2); + long a11 = (load4(a, 28) >> 7); + long b0 = 2097151 & load3(b, 0); + long b1 = 2097151 & (load4(b, 2) >> 5); + long b2 = 2097151 & (load3(b, 5) >> 2); + long b3 = 2097151 & (load4(b, 7) >> 7); + long b4 = 2097151 & (load4(b, 10) >> 4); + long b5 = 2097151 & (load3(b, 13) >> 1); + long b6 = 2097151 & (load4(b, 15) >> 6); + long b7 = 2097151 & (load3(b, 18) >> 3); + long b8 = 2097151 & load3(b, 21); + long b9 = 2097151 & (load4(b, 23) >> 5); + long b10 = 2097151 & (load3(b, 26) >> 2); + long b11 = (load4(b, 28) >> 7); + long c0 = 2097151 & load3(c, 0); + long c1 = 2097151 & (load4(c, 2) >> 5); + long c2 = 2097151 & (load3(c, 5) >> 2); + long c3 = 2097151 & (load4(c, 7) >> 7); + long c4 = 2097151 & (load4(c, 10) >> 4); + long c5 = 2097151 & (load3(c, 13) >> 1); + long c6 = 2097151 & (load4(c, 15) >> 6); + long c7 = 2097151 & (load3(c, 18) >> 3); + long c8 = 2097151 & load3(c, 21); + long c9 = 2097151 & (load4(c, 23) >> 5); + long c10 = 2097151 & (load3(c, 26) >> 2); + long c11 = (load4(c, 28) >> 7); + long s0; + long s1; + long s2; + long s3; + long s4; + long s5; + long s6; + long s7; + long s8; + long s9; + long s10; + long s11; + long s12; + long s13; + long s14; + long s15; + long s16; + long s17; + long s18; + long s19; + long s20; + long s21; + long s22; + long s23; + long carry0; + long carry1; + long carry2; + long carry3; + long carry4; + long carry5; + long carry6; + long carry7; + long carry8; + long carry9; + long carry10; + long carry11; + long carry12; + long carry13; + long carry14; + long carry15; + long carry16; + long carry17; + long carry18; + long carry19; + long carry20; + long carry21; + long carry22; + + s0 = c0 + a0 * b0; + s1 = c1 + a0 * b1 + a1 * b0; + s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0; + s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0; + s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0; + s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0; + s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0; + s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0; + s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1 + + a8 * b0; + s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2 + + a8 * b1 + a9 * b0; + s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3 + + a8 * b2 + a9 * b1 + a10 * b0; + s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4 + + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0; + s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3 + + a10 * b2 + a11 * b1; + s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3 + + a11 * b2; + s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4 + + a11 * b3; + s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4; + s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5; + s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6; + s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7; + s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8; + s20 = a9 * b11 + a10 * b10 + a11 * b9; + s21 = a10 * b11 + a11 * b10; + s22 = a11 * b11; + s23 = 0; + + carry0 = (s0 + (1 << 20)) >> 21; s1 += carry0; s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; s3 += carry2; s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; s5 += carry4; s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; s13 += carry12; s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; s15 += carry14; s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; s17 += carry16; s16 -= carry16 << 21; + carry18 = (s18 + (1 << 20)) >> 21; s19 += carry18; s18 -= carry18 << 21; + carry20 = (s20 + (1 << 20)) >> 21; s21 += carry20; s20 -= carry20 << 21; + carry22 = (s22 + (1 << 20)) >> 21; s23 += carry22; s22 -= carry22 << 21; + + carry1 = (s1 + (1 << 20)) >> 21; s2 += carry1; s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; s4 += carry3; s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; s6 += carry5; s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; s14 += carry13; s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; s16 += carry15; s15 -= carry15 << 21; + carry17 = (s17 + (1 << 20)) >> 21; s18 += carry17; s17 -= carry17 << 21; + carry19 = (s19 + (1 << 20)) >> 21; s20 += carry19; s19 -= carry19 << 21; + carry21 = (s21 + (1 << 20)) >> 21; s22 += carry21; s21 -= carry21 << 21; + + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + // s23 = 0; + + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + // s22 = 0; + + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + // s21 = 0; + + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + // s20 = 0; + + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + // s19 = 0; + + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + // s18 = 0; + + carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; s13 += carry12; s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; s15 += carry14; s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; s17 += carry16; s16 -= carry16 << 21; + + carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; s14 += carry13; s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; s16 += carry15; s15 -= carry15 << 21; + + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + // s17 = 0; + + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + // s16 = 0; + + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + // s15 = 0; + + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + // s14 = 0; + + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + // s13 = 0; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = (s0 + (1 << 20)) >> 21; s1 += carry0; s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; s3 += carry2; s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; s5 += carry4; s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21; + + carry1 = (s1 + (1 << 20)) >> 21; s2 += carry1; s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; s4 += carry3; s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; s6 += carry5; s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21; + carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21; + carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21; + carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21; + carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21; + carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21; + carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21; + carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21; + carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21; + carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21; + carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21; + carry11 = s11 >> 21; s12 += carry11; s11 -= carry11 << 21; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + // s12 = 0; + + carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21; + carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21; + carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21; + carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21; + carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21; + carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21; + carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21; + carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21; + carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21; + carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21; + carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21; + + s[0] = (byte) s0; + s[1] = (byte) (s0 >> 8); + s[2] = (byte) ((s0 >> 16) | (s1 << 5)); + s[3] = (byte) (s1 >> 3); + s[4] = (byte) (s1 >> 11); + s[5] = (byte) ((s1 >> 19) | (s2 << 2)); + s[6] = (byte) (s2 >> 6); + s[7] = (byte) ((s2 >> 14) | (s3 << 7)); + s[8] = (byte) (s3 >> 1); + s[9] = (byte) (s3 >> 9); + s[10] = (byte) ((s3 >> 17) | (s4 << 4)); + s[11] = (byte) (s4 >> 4); + s[12] = (byte) (s4 >> 12); + s[13] = (byte) ((s4 >> 20) | (s5 << 1)); + s[14] = (byte) (s5 >> 7); + s[15] = (byte) ((s5 >> 15) | (s6 << 6)); + s[16] = (byte) (s6 >> 2); + s[17] = (byte) (s6 >> 10); + s[18] = (byte) ((s6 >> 18) | (s7 << 3)); + s[19] = (byte) (s7 >> 5); + s[20] = (byte) (s7 >> 13); + s[21] = (byte) s8; + s[22] = (byte) (s8 >> 8); + s[23] = (byte) ((s8 >> 16) | (s9 << 5)); + s[24] = (byte) (s9 >> 3); + s[25] = (byte) (s9 >> 11); + s[26] = (byte) ((s9 >> 19) | (s10 << 2)); + s[27] = (byte) (s10 >> 6); + s[28] = (byte) ((s10 >> 14) | (s11 << 7)); + s[29] = (byte) (s11 >> 1); + s[30] = (byte) (s11 >> 9); + s[31] = (byte) (s11 >> 17); + } + + static byte[] getHashedScalar(final byte[] privateKey) + throws GeneralSecurityException { + MessageDigest digest = EngineFactory.MESSAGE_DIGEST.getInstance("SHA-512"); + digest.update(privateKey, 0, FIELD_LEN); + byte[] h = digest.digest(); + // https://tools.ietf.org/html/rfc8032#section-5.1.2. + // Clear the lowest three bits of the first octet. + h[0] = (byte) (h[0] & 248); + // Clear the highest bit of the last octet. + h[31] = (byte) (h[31] & 127); + // Set the second highest bit if the last octet. + h[31] = (byte) (h[31] | 64); + return h; + } + + /** + * Returns the EdDSA signature for the {@code message} based on the {@code hashedPrivateKey}. + * + * @param message to sign + * @param hashedPrivateKey {@link Ed25519#getHashedScalar(byte[])} of the private key + * @return signature for the {@code message}. + * @throws GeneralSecurityException if there is no SHA-512 algorithm defined in + * {@link EngineFactory}.MESSAGE_DIGEST. + */ + public static byte[] sign(final byte[] message, final byte[] publicKey, final byte[] hashedPrivateKey) + throws GeneralSecurityException { + // Copying the message to make it thread-safe. Otherwise, if the caller modifies the message + // between the first and the second hash then it might leak the private key. + byte[] messageCopy = Arrays.copyOfRange(message, 0, message.length); + MessageDigest digest = EngineFactory.MESSAGE_DIGEST.getInstance("SHA-512"); + digest.update(hashedPrivateKey, FIELD_LEN, FIELD_LEN); + digest.update(messageCopy); + byte[] r = digest.digest(); + reduce(r); + + byte[] rB = Arrays.copyOfRange(scalarMultWithBase(r).toBytes(), 0, FIELD_LEN); + digest.reset(); + digest.update(rB); + digest.update(publicKey); + digest.update(messageCopy); + byte[] hram = digest.digest(); + reduce(hram); + byte[] s = new byte[FIELD_LEN]; + mulAdd(s, hram, hashedPrivateKey, r); + return Bytes.concat(rB, s); + } + + + // The order of the generator as unsigned bytes in little endian order. + // (2^252 + 0x14def9dea2f79cd65812631a5cf5d3ed, cf. RFC 7748) + static final byte[] GROUP_ORDER = new byte[] { + (byte) 0xed, (byte) 0xd3, (byte) 0xf5, (byte) 0x5c, + (byte) 0x1a, (byte) 0x63, (byte) 0x12, (byte) 0x58, + (byte) 0xd6, (byte) 0x9c, (byte) 0xf7, (byte) 0xa2, + (byte) 0xde, (byte) 0xf9, (byte) 0xde, (byte) 0x14, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10}; + + // Checks whether s represents an integer smaller than the order of the group. + // This is needed to ensure that EdDSA signatures are non-malleable, as failing to check + // the range of S allows to modify signatures (cf. RFC 8032, Section 5.2.7 and Section 8.4.) + // @param s an integer in little-endian order. + private static boolean isSmallerThanGroupOrder(byte[] s) { + for (int j = FIELD_LEN - 1; j >= 0; j--) { + // compare unsigned bytes + int a = s[j] & 0xff; + int b = GROUP_ORDER[j] & 0xff; + if (a != b) { + return a < b; + } + } + return false; + } + + /** + * Returns true if the EdDSA {@code signature} with {@code message}, can be verified with + * {@code publicKey}. + * + * @throws GeneralSecurityException if there is no SHA-512 algorithm defined in + * {@link EngineFactory}.MESSAGE_DIGEST. + */ + static boolean verify(final byte[] message, final byte[] signature, + final byte[] publicKey) throws GeneralSecurityException { + if (signature.length != SIGNATURE_LEN) { + return false; + } + byte[] s = Arrays.copyOfRange(signature, FIELD_LEN, SIGNATURE_LEN); + if (!isSmallerThanGroupOrder(s)) { + return false; + } + MessageDigest digest = EngineFactory.MESSAGE_DIGEST.getInstance("SHA-512"); + digest.update(signature, 0, FIELD_LEN); + digest.update(publicKey); + digest.update(message); + byte[] h = digest.digest(); + reduce(h); + + XYZT negPublicKey = XYZT.fromBytesNegateVarTime(publicKey); + XYZ xyz = doubleScalarMultVarTime(h, negPublicKey, s); + byte[] expectedR = xyz.toBytes(); + for (int i = 0; i < FIELD_LEN; i++) { + if (expectedR[i] != signature[i]) { + return false; + } + } + return true; + } +} diff --git a/tx-signer/src/main/java/com/google/crypto/tink/subtle/Ed25519Constants.java b/tx-signer/src/main/java/com/google/crypto/tink/subtle/Ed25519Constants.java new file mode 100644 index 0000000..a8f6e40 --- /dev/null +++ b/tx-signer/src/main/java/com/google/crypto/tink/subtle/Ed25519Constants.java @@ -0,0 +1,130 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.crypto.tink.subtle; + +import java.math.BigInteger; + +/** Constants used in {@link Ed25519}. */ +final class Ed25519Constants { + + // d = -121665 / 121666 mod 2^255-19 + static final long[] D; + // 2d + static final long[] D2; + // 2^((p-1)/4) mod p where p = 2^255-19 + static final long[] SQRTM1; + + /** + * Base point for the Edwards twisted curve = (x, 4/5) and its exponentiations. B_TABLE[i][j] = + * (j+1)*256^i*B for i in [0, 32) and j in [0, 8). Base point B = B_TABLE[0][0] + * + */ + static final Ed25519.CachedXYT[][] B_TABLE; + static final Ed25519.CachedXYT[] B2; + + private static final BigInteger P_BI = + BigInteger.valueOf(2).pow(255).subtract(BigInteger.valueOf(19)); + private static final BigInteger D_BI = + BigInteger.valueOf(-121665).multiply(BigInteger.valueOf(121666).modInverse(P_BI)).mod(P_BI); + private static final BigInteger D2_BI = BigInteger.valueOf(2).multiply(D_BI).mod(P_BI); + private static final BigInteger SQRTM1_BI = + BigInteger.valueOf(2).modPow(P_BI.subtract(BigInteger.ONE).divide(BigInteger.valueOf(4)), P_BI); + + private static class Point { + private BigInteger x; + private BigInteger y; + } + + private static BigInteger recoverX(BigInteger y) { + // x^2 = (y^2 - 1) / (d * y^2 + 1) mod 2^255-19 + BigInteger xx = + y.pow(2) + .subtract(BigInteger.ONE) + .multiply(D_BI.multiply(y.pow(2)).add(BigInteger.ONE).modInverse(P_BI)); + BigInteger x = xx.modPow(P_BI.add(BigInteger.valueOf(3)).divide(BigInteger.valueOf(8)), P_BI); + if (!x.pow(2).subtract(xx).mod(P_BI).equals(BigInteger.ZERO)) { + x = x.multiply(SQRTM1_BI).mod(P_BI); + } + if (x.testBit(0)) { + x = P_BI.subtract(x); + } + return x; + } + + private static Point edwards(Point a, Point b) { + Point o = new Point(); + BigInteger xxyy = D_BI.multiply(a.x.multiply(b.x).multiply(a.y).multiply(b.y)).mod(P_BI); + o.x = + (a.x.multiply(b.y).add(b.x.multiply(a.y))) + .multiply(BigInteger.ONE.add(xxyy).modInverse(P_BI)) + .mod(P_BI); + o.y = + (a.y.multiply(b.y).add(a.x.multiply(b.x))) + .multiply(BigInteger.ONE.subtract(xxyy).modInverse(P_BI)) + .mod(P_BI); + return o; + } + + private static byte[] toLittleEndian(BigInteger n) { + byte[] b = new byte[32]; + byte[] nBytes = n.toByteArray(); + System.arraycopy(nBytes, 0, b, 32 - nBytes.length, nBytes.length); + for (int i = 0; i < b.length / 2; i++) { + byte t = b[i]; + b[i] = b[b.length - i - 1]; + b[b.length - i - 1] = t; + } + return b; + } + + private static Ed25519.CachedXYT getCachedXYT(Point p) { + return new Ed25519.CachedXYT( + Field25519.expand(toLittleEndian(p.y.add(p.x).mod(P_BI))), + Field25519.expand(toLittleEndian(p.y.subtract(p.x).mod(P_BI))), + Field25519.expand(toLittleEndian(D2_BI.multiply(p.x).multiply(p.y).mod(P_BI)))); + } + + static { + Point b = new Point(); + b.y = BigInteger.valueOf(4).multiply(BigInteger.valueOf(5).modInverse(P_BI)).mod(P_BI); + b.x = recoverX(b.y); + + D = Field25519.expand(toLittleEndian(D_BI)); + D2 = Field25519.expand(toLittleEndian(D2_BI)); + SQRTM1 = Field25519.expand(toLittleEndian(SQRTM1_BI)); + + Point bi = b; + B_TABLE = new Ed25519.CachedXYT[32][8]; + for (int i = 0; i < 32; i++) { + Point bij = bi; + for (int j = 0; j < 8; j++) { + B_TABLE[i][j] = getCachedXYT(bij); + bij = edwards(bij, bi); + } + for (int j = 0; j < 8; j++) { + bi = edwards(bi, bi); + } + } + bi = b; + Point b2 = edwards(b, b); + B2 = new Ed25519.CachedXYT[8]; + for (int i = 0; i < 8; i++) { + B2[i] = getCachedXYT(bi); + bi = edwards(bi, b2); + } + } +} diff --git a/tx-signer/src/main/java/com/google/crypto/tink/subtle/Field25519.java b/tx-signer/src/main/java/com/google/crypto/tink/subtle/Field25519.java new file mode 100644 index 0000000..1795008 --- /dev/null +++ b/tx-signer/src/main/java/com/google/crypto/tink/subtle/Field25519.java @@ -0,0 +1,597 @@ +// Copyright 2017 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.google.crypto.tink.subtle; + +import com.google.crypto.tink.annotations.Alpha; +import java.util.Arrays; + +/** + * Defines field 25519 function based on curve25519-donna C + * implementation (mostly identical). + * + *

Field elements are written as an array of signed, 64-bit limbs (an array of longs), least + * significant first. The value of the field element is: + * + *

+ * x[0] + 2^26·x[1] + 2^51·x[2] + 2^77·x[3] + 2^102·x[4] + 2^128·x[5] + 2^153·x[6] + 2^179·x[7] +
+ * 2^204·x[8] + 2^230·x[9],
+ * 
+ * + *

i.e. the limbs are 26, 25, 26, 25, ... bits wide. + */ +@Alpha +final class Field25519 { + /** + * During Field25519 computation, the mixed radix representation may be in different forms: + *

+ * + * TODO(quannguyen): + * + */ + + static final int FIELD_LEN = 32; + static final int LIMB_CNT = 10; + private static final long TWO_TO_25 = 1 << 25; + private static final long TWO_TO_26 = TWO_TO_25 << 1; + + private static final int[] EXPAND_START = {0, 3, 6, 9, 12, 16, 19, 22, 25, 28}; + private static final int[] EXPAND_SHIFT = {0, 2, 3, 5, 6, 0, 1, 3, 4, 6}; + private static final int[] MASK = {0x3ffffff, 0x1ffffff}; + private static final int[] SHIFT = {26, 25}; + + /** + * Sums two numbers: output = in1 + in2 + * + * On entry: in1, in2 are in reduced-size form. + */ + static void sum(long[] output, long[] in1, long[] in2) { + for (int i = 0; i < LIMB_CNT; i++) { + output[i] = in1[i] + in2[i]; + } + } + + /** + * Sums two numbers: output += in + * + * On entry: in is in reduced-size form. + */ + static void sum(long[] output, long[] in) { + sum(output, output, in); + } + + /** + * Find the difference of two numbers: output = in1 - in2 + * (note the order of the arguments!). + * + * On entry: in1, in2 are in reduced-size form. + */ + static void sub(long[] output, long[] in1, long[] in2) { + for (int i = 0; i < LIMB_CNT; i++) { + output[i] = in1[i] - in2[i]; + } + } + + /** + * Find the difference of two numbers: output = in - output + * (note the order of the arguments!). + * + * On entry: in, output are in reduced-size form. + */ + static void sub(long[] output, long[] in) { + sub(output, in, output); + } + + /** + * Multiply a number by a scalar: output = in * scalar + */ + static void scalarProduct(long[] output, long[] in, long scalar) { + for (int i = 0; i < LIMB_CNT; i++) { + output[i] = in[i] * scalar; + } + } + + /** + * Multiply two numbers: out = in2 * in + * + * output must be distinct to both inputs. The inputs are reduced coefficient form, + * the output is not. + * + * out[x] <= 14 * the largest product of the input limbs. + */ + static void product(long[] out, long[] in2, long[] in) { + out[0] = in2[0] * in[0]; + out[1] = in2[0] * in[1] + + in2[1] * in[0]; + out[2] = 2 * in2[1] * in[1] + + in2[0] * in[2] + + in2[2] * in[0]; + out[3] = in2[1] * in[2] + + in2[2] * in[1] + + in2[0] * in[3] + + in2[3] * in[0]; + out[4] = in2[2] * in[2] + + 2 * (in2[1] * in[3] + in2[3] * in[1]) + + in2[0] * in[4] + + in2[4] * in[0]; + out[5] = in2[2] * in[3] + + in2[3] * in[2] + + in2[1] * in[4] + + in2[4] * in[1] + + in2[0] * in[5] + + in2[5] * in[0]; + out[6] = 2 * (in2[3] * in[3] + in2[1] * in[5] + in2[5] * in[1]) + + in2[2] * in[4] + + in2[4] * in[2] + + in2[0] * in[6] + + in2[6] * in[0]; + out[7] = in2[3] * in[4] + + in2[4] * in[3] + + in2[2] * in[5] + + in2[5] * in[2] + + in2[1] * in[6] + + in2[6] * in[1] + + in2[0] * in[7] + + in2[7] * in[0]; + out[8] = in2[4] * in[4] + + 2 * (in2[3] * in[5] + in2[5] * in[3] + in2[1] * in[7] + in2[7] * in[1]) + + in2[2] * in[6] + + in2[6] * in[2] + + in2[0] * in[8] + + in2[8] * in[0]; + out[9] = in2[4] * in[5] + + in2[5] * in[4] + + in2[3] * in[6] + + in2[6] * in[3] + + in2[2] * in[7] + + in2[7] * in[2] + + in2[1] * in[8] + + in2[8] * in[1] + + in2[0] * in[9] + + in2[9] * in[0]; + out[10] = + 2 * (in2[5] * in[5] + in2[3] * in[7] + in2[7] * in[3] + in2[1] * in[9] + in2[9] * in[1]) + + in2[4] * in[6] + + in2[6] * in[4] + + in2[2] * in[8] + + in2[8] * in[2]; + out[11] = in2[5] * in[6] + + in2[6] * in[5] + + in2[4] * in[7] + + in2[7] * in[4] + + in2[3] * in[8] + + in2[8] * in[3] + + in2[2] * in[9] + + in2[9] * in[2]; + out[12] = in2[6] * in[6] + + 2 * (in2[5] * in[7] + in2[7] * in[5] + in2[3] * in[9] + in2[9] * in[3]) + + in2[4] * in[8] + + in2[8] * in[4]; + out[13] = in2[6] * in[7] + + in2[7] * in[6] + + in2[5] * in[8] + + in2[8] * in[5] + + in2[4] * in[9] + + in2[9] * in[4]; + out[14] = 2 * (in2[7] * in[7] + in2[5] * in[9] + in2[9] * in[5]) + + in2[6] * in[8] + + in2[8] * in[6]; + out[15] = in2[7] * in[8] + + in2[8] * in[7] + + in2[6] * in[9] + + in2[9] * in[6]; + out[16] = in2[8] * in[8] + + 2 * (in2[7] * in[9] + in2[9] * in[7]); + out[17] = in2[8] * in[9] + + in2[9] * in[8]; + out[18] = 2 * in2[9] * in[9]; + } + + /** + * Reduce a long form to a reduced-size form by taking the input mod 2^255 - 19. + * + * On entry: |output[i]| < 14*2^54 + * On exit: |output[0..8]| < 280*2^54 + */ + static void reduceSizeByModularReduction(long[] output) { + // The coefficients x[10], x[11],..., x[18] are eliminated by reduction modulo 2^255 - 19. + // For example, the coefficient x[18] is multiplied by 19 and added to the coefficient x[8]. + // + // Each of these shifts and adds ends up multiplying the value by 19. + // + // For output[0..8], the absolute entry value is < 14*2^54 and we add, at most, 19*14*2^54 thus, + // on exit, |output[0..8]| < 280*2^54. + output[8] += output[18] << 4; + output[8] += output[18] << 1; + output[8] += output[18]; + output[7] += output[17] << 4; + output[7] += output[17] << 1; + output[7] += output[17]; + output[6] += output[16] << 4; + output[6] += output[16] << 1; + output[6] += output[16]; + output[5] += output[15] << 4; + output[5] += output[15] << 1; + output[5] += output[15]; + output[4] += output[14] << 4; + output[4] += output[14] << 1; + output[4] += output[14]; + output[3] += output[13] << 4; + output[3] += output[13] << 1; + output[3] += output[13]; + output[2] += output[12] << 4; + output[2] += output[12] << 1; + output[2] += output[12]; + output[1] += output[11] << 4; + output[1] += output[11] << 1; + output[1] += output[11]; + output[0] += output[10] << 4; + output[0] += output[10] << 1; + output[0] += output[10]; + } + + /** + * Reduce all coefficients of the short form input so that |x| < 2^26. + * + * On entry: |output[i]| < 280*2^54 + */ + static void reduceCoefficients(long[] output) { + output[10] = 0; + + for (int i = 0; i < LIMB_CNT; i += 2) { + long over = output[i] / TWO_TO_26; + // The entry condition (that |output[i]| < 280*2^54) means that over is, at most, 280*2^28 in + // the first iteration of this loop. This is added to the next limb and we can approximate the + // resulting bound of that limb by 281*2^54. + output[i] -= over << 26; + output[i + 1] += over; + + // For the first iteration, |output[i+1]| < 281*2^54, thus |over| < 281*2^29. When this is + // added to the next limb, the resulting bound can be approximated as 281*2^54. + // + // For subsequent iterations of the loop, 281*2^54 remains a conservative bound and no + // overflow occurs. + over = output[i + 1] / TWO_TO_25; + output[i + 1] -= over << 25; + output[i + 2] += over; + } + // Now |output[10]| < 281*2^29 and all other coefficients are reduced. + output[0] += output[10] << 4; + output[0] += output[10] << 1; + output[0] += output[10]; + + output[10] = 0; + // Now output[1..9] are reduced, and |output[0]| < 2^26 + 19*281*2^29 so |over| will be no more + // than 2^16. + long over = output[0] / TWO_TO_26; + output[0] -= over << 26; + output[1] += over; + // Now output[0,2..9] are reduced, and |output[1]| < 2^25 + 2^16 < 2^26. The bound on + // |output[1]| is sufficient to meet our needs. + } + + /** + * A helpful wrapper around {@ref Field25519#product}: output = in * in2. + * + * On entry: |in[i]| < 2^27 and |in2[i]| < 2^27. + * + * The output is reduced degree (indeed, one need only provide storage for 10 limbs) and + * |output[i]| < 2^26. + */ + static void mult(long[] output, long[] in, long[] in2) { + long[] t = new long[19]; + product(t, in, in2); + // |t[i]| < 14*2^54 + reduceSizeByModularReduction(t); + reduceCoefficients(t); + // |t[i]| < 2^26 + System.arraycopy(t, 0, output, 0, LIMB_CNT); + } + + /** + * Square a number: out = in**2 + * + * output must be distinct from the input. The inputs are reduced coefficient form, the output is + * not. + * + * out[x] <= 14 * the largest product of the input limbs. + */ + private static void squareInner(long[] out, long[] in) { + out[0] = in[0] * in[0]; + out[1] = 2 * in[0] * in[1]; + out[2] = 2 * (in[1] * in[1] + in[0] * in[2]); + out[3] = 2 * (in[1] * in[2] + in[0] * in[3]); + out[4] = in[2] * in[2] + + 4 * in[1] * in[3] + + 2 * in[0] * in[4]; + out[5] = 2 * (in[2] * in[3] + in[1] * in[4] + in[0] * in[5]); + out[6] = 2 * (in[3] * in[3] + in[2] * in[4] + in[0] * in[6] + 2 * in[1] * in[5]); + out[7] = 2 * (in[3] * in[4] + in[2] * in[5] + in[1] * in[6] + in[0] * in[7]); + out[8] = in[4] * in[4] + + 2 * (in[2] * in[6] + in[0] * in[8] + 2 * (in[1] * in[7] + in[3] * in[5])); + out[9] = 2 * (in[4] * in[5] + in[3] * in[6] + in[2] * in[7] + in[1] * in[8] + in[0] * in[9]); + out[10] = 2 * (in[5] * in[5] + + in[4] * in[6] + + in[2] * in[8] + + 2 * (in[3] * in[7] + in[1] * in[9])); + out[11] = 2 * (in[5] * in[6] + in[4] * in[7] + in[3] * in[8] + in[2] * in[9]); + out[12] = in[6] * in[6] + + 2 * (in[4] * in[8] + 2 * (in[5] * in[7] + in[3] * in[9])); + out[13] = 2 * (in[6] * in[7] + in[5] * in[8] + in[4] * in[9]); + out[14] = 2 * (in[7] * in[7] + in[6] * in[8] + 2 * in[5] * in[9]); + out[15] = 2 * (in[7] * in[8] + in[6] * in[9]); + out[16] = in[8] * in[8] + 4 * in[7] * in[9]; + out[17] = 2 * in[8] * in[9]; + out[18] = 2 * in[9] * in[9]; + } + + /** + * Returns in^2. + * + * On entry: The |in| argument is in reduced coefficients form and |in[i]| < 2^27. + * + * On exit: The |output| argument is in reduced coefficients form (indeed, one need only provide + * storage for 10 limbs) and |out[i]| < 2^26. + */ + static void square(long[] output, long[] in) { + long[] t = new long[19]; + squareInner(t, in); + // |t[i]| < 14*2^54 because the largest product of two limbs will be < 2^(27+27) and SquareInner + // adds together, at most, 14 of those products. + reduceSizeByModularReduction(t); + reduceCoefficients(t); + // |t[i]| < 2^26 + System.arraycopy(t, 0, output, 0, LIMB_CNT); + } + + /** + * Takes a little-endian, 32-byte number and expands it into mixed radix form. + */ + static long[] expand(byte[] input) { + long[] output = new long[LIMB_CNT]; + for (int i = 0; i < LIMB_CNT; i++) { + output[i] = ((((long) (input[EXPAND_START[i]] & 0xff)) + | ((long) (input[EXPAND_START[i] + 1] & 0xff)) << 8 + | ((long) (input[EXPAND_START[i] + 2] & 0xff)) << 16 + | ((long) (input[EXPAND_START[i] + 3] & 0xff)) << 24) >> EXPAND_SHIFT[i]) & MASK[i & 1]; + } + return output; + } + + /** + * Takes a fully reduced mixed radix form number and contract it into a little-endian, 32-byte + * array. + * + * On entry: |input_limbs[i]| < 2^26 + */ + @SuppressWarnings("NarrowingCompoundAssignment") + static byte[] contract(long[] inputLimbs) { + long[] input = Arrays.copyOf(inputLimbs, LIMB_CNT); + for (int j = 0; j < 2; j++) { + for (int i = 0; i < 9; i++) { + // This calculation is a time-invariant way to make input[i] non-negative by borrowing + // from the next-larger limb. + int carry = -(int) ((input[i] & (input[i] >> 31)) >> SHIFT[i & 1]); + input[i] = input[i] + (carry << SHIFT[i & 1]); + input[i + 1] -= carry; + } + + // There's no greater limb for input[9] to borrow from, but we can multiply by 19 and borrow + // from input[0], which is valid mod 2^255-19. + { + int carry = -(int) ((input[9] & (input[9] >> 31)) >> 25); + input[9] += (carry << 25); + input[0] -= (carry * 19); + } + + // After the first iteration, input[1..9] are non-negative and fit within 25 or 26 bits, + // depending on position. However, input[0] may be negative. + } + + // The first borrow-propagation pass above ended with every limb except (possibly) input[0] + // non-negative. + // + // If input[0] was negative after the first pass, then it was because of a carry from input[9]. + // On entry, input[9] < 2^26 so the carry was, at most, one, since (2**26-1) >> 25 = 1. Thus + // input[0] >= -19. + // + // In the second pass, each limb is decreased by at most one. Thus the second borrow-propagation + // pass could only have wrapped around to decrease input[0] again if the first pass left + // input[0] negative *and* input[1] through input[9] were all zero. In that case, input[1] is + // now 2^25 - 1, and this last borrow-propagation step will leave input[1] non-negative. + { + int carry = -(int) ((input[0] & (input[0] >> 31)) >> 26); + input[0] += (carry << 26); + input[1] -= carry; + } + + // All input[i] are now non-negative. However, there might be values between 2^25 and 2^26 in a + // limb which is, nominally, 25 bits wide. + for (int j = 0; j < 2; j++) { + for (int i = 0; i < 9; i++) { + int carry = (int) (input[i] >> SHIFT[i & 1]); + input[i] &= MASK[i & 1]; + input[i + 1] += carry; + } + } + + { + int carry = (int) (input[9] >> 25); + input[9] &= 0x1ffffff; + input[0] += 19 * carry; + } + + // If the first carry-chain pass, just above, ended up with a carry from input[9], and that + // caused input[0] to be out-of-bounds, then input[0] was < 2^26 + 2*19, because the carry was, + // at most, two. + // + // If the second pass carried from input[9] again then input[0] is < 2*19 and the input[9] -> + // input[0] carry didn't push input[0] out of bounds. + + // It still remains the case that input might be between 2^255-19 and 2^255. In this case, + // input[1..9] must take their maximum value and input[0] must be >= (2^255-19) & 0x3ffffff, + // which is 0x3ffffed. + int mask = gte((int) input[0], 0x3ffffed); + for (int i = 1; i < LIMB_CNT; i++) { + mask &= eq((int) input[i], MASK[i & 1]); + } + + // mask is either 0xffffffff (if input >= 2^255-19) and zero otherwise. Thus this conditionally + // subtracts 2^255-19. + input[0] -= mask & 0x3ffffed; + input[1] -= mask & 0x1ffffff; + for (int i = 2; i < LIMB_CNT; i += 2) { + input[i] -= mask & 0x3ffffff; + input[i + 1] -= mask & 0x1ffffff; + } + + for (int i = 0; i < LIMB_CNT; i++) { + input[i] <<= EXPAND_SHIFT[i]; + } + byte[] output = new byte[FIELD_LEN]; + for (int i = 0; i < LIMB_CNT; i++) { + output[EXPAND_START[i]] |= input[i] & 0xff; + output[EXPAND_START[i] + 1] |= (input[i] >> 8) & 0xff; + output[EXPAND_START[i] + 2] |= (input[i] >> 16) & 0xff; + output[EXPAND_START[i] + 3] |= (input[i] >> 24) & 0xff; + } + return output; + } + + /** + * Computes inverse of z = z(2^255 - 21) + * + * Shamelessly copied from agl's code which was shamelessly copied from djb's code. Only the + * comment format and the variable namings are different from those. + */ + static void inverse(long[] out, long[] z) { + long[] z2 = new long[Field25519.LIMB_CNT]; + long[] z9 = new long[Field25519.LIMB_CNT]; + long[] z11 = new long[Field25519.LIMB_CNT]; + long[] z2To5Minus1 = new long[Field25519.LIMB_CNT]; + long[] z2To10Minus1 = new long[Field25519.LIMB_CNT]; + long[] z2To20Minus1 = new long[Field25519.LIMB_CNT]; + long[] z2To50Minus1 = new long[Field25519.LIMB_CNT]; + long[] z2To100Minus1 = new long[Field25519.LIMB_CNT]; + long[] t0 = new long[Field25519.LIMB_CNT]; + long[] t1 = new long[Field25519.LIMB_CNT]; + + square(z2, z); // 2 + square(t1, z2); // 4 + square(t0, t1); // 8 + mult(z9, t0, z); // 9 + mult(z11, z9, z2); // 11 + square(t0, z11); // 22 + mult(z2To5Minus1, t0, z9); // 2^5 - 2^0 = 31 + + square(t0, z2To5Minus1); // 2^6 - 2^1 + square(t1, t0); // 2^7 - 2^2 + square(t0, t1); // 2^8 - 2^3 + square(t1, t0); // 2^9 - 2^4 + square(t0, t1); // 2^10 - 2^5 + mult(z2To10Minus1, t0, z2To5Minus1); // 2^10 - 2^0 + + square(t0, z2To10Minus1); // 2^11 - 2^1 + square(t1, t0); // 2^12 - 2^2 + for (int i = 2; i < 10; i += 2) { // 2^20 - 2^10 + square(t0, t1); + square(t1, t0); + } + mult(z2To20Minus1, t1, z2To10Minus1); // 2^20 - 2^0 + + square(t0, z2To20Minus1); // 2^21 - 2^1 + square(t1, t0); // 2^22 - 2^2 + for (int i = 2; i < 20; i += 2) { // 2^40 - 2^20 + square(t0, t1); + square(t1, t0); + } + mult(t0, t1, z2To20Minus1); // 2^40 - 2^0 + + square(t1, t0); // 2^41 - 2^1 + square(t0, t1); // 2^42 - 2^2 + for (int i = 2; i < 10; i += 2) { // 2^50 - 2^10 + square(t1, t0); + square(t0, t1); + } + mult(z2To50Minus1, t0, z2To10Minus1); // 2^50 - 2^0 + + square(t0, z2To50Minus1); // 2^51 - 2^1 + square(t1, t0); // 2^52 - 2^2 + for (int i = 2; i < 50; i += 2) { // 2^100 - 2^50 + square(t0, t1); + square(t1, t0); + } + mult(z2To100Minus1, t1, z2To50Minus1); // 2^100 - 2^0 + + square(t1, z2To100Minus1); // 2^101 - 2^1 + square(t0, t1); // 2^102 - 2^2 + for (int i = 2; i < 100; i += 2) { // 2^200 - 2^100 + square(t1, t0); + square(t0, t1); + } + mult(t1, t0, z2To100Minus1); // 2^200 - 2^0 + + square(t0, t1); // 2^201 - 2^1 + square(t1, t0); // 2^202 - 2^2 + for (int i = 2; i < 50; i += 2) { // 2^250 - 2^50 + square(t0, t1); + square(t1, t0); + } + mult(t0, t1, z2To50Minus1); // 2^250 - 2^0 + + square(t1, t0); // 2^251 - 2^1 + square(t0, t1); // 2^252 - 2^2 + square(t1, t0); // 2^253 - 2^3 + square(t0, t1); // 2^254 - 2^4 + square(t1, t0); // 2^255 - 2^5 + mult(out, t1, z11); // 2^255 - 21 + } + + + /** + * Returns 0xffffffff iff a == b and zero otherwise. + */ + private static int eq(int a, int b) { + a = ~(a ^ b); + a &= a << 16; + a &= a << 8; + a &= a << 4; + a &= a << 2; + a &= a << 1; + return a >> 31; + } + + /** + * returns 0xffffffff if a >= b and zero otherwise, where a and b are both non-negative. + */ + private static int gte(int a, int b) { + a -= b; + // a >= 0 iff a >= b. + return ~(a >> 31); + } +} diff --git a/tx-signer/src/main/java/io/bytom/api/Keys.java b/tx-signer/src/main/java/io/bytom/api/Keys.java new file mode 100644 index 0000000..3a60880 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/api/Keys.java @@ -0,0 +1,44 @@ +package io.bytom.api; + +import com.google.gson.annotations.SerializedName; +import io.bytom.common.Utils; + +public class Keys { + + @SerializedName("alias") + public String alias; + + @SerializedName("xpub") + public String xpub; + + @SerializedName("file") + public String file; + + public String toJson() { + return Utils.serializer.toJson(this); + } + + public String getAlias() { + return alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + public String getXpub() { + return xpub; + } + + public void setXpub(String xpub) { + this.xpub = xpub; + } + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } +} diff --git a/tx-signer/src/main/java/io/bytom/api/RawTransaction.java b/tx-signer/src/main/java/io/bytom/api/RawTransaction.java new file mode 100644 index 0000000..0e36826 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/api/RawTransaction.java @@ -0,0 +1,193 @@ +package io.bytom.api; + +import com.google.gson.annotations.SerializedName; +import io.bytom.common.ParameterizedTypeImpl; +import io.bytom.common.SuccessRespon; +import io.bytom.common.Utils; +import io.bytom.exception.BytomException; +import io.bytom.http.Client; +import org.apache.log4j.Logger; +import org.bouncycastle.jcajce.provider.digest.SHA3; + +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RawTransaction { + + @SerializedName("tx_id") + public String txID; + /** + * version + */ + public Integer version; + /** + * size + */ + public Integer size; + /** + * time_range + */ + @SerializedName("time_range") + public Integer timeRange; + + /** + * status + */ + public Integer fee; + + /** + * List of specified inputs for a transaction. + */ + public List inputs; + + /** + * List of specified outputs for a transaction. + */ + public List outputs; + + private static Logger logger = Logger.getLogger(RawTransaction.class); + + public String toJson() { + return Utils.serializer.toJson(this); + } + + public static RawTransaction fromJson(String json) { + return Utils.serializer.fromJson(json, RawTransaction.class); + } + + public static RawTransaction fromSuccessRespon(String json) { + Type responType = new ParameterizedTypeImpl(SuccessRespon.class, new Class[]{RawTransaction.class}); + SuccessRespon result = Utils.serializer.fromJson(json, responType); + return result.dataObject; + } + + public static RawTransaction decode(Client client, String txId) throws BytomException { + Map req = new HashMap(); + req.put("raw_transaction", txId); + RawTransaction rawTransaction = + client.request("decode-raw-transaction", req, RawTransaction.class); + + logger.info("decode-raw-transaction:"); + logger.info(rawTransaction.toJson()); + + return rawTransaction; + } + + public static class AnnotatedInput { + + @SerializedName("input_id") + public String inputID; + /** + * address + */ + private String address; + + /** + * The number of units of the asset being issued or spent. + */ + private long amount; + + /** + * The definition of the asset being issued or spent (possibly null). + */ + @SerializedName("asset_definition") + private Map assetDefinition; + + /** + * The id of the asset being issued or spent. + */ + @SerializedName("asset_id") + private String assetId; + + /** + * The control program which must be satisfied to transfer this output. + */ + @SerializedName("control_program") + private String controlProgram; + + /** + * The id of the output consumed by this input. Null if the input is an + * issuance. + */ + @SerializedName("spent_output_id") + private String spentOutputId; + + /** + * The type of the input.
+ * Possible values are "issue" and "spend". + */ + private String type; + + @Override + public String toString() { + return Utils.serializer.toJson(this); + } + + } + + public static class AnnotatedOutput { + + /** + * address + */ + private String address; + + /** + * The number of units of the asset being controlled. + */ + private long amount; + + /** + * The definition of the asset being controlled (possibly null). + */ + @SerializedName("asset_definition") + private Map assetDefinition; + + /** + * The id of the asset being controlled. + */ + @SerializedName("asset_id") + public String assetId; + + /** + * The control program which must be satisfied to transfer this output. + */ + @SerializedName("control_program") + private String controlProgram; + + /** + * The id of the output. + */ + @SerializedName("id") + private String id; + + /** + * The output's position in a transaction's list of outputs. + */ + private Integer position; + + /** + * The type the output.
+ * Possible values are "control" and "retire". + */ + private String type; + + } + + public byte[] hashFn(byte[] hashedInputHex, byte[] txID) { + + SHA3.Digest256 digest256 = new SHA3.Digest256(); + + // data = hashedInputHex + txID + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(hashedInputHex, 0, hashedInputHex.length); + out.write(txID, 0, txID.length); + byte[] data = out.toByteArray(); + + return digest256.digest(data); + } + +} diff --git a/tx-signer/src/main/java/io/bytom/api/Receiver.java b/tx-signer/src/main/java/io/bytom/api/Receiver.java new file mode 100644 index 0000000..070542c --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/api/Receiver.java @@ -0,0 +1,48 @@ +package io.bytom.api; + +import com.google.gson.annotations.SerializedName; +import io.bytom.common.Utils; +import io.bytom.exception.JSONException; + +/** + * Receivers are used to facilitate payments between accounts on different + * cores. They contain a control program and an expiration date. In the future, + * more payment-related metadata may be placed here. + *

+ * Receivers are typically created under accounts via the + */ +public class Receiver { + + @SerializedName("address") + public String address; + /** + * Hex-encoded string representation of the control program. + */ + @SerializedName("control_program") + public String controlProgram; + + + /** + * Serializes the receiver into a form that is safe to transfer over the wire. + * + * @return the JSON-serialized representation of the Receiver object + */ + public String toJson() { + return Utils.serializer.toJson(this); + } + + /** + * Deserializes a Receiver from JSON. + * + * @param json a JSON-serialized Receiver object + * @return the deserialized Receiver object + * @throws JSONException Raised if the provided string is not valid JSON. + */ + public static Receiver fromJson(String json) throws JSONException { + try { + return Utils.serializer.fromJson(json, Receiver.class); + } catch (IllegalStateException e) { + throw new JSONException("Unable to parse serialized receiver: " + e.getMessage()); + } + } +} diff --git a/tx-signer/src/main/java/io/bytom/api/SignTransaction.java b/tx-signer/src/main/java/io/bytom/api/SignTransaction.java new file mode 100644 index 0000000..9e5002b --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/api/SignTransaction.java @@ -0,0 +1,207 @@ +package io.bytom.api; + +import com.google.gson.annotations.SerializedName; +import io.bytom.common.ExpandedPrivateKey; +import io.bytom.common.ParameterizedTypeImpl; +import io.bytom.common.SuccessRespon; +import io.bytom.common.Utils; +import io.bytom.exception.BytomException; +import io.bytom.http.Client; +import org.apache.log4j.Logger; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Type; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Created by liqiang on 2018/10/24. + */ +public class SignTransaction { + + @SerializedName("tx_id") + public String txID; + /** + * version + */ + public Integer version; + /** + * size + */ + public Integer size; + /** + * time_range + */ + @SerializedName("time_range") + public Integer timeRange; + + /** + * status + */ + public Integer fee; + + /** + * List of specified inputs for a transaction. + */ + public List inputs; + + /** + * List of specified outputs for a transaction. + */ + public List outputs; + + private static Logger logger = Logger.getLogger(SignTransaction.class); + + public String toJson() { + return Utils.serializer.toJson(this); + } + + public static SignTransaction fromJson(String json) { + return Utils.serializer.fromJson(json, SignTransaction.class); + } + + public static SignTransaction fromSuccessRespon(String json) { + Type responType = new ParameterizedTypeImpl(SuccessRespon.class, new Class[]{SignTransaction.class}); + SuccessRespon result = Utils.serializer.fromJson(json, responType); + return result.dataObject; + } + + public static SignTransaction decode(Client client, String txId) throws BytomException { + Map req = new HashMap(); + req.put("raw_transaction", txId); + SignTransaction SignTransaction = + client.request("decode-raw-transaction", req, SignTransaction.class); + + logger.info("decode-raw-transaction:"); + logger.info(SignTransaction.toJson()); + + return SignTransaction; + } + + public class AnnotatedInput { + + @SerializedName("input_id") + public String inputID; + /** + * address + */ + public String address; + + /** + * The number of units of the asset being issued or spent. + */ + public long amount; + + /** + * The definition of the asset being issued or spent (possibly null). + */ + @SerializedName("asset_definition") + private Map assetDefinition; + + /** + * The id of the asset being issued or spent. + */ + @SerializedName("asset_id") + public String assetId; + + /** + * The control program which must be satisfied to transfer this output. + */ + @SerializedName("control_program") + public String controlProgram; + + /** + * The id of the output consumed by this input. Null if the input is an + * issuance. + */ + @SerializedName("spent_output_id") + public String spentOutputId; + + /** + * The type of the input.
+ * Possible values are "issue" and "spend". + */ + private String type; + + public String sourceId; + + public long sourcePosition; + + public String chainPath; + + @SerializedName("witness_component") + public InputWitnessComponent witnessComponent; + + @Override + public String toString() { + return Utils.serializer.toJson(this); + } + + } + + public class AnnotatedOutput { + + /** + * address + */ + public String address; + + /** + * The number of units of the asset being controlled. + */ + public long amount; + + /** + * The definition of the asset being controlled (possibly null). + */ + @SerializedName("asset_definition") + public Map assetDefinition; + + /** + * The id of the asset being controlled. + */ + @SerializedName("asset_id") + public String assetId; + + /** + * The control program which must be satisfied to transfer this output. + */ + @SerializedName("control_program") + public String controlProgram; + + /** + * The id of the output. + */ + @SerializedName("id") + public String id; + + /** + * The output's position in a transaction's list of outputs. + */ + public Integer position; + + /** + * The type the output.
+ * Possible values are "control" and "retire". + */ + public String type; + + } + + /** + * A single witness component, holding information that will become the input + * witness. + */ + public static class InputWitnessComponent { + + /** + * The list of signatures made with the specified keys (null unless type is + * "signature"). + */ + public String[] signatures; + } +} diff --git a/tx-signer/src/main/java/io/bytom/api/SignTransactionImpl.java b/tx-signer/src/main/java/io/bytom/api/SignTransactionImpl.java new file mode 100644 index 0000000..57b9278 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/api/SignTransactionImpl.java @@ -0,0 +1,255 @@ +package io.bytom.api; + +import com.google.common.base.Preconditions; +import io.bytom.common.Constants; +import io.bytom.common.DeriveXpub; +import io.bytom.common.ExpandedPrivateKey; +import io.bytom.common.Utils; +import io.bytom.types.*; +import org.bouncycastle.jcajce.provider.digest.SHA3; +import org.bouncycastle.util.encoders.Hex; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; +import java.util.*; + + +/** + * Created by liqiang on 2018/10/24. + */ +public class SignTransactionImpl { + + public void mapTransaction(SignTransaction signTransaction) { + //组装计算program、inputID、sourceID(muxID)、txID, json数据中这些字段的值为测试值,需重新计算 + Map entryMap = new HashMap<>(); + ValueSource[] muxSources = new ValueSource[signTransaction.inputs.size()]; + List spends = new ArrayList<>(); + + try { + for (int i = 0; i < signTransaction.inputs.size(); i++) { + SignTransaction.AnnotatedInput input = signTransaction.inputs.get(i); + Program proc = new Program(1, Hex.decode(input.controlProgram)); + + AssetID assetID = new AssetID(input.assetId); + AssetAmount assetAmount = new AssetAmount(assetID, input.amount); + + Hash sourceID = new Hash(input.sourceId); + ValueSource src = new ValueSource(sourceID, assetAmount, input.sourcePosition); + Output prevout = new Output(src, proc, 0); + Hash prevoutID = addEntry(entryMap, prevout); + + input.spentOutputId = prevoutID.toString(); + + Spend spend = new Spend(prevoutID, i); + Hash spendID = addEntry(entryMap, spend); + + input.inputID = spendID.toString(); + + muxSources[i] = new ValueSource(spendID, assetAmount, 0); + spends.add(spend); + } + + Mux mux = new Mux(muxSources, new Program(1, new byte[]{0x51})); + Hash muxID = addEntry(entryMap, mux); + + for (Spend spend : spends) { + Output spendOutput = (Output) entryMap.get(spend.spentOutputID); + spend.setDestination(muxID, spendOutput.source.value, spend.ordinal); + } + + List resultIDList = new ArrayList<>(); + for (int i = 0; i < signTransaction.outputs.size(); i++) { + SignTransaction.AnnotatedOutput output = signTransaction.outputs.get(i); + + AssetAmount amount = new AssetAmount(new AssetID(output.assetId), output.amount); + ValueSource src = new ValueSource(muxID, amount, i); + Program prog = new Program(1, Hex.decode(output.controlProgram)); + Output oup = new Output(src, prog, i); + + Hash resultID = addEntry(entryMap, oup); + resultIDList.add(resultID); + + output.id = resultID.toString(); + + ValueDestination destination = new ValueDestination(resultID, src.value, 0); + mux.witnessDestinations.add(destination); + } + + TxHeader txHeader = new TxHeader(signTransaction.version, signTransaction.size, signTransaction.timeRange, resultIDList.toArray(new Hash[]{})); + Hash txID = addEntry(entryMap, txHeader); + signTransaction.txID = txID.toString(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String serializeTransaction(SignTransaction tx) { + + String txSign = null; + //开始序列化 + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + try { + stream.write(7); + // version + if (null != tx.version) + Utils.writeVarint(tx.version, stream); + if (null != tx.timeRange) + Utils.writeVarint(tx.timeRange, stream); + //inputs + if (null != tx.inputs && tx.inputs.size() > 0) { + Utils.writeVarint(tx.inputs.size(), stream); + for (SignTransaction.AnnotatedInput input:tx.inputs) { + //assertVersion + Utils.writeVarint(tx.version, stream); //AssetVersion是否默认为1 + + //inputCommitment + ByteArrayOutputStream inputCommitStream = new ByteArrayOutputStream(); + //spend type flag + Utils.writeVarint(Constants.INPUT_TYPE_SPEND, inputCommitStream); + //spendCommitment + ByteArrayOutputStream spendCommitSteam = new ByteArrayOutputStream(); + spendCommitSteam.write(Hex.decode(input.sourceId)); //计算muxID + spendCommitSteam.write(Hex.decode(input.assetId)); + Utils.writeVarint(input.amount, spendCommitSteam); + //sourcePosition + Utils.writeVarint(input.sourcePosition, spendCommitSteam); //db中获取position + //vmVersion + Utils.writeVarint(1, spendCommitSteam); //db中获取vm_version + //controlProgram + Utils.writeVarStr(Hex.decode(input.controlProgram), spendCommitSteam); + + byte[] dataSpendCommit = spendCommitSteam.toByteArray(); + Utils.writeVarint(dataSpendCommit.length, inputCommitStream); + inputCommitStream.write(dataSpendCommit); + byte[] dataInputCommit = inputCommitStream.toByteArray(); + //inputCommit的length + Utils.writeVarint(dataInputCommit.length, stream); + stream.write(dataInputCommit); + + //inputWitness + if (null != input.witnessComponent) { + ByteArrayOutputStream witnessStream = new ByteArrayOutputStream(); + //arguments + int lenSigs = input.witnessComponent.signatures.length; + //arguments的length + Utils.writeVarint(lenSigs, witnessStream); + for (int i =0; i 0) { + Utils.writeVarint(tx.outputs.size(), stream); + for (SignTransaction.AnnotatedOutput output:tx.outputs) { + //assertVersion + Utils.writeVarint(tx.version, stream); //AssetVersion是否默认为1 + //outputCommit + ByteArrayOutputStream outputCommitSteam = new ByteArrayOutputStream(); + //assetId + outputCommitSteam.write(Hex.decode(output.assetId)); + //amount + Utils.writeVarint(output.amount, outputCommitSteam); + //vmVersion + Utils.writeVarint(1, outputCommitSteam); //db中获取vm_version + //controlProgram + Utils.writeVarStr(Hex.decode(output.controlProgram), outputCommitSteam); + + byte[] dataOutputCommit = outputCommitSteam.toByteArray(); + //outputCommit的length + Utils.writeVarint(dataOutputCommit.length, stream); + stream.write(dataOutputCommit); + + //outputWitness + Utils.writeVarint(0, stream); + } + } + + byte[] data = stream.toByteArray(); + txSign = Hex.toHexString(data); + + } catch (IOException e) { + throw new RuntimeException(e); + } + return txSign; + } + + public Integer getTransactionSize(SignTransaction tx) { + String result = serializeTransaction(tx); + return Hex.decode(result).length; + } + + //签名组装witness + public SignTransaction buildWitness(SignTransaction signTransaction, int index, String priKey, String pubKey) { + + SignTransaction.AnnotatedInput input = signTransaction.inputs.get(index); + if (null == input.witnessComponent) + input.witnessComponent = new SignTransaction.InputWitnessComponent(); + input.witnessComponent.signatures = new String[2]; + if (null != input) { + try { + byte[] message = hashFn(Hex.decode(input.inputID), Hex.decode(signTransaction.txID)); + byte[] key = Hex.decode(priKey); + byte[] sig = Signer.Ed25519InnerSign(key, message); + input.witnessComponent.signatures[0] = Hex.toHexString(sig); + input.witnessComponent.signatures[1] = pubKey; + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + System.out.println("build witness failed."); + } + return signTransaction; + } + + public byte[] hashFn(byte[] hashedInputHex, byte[] txID) { + + SHA3.Digest256 digest256 = new SHA3.Digest256(); + + // data = hashedInputHex + txID + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(hashedInputHex, 0, hashedInputHex.length); + out.write(txID, 0, txID.length); + byte[] data = out.toByteArray(); + + return digest256.digest(data); + } + + private Hash addEntry(Map entryMap, Entry entry) { + Hash id = entry.entryID(); + entryMap.put(id, entry); + return id; + } + + + public SignTransaction generateSignatures(SignTransaction signTransaction, BigInteger[] keys) { + SignTransaction.AnnotatedInput input = signTransaction.inputs.get(0); + input.witnessComponent.signatures = new String[keys.length]; + for (int i=0; i signingInstructions; + + /** + * For core use only. + */ + @SerializedName("local") + private boolean local; + + /** + * False (the default) makes the transaction "final" when signing, preventing + * further changes - the signature program commits to the transaction's signature + * hash. True makes the transaction extensible, committing only to the elements in + * the transaction so far, permitting the addition of new elements. + */ + @SerializedName("allow_additional_actions") + private boolean allowAdditionalActions; + + /** + * allowAdditionalActions causes the transaction to be signed so that it can be + * used as a base transaction in a multiparty trade flow. To enable this setting, + * call this method after building the transaction, but before sending it to the + * signer. + *

+ * All participants in a multiparty trade flow should call this method except for + * the last signer. Do not call this option if the transaction is complete, i.e. + * if it will not be used as a base transaction. + * + * @return updated transaction template + */ + public Template allowAdditionalActions() { + this.allowAdditionalActions = true; + return this; + } + + /** + * A single signing instruction included in a transaction template. + */ + public static class SigningInstruction { + /** + * The input's position in a transaction's list of inputs. + */ + public int position; + + /** + * A list of components used to coordinate the signing of an input. + */ + @SerializedName("witness_components") + public WitnessComponent[] witnessComponents; + } + + /** + * A single witness component, holding information that will become the input + * witness. + */ + public static class WitnessComponent { + /** + * The type of witness component.
+ * Possible types are "data" and "raw_tx_signature". + */ + public String type; + + /** + * Data to be included in the input witness (null unless type is "data"). + */ + public String value; + + /** + * The number of signatures required for an input (null unless type is + * "signature"). + */ + public int quorum; + + /** + * The list of keys to sign with (null unless type is "signature"). + */ + public KeyID[] keys; + + /** + * The list of signatures made with the specified keys (null unless type is + * "signature"). + */ + public String[] signatures; + } + + /** + * A class representing a derived signing key. + */ + public static class KeyID { + /** + * The extended public key associated with the private key used to sign. + */ + public String xpub; + + /** + * The derivation path of the extended public key. + */ + @SerializedName("derivation_path") + public String[] derivationPath; + } + + /** + * Serializes the Address into a form that is safe to transfer over the wire. + * + * @return the JSON-serialized representation of the Receiver object + */ + public String toJson() { + return Utils.serializer.toJson(this); + } + + public static Template fromJson(String json) { + return Utils.serializer.fromJson(json, Template.class); + } + +} diff --git a/tx-signer/src/main/java/io/bytom/api/Transaction.java b/tx-signer/src/main/java/io/bytom/api/Transaction.java new file mode 100644 index 0000000..2f11bf2 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/api/Transaction.java @@ -0,0 +1,1061 @@ +package io.bytom.api; + +import com.google.gson.annotations.SerializedName; +import io.bytom.common.ParameterizedTypeImpl; +import io.bytom.common.Utils; +import io.bytom.exception.BytomException; +import io.bytom.http.Client; +import org.apache.log4j.Logger; + +import java.lang.reflect.Type; +import java.util.*; + +/** + *

Transaction Class

+ */ +public class Transaction { + /** + * Unique identifier, or transaction hash, of a transaction. + */ + @SerializedName("tx_id") + public String txId; + + /** + * Time of transaction. + */ + @SerializedName("block_time") + public String blockTime; + + /** + * Unique identifier, or block hash, of the block containing a transaction. + */ + @SerializedName("block_hash") + public String blockHash; + + /** + * Index of a transaction within the block. + */ + @SerializedName("block_index") + public String blockIndex; + + @SerializedName("block_transactions_count") + public String blockTransactionsCount; + + /** + * Height of the block containing a transaction. + */ + @SerializedName("block_height") + public int blockHeight; + + /** + * whether the state of the request has failed. + */ + @SerializedName("status_fail") + public boolean statusFail; + + /** + * List of specified inputs for a transaction. + */ + public List inputs; + + /** + * List of specified outputs for a transaction. + */ + public List outputs; + + private static Logger logger = Logger.getLogger(Transaction.class); + + /** + * Serializes the Address into a form that is safe to transfer over the wire. + * + * @return the JSON-serialized representation of the Receiver object + */ + public String toJson() { + return Utils.serializer.toJson(this); + } + + public static class Builder { + /** + * Hex-encoded serialization of a transaction to add to the current template. + */ + @SerializedName("base_transaction") + protected String baseTransaction; + + /** + * List of actions in a transaction. + */ + protected List actions; + + /** + * A time duration in milliseconds. If the transaction is not fully signed and + * submitted within this time, it will be rejected by the blockchain. + * Additionally, any outputs reserved when building this transaction will remain + * reserved for this duration. + */ + protected long ttl; + + /** + * Call build-transaction api.
+ * + * Builds a single transaction template. + * + * @param client client object which makes requests to the server + * @return a transaction template + */ + public Template build(Client client) throws BytomException { + return client.request("build-transaction", this, Template.class); + } + + /** + * Default constructor initializes actions list. + */ + public Builder() { + this.actions = new ArrayList<>(); + } + + /** + * Sets the baseTransaction field and initializes the actions lists.
+ * This constructor can be used when executing an atomic swap and the counter + * party has sent an initialized tx template. + */ + public Builder(String baseTransaction) { + this.setBaseTransaction(baseTransaction); + this.actions = new ArrayList<>(); + } + + /** + * Sets the base transaction that will be added to the current template. + */ + public Builder setBaseTransaction(String baseTransaction) { + this.baseTransaction = baseTransaction; + return this; + } + + /** + * Adds an action to a transaction builder. + * @param action action to add + * @return updated builder object + */ + public Builder addAction(Action action) { + this.actions.add(action); + return this; + } + + /** + * Sets a transaction's time-to-live, which indicates how long outputs will be + * reserved for, and how long the transaction will remain valid. Passing zero will + * use the default TTL, which is 300000ms (5 minutes). + * @param ms the duration of the TTL, in milliseconds. + * @return updated builder object + */ + public Builder setTtl(long ms) { + this.ttl = ms; + return this; + } + } + + public static class QueryBuilder { + + public String txId; + + public String accountId; + + public QueryBuilder setTxId(String txId) { + this.txId = txId; + return this; + } + + public QueryBuilder setAccountId(String accountId) { + this.accountId = accountId; + return this; + } + + + /** + * call list-transactions api + * + * @param client client object that makes requests to the core + * @return Transaction Info + * @throws BytomException BytomException + */ + public List list(Client client) throws BytomException { + + Type listType = new ParameterizedTypeImpl(List.class, new Class[]{Transaction.class}); + List transactionList = client.request("list-transactions", null, listType); + + logger.info("list-transactions:"); + logger.info("size of transactionList:" + transactionList.size()); + logger.info("all transactions:"); + for (int i = 0; i < transactionList.size(); i++) { + logger.info(transactionList.get(i).toJson()); + } + + return transactionList; + } + + public List listById(Client client) throws BytomException { + Map req = new HashMap(); + req.put("tx_id", this.txId); + req.put("detail", true); + + Type listType = new ParameterizedTypeImpl(List.class, new Class[]{Transaction.class}); + List transactionList = client.request("list-transactions", req, listType); + + logger.info("list-transactions:"); + logger.info("size of transactionList:" + transactionList.size()); + logger.info("all transactions:"); + for (int i = 0; i < transactionList.size(); i++) { + logger.info(transactionList.get(i).toJson()); + } + + return transactionList; + } + + public List listByAccountId(Client client) throws BytomException { + Map req = new HashMap(); + req.put("account_id", this.accountId); + + Type listType = new ParameterizedTypeImpl(List.class, new Class[]{Transaction.class}); + List transactionList = client.request("list-transactions", req, listType); + + logger.info("list-transactions:"); + logger.info("size of transactionList:" + transactionList.size()); + logger.info("all transactions:"); + for (int i = 0; i < transactionList.size(); i++) { + logger.info(transactionList.get(i).toJson()); + } + + return transactionList; + } + + /** + * call get-transaction api + * + * @param client + * @return + * @throws BytomException + */ + public Transaction get(Client client) throws BytomException { + Map req = new HashMap(); + req.put("tx_id", this.txId); + + Transaction transaction = client.request("get-transaction", req, Transaction.class); + + logger.info("get-transaction:"); + logger.info(transaction.toJson()); + + return transaction; + } + + } + + public static class SignerBuilder { + /** + * call sign-transaction api + * + * Sends a transaction template to a remote password for signing. + * + * @param client + * @param template a signed transaction template + * @param password + * @return + * @throws BytomException + */ + public Template sign(Client client, Template template, + String password) throws BytomException { + HashMap req = new HashMap(); + req.put("transaction", template); + req.put("password", password); + + Template templateResult = client.requestGet("sign-transaction", req, "transaction", + Template.class); + + logger.info("sign-transaction:"); + logger.info(templateResult.toJson()); + + return templateResult; + } + + } + + /** + * A single input included in a transaction. + */ + public static class Input { + /** + * The alias of the account transferring the asset (possibly null if the input is + * an issuance or an unspent output is specified). + */ + @SerializedName("account_alias") + public String accountAlias; + + /** + * The id of the account transferring the asset (possibly null if the input is an + * issuance or an unspent output is specified). + */ + @SerializedName("account_id") + public String accountId; + + @SerializedName("address") + public String address; + + /** + * The number of units of the asset being issued or spent. + */ + public long amount; + + /** + * The alias of the asset being issued or spent (possibly null). + */ + @SerializedName("asset_alias") + public String assetAlias; + + /** + * The definition of the asset being issued or spent (possibly null). + */ + @SerializedName("asset_definition") + public Map assetDefinition; + + /** + * The id of the asset being issued or spent. + */ + @SerializedName("asset_id") + public String assetId; + + /** + * The id of the output consumed by this input. Null if the input is an issuance. + */ + @SerializedName("spent_output_id") + public String spentOutputId; + + /** + * The type of the input.
+ * Possible values are "issue" and "spend". + */ + public String type; + + public String arbitrary; + + @SerializedName("control_program") + public String controlProgram; + + } + + /** + * A single output included in a transaction. + */ + public static class Output { + /** + * The id of the output. + */ + @SerializedName("id") + public String id; + + /** + * The type the output.
+ * Possible values are "control" and "retire". + */ + public String type; + + /** + * The output's position in a transaction's list of outputs. + */ + public int position; + + /** + * The control program which must be satisfied to transfer this output. + */ + @SerializedName("control_program") + public String controlProgram; + + /** + * The id of the asset being controlled. + */ + @SerializedName("asset_id") + public String assetId; + + /** + * The alias of the asset being controlled. + */ + @SerializedName("asset_alias") + public String assetAlias; + + /** + * The definition of the asset being controlled (possibly null). + */ + @SerializedName("asset_definition") + public Map assetDefinition; + + /** + * The number of units of the asset being controlled. + */ + public long amount; + + /** + * The id of the account controlling this output (possibly null if a control + * program is specified). + */ + @SerializedName("account_id") + public String accountId; + + /** + * The alias of the account controlling this output (possibly null if a control + * program is specified). + */ + @SerializedName("account_alias") + public String accountAlias; + + @SerializedName("address") + public String address; + + } + + /** + * Base class representing actions that can be taken within a transaction. + */ + public static class Action extends HashMap { + /** + * + */ + private static final long serialVersionUID = 7948250382060074590L; + + /** + * Default constructor initializes list and sets the client token. + */ + public Action() { + // Several action types require client_token as an idempotency key. + // It's safest to include a default value for this param. + this.put("client_token", UUID.randomUUID().toString()); + } + + /** + * Represents an issuance action. + */ + public static class Issue extends Action { + /** + * + */ + private static final long serialVersionUID = -6296543909434749786L; + + /** + * Default constructor defines the action type as "issue" + */ + public Issue() { + this.put("type", "issue"); + } + + /** + * Specifies the asset to be issued using its alias.
+ * Either this or {@link Issue#setAssetId(String)} must be + * called. + * @param alias alias of the asset to be issued + * @return updated action object + */ + public Issue setAssetAlias(String alias) { + this.put("asset_alias", alias); + return this; + } + + /** + * Specifies the asset to be issued using its id.
+ * Either this or {@link Issue#setAssetAlias(String)} must be + * called. + * @param id id of the asset to be issued + * @return updated action object + */ + public Issue setAssetId(String id) { + this.put("asset_id", id); + return this; + } + + /** + * Specifies the amount of the asset to be issued.
+ * Must be called. + * @param amount number of units of the asset to be issued + * @return updated action object + */ + public Issue setAmount(long amount) { + this.put("amount", amount); + return this; + } + } + + /** + * Represents a spend action taken on a particular account. + */ + public static class SpendFromAccount extends Action { + /** + * + */ + private static final long serialVersionUID = 6444162327409625893L; + + /** + * Default constructor defines the action type as "spend_account" + */ + public SpendFromAccount() { + this.put("type", "spend_account"); + } + + /** + * Specifies the spending account using its alias.
+ * Either this or {@link SpendFromAccount#setAccountId(String)} must + * be called.
+ * Must be used with {@link SpendFromAccount#setAssetAlias(String)} + * . + * @param alias alias of the spending account + * @return updated action object + */ + public SpendFromAccount setAccountAlias(String alias) { + this.put("account_alias", alias); + return this; + } + + /** + * Specifies the spending account using its id.
+ * Either this or {@link SpendFromAccount#setAccountAlias(String)} + * must be called.
+ * Must be used with {@link SpendFromAccount#setAssetId(String)} + * . + * @param id id of the spending account + * @return updated action object + */ + public SpendFromAccount setAccountId(String id) { + this.put("account_id", id); + return this; + } + + /** + * Specifies the asset to be spent using its alias.
+ * Either this or {@link SpendFromAccount#setAssetId(String)} must be + * called.
+ * Must be used with {@link SpendFromAccount#setAccountAlias(String)} + * . + * @param alias alias of the asset to be spent + * @return updated action object + */ + public SpendFromAccount setAssetAlias(String alias) { + this.put("asset_alias", alias); + return this; + } + + /** + * Specifies the asset to be spent using its id.
+ * Either this or {@link SpendFromAccount#setAssetAlias(String)} must + * be called.
+ * Must be used with {@link SpendFromAccount#setAccountId(String)} + * .
+ * @param id id of the asset to be spent + * @return updated action object + */ + public SpendFromAccount setAssetId(String id) { + this.put("asset_id", id); + return this; + } + + /** + * Specifies the amount of asset to be spent.
+ * Must be called. + * @param amount number of units of the asset to be spent + * @return updated action object + */ + public SpendFromAccount setAmount(long amount) { + this.put("amount", amount); + return this; + } + } + + /** + * Represents a control action taken on a particular account. + */ + public static class ControlWithAccount extends Action { + /** + * + */ + private static final long serialVersionUID = -1067464339402520620L; + + /** + * Default constructor defines the action type as "control_account" + */ + public ControlWithAccount() { + this.put("type", "control_account"); + } + + /** + * Specifies the controlling account using its alias.
+ * Either this or {@link ControlWithAccount#setAccountId(String)} must + * be called.
+ * Must be used with {@link ControlWithAccount#setAssetAlias(String)} + * . + * @param alias alias of the controlling account + * @return updated action object + */ + public ControlWithAccount setAccountAlias(String alias) { + this.put("account_alias", alias); + return this; + } + + /** + * Specifies the controlling account using its id.
+ * Either this or {@link ControlWithAccount#setAccountAlias(String)} + * must be called.
+ * Must be used with {@link ControlWithAccount#setAssetId(String)} + * . + * @param id id of the controlling account + * @return updated action object + */ + public ControlWithAccount setAccountId(String id) { + this.put("account_id", id); + return this; + } + + /** + * Specifies the asset to be controlled using its alias.
+ * Either this or {@link ControlWithAccount#setAssetId(String)} must + * be called.
+ * Must be used with + * {@link ControlWithAccount#setAccountAlias(String)}. + * @param alias alias of the asset to be controlled + * @return updated action object + */ + public ControlWithAccount setAssetAlias(String alias) { + this.put("asset_alias", alias); + return this; + } + + /** + * Specifies the asset to be controlled using its id.
+ * Either this or {@link ControlWithAccount#setAssetAlias(String)} + * must be called.
+ * Must be used with {@link ControlWithAccount#setAccountId(String)} + * . + * @param id id of the asset to be controlled + * @return updated action object + */ + public ControlWithAccount setAssetId(String id) { + this.put("asset_id", id); + return this; + } + + /** + * Specifies the amount of the asset to be controlled.
+ * Must be called. + * @param amount number of units of the asset to be controlled + * @return updated action object + */ + public ControlWithAccount setAmount(long amount) { + this.put("amount", amount); + return this; + } + + } + + /** + * Represents a control action taken on a particular address. + */ + public static class ControlWithAddress extends Action { + /** + * + */ + private static final long serialVersionUID = 1292007349260961499L; + + /** + * Default constructor defines the action type as "control_address" + */ + public ControlWithAddress() { + this.put("type", "control_address"); + } + + public ControlWithAddress setAddress(String address) { + this.put("address", address); + return this; + } + + /** + * Specifies the asset to be controlled using its alias.
+ * Either this or {@link ControlWithAddress#setAssetId(String)} must + * be called.
+ * @param alias alias of the asset to be controlled + * @return updated action object + */ + public ControlWithAddress setAssetAlias(String alias) { + this.put("asset_alias", alias); + return this; + } + + /** + * Specifies the asset to be controlled using its id.
+ * Either this or {@link ControlWithAccount#setAssetAlias(String)} + * must be called.
+ * @param id id of the asset to be controlled + * @return updated action object + */ + public ControlWithAddress setAssetId(String id) { + this.put("asset_id", id); + return this; + } + + /** + * Specifies the amount of the asset to be controlled.
+ * Must be called. + * @param amount number of units of the asset to be controlled + * @return updated action object + */ + public ControlWithAddress setAmount(long amount) { + this.put("amount", amount); + return this; + } + + } + + /** + * Use this action to pay assets into a {@link Receiver}. + */ + public static class ControlWithReceiver extends Action { + /** + * + */ + private static final long serialVersionUID = 7280759134960453401L; + + /** + * Default constructor. + */ + public ControlWithReceiver() { + this.put("type", "control_receiver"); + } + + /** + * Specifies the receiver that is being paid to. + * + * @param receiver the receiver being paid to + * @return this ControlWithReceiver object + */ + public ControlWithReceiver setReceiver(Receiver receiver) { + this.put("receiver", receiver); + return this; + } + + /** + * Specifies the asset to be controlled using its alias. + *

+ * Either this or {@link ControlWithReceiver#setAssetId(String)} must + * be called. + * @param alias unique alias of the asset to be controlled + * @return this ControlWithReceiver object + */ + public ControlWithReceiver setAssetAlias(String alias) { + this.put("asset_alias", alias); + return this; + } + + /** + * Specifies the asset to be controlled using its id. + *

+ * Either this or {@link ControlWithReceiver#setAssetAlias(String)} + * must be called. + * @param id unique ID of the asset to be controlled + * @return this ControlWithReceiver object + */ + public ControlWithReceiver setAssetId(String id) { + this.put("asset_id", id); + return this; + } + + /** + * Specifies the amount of the asset to be controlled. + *

+ * Must be called. + * @param amount the number of units of the asset to be controlled + * @return this ControlWithReceiver object + */ + public ControlWithReceiver setAmount(long amount) { + this.put("amount", amount); + return this; + } + + } + + /** + * Represents a retire action. + */ + public static class Retire extends Action { + /** + * + */ + private static final long serialVersionUID = -8434272436211832706L; + + /** + * Default constructor defines the action type as "control_program" + */ + public Retire() { + this.put("type", "retire"); + } + + /** + * Specifies the amount of the asset to be retired.
+ * Must be called. + * @param amount number of units of the asset to be retired + * @return updated action object + */ + public Retire setAmount(long amount) { + this.put("amount", amount); + return this; + } + + /** + * Specifies the asset to be retired using its alias.
+ * Either this or {@link Retire#setAssetId(String)} must be + * called. + * @param alias alias of the asset to be retired + * @return updated action object + */ + public Retire setAssetAlias(String alias) { + this.put("asset_alias", alias); + return this; + } + + /** + * Specifies the asset to be retired using its id.
+ * Either this or {@link Retire#setAssetAlias(String)} must be + * called. + * @param id id of the asset to be retired + * @return updated action object + */ + public Retire setAssetId(String id) { + this.put("asset_id", id); + return this; + } + + public Retire setAccountAlias(String alias) { + this.put("account_alias", alias); + return this; + } + + public Retire setAccountId(String id) { + this.put("account_id", id); + return this; + } + + } + + /** + * Sets a k,v parameter pair. + * @param key the key on the parameter object + * @param value the corresponding value + * @return updated action object + */ + public Action setParameter(String key, Object value) { + this.put(key, value); + return this; + } + } + + public static class SubmitResponse { + /** + * The transaction id. + */ + public String tx_id; + + public String toJson() { + return Utils.serializer.toJson(this); + } + + } + + /** + * Call submit-transaction api + * + * @param client + * @param template + * @return + * @throws BytomException + */ + public static SubmitResponse submit(Client client, Template template) + throws BytomException { + HashMap body = new HashMap<>(); + body.put("raw_transaction", template.rawTransaction); + return client.request("submit-transaction", body, SubmitResponse.class); + } + + public static class TransactionGas { + /** + * total consumed neu(1BTM = 10^8NEU) for execute transaction. + */ + @SerializedName("total_neu") + public int totalNeu; + + /** + * consumed neu for storage transaction . + */ + @SerializedName("storage_neu") + public int storageNeu; + /** + * consumed neu for execute VM. + */ + @SerializedName("vm_neu") + public int vmNeu; + } + + /** + * call estimate-transaction-gas api + * + * @param client + * @param template + * @return + * @throws BytomException + */ + public static TransactionGas estimateGas(Client client, Template template) + throws BytomException { + HashMap body = new HashMap<>(); + body.put("transaction_template", template); + return client.request("estimate-transaction-gas", body, TransactionGas.class); + } + + public static class Feed { + /** + * name of the transaction feed. + */ + public String alias; + /** + * filter, filter of the transaction feed. + */ + public String filter; + + /** + * param, param of the transaction feed. + */ + public TransactionFeedParam param; + + private static Logger logger = Logger.getLogger(Transaction.class); + + /** + * Serializes the TransactionFeed into a form that is safe to transfer over the wire. + * + * @return the JSON-serialized representation of the TransactionFeed object + */ + public String toJson() { + return Utils.serializer.toJson(this); + } + + public static class Builder { + + public String alias; + + public String filter; + + public Builder() { + } + + public Builder setAlias(String alias) { + this.alias = alias; + return this; + } + + public Builder setFilter(String filter) { + this.filter = filter; + return this; + } + + /** + * Call create-transaction-feed api + * + * @param client + * @throws BytomException + */ + public void create(Client client) throws BytomException { + client.request("create-transaction-feed", this); + logger.info("create-transaction-feed"); + } + } + + /** + * Call get-transaction-feed api + * + * @param client + * @param alias + * @return + * @throws BytomException + */ + public static Feed getByAlias(Client client, String alias) throws BytomException { + Map req = new HashMap(); + req.put("alias", alias); + + // the return param contains txfeed. + Feed transactionFeed = client.requestGet("get-transaction-feed", + req, "txfeed", Feed.class); + logger.info("get-transaction-feed."); + logger.info(transactionFeed.toJson()); + + return transactionFeed; + } + + /** + * Call update-transaction-feed api + * + * @param client + * @param alias + * @param filter + * @throws BytomException + */ + public static void update(Client client, String alias, String filter) throws BytomException { + Map req = new HashMap(); + req.put("alias", alias); + req.put("filter", filter); + client.request("update-transaction-feed", req); + logger.info("update-transaction-feed successfully"); + } + + /** + * Call list-transaction-feeds api + * @param client + * @return + * @throws BytomException + */ + public static List list(Client client) throws BytomException { + + Type listType = new ParameterizedTypeImpl(List.class, new Class[]{Feed.class}); + List transactionFeedList = client.request("list-transaction-feeds", null, listType); + + logger.info("list-transaction-feeds:"); + logger.info("size of transactionList:" + transactionFeedList.size()); + for (int i =0 ;i < transactionFeedList.size();i++) { + logger.info(transactionFeedList.get(i).toJson()); + } + return transactionFeedList; + } + + /** + * Call delete-transaction-feed api + * + * @param client + * @param alias + * @throws BytomException + */ + public static void deleteByAlias(Client client, String alias) throws BytomException { + Map req = new HashMap(); + req.put("alias", alias); + client.request("delete-transaction-feed", req); + logger.info("delete-transaction-feed successfully"); + } + + + + public class TransactionFeedParam { + + /** + * assetid + */ + public String assetid; + + /** + * lowerlimit + */ + public long lowerlimit; + + /** + * upperlimit + */ + public long upperlimit; + + } + } +} diff --git a/tx-signer/src/main/java/io/bytom/common/Configuration.java b/tx-signer/src/main/java/io/bytom/common/Configuration.java new file mode 100644 index 0000000..21dd949 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/common/Configuration.java @@ -0,0 +1,29 @@ +package io.bytom.common; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Properties; + +public class Configuration { + + private static Properties props = new Properties(); + static { + try { + props.load(Thread.currentThread().getContextClassLoader() + .getResourceAsStream("config.properties")); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static String getValue(String key) { + return props.getProperty(key); + } + + public static void updateProperties(String key, String value) { + props.setProperty(key, value); + } + +} diff --git a/tx-signer/src/main/java/io/bytom/common/Constants.java b/tx-signer/src/main/java/io/bytom/common/Constants.java new file mode 100644 index 0000000..98df866 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/common/Constants.java @@ -0,0 +1,13 @@ +package io.bytom.common; + +/** + * Created by liqiang on 2018/10/17. + */ +public class Constants { + + public static int INPUT_TYPE_ISSUANCE = 0; + + public static int INPUT_TYPE_SPEND = 1; + + public static int INPUT_TYPE_COINBASE = 2; +} diff --git a/tx-signer/src/main/java/io/bytom/common/DeriveXpub.java b/tx-signer/src/main/java/io/bytom/common/DeriveXpub.java new file mode 100644 index 0000000..92cae49 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/common/DeriveXpub.java @@ -0,0 +1,24 @@ +package io.bytom.common; + +import com.google.crypto.tink.subtle.Ed25519; + +public class DeriveXpub { + public static byte[] deriveXpub(byte[] xprv) { + byte[] xpub = new byte[xprv.length]; + byte[] scalar = new byte[xprv.length / 2]; +// for (int i = 0; i < xprv.length / 2; i++) { +// scalar[i] = xprv[i]; +// } + System.arraycopy(xprv, 0, scalar, 0, xprv.length / 2); + byte[] buf = Ed25519.scalarMultWithBaseToBytes(scalar); +// for (int i = 0; i < buf.length; i++) { +// xpub[i] = buf[i]; +// } + System.arraycopy(buf, 0, xpub, 0, buf.length); +// for (int i = xprv.length / 2; i < xprv.length; i++) { +// xpub[i] = xprv[i]; +// } + System.arraycopy(xprv, xprv.length / 2, xpub, xprv.length / 2, xprv.length / 2); + return xpub; + } +} diff --git a/tx-signer/src/main/java/io/bytom/common/ExpandedPrivateKey.java b/tx-signer/src/main/java/io/bytom/common/ExpandedPrivateKey.java new file mode 100644 index 0000000..e7a418a --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/common/ExpandedPrivateKey.java @@ -0,0 +1,33 @@ +package io.bytom.common; + +import org.bouncycastle.util.encoders.Hex; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; + +public class ExpandedPrivateKey { + public static byte[] HMacSha512(byte[] data, byte[] key) + throws SignatureException, NoSuchAlgorithmException, InvalidKeyException + { + SecretKeySpec signingKey = new SecretKeySpec(key, "HmacSHA512"); + Mac mac = Mac.getInstance("HmacSHA512"); + mac.init(signingKey); + return mac.doFinal(data); + } + + public static byte[] ExpandedPrivateKey(byte[] data) + throws SignatureException, NoSuchAlgorithmException, InvalidKeyException + { + // "457870616e64" is "Expand" hex. + byte[] res = HMacSha512(data, Hex.decode("457870616e64")); + for (int i = 0; i <= 31; i++) { + res[i] = data[i]; + } + return res; + } +} + + diff --git a/tx-signer/src/main/java/io/bytom/common/FindDst.java b/tx-signer/src/main/java/io/bytom/common/FindDst.java new file mode 100644 index 0000000..1a34844 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/common/FindDst.java @@ -0,0 +1,26 @@ +package io.bytom.common; + +import org.apache.log4j.Logger; +import org.bouncycastle.util.encoders.Hex; + +public class FindDst { + + public static Logger logger = Logger.getLogger(FindDst.class); + + public static int find(String[] privateKeys, String xpub) throws Exception { + // 多签情况下,找到xpub对应的private key的下标 dst + int dst = -1; + for (int k = 0; k < privateKeys.length; k++) { + byte[] tempXpub = DeriveXpub.deriveXpub(Hex.decode(privateKeys[k])); + if (xpub.equals(Hex.toHexString(tempXpub))) { + dst = k; + logger.info("private[dst]: "+privateKeys[dst]); + break; + } + } + if (dst == -1) { + throw new Exception("Not a proper private key to sign transaction."); + } + return dst; + } +} diff --git a/tx-signer/src/main/java/io/bytom/common/NonHardenedChild.java b/tx-signer/src/main/java/io/bytom/common/NonHardenedChild.java new file mode 100644 index 0000000..912d7c3 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/common/NonHardenedChild.java @@ -0,0 +1,85 @@ +package io.bytom.common; + +import org.bouncycastle.util.encoders.Hex; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; + +public class NonHardenedChild { + + private static byte[] HMacSha512(byte[] data, byte[] key) + throws NoSuchAlgorithmException, InvalidKeyException { + SecretKeySpec signingKey = new SecretKeySpec(key, "HmacSHA512"); + Mac mac = Mac.getInstance("HmacSHA512"); + mac.init(signingKey); + return mac.doFinal(data); + } + + public static byte[] NHchild(byte[] path, byte[] xprv, byte[] xpub) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException { + // begin build data + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write('N'); + out.write(xpub, 0, xpub.length / 2); + out.write(path, 0, path.length); + byte[] data = out.toByteArray(); + // end build data + + // begin build key + byte[] key = new byte[xpub.length / 2]; + System.arraycopy(xpub, xpub.length / 2, key, 0, xpub.length / 2); + // end build key + + // doFinal() + byte[] res = HMacSha512(data, key); + + //begin operate res[:32] + byte[] f = new byte[res.length / 2]; + System.arraycopy(res, 0, f, 0, res.length / 2); + f = pruneIntermediateScalar(f); + System.arraycopy(f, 0, res, 0, res.length / 2); + //end operate res[:32] + + //begin operate res[:32] again + int carry = 0; + int sum = 0; + for (int i = 0; i < 32; i++) { + int xprvInt = xprv[i] & 0xFF; + int resInt = res[i] & 0xFF; + sum = xprvInt + resInt + carry; + res[i] = (byte) sum; + carry = sum >> 8; + } + if ((sum >> 8) != 0) { + System.err.println("sum does not fit in 256-bit int"); + } + //end operate res[:32] again + return res; + } + + private static byte[] pruneIntermediateScalar(byte[] f) { + f[0] &= 248; // clear bottom 3 bits + f[29] &= 1; // clear 7 high bits + f[30] = 0; // clear 8 bits + f[31] = 0; // clear 8 bits + return f; + } + + public static byte[] child(byte[] xprv, String[] hpaths) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException { + byte[][] paths = new byte[][]{ + Hex.decode(hpaths[0]), + Hex.decode(hpaths[1]) + }; + byte[] res = xprv; + for (int i = 0; i < hpaths.length; i++) { + byte[] xpub = DeriveXpub.deriveXpub(res); + res = NonHardenedChild.NHchild(paths[i], res, xpub); + } + return res; + } +} + + diff --git a/tx-signer/src/main/java/io/bytom/common/ParameterizedTypeImpl.java b/tx-signer/src/main/java/io/bytom/common/ParameterizedTypeImpl.java new file mode 100644 index 0000000..d66be8c --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/common/ParameterizedTypeImpl.java @@ -0,0 +1,30 @@ +package io.bytom.common; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public class ParameterizedTypeImpl implements ParameterizedType { + + private final Class raw; + private final Type[] args; + + public ParameterizedTypeImpl(Class raw, Type[] args) { + this.raw = raw; + this.args = args != null ? args : new Type[0]; + } + + @Override + public Type[] getActualTypeArguments() { + return args; + } + + @Override + public Type getRawType() { + return raw; + } + + @Override + public Type getOwnerType() { + return null; + } +} diff --git a/tx-signer/src/main/java/io/bytom/common/SuccessRespon.java b/tx-signer/src/main/java/io/bytom/common/SuccessRespon.java new file mode 100644 index 0000000..d7813f9 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/common/SuccessRespon.java @@ -0,0 +1,9 @@ +package io.bytom.common; + +import com.google.gson.annotations.SerializedName; + +public class SuccessRespon { + public String status; + @SerializedName("data") + public T dataObject; +} diff --git a/tx-signer/src/main/java/io/bytom/common/Utils.java b/tx-signer/src/main/java/io/bytom/common/Utils.java new file mode 100644 index 0000000..7ea543d --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/common/Utils.java @@ -0,0 +1,91 @@ +package io.bytom.common; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.bytom.api.SignTransactionImpl; +import io.bytom.types.ExpandedKeys; +import org.bouncycastle.util.encoders.Hex; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.util.Arrays; + +public class Utils { + + public static String rfc3339DateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"; + + public static final Gson serializer = new GsonBuilder().setDateFormat(rfc3339DateFormat).create(); + + public static int writeVarint(long value, ByteArrayOutputStream stream) throws IOException { + byte[] varint = new byte[9]; + int n = putUvarint(varint, value); + byte[] varintTime = Arrays.copyOf(varint, n); + stream.write(varintTime); + return n; + } + + public static int writeVarStr(byte[] buf, ByteArrayOutputStream stream) throws IOException { + int n = writeVarint(buf.length, stream); + stream.write(buf); + return n + (buf.length); + } + + public static int getLengthVarInt(long x) { + byte[] varint = new byte[9]; + int n = putUvarint(varint, x); + byte[] varintTime = Arrays.copyOf(varint, n); + return varintTime.length; + } + + private static int putUvarint(byte[] buf, long x) { + int i = 0; + while (x >= 0x80) { + buf[i] = (byte) (x | 0x80); + x >>= 7; + i++; + } + buf[i] = (byte) x; + return i + 1; + } + + public static byte[] BigIntegerToBytes(BigInteger value) { + if (value == null) { + return null; + } else { + byte[] data = value.toByteArray(); + if (data.length != 1 && data[0] == 0) { + byte[] tmp = new byte[data.length - 1]; + System.arraycopy(data, 1, tmp, 0, tmp.length); + data = tmp; + } + + return data; + } + } + + public static byte[] pruneIntermediateScalar(byte[] f) { + f[0] &= 248; + f[31] &= 31; // clear top 3 bits + f[31] |= 64; // set second highest bit + return f; + } + + public static ExpandedKeys expandedPriKey(String priKey, String key) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException { + byte[] hashPriKey = ExpandedPrivateKey.HMacSha512(Hex.decode(priKey), key.getBytes()); + //begin operate res[:32] + byte[] f = new byte[hashPriKey.length / 2]; + System.arraycopy(hashPriKey, 0, f, 0, hashPriKey.length / 2); + f = pruneIntermediateScalar(f); + System.arraycopy(f, 0, hashPriKey, 0, hashPriKey.length / 2); + //end operate res[:32] + byte[] hashPubKey = DeriveXpub.deriveXpub(hashPriKey); + ExpandedKeys expandedKeys = new ExpandedKeys(); + expandedKeys.setPriKey(Hex.toHexString(hashPriKey)); + expandedKeys.setPubKey(Hex.toHexString(hashPubKey)); + return expandedKeys; + } +} diff --git a/tx-signer/src/main/java/io/bytom/exception/APIException.java b/tx-signer/src/main/java/io/bytom/exception/APIException.java new file mode 100644 index 0000000..1746409 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/exception/APIException.java @@ -0,0 +1,178 @@ +package io.bytom.exception; + +import com.google.gson.annotations.SerializedName; + +/** + * APIException wraps errors returned by the API. + * Each error contains a brief description in addition to a unique error code.
+ * The error code can be used by Chain Support to diagnose the exact cause of the error. + * The mapping of error codes to messages is as follows:

+ * + *

General errors

+ * CH001 - Request timed out + * CH002 - Not found + * CH003 - Invalid request body + * CH004 - Invalid request header + * CH006 - Not found + * + *

Account/Asset errors

+ * CH200 - Quorum must be greater than 1 and less than or equal to the length of xpubs + * CH201 - Invalid xpub format + * CH202 - At least one xpub is required + * CH203 - Retrieved type does not match expected type + * + *

Access token errors

+ * CH300 - Malformed or empty access token id + * CH301 - Access tokens must be type client or network + * CH302 - Access token id is already in use + * CH310 - The access token used to authenticate this request cannot be deleted + * + *

Query errors

+ * CH600 - Malformed pagination parameter `after` + * CH601 - Incorrect number of parameters to filter + * CH602 - Malformed query filter + * + *

Transaction errors

+ * CH700 - Reference data does not match previous transaction's reference data
+ * CH701 - Invalid action type
+ * CH702 - Invalid alias on action
+ * CH730 - Missing raw transaction
+ * CH731 - Too many signing instructions in template for transaction
+ * CH732 - Invalid transaction input index
+ * CH733 - Invalid signature script component
+ * CH734 - Missing signature in template
+ * CH735 - Transaction rejected
+ * CH760 - Insufficient funds for tx
+ * CH761 - Some outputs are reserved; try again
+ */ +public class APIException extends BytomException { + /** + * Unique identifier for the error. + */ + public String code; + + /** + * Message describing the general nature of the error. + */ + @SerializedName("message") + public String chainMessage; + + /** + * Additional information about the error (possibly null). + */ + public String detail; + + /** + * Specifies whether the error is temporary, or a change to the request is necessary. + */ + public boolean temporary; + + /** + * Unique identifier of the request to the server. + */ + public String requestId; + + /** + * HTTP status code returned by the server. + */ + public int statusCode; + + public APIException(String message) { + super(message); + } + + /** + * Initializes exception with its message and requestId attributes. + * + * @param message error message + * @param requestId unique identifier of the request + */ + public APIException(String message, String requestId) { + super(message); + this.requestId = requestId; + } + + /** + * Intitializes exception with its code, message, detail & temporary field set. + * + * @param code error code + * @param message error message + * @param detail additional error information + * @param temporary unique identifier of the request + */ + public APIException(String code, String message, String detail, boolean temporary) { + super(message); + this.chainMessage = message; + this.code = code; + this.detail = detail; + this.temporary = temporary; + } + + /** + * Initializes exception with its code, message, detail & requestId attributes. + * + * @param code error code + * @param message error message + * @param detail additional error information + * @param requestId unique identifier of the request + */ + public APIException(String code, String message, String detail, String requestId) { + super(message); + this.chainMessage = message; + this.code = code; + this.detail = detail; + this.requestId = requestId; + } + + /** + * Initializes exception with all of its attributes. + * + * @param code error code + * @param message error message + * @param detail additional error information + * @param temporary specifies if the error is temporary + * @param requestId unique identifier of the request + * @param statusCode HTTP status code + */ + public APIException( + String code, + String message, + String detail, + boolean temporary, + String requestId, + int statusCode) { + super(message); + this.chainMessage = message; + this.code = code; + this.detail = detail; + this.temporary = temporary; + this.requestId = requestId; + this.statusCode = statusCode; + } + + /** + * Constructs a detailed message of the error. + * + * @return detailed error message + */ + @Override + public String getMessage() { + String s = ""; + + if (this.code != null && this.code.length() > 0) { + s += "Code: " + this.code + " "; + } + + s += "Message: " + this.chainMessage; + + if (this.detail != null && this.detail.length() > 0) { + s += " Detail: " + this.detail; + } + + if (this.requestId != null) { + s += " Request-ID: " + this.requestId; + } + + return s; + } +} diff --git a/tx-signer/src/main/java/io/bytom/exception/BadURLException.java b/tx-signer/src/main/java/io/bytom/exception/BadURLException.java new file mode 100644 index 0000000..dd16377 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/exception/BadURLException.java @@ -0,0 +1,14 @@ +package io.bytom.exception; + +/** + * BadURLException wraps errors due to malformed URLs. + */ +public class BadURLException extends BytomException { + /** + * Initializes exception with its message attribute. + * @param message error message + */ + public BadURLException(String message) { + super(message); + } +} diff --git a/tx-signer/src/main/java/io/bytom/exception/BuildException.java b/tx-signer/src/main/java/io/bytom/exception/BuildException.java new file mode 100644 index 0000000..19a2f89 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/exception/BuildException.java @@ -0,0 +1,49 @@ +package io.bytom.exception; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +/** + * BuildException wraps errors returned by the build-transaction endpoint. + */ +public class BuildException extends APIException { + + public BuildException(String message, String requestId) { + super(message, requestId); + } + + public static class ActionError extends APIException { + + public static class Data { + /** + * The index of the action that caused this error. + */ + @SerializedName("index") + public Integer index; + } + + public ActionError(String message, String requestId) { + super(message, requestId); + } + + /** + * Additional data pertaining to the error. + */ + public Data data; + } + + public static class Data { + /** + * A list of errors resulting from building actions. + */ + @SerializedName("actions") + public List actionErrors; + } + + /** + * Extra data associated with this error, if any. + */ + @SerializedName("data") + public Data data; +} diff --git a/tx-signer/src/main/java/io/bytom/exception/BytomException.java b/tx-signer/src/main/java/io/bytom/exception/BytomException.java new file mode 100644 index 0000000..4043065 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/exception/BytomException.java @@ -0,0 +1,32 @@ +package io.bytom.exception; + +/** + * Base exception class for the Chain Core SDK. + */ +public class BytomException extends Exception { + /** + * Default constructor. + */ + public BytomException() { + super(); + } + + /** + * Initializes exception with its message attribute. + * + * @param message error message + */ + public BytomException(String message) { + super(message); + } + + /** + * Initializes a new exception while storing the original cause. + * + * @param message the error message + * @param cause the cause of the exception + */ + public BytomException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/tx-signer/src/main/java/io/bytom/exception/ConfigurationException.java b/tx-signer/src/main/java/io/bytom/exception/ConfigurationException.java new file mode 100644 index 0000000..695e873 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/exception/ConfigurationException.java @@ -0,0 +1,23 @@ +package io.bytom.exception; + +/** + * ConfigurationException wraps errors during client configuration. + */ +public class ConfigurationException extends BytomException { + /** + * Initializes exception with its message attribute. + * @param message error message + */ + public ConfigurationException(String message) { + super(message); + } + + /** + * Initializes new exception while storing original cause. + * @param message the error message + * @param cause the original cause + */ + public ConfigurationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/tx-signer/src/main/java/io/bytom/exception/ConnectivityException.java b/tx-signer/src/main/java/io/bytom/exception/ConnectivityException.java new file mode 100644 index 0000000..4f50197 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/exception/ConnectivityException.java @@ -0,0 +1,36 @@ +package io.bytom.exception; + +import com.squareup.okhttp.Response; + +import java.io.IOException; + +/** + * ConnectivityException wraps errors due to connectivity issues with the server. + */ +public class ConnectivityException extends BytomException { + /** + * Initializes exception with its message attribute. + * @param resp the server response used to create error message + */ + public ConnectivityException(Response resp) { + super(formatMessage(resp)); + } + + /** + * Parses the the server response into a detailed error message. + * @param resp the server response + * @return detailed error message + */ + private static String formatMessage(Response resp) { + String s = + "Response HTTP header field Chain-Request-ID is unset. There may be network issues. Please check your local network settings."; + // TODO(kr): include client-generated reqid here once we have that. + String body; + try { + body = resp.body().string(); + } catch (IOException ex) { + body = "[unable to read response body: " + ex.toString() + "]"; + } + return String.format("%s status=%d body=%s", s, resp.code(), body); + } +} diff --git a/tx-signer/src/main/java/io/bytom/exception/HTTPException.java b/tx-signer/src/main/java/io/bytom/exception/HTTPException.java new file mode 100644 index 0000000..04646bd --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/exception/HTTPException.java @@ -0,0 +1,23 @@ +package io.bytom.exception; + +/** + * HTTPException wraps generic HTTP errors. + */ +public class HTTPException extends BytomException { + /** + * Initializes exception with its message attribute. + * @param message error message + */ + public HTTPException(String message) { + super(message); + } + + /** + * Initializes new exception while storing original cause. + * @param message the error message + * @param cause the original cause + */ + public HTTPException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/tx-signer/src/main/java/io/bytom/exception/JSONException.java b/tx-signer/src/main/java/io/bytom/exception/JSONException.java new file mode 100644 index 0000000..a81cef6 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/exception/JSONException.java @@ -0,0 +1,39 @@ +package io.bytom.exception; + +/** + * JSONException wraps errors due to marshaling/unmarshaling json payloads. + */ +public class JSONException extends BytomException { + + /** + * Unique indentifier of the request to the server. + */ + public String requestId; + + /** + * Default constructor. + */ + public JSONException(String message) { + super(message); + } + + /** + * Initializes exception with its message and requestId attributes. + * Use this constructor in context of an API call. + * + * @param message error message + * @param requestId unique identifier of the request + */ + public JSONException(String message, String requestId) { + super(message); + this.requestId = requestId; + } + + public String getMessage() { + String message = "Message: " + super.getMessage(); + if (requestId != null) { + message += " Request-ID: " + requestId; + } + return message; + } +} diff --git a/tx-signer/src/main/java/io/bytom/http/BatchResponse.java b/tx-signer/src/main/java/io/bytom/http/BatchResponse.java new file mode 100644 index 0000000..035d900 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/http/BatchResponse.java @@ -0,0 +1,114 @@ +package io.bytom.http; + +import io.bytom.common.Utils; +import io.bytom.exception.BytomException; + +import java.util.*; + +/** + * BatchResponse provides a convenient interface for handling the results of + * batched API calls. The response contains one success or error per outgoing + * request item in the batch. Errors are always of type BytomException. + */ +public class BatchResponse { + private Map successesByIndex = new LinkedHashMap<>(); + private Map errorsByIndex = new LinkedHashMap<>(); + + public String toJson() { + return Utils.serializer.toJson(this); + } + + /** + * This constructor is used for synthetically generating a batch response + * object from a map of successes and a map of errors. It ensures that + * the successes and errors are stored in an order-preserving fashion. + */ + public BatchResponse(Map successes, Map errors) { + List successIndexes = new ArrayList<>(); + Iterator successIter = successes.keySet().iterator(); + while (successIter.hasNext()) { + successIndexes.add(successIter.next()); + } + Collections.sort(successIndexes); + + for (int i : successIndexes) { + successesByIndex.put(i, successes.get(i)); + } + + List errorIndexes = new ArrayList<>(); + Iterator errorIter = errors.keySet().iterator(); + while (errorIter.hasNext()) { + errorIndexes.add(errorIter.next()); + } + Collections.sort(errorIndexes); + for (int i : errorIndexes) { + errorsByIndex.put(i, errors.get(i)); + } + } + + /** + * Returns the total number of response objects. This should equal the number + * of request objects in the batch. + */ + public int size() { + return successesByIndex.size() + errorsByIndex.size(); + } + + /** + * Returns whether the request object at the given index produced a success. + * + * @param index the index of the request object + */ + public boolean isSuccess(int index) { + return successesByIndex.containsKey(index); + } + + /** + * Returns whether the request object at the given index produced an error. + * + * @param index the index of the request object + */ + public boolean isError(int index) { + return errorsByIndex.containsKey(index); + } + + /** + * Returns a list of successful response objects in the batch. The order of + * the list corresponds to the order of the request objects that produced the + * successes. + */ + public List successes() { + List res = new ArrayList<>(); + res.addAll(successesByIndex.values()); + return res; + } + + /** + * Returns a list of error objects in the batch. The order of the list + * corresponds to the order of the request objects that produced the + * errors. + */ + public List errors() { + List res = new ArrayList<>(); + res.addAll(errorsByIndex.values()); + return res; + } + + /** + * Returns a map of success responses, keyed by the index of the request + * object that produced the success. The set of this map's keys is mutually + * exclusive of the keys returned by errorsByIndex. + */ + public Map successesByIndex() { + return successesByIndex; + } + + /** + * Returns a map of error responses, keyed by the index of the request + * object that produced the error. The set of this map's keys is mutually + * exclusive of the keys returned by successByIndex. + */ + public Map errorsByIndex() { + return errorsByIndex; + } +} diff --git a/tx-signer/src/main/java/io/bytom/http/Client.java b/tx-signer/src/main/java/io/bytom/http/Client.java new file mode 100644 index 0000000..c416659 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/http/Client.java @@ -0,0 +1,726 @@ +package io.bytom.http; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.squareup.okhttp.*; +import io.bytom.common.Configuration; +import io.bytom.common.Utils; +import io.bytom.exception.APIException; +import io.bytom.exception.BytomException; +import io.bytom.exception.ConfigurationException; +import io.bytom.exception.JSONException; +import org.apache.log4j.Logger; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; + +import javax.net.ssl.*; +import java.io.*; +import java.lang.reflect.Type; +import java.net.MalformedURLException; +import java.net.Proxy; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +/** + * The Client object contains all information necessary to + * perform an HTTP request against a remote API. Typically, + * an application will have a client that makes requests to + * a Chain Core, and a separate Client that makes requests + * to an HSM server. + */ +public class Client { + private String url; + private String accessToken; + private OkHttpClient httpClient; + + // Used to create empty, in-memory key stores. + private static final char[] DEFAULT_KEYSTORE_PASSWORD = "123456".toCharArray(); + private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + private static String version = "dev"; // updated in the static initializer + + public static Logger logger = Logger.getLogger(Client.class); + + private static class BuildProperties { + public String version; + } + + static { + InputStream in = Client.class.getClassLoader().getResourceAsStream("properties.json"); + if (in != null) { + InputStreamReader inr = new InputStreamReader(in); + version = Utils.serializer.fromJson(inr, BuildProperties.class).version; + } + } + + public static Client generateClient() throws BytomException { + + String coreURL = Configuration.getValue("bytom.api.url"); + String accessToken = Configuration.getValue("client.access.token"); + + if (coreURL == null || coreURL.isEmpty()) { + coreURL = "http://47.91.254.104:8888"; + } + + if (coreURL.endsWith("/")) { + //split the last char "/" + coreURL = coreURL.substring(0, coreURL.length()-1); + logger.info("check the coreURL is right."); + } + + return new Client(coreURL, accessToken); + } + + public Client(Builder builder) throws ConfigurationException { + if (builder.url.endsWith("/")) { + //split the last char "/" + builder.url = builder.url.substring(0, builder.url.length()-1); + } + this.url = builder.url; + this.accessToken = builder.accessToken; + this.httpClient = buildHttpClient(builder); + } + + /** + * Create a new http Client object using the default development host URL. + */ + public Client() throws BytomException { + this(new Builder()); + } + + /** + * Create a new http Client object + * + * @param url the URL of the Chain Core or HSM + */ + public Client(String url) throws BytomException { + this(new Builder().setUrl(url)); + } + + /** + * Create a new http Client object + * + * @param url the URL of the Chain Core or HSM + * @param accessToken a Client API access token + */ + public Client(String url, String accessToken) throws BytomException { + this(new Builder().setUrl(url).setAccessToken(accessToken)); + } + + /** + * Perform a single HTTP POST request against the API for a specific action. + * + * @param action The requested API action + * @param body Body payload sent to the API as JSON + * @param tClass Type of object to be deserialized from the response JSON + * @return the result of the post request + * @throws BytomException + */ + public T request(String action, Object body, final Type tClass) throws BytomException { + ResponseCreator rc = + new ResponseCreator() { + public T create(Response response, Gson deserializer) throws IOException, BytomException { + JsonElement root = new JsonParser().parse(response.body().charStream()); + JsonElement status = root.getAsJsonObject().get("status"); + JsonElement data = root.getAsJsonObject().get("data"); + if (status != null && status.toString().contains("fail")) { + throw new BytomException(root.getAsJsonObject().get("msg").toString()); + } else if (data != null) { + return deserializer.fromJson(data, tClass); + } else { + return deserializer.fromJson(response.body().charStream(), tClass); + } + } + }; + return post(action, body, rc); + } + + /** + * Perform a single HTTP POST request against the API for a specific action, + * ignoring the body of the response. + * + * @param action The requested API action + * @param body Body payload sent to the API as JSON + * @throws BytomException + */ + public void request(String action, Object body) throws BytomException { + ResponseCreator rc = + new ResponseCreator() { + public Object create(Response response, Gson deserializer) throws IOException, BytomException { + JsonElement root = new JsonParser().parse(response.body().charStream()); + JsonElement status = root.getAsJsonObject().get("status"); + JsonElement data = root.getAsJsonObject().get("data"); + if (status != null && status.toString().contains("fail")) { + throw new BytomException(root.getAsJsonObject().get("msg").toString()); + } + return null; + } + }; + post(action, body, rc); + } + + /** + * return the value of named as key from json + * + * @param action + * @param body + * @param key + * @param tClass + * @param + * @return + * @throws BytomException + */ + public T requestGet(String action, Object body, final String key, final Type tClass) + throws BytomException { + ResponseCreator rc = new ResponseCreator() { + public T create(Response response, Gson deserializer) throws IOException, + BytomException { + JsonElement root = new JsonParser().parse(response.body().charStream()); + JsonElement status = root.getAsJsonObject().get("status"); + JsonElement data = root.getAsJsonObject().get("data"); + + if (status != null && status.toString().contains("fail")) + throw new BytomException(root.getAsJsonObject().get("msg").toString()); + else if (data != null) + return deserializer.fromJson(data.getAsJsonObject().get(key), tClass); + else + return deserializer.fromJson(response.body().charStream(), tClass); + } + }; + return post(action, body, rc); + } + + /** + * Perform a single HTTP POST request against the API for a specific action. + * Use this method if you want batch semantics, i.e., the endpoint response + * is an array of valid objects interleaved with arrays, once corresponding to + * each input object. + * + * @param action The requested API action + * @param body Body payload sent to the API as JSON + * @param tClass Type of object to be deserialized from the response JSON + * @return the result of the post request + * @throws BytomException + */ + /* + public T requestBatch(String action, Object body, final Type tClass) throws BytomException { + ResponseCreator rc = + new ResponseCreator() { + public T create(Response response, Gson deserializer) throws IOException, BytomException { + JsonElement root = new JsonParser().parse(response.body().charStream()); + JsonElement status = root.getAsJsonObject().get("status"); + JsonElement data = root.getAsJsonObject().get("data"); + if (status != null && status.toString().contains("fail")) { + throw new BytomException(root.getAsJsonObject().get("msg").toString()); + } else if (data != null) { + return deserializer.fromJson(data, tClass); + } else { + return deserializer.fromJson(response.body().charStream(), tClass); + } + } + }; + //把object转换为json对象数组(有点难) + + //根据数组的大小循环调用post()方法 + + //重写create()接口方法,对成功和失败做不同的处理 + + //调用BatchResponse(Map successes, Map errors) + //构造方法,最后返回BatchResponse实例对象 + + return post(action, body, rc); + } + */ + + + /** + * Returns true if a client access token stored in the client. + * + * @return a boolean + */ + public boolean hasAccessToken() { + return this.accessToken != null && !this.accessToken.isEmpty(); + } + + /** + * Returns the client access token (possibly null). + * + * @return the client access token + */ + public String accessToken() { + return accessToken; + } + + public String getUrl() { + return url; + } + + /** + * Pins a public key to the HTTP client. + * + * @param provider certificate provider + * @param subjPubKeyInfoHash public key hash + */ + public void pinCertificate(String provider, String subjPubKeyInfoHash) { + CertificatePinner cp = + new CertificatePinner.Builder().add(provider, subjPubKeyInfoHash).build(); + this.httpClient.setCertificatePinner(cp); + } + + /** + * Sets the default connect timeout for new connections. A value of 0 means no timeout. + * + * @param timeout the number of time units for the default timeout + * @param unit the unit of time + */ + public void setConnectTimeout(long timeout, TimeUnit unit) { + this.httpClient.setConnectTimeout(timeout, unit); + } + + /** + * Sets the default read timeout for new connections. A value of 0 means no timeout. + * + * @param timeout the number of time units for the default timeout + * @param unit the unit of time + */ + public void setReadTimeout(long timeout, TimeUnit unit) { + this.httpClient.setReadTimeout(timeout, unit); + } + + /** + * Sets the default write timeout for new connections. A value of 0 means no timeout. + * + * @param timeout the number of time units for the default timeout + * @param unit the unit of time + */ + public void setWriteTimeout(long timeout, TimeUnit unit) { + this.httpClient.setWriteTimeout(timeout, unit); + } + + /** + * Sets the proxy information for the HTTP client. + * + * @param proxy proxy object + */ + public void setProxy(Proxy proxy) { + this.httpClient.setProxy(proxy); + } + + /** + * Defines an interface for deserializing HTTP responses into objects. + * + * @param the type of object to return + */ + public interface ResponseCreator { + /** + * Deserializes an HTTP response into a Java object of type T. + * + * @param response HTTP response object + * @param deserializer json deserializer + * @return an object of type T + * @throws BytomException + * @throws IOException + */ + T create(Response response, Gson deserializer) throws BytomException, IOException; + } + + /** + * Builds and executes an HTTP Post request. + * + * @param path the path to the endpoint + * @param body the request body + * @param respCreator object specifying the response structure + * @return a response deserialized into type T + * @throws BytomException + */ + private T post(String path, Object body, ResponseCreator respCreator) + throws BytomException { + + RequestBody requestBody = RequestBody.create(this.JSON, Utils.serializer.toJson(body)); + Request req; + + BytomException exception = null; + URL endpointURL = null; + + try { + endpointURL = new URL(url + "/" + path); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + + Request.Builder builder = + new Request.Builder() + .header("User-Agent", "bytom-sdk-java/" + version) + .url(endpointURL) + .method("POST", requestBody); + if (hasAccessToken()) { + builder = builder.header("Authorization", buildCredentials()); + } + req = builder.build(); + + Response resp = null; + + T object = null; + + try { + resp = this.checkError(this.httpClient.newCall(req).execute()); + object = respCreator.create(resp, Utils.serializer); + } catch (IOException e) { + e.printStackTrace(); + } + + return object; + } + + private OkHttpClient buildHttpClient(Builder builder) throws ConfigurationException { + OkHttpClient httpClient = builder.baseHttpClient.clone(); + + try { + if (builder.trustManagers != null) { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(builder.keyManagers, builder.trustManagers, null); + httpClient.setSslSocketFactory(sslContext.getSocketFactory()); + } + } catch (GeneralSecurityException ex) { + throw new ConfigurationException("Unable to configure TLS", ex); + } + if (builder.readTimeoutUnit != null) { + httpClient.setReadTimeout(builder.readTimeout, builder.readTimeoutUnit); + } + if (builder.writeTimeoutUnit != null) { + httpClient.setWriteTimeout(builder.writeTimeout, builder.writeTimeoutUnit); + } + if (builder.connectTimeoutUnit != null) { + httpClient.setConnectTimeout(builder.connectTimeout, builder.connectTimeoutUnit); + } + if (builder.pool != null) { + httpClient.setConnectionPool(builder.pool); + } + if (builder.proxy != null) { + httpClient.setProxy(builder.proxy); + } + if (builder.cp != null) { + httpClient.setCertificatePinner(builder.cp); + } + + return httpClient; + } + + private static final Random randomGenerator = new Random(); + private static final int MAX_RETRIES = 10; + private static final int RETRY_BASE_DELAY_MILLIS = 40; + + // the max amount of time cored leader election could take + private static final int RETRY_MAX_DELAY_MILLIS = 15000; + + private static int retryDelayMillis(int retryAttempt) { + // Calculate the max delay as base * 2 ^ (retryAttempt - 1). + int max = RETRY_BASE_DELAY_MILLIS * (1 << (retryAttempt - 1)); + max = Math.min(max, RETRY_MAX_DELAY_MILLIS); + + // To incorporate jitter, use a pseudo random delay between [max/2, max] millis. + return randomGenerator.nextInt(max / 2) + max / 2 + 1; + } + + private static final int[] RETRIABLE_STATUS_CODES = { + 408, // Request Timeout + 429, // Too Many Requests + 500, // Internal Server Error + 502, // Bad Gateway + 503, // Service Unavailable + 504, // Gateway Timeout + 509, // Bandwidth Limit Exceeded + }; + + private static boolean isRetriableStatusCode(int statusCode) { + for (int i = 0; i < RETRIABLE_STATUS_CODES.length; i++) { + if (RETRIABLE_STATUS_CODES[i] == statusCode) { + return true; + } + } + return false; + } + + private Response checkError(Response response) throws BytomException { + /* + String rid = response.headers().get("Bytom-Request-ID"); + if (rid == null || rid.length() == 0) { + // Header field Bytom-Request-ID is set by the backend + // API server. If this field is set, then we can expect + // the body to be well-formed JSON. If it's not set, + // then we are probably talking to a gateway or proxy. + throw new ConnectivityException(response); + } */ + + if ((response.code() / 100) != 2) { + try { + APIException err = + Utils.serializer.fromJson(response.body().charStream(), APIException.class); + if (err.code != null) { + //err.requestId = rid; + err.statusCode = response.code(); + throw err; + } + } catch (IOException ex) { + //throw new JSONException("Unable to read body. " + ex.getMessage(), rid); + throw new JSONException("Unable to read body. "); + } + } + return response; + } + + private String buildCredentials() { + String user = ""; + String pass = ""; + if (hasAccessToken()) { + String[] parts = accessToken.split(":"); + if (parts.length >= 1) { + user = parts[0]; + } + if (parts.length >= 2) { + pass = parts[1]; + } + } + return Credentials.basic(user, pass); + } + + /** + * Overrides {@link Object#hashCode()} + * + * @return the hash code + */ + @Override + public int hashCode() { + int code = this.url.hashCode(); + if (this.hasAccessToken()) { + code = code * 31 + this.accessToken.hashCode(); + } + return code; + } + + /** + * Overrides {@link Object#equals(Object)} + * + * @param o the object to compare + * @return a boolean specifying equality + */ + @Override + public boolean equals(Object o) { + if (o == null) return false; + if (!(o instanceof Client)) return false; + + Client other = (Client) o; + if (!this.url.equalsIgnoreCase(other.url)) { + return false; + } + return Objects.equals(this.accessToken, other.accessToken); + } + + /** + * A builder class for creating client objects + */ + public static class Builder { + + private String url; + + private OkHttpClient baseHttpClient; + private String accessToken; + private CertificatePinner cp; + private KeyManager[] keyManagers; + private TrustManager[] trustManagers; + private long connectTimeout; + private TimeUnit connectTimeoutUnit; + private long readTimeout; + private TimeUnit readTimeoutUnit; + private long writeTimeout; + private TimeUnit writeTimeoutUnit; + private Proxy proxy; + private ConnectionPool pool; + + public Builder() { + this.baseHttpClient = new OkHttpClient(); + this.baseHttpClient.setFollowRedirects(false); + this.setDefaults(); + } + + public Builder(Client client) { + this.baseHttpClient = client.httpClient.clone(); + this.url = client.url; + this.accessToken = client.accessToken; + } + + private void setDefaults() { + this.setReadTimeout(30, TimeUnit.SECONDS); + this.setWriteTimeout(30, TimeUnit.SECONDS); + this.setConnectTimeout(30, TimeUnit.SECONDS); + this.setConnectionPool(50, 2, TimeUnit.MINUTES); + } + + public Builder setUrl(String url) { + this.url = url; + return this; + } + + /** + * Sets the access token for the client + * + * @param accessToken The access token for the Chain Core or HSM + */ + public Builder setAccessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + + + + + /** + * Trusts the given CA certs, and no others. Use this if you are running + * your own CA, or are using a self-signed server certificate. + * + * @param is input stream of the certificates to trust, in PEM format. + */ + public Builder setTrustedCerts(InputStream is) throws ConfigurationException { + try { + // Extract certs from PEM-encoded input. + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + Collection certificates = + certificateFactory.generateCertificates(is); + if (certificates.isEmpty()) { + throw new IllegalArgumentException("expected non-empty set of trusted certificates"); + } + + // Create a new key store and input the cert. + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, DEFAULT_KEYSTORE_PASSWORD); + int index = 0; + for (Certificate certificate : certificates) { + String certificateAlias = Integer.toString(index++); + keyStore.setCertificateEntry(certificateAlias, certificate); + } + + // Use key store to build an X509 trust manager. + KeyManagerFactory keyManagerFactory = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, DEFAULT_KEYSTORE_PASSWORD); + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException( + "Unexpected default trust managers:" + Arrays.toString(trustManagers)); + } + + this.trustManagers = trustManagers; + return this; + } catch (GeneralSecurityException | IOException ex) { + throw new ConfigurationException("Unable to configure trusted CA certs", ex); + } + } + + /** + * Trusts the given CA certs, and no others. Use this if you are running + * your own CA, or are using a self-signed server certificate. + * + * @param path The path of a file containing certificates to trust, in PEM format. + */ + public Builder setTrustedCerts(String path) throws ConfigurationException { + try (InputStream is = new FileInputStream(path)) { + return setTrustedCerts(is); + } catch (IOException ex) { + throw new ConfigurationException("Unable to configure trusted CA certs", ex); + } + } + + /** + * Sets the certificate pinner for the client + * + * @param provider certificate provider + * @param subjPubKeyInfoHash public key hash + */ + public Builder pinCertificate(String provider, String subjPubKeyInfoHash) { + this.cp = new CertificatePinner.Builder().add(provider, subjPubKeyInfoHash).build(); + return this; + } + + /** + * Sets the connect timeout for the client + * + * @param timeout the number of time units for the default timeout + * @param unit the unit of time + */ + public Builder setConnectTimeout(long timeout, TimeUnit unit) { + this.connectTimeout = timeout; + this.connectTimeoutUnit = unit; + return this; + } + + /** + * Sets the read timeout for the client + * + * @param timeout the number of time units for the default timeout + * @param unit the unit of time + */ + public Builder setReadTimeout(long timeout, TimeUnit unit) { + this.readTimeout = timeout; + this.readTimeoutUnit = unit; + return this; + } + + /** + * Sets the write timeout for the client + * + * @param timeout the number of time units for the default timeout + * @param unit the unit of time + */ + public Builder setWriteTimeout(long timeout, TimeUnit unit) { + this.writeTimeout = timeout; + this.writeTimeoutUnit = unit; + return this; + } + + /** + * Sets the proxy for the client + * + * @param proxy + */ + public Builder setProxy(Proxy proxy) { + this.proxy = proxy; + return this; + } + + /** + * Sets the connection pool for the client + * + * @param maxIdle the maximum number of idle http connections in the pool + * @param timeout the number of time units until an idle http connection in the pool is closed + * @param unit the unit of time + */ + public Builder setConnectionPool(int maxIdle, long timeout, TimeUnit unit) { + this.pool = new ConnectionPool(maxIdle, unit.toMillis(timeout)); + return this; + } + + /** + * Builds a client with all of the provided parameters. + */ + public Client build() throws ConfigurationException { + return new Client(this); + } + } +} diff --git a/tx-signer/src/main/java/io/bytom/types/AssetAmount.java b/tx-signer/src/main/java/io/bytom/types/AssetAmount.java new file mode 100644 index 0000000..04e79a8 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/types/AssetAmount.java @@ -0,0 +1,15 @@ +package io.bytom.types; + +public class AssetAmount { + + public AssetID assetID; + + public long amount; + + public AssetAmount() {} + + public AssetAmount(AssetID assetID, long amount) { + this.assetID = assetID; + this.amount = amount; + } +} diff --git a/tx-signer/src/main/java/io/bytom/types/AssetID.java b/tx-signer/src/main/java/io/bytom/types/AssetID.java new file mode 100644 index 0000000..c5d7fb7 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/types/AssetID.java @@ -0,0 +1,42 @@ +package io.bytom.types; + +import org.bouncycastle.util.encoders.Hex; + +import java.util.Objects; + +public class AssetID { + + private String hexValue; + + public AssetID() {} + + public AssetID(String hexValue) { + this.hexValue = hexValue; + } + + public AssetID(byte[] byteArray) { + this.hexValue = Hex.toHexString(byteArray); + } + + public byte[] toByteArray() { + return Hex.decode(this.hexValue); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AssetID assetID = (AssetID) o; + return Objects.equals(hexValue, assetID.hexValue); + } + + @Override + public int hashCode() { + return Objects.hash(hexValue); + } + + @Override + public String toString() { + return this.hexValue; + } +} diff --git a/tx-signer/src/main/java/io/bytom/types/Entry.java b/tx-signer/src/main/java/io/bytom/types/Entry.java new file mode 100644 index 0000000..6efb901 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/types/Entry.java @@ -0,0 +1,76 @@ +package io.bytom.types; + +import io.bytom.util.OutputUtil; +import org.bouncycastle.jcajce.provider.digest.SHA3; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Field; + +public abstract class Entry { + + public abstract String typ(); + + public abstract void writeForHash(ByteArrayOutputStream out); + + public void mustWriteForHash(ByteArrayOutputStream out, Object data) { + try { + if (data == null) { + return; + } + if (data instanceof Byte) { + OutputUtil.writeByte(out, (byte) data); + } else if (data instanceof Long) { + OutputUtil.writeLong(out, (long) data); + } else if (data instanceof byte[]) { + OutputUtil.writeVarstr(out, (byte[]) data); + } else if (data instanceof byte[][]) { + OutputUtil.writeVarstrList(out, (byte[][]) data); + } else if (data instanceof String) { + OutputUtil.writeVarstr(out, ((String) data).getBytes()); + } else if (data instanceof Hash) { + out.write(((Hash) data).toByteArray()); + } else if (data instanceof AssetID) { + out.write(((AssetID) data).toByteArray()); + } else if (data.getClass().isArray()) { + Object[] array = (Object[]) data; + OutputUtil.writeVarint(out, array.length); + for (Object obj : array) { + mustWriteForHash(out, obj); + } + } else { + Class cls = data.getClass(); + Field[] fields = cls.getFields(); + for (Field field : fields) { + mustWriteForHash(out, field.get(data)); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public Hash entryID() { + SHA3.Digest256 digest256 = new SHA3.Digest256(); + ByteArrayOutputStream hasher = new ByteArrayOutputStream(); + ByteArrayOutputStream bh = new ByteArrayOutputStream(); + try { + hasher.write("entryid:".getBytes()); + hasher.write(this.typ().getBytes()); + hasher.write(":".getBytes()); + + this.writeForHash(bh); + hasher.write(digest256.digest(bh.toByteArray())); + + return new Hash(digest256.digest(hasher.toByteArray())); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + try { + bh.close(); + hasher.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/tx-signer/src/main/java/io/bytom/types/ExpandedKeys.java b/tx-signer/src/main/java/io/bytom/types/ExpandedKeys.java new file mode 100644 index 0000000..1a61ef4 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/types/ExpandedKeys.java @@ -0,0 +1,24 @@ +package io.bytom.types; + +public class ExpandedKeys { + + private String expandedPriKey; + + private String expandedPubKey; + + public void setPriKey(String priKey) { + this.expandedPriKey = priKey; + } + + public String getPriKey() { + return this.expandedPriKey; + } + + public void setPubKey(String pubKey) { + this.expandedPubKey = pubKey; + } + + public String getPubKey() { + return this.expandedPubKey; + } +} diff --git a/tx-signer/src/main/java/io/bytom/types/Hash.java b/tx-signer/src/main/java/io/bytom/types/Hash.java new file mode 100644 index 0000000..11b3b3e --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/types/Hash.java @@ -0,0 +1,42 @@ +package io.bytom.types; + +import org.bouncycastle.util.encoders.Hex; + +import java.util.Objects; + +public class Hash { + + private String hexValue; + + public Hash() {} + + public Hash(String hexValue) { + this.hexValue = hexValue; + } + + public Hash(byte[] byteArray) { + this.hexValue = Hex.toHexString(byteArray); + } + + public byte[] toByteArray() { + return Hex.decode(this.hexValue); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Hash hash = (Hash) o; + return Objects.equals(hexValue, hash.hexValue); + } + + @Override + public int hashCode() { + return Objects.hash(hexValue); + } + + @Override + public String toString() { + return this.hexValue; + } +} diff --git a/tx-signer/src/main/java/io/bytom/types/Mux.java b/tx-signer/src/main/java/io/bytom/types/Mux.java new file mode 100644 index 0000000..760da33 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/types/Mux.java @@ -0,0 +1,33 @@ +package io.bytom.types; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; + +public class Mux extends Entry { + + public ValueSource[] sources; + + public Program program; + + public List witnessDestinations = new ArrayList<>(); + + public Mux() {} + + public Mux(ValueSource[] sources, Program program) { + this(); + this.sources = sources; + this.program = program; + } + + @Override + public String typ() { + return "mux1"; + } + + @Override + public void writeForHash(ByteArrayOutputStream out) { + mustWriteForHash(out, this.sources); + mustWriteForHash(out, this.program); + } +} diff --git a/tx-signer/src/main/java/io/bytom/types/Output.java b/tx-signer/src/main/java/io/bytom/types/Output.java new file mode 100644 index 0000000..b789614 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/types/Output.java @@ -0,0 +1,35 @@ +package io.bytom.types; + +import java.io.ByteArrayOutputStream; + +public class Output extends Entry { + + public ValueSource source; + + public Program controlProgram; + + public Integer ordinal; + + public Output() { + this.source = new ValueSource(); + this.controlProgram = new Program(); + } + + + public Output(ValueSource source, Program controlProgram, Integer ordinal) { + this.source = source; + this.controlProgram = controlProgram; + this.ordinal = ordinal; + } + + @Override + public String typ() { + return "output1"; + } + + @Override + public void writeForHash(ByteArrayOutputStream out) { + mustWriteForHash(out, this.source); + mustWriteForHash(out, this.controlProgram); + } +} diff --git a/tx-signer/src/main/java/io/bytom/types/Program.java b/tx-signer/src/main/java/io/bytom/types/Program.java new file mode 100644 index 0000000..11e5d58 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/types/Program.java @@ -0,0 +1,16 @@ +package io.bytom.types; + + +public class Program { + + public long vmVersion; + + public byte[] code; + + public Program() {} + + public Program(long vmVersion, byte[] code) { + this.vmVersion = vmVersion; + this.code = code; + } +} diff --git a/tx-signer/src/main/java/io/bytom/types/Spend.java b/tx-signer/src/main/java/io/bytom/types/Spend.java new file mode 100644 index 0000000..87c5928 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/types/Spend.java @@ -0,0 +1,33 @@ +package io.bytom.types; + +import java.io.ByteArrayOutputStream; + +public class Spend extends Entry { + + public Hash spentOutputID; + + public int ordinal; + + public ValueDestination witnessDestination; + + public byte[][] witnessArguments; + + public Spend(Hash spentOutputID, int ordinal) { + this.spentOutputID = spentOutputID; + this.ordinal = ordinal; + } + + public void setDestination(Hash id, AssetAmount val, long pos) { + this.witnessDestination = new ValueDestination(id, val, pos); + } + + @Override + public String typ() { + return "spend1"; + } + + @Override + public void writeForHash(ByteArrayOutputStream out) { + mustWriteForHash(out, this.spentOutputID); + } +} diff --git a/tx-signer/src/main/java/io/bytom/types/TxHeader.java b/tx-signer/src/main/java/io/bytom/types/TxHeader.java new file mode 100644 index 0000000..8d902a8 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/types/TxHeader.java @@ -0,0 +1,35 @@ +package io.bytom.types; + +import java.io.ByteArrayOutputStream; + +public class TxHeader extends Entry { + + public long version; + + public long serializedSize; + + public long timeRange; + + public Hash[] resultIDs; + + public TxHeader() {} + + public TxHeader(long version, long serializedSize, long timeRange, Hash[] resultIDs) { + this.version = version; + this.serializedSize = serializedSize; + this.timeRange = timeRange; + this.resultIDs = resultIDs; + } + + @Override + public String typ() { + return "txheader"; + } + + @Override + public void writeForHash(ByteArrayOutputStream out) { + mustWriteForHash(out, this.version); + mustWriteForHash(out, this.timeRange); + mustWriteForHash(out, this.resultIDs); + } +} diff --git a/tx-signer/src/main/java/io/bytom/types/ValueDestination.java b/tx-signer/src/main/java/io/bytom/types/ValueDestination.java new file mode 100644 index 0000000..9981afc --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/types/ValueDestination.java @@ -0,0 +1,18 @@ +package io.bytom.types; + +public class ValueDestination { + + public Hash ref; + + public AssetAmount value; + + public long position; + + public ValueDestination() {} + + public ValueDestination(Hash ref, AssetAmount value, long position) { + this.ref = ref; + this.value = value; + this.position = position; + } +} diff --git a/tx-signer/src/main/java/io/bytom/types/ValueSource.java b/tx-signer/src/main/java/io/bytom/types/ValueSource.java new file mode 100644 index 0000000..00899fb --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/types/ValueSource.java @@ -0,0 +1,20 @@ +package io.bytom.types; + + +public class ValueSource { + + public Hash ref; + + public AssetAmount value; + + public long position; + + public ValueSource() {} + + public ValueSource(Hash ref, AssetAmount value, long position) { + this.ref = ref; + this.value = value; + this.position = position; + } + +} diff --git a/tx-signer/src/main/java/io/bytom/util/OutputUtil.java b/tx-signer/src/main/java/io/bytom/util/OutputUtil.java new file mode 100644 index 0000000..8c6b000 --- /dev/null +++ b/tx-signer/src/main/java/io/bytom/util/OutputUtil.java @@ -0,0 +1,47 @@ +package io.bytom.util; + +import java.io.IOException; +import java.io.OutputStream; + +public final class OutputUtil { + + public static void writeByte(OutputStream out, byte data) throws IOException { + out.write(data); + } + + public static void writeLong(OutputStream out, long data) throws IOException { + for (int i = 0; i < 8; i++) { + out.write((byte) data); + data >>= 8; + } + } + + public static void writeVarint(OutputStream out, long data) throws IOException { + byte[] buf = new byte[9]; + int n = putUvarint(buf, data); + out.write(buf, 0, n); + } + + public static void writeVarstr(OutputStream out, byte[] str) throws IOException { + writeVarint(out, str.length); + out.write(str); + } + + public static void writeVarstrList(OutputStream out, byte[][] l) throws IOException { + writeVarint(out, l.length); + for (byte[] str : l) { + writeVarstr(out, str); + } + } + + private static int putUvarint(byte[] buf, long x) { + int i = 0; + while (x >= 0x80) { + buf[i] = (byte)(x | 0x80); + x >>= 7; + i++; + } + buf[i] = (byte)x; + return i + 1; + } +} diff --git a/tx-signer/src/main/resources/config.properties b/tx-signer/src/main/resources/config.properties new file mode 100644 index 0000000..e9252be --- /dev/null +++ b/tx-signer/src/main/resources/config.properties @@ -0,0 +1,8 @@ +bytom.api.url= +client.access.token= + +# bytom.api.url=http://10.100.7.47:9888/ +# client.access.token=wt:3d17dbb953cedd53353bf3f342bb2929e9505105ffeb21670e6bd00abeef3772 + +#bytom.api.url=http://127.0.0.1:9888/ +#client.access.token=sheng:49d1623f5991c62a5094e761477ddd2838dceb49c22fbf84b492a54f1df88123 diff --git a/tx-signer/src/main/resources/log4j.properties b/tx-signer/src/main/resources/log4j.properties new file mode 100644 index 0000000..b64ec56 --- /dev/null +++ b/tx-signer/src/main/resources/log4j.properties @@ -0,0 +1,20 @@ + log4j.rootLogger=debug, stdout, R + log4j.appender.stdout=org.apache.log4j.ConsoleAppender + log4j.appender.stdout.layout=org.apache.log4j.PatternLayout + + log4j.logger.org.apache.commons.httpclient=info + log4j.logger.httpclient.wire.content=info + log4j.logger.httpclient.wire.header=info + + # Pattern to output the caller's file name and line number. + log4j.appender.stdout.layout.ConversionPattern=%-4r %-5p [%d{yyyy-MM-dd HH:mm:ss}] %m%n + + log4j.appender.R=org.apache.log4j.RollingFileAppender + log4j.appender.R.File=bytom.log + log4j.appender.R.MaxFileSize= 1000KB + + # Keep one backup file + log4j.appender.R.MaxBackupIndex=1 + + log4j.appender.R.layout=org.apache.log4j.PatternLayout + log4j.appender.R.layout.ConversionPattern=%-4r %-5p [%d{yyyy-MM-dd HH:mm:ss}] %m%n \ No newline at end of file diff --git a/tx-signer/src/test/java/io/bytom/.DS_Store b/tx-signer/src/test/java/io/bytom/.DS_Store new file mode 100644 index 0000000..941feef Binary files /dev/null and b/tx-signer/src/test/java/io/bytom/.DS_Store differ diff --git a/tx-signer/src/test/java/io/bytom/api/RawTransactionTest.java b/tx-signer/src/test/java/io/bytom/api/RawTransactionTest.java new file mode 100644 index 0000000..e78f99e --- /dev/null +++ b/tx-signer/src/test/java/io/bytom/api/RawTransactionTest.java @@ -0,0 +1,122 @@ +package io.bytom.api; + +import io.bytom.exception.BytomException; +import io.bytom.http.Client; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; + +public class RawTransactionTest { + + @Test + public void testHashFn() throws BytomException { + String raw_tx = "070100010161015fc8215913a270d3d953ef431626b19a89adf38e2486bb235da732f0afed515299ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8099c4d59901000116001456ac170c7965eeac1cc34928c9f464e3f88c17d8630240b1e99a3590d7db80126b273088937a87ba1e8d2f91021a2fd2c36579f7713926e8c7b46c047a43933b008ff16ecc2eb8ee888b4ca1fe3fdf082824e0b3899b02202fb851c6ed665fcd9ebc259da1461a1e284ac3b27f5e86c84164aa518648222602013effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80bbd0ec980101160014c3d320e1dc4fe787e9f13c1464e3ea5aae96a58f00013cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8084af5f01160014bb93cdb4eca74b068321eeb84ac5d33686281b6500"; + String tx_id = "4c97d7412b04d49acc33762fc748cd0780d8b44086c229c1a6d0f2adfaaac2db"; + String input_id = "9963265eb601df48501cc240e1480780e9ed6e0c8f18fd7dd57954068c5dfd02"; + Client client = Client.generateClient(); + RawTransaction decodedTx = RawTransaction.decode(client, raw_tx); + + byte[] signedHash = decodedTx.hashFn(Hex.decode(input_id), Hex.decode(tx_id)); + System.out.println("signedHash: "+Hex.toHexString(signedHash)); + // expect: 8d2bb534c819464472a94b41cea788e97a2c9dae09a6cb3b7024a44ce5a27835 + // 8d2bb534c819464472a94b41cea788e97a2c9dae09a6cb3b7024a44ce5a27835 + } + + @Test + public void testFromJson() { +// String raw_tx = "0701dfd5c8d505010161015f0434bc790dbb3746c88fd301b9839a0f7c990bb8bdc96881d17bc2fb47525ad8ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80d0dbc3f4020101160014f54622eeb837e39d359f7530b6fbbd7256c9e73d010002013effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8c98d2b0f402011600144453a011caf735428d0291d82b186e976e286fc100013afffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff40301160014613908c28df499e3aa04e033100efaa24ca8fd0100"; + String rawTxJson = "{\n" + + " \"tx_id\": \"88367f8df0e3bbb0027b1133b3de36ab779e26af00fc256bde7228c9727d20ef\",\n" + + " \"version\": 1,\n" + + " \"size\": 236,\n" + + " \"time_range\": 1521625823,\n" + + " \"inputs\": [\n" + + " {\n" + + " \"type\": \"spend\",\n" + + " \"asset_id\": \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n" + + " \"asset_definition\": {},\n" + + " \"amount\": 100000000000,\n" + + " \"control_program\": \"0014f54622eeb837e39d359f7530b6fbbd7256c9e73d\",\n" + + " \"address\": \"sm1q74rz9m4cxl3e6dvlw5ctd7aawftvneeaqsuq3v\",\n" + + " \"spent_output_id\": \"34d739d5020d7e92477222b652e8fbe08467f5eb03700ce2ef57752930b05ff1\",\n" + + " \"input_id\": \"4ae0a25ea92e8c2749099576a234e7dfacb643597545873549c5000ba83fdd9a\"\n" + + " }\n" + + " ],\n" + + " \"outputs\": [\n" + + " {\n" + + " \"type\": \"control\",\n" + + " \"id\": \"8a511581e2fb6986abc3be3bbd842434f642db7c56a1fc5c4c7adf93c750e9a4\",\n" + + " \"position\": 0,\n" + + " \"asset_id\": \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n" + + " \"asset_definition\": {},\n" + + " \"amount\": 99959999500,\n" + + " \"control_program\": \"00144453a011caf735428d0291d82b186e976e286fc1\",\n" + + " \"address\": \"sm1qg3f6qyw27u659rgzj8vzkxrwjahzsm7pyjen5j\"\n" + + " },\n" + + " {\n" + + " \"type\": \"control\",\n" + + " \"id\": \"03b6ac529c2d1c7d422a7c063d74893e8ca2003b2b3368c27d0ede2d2f6ea3ba\",\n" + + " \"position\": 1,\n" + + " \"asset_id\": \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n" + + " \"asset_definition\": {},\n" + + " \"amount\": 500,\n" + + " \"control_program\": \"0014613908c28df499e3aa04e033100efaa24ca8fd01\",\n" + + " \"address\": \"sm1qvyus3s5d7jv782syuqe3qrh65fx23lgpzf33em\"\n" + + " }\n" + + " ],\n" + + " \"fee\": 40000000\n" + + " }"; + RawTransaction decodedTx = RawTransaction.fromJson(rawTxJson); + System.out.println(decodedTx.toJson()); + } + + @Test + public void testFromSuccessRespon() { + String successRespon = "{\n" + + " \"status\": \"success\",\n" + + " \"data\": {\n" + + " \"tx_id\": \"88367f8df0e3bbb0027b1133b3de36ab779e26af00fc256bde7228c9727d20ef\",\n" + + " \"version\": 1,\n" + + " \"size\": 236,\n" + + " \"time_range\": 1521625823,\n" + + " \"inputs\": [\n" + + " {\n" + + " \"type\": \"spend\",\n" + + " \"asset_id\": \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n" + + " \"asset_definition\": {},\n" + + " \"amount\": 100000000000,\n" + + " \"control_program\": \"0014f54622eeb837e39d359f7530b6fbbd7256c9e73d\",\n" + + " \"address\": \"sm1q74rz9m4cxl3e6dvlw5ctd7aawftvneeaqsuq3v\",\n" + + " \"spent_output_id\": \"34d739d5020d7e92477222b652e8fbe08467f5eb03700ce2ef57752930b05ff1\",\n" + + " \"input_id\": \"4ae0a25ea92e8c2749099576a234e7dfacb643597545873549c5000ba83fdd9a\"\n" + + " }\n" + + " ],\n" + + " \"outputs\": [\n" + + " {\n" + + " \"type\": \"control\",\n" + + " \"id\": \"8a511581e2fb6986abc3be3bbd842434f642db7c56a1fc5c4c7adf93c750e9a4\",\n" + + " \"position\": 0,\n" + + " \"asset_id\": \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n" + + " \"asset_definition\": {},\n" + + " \"amount\": 99959999500,\n" + + " \"control_program\": \"00144453a011caf735428d0291d82b186e976e286fc1\",\n" + + " \"address\": \"sm1qg3f6qyw27u659rgzj8vzkxrwjahzsm7pyjen5j\"\n" + + " },\n" + + " {\n" + + " \"type\": \"control\",\n" + + " \"id\": \"03b6ac529c2d1c7d422a7c063d74893e8ca2003b2b3368c27d0ede2d2f6ea3ba\",\n" + + " \"position\": 1,\n" + + " \"asset_id\": \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n" + + " \"asset_definition\": {},\n" + + " \"amount\": 500,\n" + + " \"control_program\": \"0014613908c28df499e3aa04e033100efaa24ca8fd01\",\n" + + " \"address\": \"sm1qvyus3s5d7jv782syuqe3qrh65fx23lgpzf33em\"\n" + + " }\n" + + " ],\n" + + " \"fee\": 40000000\n" + + " }\n" + + "}"; + RawTransaction decodeTx = RawTransaction.fromSuccessRespon(successRespon); + System.out.println(decodeTx.toJson()); + } + +} \ No newline at end of file diff --git a/tx-signer/src/test/java/io/bytom/api/SignTransactionTest.java b/tx-signer/src/test/java/io/bytom/api/SignTransactionTest.java new file mode 100644 index 0000000..f91201d --- /dev/null +++ b/tx-signer/src/test/java/io/bytom/api/SignTransactionTest.java @@ -0,0 +1,301 @@ +package io.bytom.api; + +import io.bytom.types.*; +import org.junit.Test; +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import org.bouncycastle.util.encoders.Hex; +/** + * Created by liqiang on 2018/10/24. + */ +public class SignTransactionTest { + + //以下为测试用的区块上的交易utxo,即output中第二个输出 + //新交易接收地址为bm1qdpc5sejdkm22uv23jwd8pg6lyqz2trz4trgxh0,需要找零 + /*{ + "id": "3b36453f7dc03b13523d6431afd7e544f60339daed52ba8fca7ebf88cd5e5939", + "version": 1, + "size": 330, + "time_range": 0, + "inputs": [ + { + "type": "spend", + "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "asset_definition": {}, + "amount": 482000000, + "control_program": "00148da6ccbc216f9019cf80d23fd2083c80e29fcba2", + "address": "bm1q3knve0ppd7gpnnuq6glayzpusr3fljazzcq0eh", + "spent_output_id": "d11967ce15741217c650bc0b9dd7a390aaedd8ea5c645266920a7d19d8be681a", + "input_id": "caae7c37f6cecce6854e6488cc389379e312acd2f7495337633501fc7f72b5f3" + } + ], + "outputs": [ + { + "type": "control", + "id": "3110bc8e7d713c17fb3dc3c9deadbfc419a25c25252c8e613d1fa54cc4d05dbd", + "position": 0, + "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "asset_definition": {}, + "amount": 281500000, + "control_program": "00145d6ba5bf0cfdb2487abd594429cd04c2ba566f9f", + "address": "bm1qt446t0cvlkeys74at9zznngyc2a9vmulcr2xy6" + }, + { + "type": "control", + "id": "db5afebb5b33aec2c46fcebb20b98fffa8c065a101f4c1789fe5491b34dc1b8f", + "position": 1, + "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "asset_definition": {}, + "amount": 200000000, + "control_program": "00140d074bc86bd388a45f1c8911a41b8f0705d9058b", + "address": "bm1qp5r5hjrt6wy2ghcu3yg6gxu0quzajpvtsm2gnc" + } + ], + "status_fail": false, + "mux_id": "0e97230a7347967764fd77c8cfa96b38ec6ff08465300a01900c645dfb694f24" + }*/ + + @Test + public void testSerializeRawTransaction() { + //组装计算program、inputID、sourceID(muxID)、txID, json数据中这些字段的值为测试值,需重新计算 + String txJson = "{\n" + + " \"tx_id\": \"0434bc790dbb3746c88fd301b9839a0f7c990bb8bdc96881d17bc2fb47525ad8\",\n" + + " \"version\": 1,\n" + + " \"size\": 236,\n" + + " \"time_range\": 1521625823,\n" + + " \"inputs\": [\n" + + " {\n" + + " \"asset_id\": \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n" + + " \"asset_definition\": {},\n" + + " \"amount\": 200000000,\n" + + " \"sourceId\": \"0e97230a7347967764fd77c8cfa96b38ec6ff08465300a01900c645dfb694f24\",\n" + + " \"sourcePosition\": 1,\n" + + " \"control_program\": \"00140d074bc86bd388a45f1c8911a41b8f0705d9058b\",\n" + + " \"address\": \"bm1qp5r5hjrt6wy2ghcu3yg6gxu0quzajpvtsm2gnc\",\n" + + " \"spent_output_id\": \"\",\n" + + " \"input_id\": \"3b36453f7dc03b13523d6431afd7e544f60339daed52ba8fca7ebf88cd5e5939\",\n" + + " \"witness_component\": \n" + + " {\n" + + " \"signatures\": []\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"outputs\": [\n" + + " {\n" + + " \"id\": \"8a511581e2fb6986abc3be3bbd842434f642db7c56a1fc5c4c7adf93c750e9a4\",\n" + + " \"position\": 0,\n" + + " \"asset_id\": \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n" + + " \"asset_definition\": {},\n" + + " \"amount\": 10000000,\n" + + " \"control_program\": \"0014687148664db6d4ae3151939a70a35f2004a58c55\",\n" + + " \"address\": \"bm1qdpc5sejdkm22uv23jwd8pg6lyqz2trz4trgxh0\"\n" + + " },\n" + + " {\n" + + " \"type\": \"control\",\n" + + " \"id\": \"03b6ac529c2d1c7d422a7c063d74893e8ca2003b2b3368c27d0ede2d2f6ea3ba\",\n" + + " \"position\": 1,\n" + + " \"asset_id\": \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n" + + " \"asset_definition\": {},\n" + + " \"amount\": 185000000,\n" + + " \"control_program\": \"00140d074bc86bd388a45f1c8911a41b8f0705d9058b\",\n" + + " \"address\": \"bm1qp5r5hjrt6wy2ghcu3yg6gxu0quzajpvtsm2gnc\"\n" + + " }\n" + + " ]\n" + + " }"; + //0701dfd5c8d50501015f015d0e97230a7347967764fd77c8cfa96b38ec6ff08465300a01900c645dfb694f24ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8084af5f01011600140d074bc86bd388a45f1c8911a41b8f0705d9058b8301024038c7df4301e070dc888b49309fa9b20266cde899069b099fe0bb562b93518e7876ea0f2676e488dd0a4946802e0ad8447521771d7163449ed222048a1443e801406488f52223be8da187f13348dadff6e829efb807540007eeaa51feefd6cc8be238eec83aff903cfd9de7a712debb5a81c96d2cb8937a4c0384c12dbd17b6e20802013cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80ade20401160014687148664db6d4ae3151939a70a35f2004a58c5500013cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0c09b58011600140d074bc86bd388a45f1c8911a41b8f0705d9058b00 + + //String result = "07 01 dfd5c8d505 01 01 5f 01 5d 0e97230a7347967764fd77c8cfa96b38ec6ff08465300a01900c645dfb694f24 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 8084af5f 01 01 16 00140d074bc86bd388a45f1c8911a41b8f0705d9058b + // serflag version timeTrange input.len assertVerison len type len sourceID assetId amount sourcePos vmVersion spendLen + + // 8301 02 40 38c7df4301e070dc888b49309fa9b20266cde899069b099fe0bb562b93518e7876ea0f2676e488dd0a4946802e0ad8447521771d7163449ed222048a1443e801 40 6488f52223be8da187f13348dadff6e829efb807540007eeaa51feefd6cc8be238eec83aff903cfd9de7a712debb5a81c96d2cb8937a4c0384c12dbd17b6e208 + // witnessLen argulen argu1Len argu1data argu2Len argu2Data + + // 02 01 3c ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 80ade20401160014687148664db6d4ae3151939a70a35f2004a58c5500 01 3c ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff c0c09b58011600140d074bc86bd388a45f1c8911a41b8f0705d9058b00 + // outputlen version serialen assetid + + txJson = "{\n" + + " \"tx_id\": \"6c846ae763c64b69040f9bad0402e1a34f5a2065f67622bc9d849a1547af0092\",\n" + + " \"version\": 1,\n" + + " \"size\": 236,\n" + + " \"time_range\": 1521625823,\n" + + " \"inputs\": [\n" + + " {\n" + + " \"asset_id\": \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n" + + " \"asset_definition\": {},\n" + + " \"amount\": 200000000,\n" + + " \"sourceId\": \"45729e2c2211128049f44d35d2ea782ef0a1cdc363bd0ca37e4028db82504499\",\n" + + " \"sourcePosition\": 1,\n" + + " \"control_program\": \"0014e18654db91f9d0f4e4a86271b189dc79b0be6adf\",\n" + + " \"address\": \"bm1quxr9fku3l8g0fe9gvfcmrzwu0xctu6klf0xf7j\",\n" + + " \"spent_output_id\": \"\",\n" + + " \"input_id\": \"3b36453f7dc03b13523d6431afd7e544f60339daed52ba8fca7ebf88cd5e5939\",\n" + + " \"witness_component\": \n" + + " {\n" + + " \"signatures\": []\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"outputs\": [\n" + + " {\n" + + " \"id\": \"8a511581e2fb6986abc3be3bbd842434f642db7c56a1fc5c4c7adf93c750e9a4\",\n" + + " \"position\": 0,\n" + + " \"asset_id\": \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n" + + " \"asset_definition\": {},\n" + + " \"amount\": 10000000,\n" + + " \"control_program\": \"0014adf7d56795abb622edaece42d5ea193463ddf06a\",\n" + + " \"address\": \"bm1q4hma2eu44wmz9mdweepdt6sex33amur25rwjre\"\n" + + " },\n" + + " {\n" + + " \"type\": \"control\",\n" + + " \"id\": \"03b6ac529c2d1c7d422a7c063d74893e8ca2003b2b3368c27d0ede2d2f6ea3ba\",\n" + + " \"position\": 1,\n" + + " \"asset_id\": \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n" + + " \"asset_definition\": {},\n" + + " \"amount\": 185000000,\n" + + " \"control_program\": \"0014e18654db91f9d0f4e4a86271b189dc79b0be6adf\",\n" + + " \"address\": \"bm1quxr9fku3l8g0fe9gvfcmrzwu0xctu6klf0xf7j\"\n" + + " }\n" + + " ]\n" + + " }"; + + SignTransaction tx = SignTransaction.fromJson(txJson); + SignTransactionImpl signImpl = new SignTransactionImpl(); + + //组装交易计算inputId,txID + signImpl.mapTransaction(tx); + //签名组装交易 + String priKey = "e089bf65a759318960225c7b03954a606c39260f4773213b044e9e2df52cb556f75e6e31880a3418d06b6c578415a3b57c0d925ca2151f5313e5afc88c324a35"; + String pubKey = "5b9adf91a5d20f4d45cabe72f21077335152b3f1acf6f7ffee9e1ed975550ed7f75e6e31880a3418d06b6c578415a3b57c0d925ca2151f5313e5afc88c324a35"; + String key = pubKey.substring(0,64); + int index = 0; + signImpl.buildWitness(tx, index, priKey, key); + //序列化 + String txSign = signImpl.serializeTransaction(tx); + + //0701dfd5c8d50501015f015d45729e2c2211128049f44d35d2ea782ef0a1cdc363bd0ca37e4028db82504499ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8084af5f0101160014e18654db91f9d0f4e4a86271b189dc79b0be6adf6302407ca47a522ae0035a3c4aaa7406c1fbecbf4fc6c66cdcffe6dddcaf309f4b2a68f1e90e78dbf88b6c9af67f451a43df338f9073741bfc75cf674573453d528c03205b9adf91a5d20f4d45cabe72f21077335152b3f1acf6f7ffee9e1ed975550ed702013cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80ade20401160014adf7d56795abb622edaece42d5ea193463ddf06a00013cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0c09b5801160014e18654db91f9d0f4e4a86271b189dc79b0be6adf00 + + System.out.print(txSign); + } + + @Test + public void testMustWriteForHash() throws Exception { + Entry entry = new Entry() { + @Override + public String typ() { + return null; + } + + @Override + public void writeForHash(ByteArrayOutputStream out) { + + } + }; + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + entry.mustWriteForHash(out, (byte) 2); + assert Hex.toHexString(out.toByteArray()).equals("02"); + + out.reset(); + entry.mustWriteForHash(out, 1L); + assert Hex.toHexString(out.toByteArray()).equals("0100000000000000"); + + out.reset(); + entry.mustWriteForHash(out, 0x3456584738473837L); + assert Hex.toHexString(out.toByteArray()).equals("3738473847585634"); + + out.reset(); + entry.mustWriteForHash(out, new byte[]{0x12, 0x34, (byte) 0x85}); + assert Hex.toHexString(out.toByteArray()).equals("03123485"); + + out.reset(); + entry.mustWriteForHash(out, new byte[][]{{0x12, 0x34, (byte) 0x85}, {(byte) 0x86, 0x17, 0x40}}); + assert Hex.toHexString(out.toByteArray()).equals("020312348503861740"); + + out.reset(); + entry.mustWriteForHash(out, "hello, 世界"); + assert Hex.toHexString(out.toByteArray()).equals("0d68656c6c6f2c20e4b896e7958c"); + + out.reset(); + entry.mustWriteForHash(out, new String[]{"hi", "你好", "hello"}); + assert Hex.toHexString(out.toByteArray()).equals("0302686906e4bda0e5a5bd0568656c6c6f"); + + out.reset(); + String hash = "d8ab56a5c9296f591db071a8b63f395e1485b12d4b105b49fee287c03888331f"; + entry.mustWriteForHash(out, new Hash(hash)); + assert Hex.toHexString(out.toByteArray()).equals(hash); + + out.reset(); + ValueSource valueSource = new ValueSource(new Hash(hash), null, 1); + Program program = new Program(1, new byte[]{1}); + Mux mux = new Mux(); + mux.sources = new ValueSource[]{valueSource}; + mux.program = program; + entry.mustWriteForHash(out, mux); + assert Hex.toHexString(out.toByteArray()).equals("01d8ab56a5c9296f591db071a8b63f395e1485b12d4b105b49fee287c03888331f010000000000000001000000000000000101"); + } + + @Test + public void testEntryID() throws Exception { + String hash = "d8ab56a5c9296f591db071a8b63f395e1485b12d4b105b49fee287c03888331f"; + ValueSource valueSource = new ValueSource(new Hash(hash), null, 1); + Program program = new Program(1, new byte[]{1}); + Mux mux = new Mux(); + mux.sources = new ValueSource[]{valueSource}; + mux.program = program; + String entryID = mux.entryID().toString(); + assert entryID.equals("ebd967df33a3373ab85521fba24c22bf993c73f46fa96254b0c86646093184e9"); + } + + @Test + public void testMapTransaction() throws Exception { + String txJson = "{\n" + + " \"tx_id\": \"\",\n" + + " \"version\": 1,\n" + + " \"size\": 236,\n" + + " \"time_range\": 1521625823,\n" + + " \"inputs\": [\n" + + " {\n" + + " \"asset_id\": \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n" + + " \"asset_definition\": {},\n" + + " \"amount\": 200000000,\n" + + " \"sourceId\": \"0e97230a7347967764fd77c8cfa96b38ec6ff08465300a01900c645dfb694f24\",\n" + + " \"sourcePosition\": 1,\n" + + " \"control_program\": \"00140d074bc86bd388a45f1c8911a41b8f0705d9058b\",\n" + + " \"address\": \"bm1qp5r5hjrt6wy2ghcu3yg6gxu0quzajpvtsm2gnc\",\n" + + " \"spent_output_id\": \"\",\n" + + " \"input_id\": \"\",\n" + + " \"witness_component\": \n" + + " {\n" + + " \"signatures\": []\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"outputs\": [\n" + + " {\n" + + " \"id\": \"\",\n" + + " \"position\": 0,\n" + + " \"asset_id\": \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n" + + " \"asset_definition\": {},\n" + + " \"amount\": 10000000,\n" + + " \"control_program\": \"0014687148664db6d4ae3151939a70a35f2004a58c55\",\n" + + " \"address\": \"bm1qdpc5sejdkm22uv23jwd8pg6lyqz2trz4trgxh0\"\n" + + " },\n" + + " {\n" + + " \"type\": \"control\",\n" + + " \"id\": \"\",\n" + + " \"position\": 1,\n" + + " \"asset_id\": \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n" + + " \"asset_definition\": {},\n" + + " \"amount\": 185000000,\n" + + " \"control_program\": \"00140d074bc86bd388a45f1c8911a41b8f0705d9058b\",\n" + + " \"address\": \"bm1qp5r5hjrt6wy2ghcu3yg6gxu0quzajpvtsm2gnc\"\n" + + " }\n" + + " ]\n" + + " }"; + + SignTransaction transaction = SignTransaction.fromJson(txJson); + SignTransactionImpl signTransactionImpl = new SignTransactionImpl(); + signTransactionImpl.mapTransaction(transaction); + assert transaction.txID.equals("4cb066e3d10e72b9c79b39109b69f142fc120e54f4a5ef3e5195a443b497f9d5"); + } +} diff --git a/tx-signer/src/test/java/io/bytom/api/SignaturesImplTest.java b/tx-signer/src/test/java/io/bytom/api/SignaturesImplTest.java new file mode 100644 index 0000000..c9ffd53 --- /dev/null +++ b/tx-signer/src/test/java/io/bytom/api/SignaturesImplTest.java @@ -0,0 +1,213 @@ +package io.bytom.api; + +import io.bytom.exception.BytomException; +import io.bytom.http.Client; +import org.apache.log4j.Logger; +import org.junit.Test; + +public class SignaturesImplTest { + + public static Logger logger = Logger.getLogger(SignaturesImplTest.class); + + @Test + // 使用build transaction 得到的 template json 来构造 Template 对象参数 + public void testSignUseTemplateJson() throws BytomException { + String[] privates = new String[1]; + privates[0] = "10fdbc41a4d3b8e5a0f50dd3905c1660e7476d4db3dbd9454fa4347500a633531c487e8174ffc0cfa76c3be6833111a9b8cd94446e37a76ee18bb21a7d6ea66b"; + + String raw_tx = "0701dfd5c8d505010161015f0434bc790dbb3746c88fd301b9839a0f7c990bb8bdc96881d17bc2fb47525ad8ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80d0dbc3f4020101160014f54622eeb837e39d359f7530b6fbbd7256c9e73d010002013effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8c98d2b0f402011600144453a011caf735428d0291d82b186e976e286fc100013afffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff40301160014613908c28df499e3aa04e033100efaa24ca8fd0100"; + Client client = Client.generateClient(); + RawTransaction rawTransaction = RawTransaction.decode(client, raw_tx); + + String json = "{\n" + + " \"raw_transaction\": \"0701dfd5c8d505010161015f0434bc790dbb3746c88fd301b9839a0f7c990bb8bdc96881d17bc2fb47525ad8ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80d0dbc3f4020101160014f54622eeb837e39d359f7530b6fbbd7256c9e73d010002013effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8c98d2b0f402011600144453a011caf735428d0291d82b186e976e286fc100013afffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff40301160014613908c28df499e3aa04e033100efaa24ca8fd0100\",\n" + + " \"signing_instructions\": [\n" + + " {\n" + + " \"position\": 0,\n" + + " \"witness_components\": [\n" + + " {\n" + + " \"type\": \"raw_tx_signature\",\n" + + " \"quorum\": 1,\n" + + " \"keys\": [\n" + + " {\n" + + " \"xpub\": \"d9c7b41f030a398dada343096040c675be48278046623849977cb0fd01d395a51c487e8174ffc0cfa76c3be6833111a9b8cd94446e37a76ee18bb21a7d6ea66b\",\n" + + " \"derivation_path\": [\n" + + " \"010400000000000000\",\n" + + " \"0100000000000000\"\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"signatures\": null\n" + + " },\n" + + " {\n" + + " \"type\": \"data\",\n" + + " \"value\": \"5024b9d7cdfe9b3ece98bc06111e06dd79d425411614bfbb473d07ca44795612\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"allow_additional_actions\": false\n" + + " }"; + Template template = Template.fromJson(json); + Signatures signatures = new SignaturesImpl(); + Template result = signatures.generateSignatures(privates, template, rawTransaction); + System.out.println(result.toJson()); + // use result's raw_transaction call sign transaction api to build another data but not need password or private key. + } + + @Test + // 使用 SDK 来构造 Template 对象参数, 单签 + public void testSignSingleKey() throws BytomException { + Client client = Client.generateClient(); + + String asset_id = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + String address = "sm1qvyus3s5d7jv782syuqe3qrh65fx23lgpzf33em"; + // build transaction obtain a Template object + Template template = new Transaction.Builder() + .addAction( + new Transaction.Action.SpendFromAccount() + .setAccountId("0G0NLBNU00A02") + .setAssetId(asset_id) + .setAmount(40000000) + ) + .addAction( + new Transaction.Action.SpendFromAccount() + .setAccountId("0G0NLBNU00A02") + .setAssetId(asset_id) + .setAmount(300000000) + ) + .addAction( + new Transaction.Action.ControlWithAddress() + .setAddress(address) + .setAssetId(asset_id) + .setAmount(30000000) + ).build(client); + logger.info("template: " + template.toJson()); + // use Template object's raw_transaction id to decode raw_transaction obtain a RawTransaction object + RawTransaction decodedTx = RawTransaction.decode(client, template.rawTransaction); + logger.info("decodeTx: " + decodedTx.toJson()); + // need a private key array + String[] privateKeys = new String[]{"10fdbc41a4d3b8e5a0f50dd3905c1660e7476d4db3dbd9454fa4347500a633531c487e8174ffc0cfa76c3be6833111a9b8cd94446e37a76ee18bb21a7d6ea66b"}; + logger.info("private key:" + privateKeys[0]); + // call offline sign method to obtain a basic offline signed template + Signatures signatures = new SignaturesImpl(); + Template basicSigned = signatures.generateSignatures(privateKeys, template, decodedTx); + logger.info("basic signed raw: " + basicSigned.toJson()); + // call sign transaction api to calculate whole raw_transaction id + // sign password is None or another random String + Template result = new Transaction.SignerBuilder().sign(client, + basicSigned, ""); + logger.info("result raw_transaction: " + result.toJson()); + // success to submit transaction + } + + @Test + // 使用 SDK 来构造 Template 对象参数, 多签 + public void testSignMultiKeys() throws BytomException { + Client client = Client.generateClient(); + + String asset_id = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + String address = "sm1qvyus3s5d7jv782syuqe3qrh65fx23lgpzf33em"; + // build transaction obtain a Template object + Template template = new Transaction.Builder() + .setTtl(10) + .addAction( + new Transaction.Action.SpendFromAccount() + .setAccountId("0G1RPP6OG0A06") + .setAssetId(asset_id) + .setAmount(40000000) + ) + .addAction( + new Transaction.Action.SpendFromAccount() + .setAccountId("0G1RPP6OG0A06") + .setAssetId(asset_id) + .setAmount(300000000) + ) + .addAction( + new Transaction.Action.ControlWithAddress() + .setAddress(address) + .setAssetId(asset_id) + .setAmount(30000000) + ).build(client); + logger.info("template: " + template.toJson()); + // use Template object's raw_transaction id to decode raw_transaction obtain a RawTransaction object + RawTransaction decodedTx = RawTransaction.decode(client, template.rawTransaction); + logger.info("decodeTx: " + decodedTx.toJson()); + // need a private key array + String[] privateKeys = new String[]{"08bdbd6c22856c5747c930f64d0e5d58ded17c4473910c6c0c3f94e485833a436247976253c8e29e961041ad8dfad9309744255364323163837cbef2483b4f67", + "40c821f736f60805ad59b1fea158762fa6355e258601dfb49dda6f672092ae5adf072d5cab2ceaaa0d68dd3fe7fa04869d95afed8c20069f446a338576901e1b"}; + logger.info("private key 1:" + privateKeys[0]); + logger.info("private key 2:" + privateKeys[1]); + // call offline sign method to obtain a basic offline signed template + Signatures signatures = new SignaturesImpl(); + Template basicSigned = signatures.generateSignatures(privateKeys, template, decodedTx); + logger.info("basic signed raw: " + basicSigned.toJson()); + // call sign transaction api to calculate whole raw_transaction id + // sign password is None or another random String + Template result = new Transaction.SignerBuilder().sign(client, + basicSigned, ""); + logger.info("result raw_transaction: " + result.toJson()); + // success to submit transaction + } + @Test + // 使用 SDK 来构造 Template 对象参数, 多签, 多输入 + public void testSignMultiKeysMultiInputs() throws BytomException { + Client client = Client.generateClient(); + + String asset_id = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + String address = "sm1qvyus3s5d7jv782syuqe3qrh65fx23lgpzf33em"; + // build transaction obtain a Template object + Template template = new Transaction.Builder() + .setTtl(10) + .addAction( + new Transaction.Action.SpendFromAccount() + .setAccountId("0G1RPP6OG0A06") + .setAssetId(asset_id) + .setAmount(40000000) + ) + .addAction( + new Transaction.Action.SpendFromAccount() + .setAccountId("0G1RPP6OG0A06") + .setAssetId(asset_id) + .setAmount(300000000) + ) + .addAction( + new Transaction.Action.SpendFromAccount() + .setAccountId("0G1Q6V1P00A02") + .setAssetId(asset_id) + .setAmount(40000000) + ) + .addAction( + new Transaction.Action.SpendFromAccount() + .setAccountId("0G1Q6V1P00A02") + .setAssetId(asset_id) + .setAmount(300000000) + ) + .addAction( + new Transaction.Action.ControlWithAddress() + .setAddress(address) + .setAssetId(asset_id) + .setAmount(60000000) + ).build(client); + logger.info("template: " + template.toJson()); + // use Template object's raw_transaction id to decode raw_transaction obtain a RawTransaction object + RawTransaction decodedTx = RawTransaction.decode(client, template.rawTransaction); + logger.info("decodeTx: " + decodedTx.toJson()); + // need a private key array + String[] privateKeys = new String[]{"08bdbd6c22856c5747c930f64d0e5d58ded17c4473910c6c0c3f94e485833a436247976253c8e29e961041ad8dfad9309744255364323163837cbef2483b4f67", + "40c821f736f60805ad59b1fea158762fa6355e258601dfb49dda6f672092ae5adf072d5cab2ceaaa0d68dd3fe7fa04869d95afed8c20069f446a338576901e1b", + "08bdbd6c22856c5747c930f64d0e5d58ded17c4473910c6c0c3f94e485833a436247976253c8e29e961041ad8dfad9309744255364323163837cbef2483b4f67"}; + logger.info("private key 1:" + privateKeys[0]); + logger.info("private key 2:" + privateKeys[1]); + // call offline sign method to obtain a basic offline signed template + Signatures signatures = new SignaturesImpl(); + Template basicSigned = signatures.generateSignatures(privateKeys, template, decodedTx); + logger.info("basic signed raw: " + basicSigned.toJson()); + // call sign transaction api to calculate whole raw_transaction id + // sign password is None or another random String + Template result = new Transaction.SignerBuilder().sign(client, + basicSigned, ""); + logger.info("result raw_transaction: " + result.toJson()); + // success to submit transaction + } +} \ No newline at end of file diff --git a/tx-signer/src/test/java/io/bytom/api/SignerTest.java b/tx-signer/src/test/java/io/bytom/api/SignerTest.java new file mode 100644 index 0000000..190967c --- /dev/null +++ b/tx-signer/src/test/java/io/bytom/api/SignerTest.java @@ -0,0 +1,24 @@ +package io.bytom.api; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; + +public class SignerTest { + + @Test + public void testEd25519InnerSign() throws NoSuchAlgorithmException, SignatureException, InvalidKeyException { + String rootXprv = "10fdbc41a4d3b8e5a0f50dd3905c1660e7476d4db3dbd9454fa4347500a633531c487e8174ffc0cfa76c3be6833111a9b8cd94446e37a76ee18bb21a7d6ea66b"; + String childXprv = "e8c0965af60563c4cabcf2e947b1cd955c4f501eb946ffc8c3447e5ec8a6335398a3720b3f96077fa187fdde48fe7dc293984b196f5e292ef8ed78fdbd8ed954"; + String expandedXprv = "e8c0965af60563c4cabcf2e947b1cd955c4f501eb946ffc8c3447e5ec8a633535b899d45316cd83e027913d3ff3dc52f6a951a686fd2b750099e1f7c70cb98c3"; + String hashedMessage = "02eda3cd8d1b0efaf7382af6ea53a429ed3ed6042998d2b4a382575248ebc922"; + byte[] sig = Signer.Ed25519InnerSign(Hex.decode(expandedXprv), Hex.decode(hashedMessage)); + System.out.println("sig:" + Hex.toHexString(sig)); + //expected: 38b11090e8dd5372018acc24ea4db2c3d82cf01ed5c69a0fae95bff2379c1630f8c8f96937b22685142b4181e6ef5072e7945c101eb81814a20d90cb1d1f0c08 + // 38b11090e8dd5372018acc24ea4db2c3d82cf01ed5c69a0fae95bff2379c1630f8c8f96937b22685142b4181e6ef5072e7945c101eb81814a20d90cb1d1f0c08 + } + +} \ No newline at end of file diff --git a/tx-signer/src/test/java/io/bytom/api/TemplateTest.java b/tx-signer/src/test/java/io/bytom/api/TemplateTest.java new file mode 100644 index 0000000..6e333cb --- /dev/null +++ b/tx-signer/src/test/java/io/bytom/api/TemplateTest.java @@ -0,0 +1,43 @@ +package io.bytom.api; + +import org.junit.Test; + + +public class TemplateTest { + + @Test + public void testFromJson() { + String json = "{\n" + + " \"raw_transaction\": \"0701dfd5c8d505010161015f0434bc790dbb3746c88fd301b9839a0f7c990bb8bdc96881d17bc2fb47525ad8ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80d0dbc3f4020101160014f54622eeb837e39d359f7530b6fbbd7256c9e73d010002013effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8c98d2b0f402011600144453a011caf735428d0291d82b186e976e286fc100013afffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff40301160014613908c28df499e3aa04e033100efaa24ca8fd0100\",\n" + + " \"signing_instructions\": [\n" + + " {\n" + + " \"position\": 0,\n" + + " \"witness_components\": [\n" + + " {\n" + + " \"type\": \"raw_tx_signature\",\n" + + " \"quorum\": 1,\n" + + " \"keys\": [\n" + + " {\n" + + " \"xpub\": \"d9c7b41f030a398dada343096040c675be48278046623849977cb0fd01d395a51c487e8174ffc0cfa76c3be6833111a9b8cd94446e37a76ee18bb21a7d6ea66b\",\n" + + " \"derivation_path\": [\n" + + " \"010400000000000000\",\n" + + " \"0100000000000000\"\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"signatures\": null\n" + + " },\n" + + " {\n" + + " \"type\": \"data\",\n" + + " \"value\": \"5024b9d7cdfe9b3ece98bc06111e06dd79d425411614bfbb473d07ca44795612\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"allow_additional_actions\": false\n" + + " }"; + Template template = Template.fromJson(json); + System.out.println(template.toJson()); + } + +} \ No newline at end of file diff --git a/tx-signer/src/test/java/io/bytom/common/DeriveXpubTest.java b/tx-signer/src/test/java/io/bytom/common/DeriveXpubTest.java new file mode 100644 index 0000000..95a5d5f --- /dev/null +++ b/tx-signer/src/test/java/io/bytom/common/DeriveXpubTest.java @@ -0,0 +1,17 @@ +package io.bytom.common; + + +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; + +public class DeriveXpubTest { + + @Test + public void testDeriveXpub() { + String hxprv = "10fdbc41a4d3b8e5a0f50dd3905c1660e7476d4db3dbd9454fa4347500a633531c487e8174ffc0cfa76c3be6833111a9b8cd94446e37a76ee18bb21a7d6ea66b"; + byte[] xpub = DeriveXpub.deriveXpub(Hex.decode(hxprv)); + System.out.println("hxpub: "+Hex.toHexString(xpub)); + //expected: d9c7b41f030a398dada343096040c675be48278046623849977cb0fd01d395a51c487e8174ffc0cfa76c3be6833111a9b8cd94446e37a76ee18bb21a7d6ea66b + // d9c7b41f030a398dada343096040c675be48278046623849977cb0fd01d395a51c487e8174ffc0cfa76c3be6833111a9b8cd94446e37a76ee18bb21a7d6ea66b + } +} \ No newline at end of file diff --git a/tx-signer/src/test/java/io/bytom/common/ExpandedPrivateKeyTest.java b/tx-signer/src/test/java/io/bytom/common/ExpandedPrivateKeyTest.java new file mode 100644 index 0000000..ceb3e76 --- /dev/null +++ b/tx-signer/src/test/java/io/bytom/common/ExpandedPrivateKeyTest.java @@ -0,0 +1,22 @@ +package io.bytom.common; + + +import io.bytom.api.SignTransactionImpl; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; + +public class ExpandedPrivateKeyTest { + + @Test + public void testExpandedKey() throws NoSuchAlgorithmException, SignatureException, InvalidKeyException { + String childXprv = "e8c0965af60563c4cabcf2e947b1cd955c4f501eb946ffc8c3447e5ec8a6335398a3720b3f96077fa187fdde48fe7dc293984b196f5e292ef8ed78fdbd8ed954"; + byte[] z = ExpandedPrivateKey.ExpandedPrivateKey(Hex.decode(childXprv)); + System.out.println(Hex.toHexString(z)); + //expect: e8c0965af60563c4cabcf2e947b1cd955c4f501eb946ffc8c3447e5ec8a633535b899d45316cd83e027913d3ff3dc52f6a951a686fd2b750099e1f7c70cb98c3 + // e8c0965af60563c4cabcf2e947b1cd955c4f501eb946ffc8c3447e5ec8a633535b899d45316cd83e027913d3ff3dc52f6a951a686fd2b750099e1f7c70cb98c3 + } +} \ No newline at end of file diff --git a/tx-signer/src/test/java/io/bytom/common/NonHardenedChildTest.java b/tx-signer/src/test/java/io/bytom/common/NonHardenedChildTest.java new file mode 100644 index 0000000..fe5dbc5 --- /dev/null +++ b/tx-signer/src/test/java/io/bytom/common/NonHardenedChildTest.java @@ -0,0 +1,43 @@ +package io.bytom.common; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; + + +public class NonHardenedChildTest { + + @Test + public void testNHChild() throws NoSuchAlgorithmException, SignatureException, InvalidKeyException { + String hxprv = "10fdbc41a4d3b8e5a0f50dd3905c1660e7476d4db3dbd9454fa4347500a633531c487e8174ffc0cfa76c3be6833111a9b8cd94446e37a76ee18bb21a7d6ea66b"; + byte[] xprv = Hex.decode(hxprv); + //expected: d9c7b41f030a398dada343096040c675be48278046623849977cb0fd01d395a51c487e8174ffc0cfa76c3be6833111a9b8cd94446e37a76ee18bb21a7d6ea66b + String[] hpaths = {"010400000000000000", "0100000000000000"}; + byte[][] paths = new byte[][]{ + Hex.decode(hpaths[0]), + Hex.decode(hpaths[1]) + }; + byte[] res = xprv; + for (int i = 0; i < hpaths.length; i++) { + byte[] xpub = DeriveXpub.deriveXpub(res); +// System.out.println("xpub: "+Hex.toHexString(xpub)); + res = NonHardenedChild.NHchild(paths[i], res, xpub); + } + System.out.println("res: "+Hex.toHexString(res)); + //expected: e8c0965af60563c4cabcf2e947b1cd955c4f501eb946ffc8c3447e5ec8a6335398a3720b3f96077fa187fdde48fe7dc293984b196f5e292ef8ed78fdbd8ed954 + // e8c0965af60563c4cabcf2e947b1cd955c4f501eb946ffc8c3447e5ec8a6335398a3720b3f96077fa187fdde48fe7dc293984b196f5e292ef8ed78fdbd8ed954 + } + + @Test + public void testChild() throws NoSuchAlgorithmException, SignatureException, InvalidKeyException { + String hxprv = "10fdbc41a4d3b8e5a0f50dd3905c1660e7476d4db3dbd9454fa4347500a633531c487e8174ffc0cfa76c3be6833111a9b8cd94446e37a76ee18bb21a7d6ea66b"; + String[] hpaths = {"010400000000000000", "0100000000000000"}; + byte[] childXprv = NonHardenedChild.child(Hex.decode(hxprv), hpaths); + System.out.println("childXprv: "+Hex.toHexString(childXprv)); + //expected: e8c0965af60563c4cabcf2e947b1cd955c4f501eb946ffc8c3447e5ec8a6335398a3720b3f96077fa187fdde48fe7dc293984b196f5e292ef8ed78fdbd8ed954 + // e8c0965af60563c4cabcf2e947b1cd955c4f501eb946ffc8c3447e5ec8a6335398a3720b3f96077fa187fdde48fe7dc293984b196f5e292ef8ed78fdbd8ed954 + } +} \ No newline at end of file