├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── pom.xml └── src └── main └── java └── iotatools ├── CurlSigning.java ├── KeyfileBuilder.java ├── Main.java ├── PeriodicCoordinator.java ├── SignedCoordinator.java ├── SnapshotSigner.java ├── TestnetCoordinator.java └── TestnetSnapshotBuilder.java /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.classpath 3 | /.project 4 | /.settings 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk 2 | MAINTAINER Johannes Innerbichler 3 | 4 | COPY ./target/iota-testnet-tools-0.1-SNAPSHOT-jar-with-dependencies.jar /usr/src/app.jar 5 | 6 | CMD ["--host=localhost", "--port=14265", "--interval=30"] 7 | ENTRYPOINT ["java", "-jar", "/usr/src/app.jar", "PeriodicCoordinator"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Michael Schierl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **NOTE** Meanwhile, the IOTA Foundation published its own official documentation how to run a private tangle, so this repo is now obsolete. New users please go to 2 | 3 | https://github.com/iotaledger/compass/blob/master/docs/HOWTO_private_tangle.md 4 | 5 | --- 6 | 7 | ## Private IOTA Testnet 8 | 9 | When you want to perform some test/auditing against the IOTA Reference implementation (IRI), you probably want to test your assumptions against your own testnet (for some scenarios one node is enough, other require more nodes). Using the public testnet has the disadvantage that you are not alone on your tangle, so somebody else might confirm transactions you want to stay unconfirmed, or trigger your breakpoints via their own transactions. There are a few obstacles with running your own private testnet, including: 10 | 11 | - The official wallet will not connect to any node which has not received at least one milestone from the coordinator 12 | - As IOTA is not mineable, your accounts will start with 0 IOTA, and you cannot change this by mining some 13 | 14 | To get around these obstacles, you need to perform these steps 15 | 16 | - Build your own Snapshot.txt and recompile IRI to use it 17 | - Once your recompiled IRI is started (or later whenever you want the milestone index to increase), run a testnet coordinator that will create a new milestone for you 18 | 19 | This repository contains tools (written in Java) to build your own Snapshot and to run a coordinator once to create a new milestone. 20 | 21 | ## Step by step instructions 22 | 23 | Get and compile this ([private-iota-testnet](https://github.com/schierlm/private-iota-testnet)) repository 24 | 25 | mvn package 26 | 27 | Now it is time to build your own Snapshot. First decide how you want to split the 2,779,530,283,277,761 available IOTA to addresses, and which of them should belong to the same wallet. 28 | 29 | Then start the interactive process: 30 | 31 | java -jar target/iota-testnet-tools-0.1-SNAPSHOT-jar-with-dependencies.jar SnapshotBuilder 32 | 33 | It will ask you about the IOTA you want to assign (you can also use suffixes like Gi or Pi). Once you have assigned all IOTA, it will write a `Snapshot.txt` (to be compiled into iri) and a `Snapshot.log` (for you to remember the seeds to the addresses). 34 | 35 | Get [iri](https://github.com/iotaledger/iri/) if you do not already have it. This process has (last) been tested with iri 1.5.4, so if you are trying a later version, you do it at your own risk. Please re-test with 1.5.4 before opening a ticket. 36 | 37 | As of iri 1.4.2.4 it is no longer required to patch and recompile iri, so you can decide by yourself if you prefer to compile it yourself or if you take the precompiled jar file. 38 | 39 | Copy `Snapshot.txt` in the same directory as `iri-1.5.4.jar`. 40 | 41 | When starting iri, make sure to include the `--testnet` switch, as well as `--testnet-no-coo-validation` to skip milestone validation, and your custom snapshot. The current directory should not contain any `testnetdb` files from previous runs. 42 | 43 | java -jar iri-1.5.4.jar --testnet --testnet-no-coo-validation --snapshot=Snapshot.txt -p 14700 44 | 45 | Before you can connect to your iri with your wallet, you need to run the coordinator to create the first milestone 46 | 47 | java -jar target/iota-testnet-tools-0.1-SNAPSHOT-jar-with-dependencies.jar Coordinator localhost 14700 48 | 49 | After that you can use your wallet, log into one of your seeds, and attach addresses until you see your full balance. 50 | 51 | In case you want a new milestone, just run the coordinator again. 52 | 53 | ***Have fun!*** 54 | 55 | ## Reducing PoW 56 | 57 | Testnet by default requires PoW for minWeightMagnitude=9. When performing larger scale tests, you might want to decrease this. To do so, no more patching of iri is needed; just pass the `--mwm` option with your desired value. 58 | Be aware that the UDP protocol between neighbors will break as the packets are not big enough to hold the full transaction hash if the hash does not end with a sufficient amount of zeroes (so best test with only a single isolated node or increase `TESTNET_PACKET_SIZE`). 59 | 60 | When using the official wallet, you also have to patch this, since the wallet does not allow MWM smaller than 13. If you (like me) have trouble recompiling the Windows wallet, you can instead patch it in-place. Have a look at `AppData\Local\Programs\iota\resources\ui\js\ui.update.js` (search for `connection.minWeightMagnitude`) and `AppData\Local\Programs\iota\resources\app.asar` (search for `var minWeightMagnitudeMinimum`). Note that the second file is a binary file, so when patching it make sure not to destroy any control characters (use a hex editor or an editor like Notepad++ that can keep them intact), and to keep the file size the same. Custom code usually passes the minWeightMagnitude as a parameter anyway. 61 | 62 | In most cases, however, it is enough to make sure you use minWeightMagnitude=9 (or 13) instead of 14 and it will be "fast enough". 63 | 64 | ## Signing snapshots and milestones 65 | 66 | iri uses Merkle-Winternitz signatures to sign snapshots and milestones, using CurlP27 and CurlP81 hash function, respectively. As the Merkle hash tree for 67 | the milestons is rather large (1048576 subkeys) and generation can take a while, this project provides a way to compute partial Merkle hash trees where 68 | only some indices (e. g. 243000 to 243100) are filled. This makes generation much faster, while still allowing to test signature verification in real-life 69 | conditions. 70 | 71 | For signed milestones, you need to change the Coordinator address using `--testnet-coordinator` switch and may also need to enable milestone signature verification (by omitting `--testnet-no-coo-validation` switch). 72 | 73 | For signed snapshots, you need to patch the [Snapshot signing public key](https://github.com/iotaledger/iri/blob/12a4bc6307426ee17ca47dd805acac634e26fff8/src/main/java/com/iota/iri/Snapshot.java#L23) in the iri source (as well as replacing the included snapshot and signature file in `src/main/resources`) before recompiling iri. 74 | 75 | 76 | Generate key files for snapshots and milestones: 77 | 78 | java -cp target/iota-testnet-tools-0.1-SNAPSHOT-jar-with-dependencies.jar iotatools.KeyfileBuilder Snapshot.key 3 CURLP81 6 1 3 79 | 80 | java -cp target/iota-testnet-tools-0.1-SNAPSHOT-jar-with-dependencies.jar iotatools.KeyfileBuilder Coordinator.key 1 CURLP27 20 243000 100 81 | 82 | The keyfile parameters are security (3 for snapshot, 1 for milestones), hash algorithm, Merkle tree depth, and optionally first used key and number of keys 83 | (to make generation faster). The pubkey is printed on the console and also in the last line of the key file. 84 | 85 | 86 | Signing a snapshot: 87 | 88 | java -cp target/iota-testnet-tools-0.1-SNAPSHOT-jar-with-dependencies.jar iotatools.SnapshotSigner Snapshot.key 1 89 | 90 | Creating a signed milestone: 91 | 92 | java -cp target/iota-testnet-tools-0.1-SNAPSHOT-jar-with-dependencies.jar iotatools.SignedCoordinator localhost 14700 93 | 94 | Always remember: With great power comes great responsibility - I do not want to see this repo end as the base of yet another Aidos Kuneen... 95 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | org.iota 5 | iota-testnet-tools 6 | 0.1-SNAPSHOT 7 | IOTA Testnet Tools 8 | Tools for running a IOTA testnet node (iri) that is not connected to other public testnet/mainnet nodes. 9 | 10 | 11 | MIT License 12 | http://www.opensource.org/licenses/mit-license.php 13 | repo 14 | 15 | 16 | 17 | 1.7 18 | UTF-8 19 | 20 | 21 | 22 | org.iota 23 | jota 24 | 0.9.10 25 | 26 | 27 | commons-cli 28 | commons-cli 29 | 1.4 30 | 31 | 32 | 33 | 34 | 35 | org.apache.maven.plugins 36 | maven-compiler-plugin 37 | 3.3 38 | 39 | ${java-version} 40 | ${java-version} 41 | ${java-version} 42 | ${java-version} 43 | 44 | 45 | 46 | org.apache.maven.plugins 47 | maven-assembly-plugin 48 | 3.1.1 49 | 50 | 51 | jar-with-dependencies 52 | 53 | 54 | 55 | iotatools.Main 56 | 57 | 58 | 59 | 60 | 61 | assemble-all 62 | package 63 | 64 | single 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | false 75 | 76 | bintray-iotaledger-maven 77 | bintray 78 | https://dl.bintray.com/iotaledger/maven 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/main/java/iotatools/CurlSigning.java: -------------------------------------------------------------------------------- 1 | package iotatools; 2 | 3 | import static jota.pow.JCurl.HASH_LENGTH; 4 | 5 | import jota.error.ArgumentException; 6 | import jota.pow.ICurl; 7 | import jota.utils.Checksum; 8 | import jota.utils.Converter; 9 | import jota.utils.IotaAPIUtils; 10 | 11 | /** 12 | * TODO: Remove me when 13 | * this 14 | * issue is fixed. 15 | */ 16 | public class CurlSigning { 17 | public final static int KEY_LENGTH = 6561; 18 | 19 | /** @see IotaAPIUtils#newAddress(String, int, int, boolean, ICurl) */ 20 | public static String newAddress(String seed, int security, int index, boolean checksum, ICurl curl) throws ArgumentException { 21 | CurlSigning signing = new CurlSigning(curl); 22 | final int[] key = signing.key(Converter.trits(seed), index, security); 23 | final int[] digests = signing.digests(key); 24 | final int[] addressTrits = signing.address(digests); 25 | String address = Converter.trytes(addressTrits); 26 | if (checksum) { 27 | address = Checksum.addChecksum(address); 28 | } 29 | return address; 30 | } 31 | 32 | private ICurl curl; 33 | 34 | public CurlSigning(ICurl curl) { 35 | this.curl = curl; 36 | } 37 | 38 | public int[] key(final int[] inSeed, final int index, int security) throws ArgumentException { 39 | int[] seed = inSeed.clone(); 40 | for (int i = 0; i < index; i++) { 41 | for (int j = 0; j < seed.length; j++) { 42 | if (++seed[j] > 1) { 43 | seed[j] = -1; 44 | } else { 45 | break; 46 | } 47 | } 48 | } 49 | curl.reset(); 50 | curl.absorb(seed, 0, seed.length); 51 | curl.squeeze(seed, 0, seed.length); 52 | curl.reset(); 53 | curl.absorb(seed, 0, seed.length); 54 | final int[] key = new int[security * HASH_LENGTH * 27]; 55 | final int[] buffer = new int[seed.length]; 56 | int offset = 0; 57 | while (security-- > 0) { 58 | for (int i = 0; i < 27; i++) { 59 | curl.squeeze(buffer, 0, seed.length); 60 | System.arraycopy(buffer, 0, key, offset, HASH_LENGTH); 61 | offset += HASH_LENGTH; 62 | } 63 | } 64 | return key; 65 | } 66 | 67 | public int[] signatureFragment(int[] normalizedBundleFragment, int[] keyFragment) { 68 | int[] signatureFragment = keyFragment.clone(); 69 | for (int i = 0; i < 27; i++) { 70 | for (int j = 0; j < 13 - normalizedBundleFragment[i]; j++) { 71 | curl.reset() 72 | .absorb(signatureFragment, i * HASH_LENGTH, HASH_LENGTH) 73 | .squeeze(signatureFragment, i * HASH_LENGTH, HASH_LENGTH); 74 | } 75 | } 76 | return signatureFragment; 77 | } 78 | 79 | public int[] address(int[] digests) { 80 | int[] address = new int[HASH_LENGTH]; 81 | curl.reset() 82 | .absorb(digests) 83 | .squeeze(address); 84 | return address; 85 | } 86 | 87 | public int[] digests(int[] key) { 88 | int security = (int) Math.floor(key.length / KEY_LENGTH); 89 | int[] digests = new int[security * HASH_LENGTH]; 90 | int[] keyFragment = new int[KEY_LENGTH]; 91 | for (int i = 0; i < Math.floor(key.length / KEY_LENGTH); i++) { 92 | System.arraycopy(key, i * KEY_LENGTH, keyFragment, 0, KEY_LENGTH); 93 | for (int j = 0; j < 27; j++) { 94 | for (int k = 0; k < 26; k++) { 95 | curl.reset() 96 | .absorb(keyFragment, j * HASH_LENGTH, HASH_LENGTH) 97 | .squeeze(keyFragment, j * HASH_LENGTH, HASH_LENGTH); 98 | } 99 | } 100 | curl.reset(); 101 | curl.absorb(keyFragment, 0, keyFragment.length); 102 | curl.squeeze(digests, i * HASH_LENGTH, HASH_LENGTH); 103 | } 104 | return digests; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/iotatools/KeyfileBuilder.java: -------------------------------------------------------------------------------- 1 | package iotatools; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.BufferedWriter; 5 | import java.io.File; 6 | import java.io.FileReader; 7 | import java.io.FileWriter; 8 | import java.io.IOException; 9 | 10 | import jota.pow.ICurl; 11 | import jota.pow.SpongeFactory; 12 | import jota.utils.Converter; 13 | import jota.utils.SeedRandomGenerator; 14 | 15 | /** 16 | * Build a keyfile for Merkle-Winternitz signature scheme. Can be used for 17 | * {@link SnapshotSigner} or {@link SigningCoordinator}. 18 | * 19 | * (c) 2017 Michael Schierl. Licensed under MIT License. 20 | */ 21 | public class KeyfileBuilder { 22 | 23 | private static final String NULL_ADDRESS = "999999999999999999999999999999999999999999999999999999999999999999999999999999999"; 24 | 25 | public static void main(String[] args) throws Exception { 26 | if (args.length < 4) { 27 | System.out.println("Usage: KeyfileBuilder [[] ]"); 28 | return; 29 | } 30 | final String filename = args[0]; 31 | final int security = Integer.parseInt(args[1]); 32 | SpongeFactory.Mode algorithm = SpongeFactory.Mode.valueOf(args[2]); 33 | final int pubkeyDepth = Integer.parseInt(args[3]); 34 | final int firstIndex, pubkeyCount; 35 | if (args.length == 4) { 36 | firstIndex = 0; 37 | pubkeyCount = 1 << pubkeyDepth; 38 | } else if (args.length == 5) { 39 | firstIndex = 0; 40 | pubkeyCount = Integer.parseInt(args[4]); 41 | } else { 42 | firstIndex = Integer.parseInt(args[4]); 43 | pubkeyCount = Integer.parseInt(args[5]); 44 | } 45 | final String seed = SeedRandomGenerator.generateNewSeed(); 46 | System.out.println("Seed: " + seed); 47 | ICurl curl = SpongeFactory.create(algorithm); 48 | try (BufferedWriter bw = new BufferedWriter(new FileWriter(filename))) { 49 | bw.write(pubkeyDepth + " " + seed); 50 | bw.newLine(); 51 | String[] keys = new String[1 << pubkeyDepth]; 52 | for (int i = 0; i < pubkeyCount; i++) { 53 | int idx = firstIndex + i; 54 | System.out.println("Generating subkey " + idx); 55 | keys[idx] = CurlSigning.newAddress(seed, security, idx, false, curl); 56 | } 57 | writeKeys(bw, keys); 58 | int[] trits = new int[243]; 59 | while (keys.length > 1) { 60 | String[] nextKeys = new String[keys.length / 2]; 61 | for (int i = 0; i < nextKeys.length; i++) { 62 | if (keys[i * 2] == null && keys[i * 2 + 1] == null) { 63 | // leave the combined key null as well 64 | continue; 65 | } 66 | curl.reset(); 67 | String k1 = keys[i * 2], k2 = keys[i * 2 + 1]; 68 | Converter.copyTrits(k1 == null ? NULL_ADDRESS : k1, trits); 69 | curl.absorb(trits); 70 | Converter.copyTrits(k2 == null ? NULL_ADDRESS : k2, trits); 71 | curl.absorb(trits); 72 | curl.squeeze(trits, 0, trits.length); 73 | nextKeys[i] = Converter.trytes(trits); 74 | } 75 | keys = nextKeys; 76 | writeKeys(bw, keys); 77 | } 78 | System.out.println("Public key: " + keys[0]); 79 | System.out.println("Keyfile created."); 80 | } 81 | } 82 | 83 | private static void writeKeys(BufferedWriter bw, String[] keys) throws IOException { 84 | int leadingNulls = 0; 85 | while (keys[leadingNulls] == null) 86 | leadingNulls++; 87 | bw.write(leadingNulls + " "); 88 | for (int i = leadingNulls; i < keys.length; i++) { 89 | if (keys[i] == null) 90 | break; 91 | bw.write(keys[i]); 92 | } 93 | bw.newLine(); 94 | } 95 | 96 | public static String[][] loadKeyfile(File keyfile, StringBuilder seedBuilder) throws IOException { 97 | try (BufferedReader br = new BufferedReader(new FileReader(keyfile))) { 98 | String[] fields = br.readLine().split(" "); 99 | int depth = Integer.parseInt(fields[0]); 100 | seedBuilder.append(fields[1]); 101 | String[][] result = new String[depth + 1][]; 102 | for (int i = 0; i <= depth; i++) { 103 | result[i] = new String[1 << (depth - i)]; 104 | fields = br.readLine().split(" "); 105 | int leadingNulls = Integer.parseInt(fields[0]); 106 | for (int j = 0; j < fields[1].length() / 81; j++) { 107 | result[i][j + leadingNulls] = fields[1].substring(j * 81, j * 81 + 81); 108 | } 109 | } 110 | return result; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/iotatools/Main.java: -------------------------------------------------------------------------------- 1 | package iotatools; 2 | 3 | import java.util.Arrays; 4 | 5 | /** 6 | * Tools needed to run a private IOTA testnet. 7 | * 8 | * (c) 2017 Michael Schierl. Licensed under MIT License. 9 | */ 10 | public class Main { 11 | 12 | public static void main(String[] args) throws Exception { 13 | if (args.length == 1 && args[0].equals("SnapshotBuilder")) { 14 | TestnetSnapshotBuilder.main(args); 15 | } else if (args.length >= 1 && args[0].equals("Coordinator")) { 16 | TestnetCoordinator.main(Arrays.copyOfRange(args, 1, args.length)); 17 | } else if (args.length >= 1 && args[0].equals("PeriodicCoordinator")) { 18 | PeriodicCoordinator.main(Arrays.copyOfRange(args, 1, args.length)); 19 | } else { 20 | System.out.println("Do you want Coordinator or SnapshotBuilder?"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/iotatools/PeriodicCoordinator.java: -------------------------------------------------------------------------------- 1 | package iotatools; 2 | 3 | import jota.IotaAPI; 4 | import jota.dto.response.GetNodeInfoResponse; 5 | import jota.dto.response.GetTransactionsToApproveResponse; 6 | import org.apache.commons.cli.*; 7 | 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.ScheduledExecutorService; 10 | import java.util.concurrent.TimeUnit; 11 | import java.util.logging.Level; 12 | import java.util.logging.Logger; 13 | 14 | import static iotatools.TestnetCoordinator.NULL_HASH; 15 | import static iotatools.TestnetCoordinator.newMilestone; 16 | 17 | public class PeriodicCoordinator { 18 | 19 | private final static Logger logger = Logger.getLogger(PeriodicCoordinator.class.getName()); 20 | 21 | private static final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); 22 | 23 | public static void main(String[] args) { 24 | 25 | // parse arguments 26 | CommandLine parameter = parseArgs(args); 27 | String host = parameter.getOptionValue("host", "localhost"); 28 | String port = parameter.getOptionValue("port", "14265"); 29 | final Integer interval = Integer.valueOf(parameter.getOptionValue("interval", "60")); 30 | final IotaAPI api = new IotaAPI.Builder().host(host).port(port).build(); 31 | 32 | Runnable task = new Runnable() { 33 | @Override 34 | public void run() { 35 | try { 36 | GetNodeInfoResponse nodeInfo = api.getNodeInfo(); 37 | int updatedMilestone = nodeInfo.getLatestMilestoneIndex() + 1; 38 | if (nodeInfo.getLatestMilestone().equals(NULL_HASH)) { 39 | newMilestone(api, NULL_HASH, NULL_HASH, updatedMilestone); 40 | } else { 41 | GetTransactionsToApproveResponse x = api.getTransactionsToApprove(10); 42 | newMilestone(api, x.getTrunkTransaction(), x.getBranchTransaction(), updatedMilestone); 43 | } 44 | logger.info("New milestone " + updatedMilestone + " created."); 45 | } 46 | catch (Exception e) { 47 | logger.log(Level.SEVERE, e.getLocalizedMessage(), e); 48 | } 49 | 50 | executor.schedule(this, interval, TimeUnit.SECONDS); 51 | } 52 | }; 53 | 54 | // start execution 55 | task.run(); 56 | } 57 | 58 | private static CommandLine parseArgs(String[] args) { 59 | Options options = new Options(); 60 | options.addOption("h", "host", true, "IRI host"); 61 | options.addOption("p", "port", true, "IRI port"); 62 | options.addOption("i", "interval", true, "Interval (seconds) for issuing new milestones"); 63 | 64 | try { 65 | CommandLineParser parser = new DefaultParser(); 66 | return parser.parse(options, args); 67 | } catch (ParseException e) { 68 | HelpFormatter formatter = new HelpFormatter(); 69 | logger.info(e.getMessage()); 70 | formatter.printHelp("utility-name", options); 71 | System.exit(1); 72 | } 73 | return null; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/iotatools/SignedCoordinator.java: -------------------------------------------------------------------------------- 1 | package iotatools; 2 | 3 | import java.io.File; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | import cfb.pearldiver.PearlDiverLocalPoW; 10 | import jota.IotaAPI; 11 | import jota.IotaLocalPoW; 12 | import jota.dto.response.GetNodeInfoResponse; 13 | import jota.dto.response.GetTransactionsToApproveResponse; 14 | import jota.model.Bundle; 15 | import jota.model.Transaction; 16 | import jota.pow.ICurl; 17 | import jota.pow.SpongeFactory; 18 | import jota.utils.Converter; 19 | 20 | /** 21 | * Run a coordinator that produces signed milestones. 22 | * 23 | * (c) 2017 Michael Schierl. Licensed under MIT License. 24 | */ 25 | public class SignedCoordinator { 26 | 27 | private static final String NULL_HASH = "999999999999999999999999999999999999999999999999999999999999999999999999999999999"; 28 | private static final String NULL_ADDRESS = "999999999999999999999999999999999999999999999999999999999999999999999999999999999"; 29 | private static final int TAG_TRINARY_SIZE = 81; 30 | 31 | public static void main(String[] args) throws Exception { 32 | if (args.length == 0) 33 | args = new String[] { "localhost", "14700" }; 34 | else if (args.length == 1) 35 | args = new String[] { "localhost", "14700", args[0] }; 36 | 37 | IotaAPI api = new IotaAPI.Builder().host(args[0]).port(args[1]).build(); 38 | GetNodeInfoResponse nodeInfo = api.getNodeInfo(); 39 | int milestone = nodeInfo.getLatestMilestoneIndex(); 40 | if (nodeInfo.getLatestMilestone().equals(NULL_HASH)) { 41 | newMilestone(api, NULL_HASH, NULL_HASH, milestone + 1); 42 | } else { 43 | GetTransactionsToApproveResponse x = api.getTransactionsToApprove(10); 44 | String secondTransaction = args.length > 2 ? args[2] : x.getBranchTransaction(); 45 | newMilestone(api, x.getTrunkTransaction(), secondTransaction, milestone + 1); 46 | } 47 | System.out.println("New milestone created."); 48 | } 49 | 50 | private static void newMilestone(IotaAPI api, String tip1, String tip2, long index) throws Exception { 51 | StringBuilder seedBuilder = new StringBuilder(); 52 | String[][] keyfile = KeyfileBuilder.loadKeyfile(new File("Coordinator.key"), seedBuilder); 53 | String seed = seedBuilder.toString(), coordinatorAddress = keyfile[keyfile.length - 1][0]; 54 | System.out.println("Coordinator address: " + coordinatorAddress); 55 | System.out.println("Milestone index: " + index); 56 | System.out.println("Public subkey: " + keyfile[0][(int) index]); 57 | int keyIndex = (int) index; 58 | StringBuilder merklePath = new StringBuilder(); 59 | for (int i = 0; i < keyfile.length - 1; i++) { 60 | String subkey = keyfile[i][keyIndex ^ 1]; 61 | merklePath.append(subkey == null ? NULL_ADDRESS : subkey); 62 | keyIndex /= 2; 63 | } 64 | String tag = Converter.trytes(Converter.trits(index, TAG_TRINARY_SIZE)); 65 | long timestamp = System.currentTimeMillis() / 1000; 66 | Bundle bundle; 67 | bundle = new Bundle(); 68 | bundle.addEntry(1, coordinatorAddress, 0, tag, timestamp); 69 | bundle.addEntry(1, NULL_ADDRESS, 0, tag, timestamp); 70 | int[] hash = new int[243]; 71 | String hashInTrytes; 72 | ICurl curl = SpongeFactory.create(SpongeFactory.Mode.KERL); 73 | curl.reset(); 74 | for (int i = 0; i < bundle.getTransactions().size(); i++) { 75 | int[] valueTrits = Converter.trits(bundle.getTransactions().get(i).getValue(), 81); 76 | int[] timestampTrits = Converter.trits(bundle.getTransactions().get(i).getTimestamp(), 27); 77 | bundle.getTransactions().get(i).setCurrentIndex(i); 78 | int[] currentIndexTrits = Converter.trits(bundle.getTransactions().get(i).getCurrentIndex(), 27); 79 | bundle.getTransactions().get(i).setLastIndex(bundle.getTransactions().size() - 1); 80 | int[] lastIndexTrits = Converter.trits(bundle.getTransactions().get(i).getLastIndex(), 27); 81 | int[] t = Converter.trits(bundle.getTransactions().get(i).getAddress() + Converter.trytes(valueTrits) + bundle.getTransactions().get(i).getObsoleteTag() + Converter.trytes(timestampTrits) + Converter.trytes(currentIndexTrits) + Converter.trytes(lastIndexTrits)); 82 | curl.absorb(t, 0, t.length); 83 | } 84 | curl.squeeze(hash, 0, hash.length); 85 | hashInTrytes = Converter.trytes(hash); 86 | for (int i = 0; i < bundle.getTransactions().size(); i++) { 87 | bundle.getTransactions().get(i).setBundle(hashInTrytes); 88 | } 89 | bundle.addTrytes(Collections. emptyList()); 90 | List trytes = new ArrayList<>(); 91 | for (Transaction trx : bundle.getTransactions()) { 92 | trytes.add(trx.toTrytes()); 93 | } 94 | Collections.reverse(trytes); 95 | String[] trytes1 = (String[]) trytes.toArray(new String[2]); 96 | final String[] resultTrytes = new String[2]; 97 | IotaLocalPoW localPoW = new PearlDiverLocalPoW(); 98 | CurlSigning signing = new CurlSigning(SpongeFactory.create(SpongeFactory.Mode.CURLP27)); 99 | Transaction txn1 = new Transaction(trytes1[0]); 100 | txn1.setSignatureFragments(merklePath.append(txn1.getSignatureFragments().substring(merklePath.length())).toString()); 101 | txn1.setTrunkTransaction(tip1); 102 | txn1.setBranchTransaction(tip2); 103 | if (txn1.getTag().isEmpty() || txn1.getTag().matches("9*")) 104 | txn1.setTag(txn1.getObsoleteTag()); 105 | txn1.setAttachmentTimestamp(System.currentTimeMillis()); 106 | txn1.setAttachmentTimestampLowerBound(0); 107 | txn1.setAttachmentTimestampUpperBound(3_812_798_742_493L); 108 | resultTrytes[0] = localPoW.performPoW(txn1.toTrytes(), 9); 109 | String previousTransaction = new Transaction(resultTrytes[0]).getHash(); 110 | System.out.println("Signed trunk transaction: " + previousTransaction); 111 | Transaction txn2 = new Transaction(trytes1[1]); 112 | int[] normalizedBundle = bundle.normalizedBundle(previousTransaction); 113 | final int[] key = signing.key(Converter.trits(seed), (int) index, 1); 114 | if (!keyfile[0][(int) index].equals(Converter.trytes(signing.address(signing.digests(key))))) { 115 | System.out.println("Keyfile corrupted"); 116 | return; 117 | } 118 | txn2.setSignatureFragments(Converter.trytes(signing.signatureFragment(Arrays.copyOfRange(normalizedBundle, 0, 27), key))); 119 | txn2.setTrunkTransaction(previousTransaction); 120 | txn2.setBranchTransaction(tip1); 121 | if (txn2.getTag().isEmpty() || txn2.getTag().matches("9*")) 122 | txn2.setTag(txn2.getObsoleteTag()); 123 | txn2.setAttachmentTimestamp(System.currentTimeMillis()); 124 | txn2.setAttachmentTimestampLowerBound(0); 125 | txn2.setAttachmentTimestampUpperBound(3_812_798_742_493L); 126 | resultTrytes[1] = localPoW.performPoW(txn2.toTrytes(), 9); 127 | api.storeTransactions(resultTrytes); 128 | api.broadcastTransactions(resultTrytes); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/iotatools/SnapshotSigner.java: -------------------------------------------------------------------------------- 1 | package iotatools; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.BufferedWriter; 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.FileOutputStream; 8 | import java.io.InputStreamReader; 9 | import java.io.OutputStreamWriter; 10 | import java.util.Arrays; 11 | 12 | import jota.model.Bundle; 13 | import jota.pow.ICurl; 14 | import jota.pow.SpongeFactory; 15 | import jota.utils.Converter; 16 | 17 | /** 18 | * Sign a snapshot with a key built with {@link KeyfileBuilder}. 19 | * 20 | * (c) 2017 Michael Schierl. Licensed under MIT License. 21 | */ 22 | public class SnapshotSigner { 23 | 24 | private static final String NULL_ADDRESS = "999999999999999999999999999999999999999999999999999999999999999999999999999999999"; 25 | 26 | private static final String TRYTE_ALPHABET = "9ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 27 | 28 | public static void main(String[] args) throws Exception { 29 | if (args.length < 2) { 30 | System.out.println("Usage: SnapshotSigner "); 31 | return; 32 | } 33 | StringBuilder seedBuilder = new StringBuilder(); 34 | String[][] keyfile = KeyfileBuilder.loadKeyfile(new File(args[0]), seedBuilder); 35 | int keyIndex = Integer.parseInt(args[1]); 36 | String seed = seedBuilder.toString(); 37 | ICurl kerl = SpongeFactory.create(SpongeFactory.Mode.KERL); 38 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("Snapshot.txt")))) { 39 | int[] trits = new int[243 * 3]; 40 | String line; 41 | while ((line = reader.readLine()) != null) { 42 | StringBuilder sb = new StringBuilder(80); 43 | for (int i = 0; i < line.length(); i++) { 44 | int asciiValue = line.charAt(i); 45 | int firstValue = asciiValue % 27; 46 | int secondValue = (asciiValue - firstValue) / 27; 47 | sb.append(TRYTE_ALPHABET.charAt(firstValue)); 48 | sb.append(TRYTE_ALPHABET.charAt(secondValue)); 49 | } 50 | Converter.copyTrits(sb.toString(), trits); 51 | kerl.absorb(trits); 52 | Arrays.fill(trits, 0); 53 | } 54 | } 55 | int[] trits = new int[243]; 56 | kerl.squeeze(trits); 57 | System.out.println("Snapshot hash: " + Converter.trytes(trits)); 58 | System.out.println("Public subkey: " + keyfile[0][keyIndex]); 59 | int[] bundle = new Bundle().normalizedBundle(Converter.trytes(trits)); 60 | try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("Snapshot.sig")))) { 61 | CurlSigning signing = new CurlSigning(SpongeFactory.create(SpongeFactory.Mode.CURLP81)); 62 | final int[] key = signing.key(Converter.trits(seed), keyIndex, 3); 63 | if (!keyfile[0][keyIndex].equals(Converter.trytes(signing.address(signing.digests(key))))) { 64 | System.out.println("Keyfile corrupted"); 65 | return; 66 | } 67 | for (int i = 0; i < 3; i++) { 68 | bw.write(Converter.trytes(signing.signatureFragment(Arrays.copyOfRange(bundle, i * 27, (i + 1) * 27), Arrays.copyOfRange(key, i * 6561, (i + 1) * 6561)))); 69 | bw.newLine(); 70 | } 71 | for (int i = 0; i < keyfile.length - 1; i++) { 72 | String subkey = keyfile[i][keyIndex ^ 1]; 73 | bw.write(subkey == null ? NULL_ADDRESS : subkey); 74 | keyIndex /= 2; 75 | } 76 | bw.newLine(); 77 | } 78 | System.out.println("Snapshot.sig created."); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/iotatools/TestnetCoordinator.java: -------------------------------------------------------------------------------- 1 | package iotatools; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.Collections; 6 | import java.util.HashSet; 7 | import java.util.List; 8 | import java.util.Set; 9 | 10 | import org.apache.commons.cli.*; 11 | 12 | import jota.IotaAPI; 13 | import jota.dto.response.GetAttachToTangleResponse; 14 | import jota.dto.response.GetNodeInfoResponse; 15 | import jota.dto.response.GetTransactionsToApproveResponse; 16 | import jota.model.Bundle; 17 | import jota.model.Transaction; 18 | import jota.utils.Converter; 19 | 20 | /** 21 | * Run a coordinator for IOTA testnet. Whenever this coordinator is run, the 22 | * milestone index will be incremented by one. 23 | * 24 | * (c) 2017 Michael Schierl. Licensed under MIT License. 25 | */ 26 | public class TestnetCoordinator { 27 | 28 | public static final String NULL_HASH = "999999999999999999999999999999999999999999999999999999999999999999999999999999999"; 29 | public static final String TESTNET_COORDINATOR_ADDRESS = "EQQFCZBIHRHWPXKMTOLMYUYPCN9XLMJPYZVFJSAY9FQHCCLWTOLLUGKKMXYFDBOOYFBLBI9WUEILGECYM"; 30 | public static final String NULL_ADDRESS = "999999999999999999999999999999999999999999999999999999999999999999999999999999999"; 31 | public static final int TAG_TRINARY_SIZE = 81; 32 | 33 | public static void main(String[] args) throws Exception { 34 | if (args.length == 0) 35 | args = new String[] { "localhost", "14700" }; 36 | else if (args.length == 1) 37 | args = new String[] { "localhost", "14700", args[0] }; 38 | 39 | IotaAPI api = new IotaAPI.Builder().host(args[0]).port(args[1]).build(); 40 | GetNodeInfoResponse nodeInfo = api.getNodeInfo(); 41 | int milestone = nodeInfo.getLatestMilestoneIndex(); 42 | String newMilestoneHash; 43 | if (nodeInfo.getLatestMilestone().equals(NULL_HASH)) { 44 | // As of 1.4.2.4, at least two milestones are required so that the latest solid subtangle milestone gets updated. 45 | newMilestoneHash = newMilestone(api, NULL_HASH, NULL_HASH, milestone + 1); 46 | newMilestoneHash = newMilestone(api, newMilestoneHash, newMilestoneHash, milestone + 2); 47 | } else if (nodeInfo.getLatestSolidSubtangleMilestone().equals(NULL_HASH)) { 48 | newMilestoneHash = newMilestone(api, NULL_HASH, NULL_HASH, milestone + 1); 49 | } else { 50 | GetTransactionsToApproveResponse x = api.getTransactionsToApprove(10); 51 | String secondTransaction = args.length > 2 ? args[2] : x.getBranchTransaction(); 52 | String firstTransaction = x.getTrunkTransaction(); 53 | boolean firstTransactionConfirmingMilestone = isConfirming(api, firstTransaction, nodeInfo.getLatestMilestone()); 54 | boolean secondTransactionConfirmingMilestone = isConfirming(api, secondTransaction, nodeInfo.getLatestMilestone()); 55 | if (!firstTransactionConfirmingMilestone && !secondTransactionConfirmingMilestone) { 56 | System.out.println("Oops, it happened that the tips do not confirm the previous milestone :-)"); 57 | firstTransaction = nodeInfo.getLatestMilestone(); 58 | } 59 | newMilestoneHash = newMilestone(api, firstTransaction, secondTransaction, milestone + 1); 60 | } 61 | System.out.println("New milestone "+newMilestoneHash+" created."); 62 | } 63 | 64 | private static boolean isConfirming(IotaAPI api, String confirming, String confirmed) throws Exception { 65 | // getInclusionStates is broken for non-milestones, therefore approximate the result by a (depth-limited) breadth-first search. 66 | if (confirming.equals(confirmed)) 67 | return true; 68 | Set currentDepth = new HashSet<>(); 69 | Set seen = new HashSet<>(); 70 | currentDepth.add(confirming); 71 | seen.add(confirming); 72 | for (int depth = 0; depth < 10; depth++) { 73 | Set nextDepth = new HashSet<>(); 74 | for (String hash : currentDepth) { 75 | if (hash.equals(confirmed)) 76 | return true; 77 | if (hash.equals(NULL_HASH)) 78 | continue; 79 | Transaction tx = new Transaction(api.getTrytes(hash).getTrytes()[0]); 80 | if (seen.add(tx.getTrunkTransaction())) 81 | nextDepth.add(tx.getTrunkTransaction()); 82 | if (seen.add(tx.getBranchTransaction())) 83 | nextDepth.add(tx.getBranchTransaction()); 84 | } 85 | currentDepth = nextDepth; 86 | } 87 | // not found, so we assume it is not 88 | return false; 89 | } 90 | 91 | static String newMilestone(IotaAPI api, String tip1, String tip2, long index) throws Exception { 92 | final Bundle bundle = new Bundle(); 93 | String tag = Converter.trytes(Converter.trits(index, TAG_TRINARY_SIZE)); 94 | long timestamp = System.currentTimeMillis() / 1000; 95 | bundle.addEntry(1, TESTNET_COORDINATOR_ADDRESS, 0, tag, timestamp); 96 | bundle.addEntry(1, NULL_ADDRESS, 0, tag, timestamp); 97 | bundle.finalize(null); 98 | bundle.addTrytes(Collections. emptyList()); 99 | List trytes = new ArrayList<>(); 100 | for (Transaction trx : bundle.getTransactions()) { 101 | trytes.add(trx.toTrytes()); 102 | } 103 | Collections.reverse(trytes); 104 | GetAttachToTangleResponse rrr = api.attachToTangle(tip1, tip2, 9, (String[]) trytes.toArray(new String[trytes.size()])); 105 | String[] finalTrytes = rrr.getTrytes(); 106 | api.storeTransactions(finalTrytes); 107 | api.broadcastTransactions(finalTrytes); 108 | return new Transaction(finalTrytes[0]).getHash(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/iotatools/TestnetSnapshotBuilder.java: -------------------------------------------------------------------------------- 1 | package iotatools; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.File; 5 | import java.io.FileOutputStream; 6 | import java.io.InputStreamReader; 7 | import java.io.OutputStream; 8 | import java.io.PrintStream; 9 | import java.nio.charset.StandardCharsets; 10 | 11 | import org.apache.commons.io.output.TeeOutputStream; 12 | 13 | import jota.utils.Checksum; 14 | import jota.utils.IotaAPIUtils; 15 | import jota.utils.SeedRandomGenerator; 16 | 17 | /** 18 | * Build a snapshot for IOTA testnet. Balances are read interactively from 19 | * stdin. 20 | * 21 | * (c) 2017 Michael Schierl. Licensed under MIT License. 22 | */ 23 | public class TestnetSnapshotBuilder { 24 | public static void main(String[] args) throws Exception { 25 | long remainingIOTA = 2_779_530_283_277_761L; 26 | try (OutputStream logStream = new FileOutputStream("Snapshot.log"); 27 | OutputStream snapshotStream = new FileOutputStream("Snapshot.txt"); 28 | BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 29 | PrintStream out = new PrintStream(new TeeOutputStream(System.out, logStream), true)) { 30 | 31 | out.println("Generating new Snapshot.txt file..."); 32 | out.println("Enter amounts to be stored on the given addresses."); 33 | out.println("Amounts can be given as numbers, or with suffixes Ki Mi Gi Ti Pi."); 34 | out.println("Hit Return without entering an amount to switch to another seed."); 35 | out.println("Hit return for the first address of a seed assign the remaining IOTA"); 36 | out.println("and finish the process."); 37 | 38 | out.println(); 39 | out.println("Remaining IOTA: "+remainingIOTA); 40 | 41 | while (remainingIOTA > 0) { 42 | out.println(); 43 | String seed = SeedRandomGenerator.generateNewSeed(); 44 | out.println("Seed: " + seed); 45 | for (int i = 0;; i++) { 46 | String addr = IotaAPIUtils.newAddress(seed, 2, i, false, null); 47 | out.print(" " + Checksum.addChecksum(addr) + ": "); 48 | out.flush(); 49 | String line = in.readLine().trim(); 50 | logStream.write((line + System.lineSeparator()).getBytes()); 51 | logStream.flush(); 52 | if (line.isEmpty() && i == 0) { 53 | out.println(" " + Checksum.addChecksum(addr) + ": "+remainingIOTA); 54 | snapshotStream.write((addr + ";" + remainingIOTA + "\n").getBytes(StandardCharsets.ISO_8859_1)); 55 | remainingIOTA = 0; 56 | } 57 | if (line.isEmpty()) 58 | break; 59 | if (line.toLowerCase().endsWith("i")) 60 | line = line.substring(0, line.length() - 1).trim(); 61 | int suffix = "KMGTP".indexOf(line.toUpperCase().charAt(line.length() - 1)); 62 | long value; 63 | if (suffix >= 0) { 64 | value = Long.parseLong(line.substring(0, line.length() - 1).trim()); 65 | for (int j = 0; j < suffix + 1; j++) { 66 | value *= 1000; 67 | } 68 | } else { 69 | value = Long.parseLong(line); 70 | } 71 | if (value > remainingIOTA) { 72 | snapshotStream.close(); 73 | new File("Snapshot.txt").delete(); 74 | throw new NumberFormatException("Cheater. You do not have that many IOTA any more..."); 75 | } 76 | snapshotStream.write((addr + ";" + value + "\n").getBytes(StandardCharsets.ISO_8859_1)); 77 | remainingIOTA -= value; 78 | } 79 | } 80 | out.println(); 81 | out.println("Done."); 82 | } 83 | } 84 | } 85 | --------------------------------------------------------------------------------