diff --git a/src/main/java/net/helix/pendulum/Pendulum.java b/src/main/java/net/helix/pendulum/Pendulum.java
index a32991d8..1734dd8a 100644
--- a/src/main/java/net/helix/pendulum/Pendulum.java
+++ b/src/main/java/net/helix/pendulum/Pendulum.java
@@ -11,11 +11,7 @@
import net.helix.pendulum.network.replicator.Replicator;
import net.helix.pendulum.service.TipsSolidifier;
import net.helix.pendulum.service.ledger.impl.LedgerServiceImpl;
-import net.helix.pendulum.service.milestone.impl.LatestSolidMilestoneTrackerImpl;
-import net.helix.pendulum.service.milestone.impl.MilestoneServiceImpl;
-import net.helix.pendulum.service.milestone.impl.MilestoneSolidifierImpl;
-import net.helix.pendulum.service.milestone.impl.MilestoneTrackerImpl;
-import net.helix.pendulum.service.milestone.impl.SeenMilestonesRetrieverImpl;
+import net.helix.pendulum.service.milestone.impl.*;
import net.helix.pendulum.service.snapshot.SnapshotException;
import net.helix.pendulum.service.snapshot.impl.LocalSnapshotManagerImpl;
import net.helix.pendulum.service.snapshot.impl.SnapshotProviderImpl;
@@ -106,6 +102,7 @@ public class Pendulum {
public final MilestoneSolidifierImpl milestoneSolidifier;
public final CandidateSolidifierImpl candidateSolidifier;
public final TransactionRequesterWorkerImpl transactionRequesterWorker;
+ public final VirtualTransactionServiceImpl virtualTransactionService;
public final Tangle tangle;
public final TransactionValidator transactionValidator;
@@ -153,7 +150,7 @@ public Pendulum(PendulumConfig configuration) throws TransactionPruningException
? new AsyncTransactionPruner()
: null;
transactionRequesterWorker = new TransactionRequesterWorkerImpl();
-
+ virtualTransactionService = new VirtualTransactionServiceImpl();
// legacy code
bundleValidator = new BundleValidator();
tangle = new Tangle();
@@ -161,7 +158,7 @@ public Pendulum(PendulumConfig configuration) throws TransactionPruningException
transactionRequester = new TransactionRequester(tangle, snapshotProvider);
transactionValidator = new TransactionValidator(tangle, snapshotProvider, tipsViewModel, transactionRequester, configuration);
node = new Node(tangle, snapshotProvider, transactionValidator, transactionRequester, tipsViewModel,
- latestMilestoneTracker, configuration);
+ latestMilestoneTracker, virtualTransactionService, configuration);
replicator = new Replicator(node, configuration);
udpReceiver = new UDPReceiver(node, configuration);
tipsSolidifier = new TipsSolidifier(tangle, transactionValidator, tipsViewModel, configuration);
@@ -235,6 +232,7 @@ private void injectDependencies() throws SnapshotException, TransactionPruningEx
localSnapshotManager.init(snapshotProvider, snapshotService, transactionPruner, configuration);
}
milestoneService.init(tangle, snapshotProvider, snapshotService, transactionValidator, configuration);
+ virtualTransactionService.init(tangle, snapshotProvider, transactionValidator);
validatorManagerService.init(tangle, snapshotProvider, snapshotService, configuration);
candidateTracker.init(tangle, snapshotProvider, validatorManagerService, candidateSolidifier, configuration);
latestMilestoneTracker.init(tangle, snapshotProvider, milestoneService, milestoneSolidifier, candidateTracker, configuration);
diff --git a/src/main/java/net/helix/pendulum/SignedFiles.java b/src/main/java/net/helix/pendulum/SignedFiles.java
index 415ba60f..0a4c96b0 100644
--- a/src/main/java/net/helix/pendulum/SignedFiles.java
+++ b/src/main/java/net/helix/pendulum/SignedFiles.java
@@ -1,20 +1,16 @@
package net.helix.pendulum;
-import net.helix.pendulum.crypto.Merkle;
import net.helix.pendulum.crypto.Sha3;
import net.helix.pendulum.crypto.Sponge;
import net.helix.pendulum.crypto.SpongeFactory;
import net.helix.pendulum.crypto.Winternitz;
+import net.helix.pendulum.crypto.merkle.MerkleTree;
+import net.helix.pendulum.crypto.merkle.impl.MerkleTreeImpl;
import net.helix.pendulum.model.Hash;
import org.apache.commons.lang3.ArrayUtils;
import org.bouncycastle.util.encoders.Hex;
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.UncheckedIOException;
+import java.io.*;
import java.util.Arrays;
public class SignedFiles {
@@ -31,7 +27,7 @@ private static boolean validateSignature(String signatureFilename, String public
byte[] bundle = Winternitz.normalizedBundle(digest);
byte[] root;
int i;
-
+ MerkleTree merkle = new MerkleTreeImpl();
try (InputStream inputStream = SignedFiles.class.getResourceAsStream(signatureFilename);
BufferedReader reader = new BufferedReader((inputStream == null)
? new FileReader(signatureFilename) : new InputStreamReader(inputStream))) {
@@ -46,7 +42,7 @@ private static boolean validateSignature(String signatureFilename, String public
if ((line = reader.readLine()) != null) {
byte[] lineBytes = Hex.decode(line);
- root = Merkle.getMerkleRoot(mode, Winternitz.address(mode, digests), lineBytes, 0, index, depth);
+ root = merkle.getMerkleRoot(mode, Winternitz.address(mode, digests), lineBytes, 0, index, depth);
} else {
root = Winternitz.address(mode, digests);
@@ -75,7 +71,7 @@ private static byte[] digestFile(String filename, Sponge sha3) throws IOExceptio
messageBytes = Hash.NULL_HASH.bytes();
}
int requiredLength = (int) Math.ceil(messageBytes.length / 32.0) * 32;
- byte[] finalizedMessage = Merkle.padding(messageBytes, requiredLength);
+ byte[] finalizedMessage = MerkleTree.padding(messageBytes, requiredLength);
// crypto snapshot message
sha3.absorb(finalizedMessage, 0, finalizedMessage.length);
byte[] signature = new byte[Sha3.HASH_LENGTH];
diff --git a/src/main/java/net/helix/pendulum/TransactionValidator.java b/src/main/java/net/helix/pendulum/TransactionValidator.java
index fda7aabf..ed81c572 100644
--- a/src/main/java/net/helix/pendulum/TransactionValidator.java
+++ b/src/main/java/net/helix/pendulum/TransactionValidator.java
@@ -144,9 +144,6 @@ public int getMinWeightMagnitude() {
*/
private boolean hasInvalidTimestamp(TransactionViewModel transactionViewModel) {
// ignore invalid timestamps for transactions that were requested by our node while solidifying a milestone
- if(transactionRequester.isTransactionRequested(transactionViewModel.getHash(), true)) {
- return false;
- }
if (transactionViewModel.getAttachmentTimestamp() == 0) {
return transactionViewModel.getTimestamp() < snapshotProvider.getInitialSnapshot().getTimestamp() && !snapshotProvider.getInitialSnapshot().hasSolidEntryPoint(transactionViewModel.getHash())
|| transactionViewModel.getTimestamp() > (System.currentTimeMillis() / 1000) + MAX_TIMESTAMP_FUTURE;
@@ -155,6 +152,14 @@ private boolean hasInvalidTimestamp(TransactionViewModel transactionViewModel) {
|| transactionViewModel.getAttachmentTimestamp() > System.currentTimeMillis() + MAX_TIMESTAMP_FUTURE_MS;
}
+ private boolean isTransactionRequested(TransactionViewModel transactionViewModel) {
+ if(transactionRequester.isTransactionRequested(transactionViewModel.getHash(), true)) {
+ //todo if is virtual compute it locally
+ return true;
+ }
+ return false;
+ }
+
/**
* Runs the following validation checks on a transaction:
*
@@ -174,15 +179,17 @@ private boolean hasInvalidTimestamp(TransactionViewModel transactionViewModel) {
public void runValidation(TransactionViewModel transactionViewModel, final int minWeightMagnitude) {
transactionViewModel.setMetadata();
transactionViewModel.setAttachmentData();
+
+ if (!transactionViewModel.isVirtual() && isTransactionRequested(transactionViewModel)) {
+ log.debug("Waiting for transaction... " + transactionViewModel.getHash());
+ throw new IllegalStateException("Transaction is requested {} " + transactionViewModel.getHash());
+ }
if(hasInvalidTimestamp(transactionViewModel)) {
throw new StaleTimestampException("Invalid transaction timestamp.");
}
- for (int i = VALUE_OFFSET + VALUE_USABLE_SIZE; i < VALUE_OFFSET + VALUE_SIZE; i++) { // todo always false.
- if (transactionViewModel.getBytes()[i] != 0) {
- throw new IllegalStateException("Invalid transaction value");
- }
+ if(transactionViewModel.isVirtual()){
+ return;
}
-
int weightMagnitude = transactionViewModel.weightMagnitude;
if((weightMagnitude < minWeightMagnitude)) {
throw new IllegalStateException("Invalid transaction hash");
@@ -203,7 +210,8 @@ public void runValidation(TransactionViewModel transactionViewModel, final int m
* @throws RuntimeException if validation fails
*/
public TransactionViewModel validateBytes(final byte[] bytes, int minWeightMagnitude) {
- TransactionViewModel transactionViewModel = new TransactionViewModel(bytes, TransactionHash.calculate(bytes, 0, bytes.length, SpongeFactory.create(SpongeFactory.Mode.S256)));
+ TransactionViewModel transactionViewModel = new TransactionViewModel(bytes, SpongeFactory.Mode.S256);
+
runValidation(transactionViewModel, minWeightMagnitude);
return transactionViewModel;
}
diff --git a/src/main/java/net/helix/pendulum/controllers/BundleNonceViewModel.java b/src/main/java/net/helix/pendulum/controllers/BundleNonceViewModel.java
new file mode 100644
index 00000000..78393117
--- /dev/null
+++ b/src/main/java/net/helix/pendulum/controllers/BundleNonceViewModel.java
@@ -0,0 +1,141 @@
+package net.helix.pendulum.controllers;
+
+import net.helix.pendulum.model.BundleHash;
+import net.helix.pendulum.model.Hash;
+import net.helix.pendulum.model.persistables.Bundle;
+import net.helix.pendulum.model.persistables.BundleNonce;
+import net.helix.pendulum.storage.Indexable;
+import net.helix.pendulum.storage.Persistable;
+import net.helix.pendulum.storage.Tangle;
+import net.helix.pendulum.utils.Pair;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+* The BundleViewModel class is an implementation of the HashesViewModel interface.
+* It consists of an Indexable bundle hash and the Bundle model,
+* which contains the set of transaction hashes that are part of the bundle.
+*/
+public class BundleNonceViewModel implements HashesViewModel {
+ private BundleNonce self;
+ private Indexable hash;
+
+ /**
+ * Constructor with bundle hash
+ * @param hash bundle hash
+ */
+ public BundleNonceViewModel(Hash hash) {
+ this.hash = hash;
+ }
+
+ /**
+ * Constructor with bundle hash and related bundle model
+ * @param hashes bundle model
+ * @param hash transaction hash
+ */
+ private BundleNonceViewModel(BundleNonce hashes, Indexable hash) {
+ self = hashes == null || hashes.set == null ? new BundleNonce(): hashes;
+ this.hash = hash;
+ }
+
+ /**
+ * Get the BundleNonceViewModel of a given bundle hash from the database.
+ * @param tangle
+ * @param hash bundle hash
+ * @return BundleNoncefViewModel
+ */
+ public static BundleNonceViewModel load(Tangle tangle, Indexable hash) throws Exception {
+ return new BundleNonceViewModel((BundleNonce) tangle.load(BundleNonce.class, hash), hash);
+ }
+
+ /**
+ * Convert a transaction set hash into the bundle model.
+ * @param hash transaction hash
+ * @param hashToMerge mergable set hash
+ * @return Map.Entry map entry of bundle hash and Bundle model
+ */
+ public static Map.Entry getEntry(Hash hash, Hash hashToMerge) throws Exception {
+ BundleNonce hashes = new BundleNonce();
+ hashes.set.add(hashToMerge);
+ return new HashMap.SimpleEntry<>(hash, hashes);
+ }
+
+ /**
+ * Store the bundle hash (key) + belonging transaction hashes (value) in the database.
+ * @param tangle
+ * @return boolean success
+ */
+ public boolean store(Tangle tangle) throws Exception {
+ return tangle.save(self, hash);
+ }
+
+ /**
+ * Get the number of transactions belonging to the bundle.
+ * @return int number
+ */
+ public int size() {
+ return self.set.size();
+ }
+
+ /**
+ * Add a transaction hash to the bundle.
+ * @param theHash transaction hash
+ * @return boolean success
+ */
+ public boolean addHash(Hash theHash) {
+ return getHashes().add(theHash);
+ }
+
+ /**
+ * Get the bundle hash / index.
+ * @return Indexable bundle hash
+ */
+ public Indexable getIndex() {
+ return hash;
+ }
+
+ /**
+ * Get the set of transaction hashes belonging to the bundle.
+ * @return Set transaction hashes
+ */
+ public Set getHashes() {
+ return self.set;
+ }
+
+ /**
+ * Delete the bundle from the database.
+ * @param tangle
+ */
+ @Override
+ public void delete(Tangle tangle) throws Exception {
+ tangle.delete(BundleNonce.class,hash);
+ }
+
+ /**
+ * Get the first bundle from the database.
+ * @param tangle
+ * @return bundleNonceViewModel
+ */
+ public static BundleNonceViewModel first(Tangle tangle) throws Exception {
+ Pair bundlePair = tangle.getFirst(Bundle.class, BundleHash.class);
+ if(bundlePair != null && bundlePair.hi != null) {
+ return new BundleNonceViewModel((BundleNonce) bundlePair.hi, bundlePair.low);
+ }
+ return null;
+ }
+
+ /**
+ * Get the next bundle from the database.
+ * @param tangle
+ * @return bundleNonceViewModel
+ */
+ public BundleNonceViewModel next(Tangle tangle) throws Exception {
+ Pair bundlePair = tangle.next(Bundle.class, hash);
+ if(bundlePair != null && bundlePair.hi != null) {
+ return new BundleNonceViewModel((BundleNonce) bundlePair.hi, bundlePair.low);
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/net/helix/pendulum/controllers/RoundViewModel.java b/src/main/java/net/helix/pendulum/controllers/RoundViewModel.java
index 6f90b594..8d215891 100644
--- a/src/main/java/net/helix/pendulum/controllers/RoundViewModel.java
+++ b/src/main/java/net/helix/pendulum/controllers/RoundViewModel.java
@@ -1,7 +1,9 @@
package net.helix.pendulum.controllers;
import net.helix.pendulum.TransactionValidator;
-import net.helix.pendulum.crypto.Merkle;
+import net.helix.pendulum.crypto.merkle.MerkleOptions;
+import net.helix.pendulum.crypto.merkle.MerkleTree;
+import net.helix.pendulum.crypto.merkle.impl.MerkleTreeImpl;
import net.helix.pendulum.model.Hash;
import net.helix.pendulum.model.HashFactory;
import net.helix.pendulum.model.IntegerIndex;
@@ -13,14 +15,7 @@
import net.helix.pendulum.utils.Pair;
import net.helix.pendulum.utils.Serializer;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Random;
-import java.util.Set;
+import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@@ -256,8 +251,9 @@ public static Set getMilestoneTrunk(Tangle tangle, TransactionViewModel tr
}
} else {
Set prevMilestones = prevMilestone.getHashes();
- List> merkleTree = Merkle.buildMerkleTree(new ArrayList<>(prevMilestones));
- if (transaction.getTrunkTransactionHash().equals(merkleTree.get(merkleTree.size() - 1).get(0))) {
+ MerkleTree merkle = new MerkleTreeImpl();
+ byte[] merkleTreeRoot = merkle.getMerkleRoot(new ArrayList<>(prevMilestones), MerkleOptions.getDefault());
+ if (transaction.getTrunkTransactionHash().equals(HashFactory.TRANSACTION.create(merkleTreeRoot))) {
//System.out.println("trunk (prev. milestones): ");
if (prevMilestones.isEmpty()) {
trunk.add(Hash.NULL_HASH);
@@ -286,10 +282,11 @@ public static Set getMilestoneBranch(Tangle tangle, TransactionViewModel t
if (transaction.getCurrentIndex() == transaction.lastIndex()) {
// tips merkle root
Set confirmedTips = getTipSet(tangle, milestoneTx.getHash(), security);
- List> merkleTree = Merkle.buildMerkleTree(new ArrayList<>(confirmedTips));
+ MerkleTree merkle = new MerkleTreeImpl();
+ byte[] root = merkle.getMerkleRoot(new ArrayList<>(confirmedTips), MerkleOptions.getDefault());
//System.out.println("merkleRoot: " + transaction.getBranchTransactionHash().hexString());
//System.out.println("recalculated merkleRoot: " + merkleTree.get(merkleTree.size()-1).get(0).hexString());
- if (transaction.getBranchTransactionHash().equals(merkleTree.get(merkleTree.size()-1).get(0))) {
+ if (transaction.getBranchTransactionHash().equals(HashFactory.TRANSACTION.create(root))) {
//System.out.println("branch (tips): ");
if (confirmedTips.isEmpty()){
branch.add(Hash.NULL_HASH);
@@ -309,8 +306,8 @@ public static Set getMilestoneBranch(Tangle tangle, TransactionViewModel t
}
} else {
Set prevMilestones = prevMilestone.getHashes();
- List> merkleTree = Merkle.buildMerkleTree(new ArrayList<>(prevMilestones));
- if (transaction.getBranchTransactionHash().equals(merkleTree.get(merkleTree.size() - 1).get(0))) {
+ byte[] merkleTreeRoot = new MerkleTreeImpl().getMerkleRoot(new ArrayList<>(prevMilestones), MerkleOptions.getDefault());
+ if (transaction.getBranchTransactionHash().equals(HashFactory.TRANSACTION.create(merkleTreeRoot))) {
//System.out.println("branch (prev. milestones): ");
if (prevMilestones.isEmpty()) {
branch.add(Hash.NULL_HASH);
@@ -433,9 +430,8 @@ public Integer index() {
}
public Hash getMerkleRoot() {
- List> merkleTree = Merkle.buildMerkleTree(new LinkedList<>(getHashes()));
- Hash root = merkleTree.get(merkleTree.size()-1).get(0);
- return root;
+ MerkleTree merkle = new MerkleTreeImpl();
+ return HashFactory.TRANSACTION.create(merkle.getMerkleRoot(new LinkedList<>(getHashes()), MerkleOptions.getDefault()));
}
/**
diff --git a/src/main/java/net/helix/pendulum/controllers/TransactionViewModel.java b/src/main/java/net/helix/pendulum/controllers/TransactionViewModel.java
index de847bdb..f84c5a04 100644
--- a/src/main/java/net/helix/pendulum/controllers/TransactionViewModel.java
+++ b/src/main/java/net/helix/pendulum/controllers/TransactionViewModel.java
@@ -1,5 +1,8 @@
package net.helix.pendulum.controllers;
+import net.helix.pendulum.TransactionValidator;
+import net.helix.pendulum.crypto.SpongeFactory;
+import net.helix.pendulum.crypto.merkle.MerkleNode;
import net.helix.pendulum.model.*;
import net.helix.pendulum.model.persistables.*;
import net.helix.pendulum.service.milestone.MilestoneTracker;
@@ -9,6 +12,7 @@
import net.helix.pendulum.storage.Tangle;
import net.helix.pendulum.utils.Converter;
import net.helix.pendulum.utils.Pair;
+import net.helix.pendulum.utils.Serializer;
import java.util.*;
@@ -18,7 +22,7 @@
* of trunk and branch and the hash of a transaction.
* The size and offset of the transaction attributes and the supply are also defined here.
*/
-public class TransactionViewModel {
+public class TransactionViewModel implements MerkleNode {
private final Transaction transaction;
@@ -148,6 +152,28 @@ public TransactionViewModel(final byte[] bytes, Hash hash) throws RuntimeExcepti
transaction.type = FILLED_SLOT;
}
+ /**
+ * Constructor with transaction bytes and transaction hash.
+ * @param bytes transaction bytes
+ * @param mode mode of the hash function used to compute transaction hash
+ */
+ public TransactionViewModel(final byte[] bytes, SpongeFactory.Mode mode) throws RuntimeException {
+ transaction = new Transaction();
+ transaction.bytes = new byte[SIZE];
+ System.arraycopy(bytes, 0, transaction.bytes, 0, SIZE);
+ if (isVirtual()) {
+ byte[] buffer = new byte[2 * Hash.SIZE_IN_BYTES];
+ System.arraycopy(bytes, BRANCH_TRANSACTION_OFFSET, buffer, 0, Hash.SIZE_IN_BYTES);
+ System.arraycopy(bytes, TRUNK_TRANSACTION_OFFSET, buffer, Hash.SIZE_IN_BYTES, Hash.SIZE_IN_BYTES);
+ this.hash = TransactionHash.calculate(buffer, 0, buffer.length, SpongeFactory.create(mode));
+ } else {
+ this.hash = TransactionHash.calculate(bytes, 0, bytes.length, SpongeFactory.create(mode));
+ }
+
+ weightMagnitude = this.hash.leadingZeros();
+ transaction.type = FILLED_SLOT;
+ }
+
/**
* Get the number of transactins in the database.
* @param tangle
@@ -157,6 +183,7 @@ public static int getNumberOfStoredTransactions(Tangle tangle) throws Exception
return tangle.getCount(Transaction.class).intValue();
}
+
/**
* This method updates the metadata contained in the {@link Transaction} object, and updates the object in the
* database. First, all the most recent {@link Hash} identifiers are fetched to make sure the object's metadata is
@@ -314,6 +341,19 @@ public boolean store(Tangle tangle, Snapshot initialSnapshot) throws Exception {
return tangle.saveBatch(batch);
}
+
+ public void storeTransactionLocal(Tangle tangle, Snapshot initialSnapshot, TransactionValidator transactionValidator) throws Exception {
+ if(store(tangle, initialSnapshot)) {
+ setArrivalTime(System.currentTimeMillis() / 1000L);
+ if (isMilestoneBundle(tangle) == null) {
+ transactionValidator.updateStatus(this);
+ }
+ updateSender("local");
+ update(tangle, initialSnapshot, "sender");
+ }
+ }
+
+
/**
* Creates a copy of the underlying {@link Transaction} object.
*
@@ -386,6 +426,10 @@ public byte[] getBytes() {
return transaction.bytes;
}
+ public byte[] bytes(){
+ return getBytes();
+ }
+
public Hash getHash() {
return hash;
}
@@ -487,6 +531,15 @@ public Hash getTagValue() {
return transaction.tag;
}
+ /**
+ * Gets the {@link Long} identifier of a {@link Transaction}.
+ *
+ * @return The {@link Long} identifier.
+ */
+ public long getTagLongValue() {
+ return Serializer.getLong(getBytes(), TransactionViewModel.TAG_OFFSET);
+ }
+
/**
* Gets the {@link Transaction#attachmentTimestamp}. The Attachment Timestapm is used to show when a
* transaction has been attached to the database.
@@ -675,6 +728,16 @@ public boolean isMilestone() {
return transaction.milestone;
}
+ /**
+ * The {@link Transaction#milestone} flag indicates if the {@link Transaction} is a virtual transaction
+ * @return true if the {@link Transaction} is virtual and false otherwise
+ */
+ public boolean isVirtual() {
+ byte[] nonceForVirtual = new byte[NONCE_SIZE];
+ Arrays.fill(nonceForVirtual, (byte) 0xff);
+ return Arrays.equals(getNonce(),nonceForVirtual);
+ }
+
public TransactionViewModel isMilestoneBundle(Tangle tangle) throws Exception{
for (Hash bundleTx : BundleViewModel.load(tangle, getBundleHash()).getHashes()){
TransactionViewModel tx = TransactionViewModel.fromHash(tangle, bundleTx);
diff --git a/src/main/java/net/helix/pendulum/crypto/Merkle.java b/src/main/java/net/helix/pendulum/crypto/Merkle.java
deleted file mode 100644
index ef55826c..00000000
--- a/src/main/java/net/helix/pendulum/crypto/Merkle.java
+++ /dev/null
@@ -1,203 +0,0 @@
-package net.helix.pendulum.crypto;
-
-import net.helix.pendulum.controllers.RoundViewModel;
-import net.helix.pendulum.controllers.TransactionViewModel;
-import net.helix.pendulum.model.Hash;
-import net.helix.pendulum.model.HashFactory;
-import org.bouncycastle.util.encoders.Hex;
-
-import java.io.*;
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-public class Merkle {
-
- public static byte[] getMerkleRoot(SpongeFactory.Mode mode, byte[] hash, byte[] bytes, int offset, final int indexIn, int size) {
- int index = indexIn;
- final Sponge sha3 = SpongeFactory.create(mode);
- for (int i = 0; i < size; i++) {
- sha3.reset();
- if ((index & 1) == 0) {
- sha3.absorb(hash, 0, hash.length);
- sha3.absorb(bytes, offset + i * Sha3.HASH_LENGTH, Sha3.HASH_LENGTH);
- } else {
- sha3.absorb(bytes, offset + i * Sha3.HASH_LENGTH, Sha3.HASH_LENGTH);
- sha3.absorb(hash, 0, hash.length);
- }
- sha3.squeeze(hash, 0, hash.length);
- index >>= 1;
- }
- if(index != 0) {
- return Hash.NULL_HASH.bytes();
- }
- return hash;
- }
-
- public static List getMerklePath(List> merkleTree, int keyIndex){
- List merklePath = new ArrayList<>((merkleTree.size()-1) * 32);
- for (int i = 0; i < merkleTree.size()-1; i++) {
- Hash subkey = merkleTree.get(i).get(keyIndex ^ 1);
- merklePath.add(subkey == null ? Hash.NULL_HASH : subkey);
- keyIndex /= 2;
- }
- return merklePath;
- }
-
- public static List> buildMerkleKeyTree(String seed, int pubkeyDepth, int firstIndex, int pubkeyCount, int security){
- List keys = new ArrayList<>(1 << pubkeyDepth);
- for (int i = 0; i < pubkeyCount; i++) {
- int idx = firstIndex + i;
- keys.add(HashFactory.ADDRESS.create(Winternitz.generateAddress(Hex.decode(seed), idx, security)));
- }
- return buildMerkleTree(keys);
- }
-
-
- public static List> buildMerkleTree(List leaves){
- if (leaves.isEmpty()) {
- leaves.add(Hash.NULL_HASH);
- }
- byte[] buffer;
- Sponge sha3 = SpongeFactory.create(SpongeFactory.Mode.S256);
- int depth = (int) Math.ceil(Math.sqrt(leaves.size()));
- List> merkleTree = new ArrayList<>(depth + 1);
- merkleTree.add(0, leaves);
- int row = 1;
- // hash two following keys together until only one is left -> merkle tree
- while (leaves.size() > 1) {
- // Take two following keys (i=0: (k0,k1), i=1: (k2,k3), ...) and get one crypto of them
- List nextKeys = Arrays.asList(new Hash[(leaves.size() / 2)]);
- for (int i = 0; i < nextKeys.size(); i++) {
- if (leaves.get(i * 2) == null && leaves.get(i * 2 + 1) == null) {
- // leave the combined key null as well
- continue;
- }
- sha3.reset();
- Hash k1 = leaves.get(i * 2);
- Hash k2 = leaves.get(i * 2 + 1);
- buffer = Arrays.copyOfRange(k1 == null ? Hex.decode("0000000000000000000000000000000000000000000000000000000000000000") : k1.bytes(), 0, 32);
- sha3.absorb(buffer, 0, buffer.length);
- buffer = Arrays.copyOfRange(k2 == null ? Hex.decode("0000000000000000000000000000000000000000000000000000000000000000") : k2.bytes(), 0, 32);
- sha3.absorb(buffer, 0, buffer.length);
- sha3.squeeze(buffer, 0, buffer.length);
- nextKeys.set(i, HashFactory.TRANSACTION.create(buffer));
- }
- leaves = nextKeys;
- merkleTree.add(row++, leaves);
- }
- return merkleTree;
- }
-
- public static boolean validateMerkleSignature(List bundleTransactionViewModels, SpongeFactory.Mode mode, Hash validationAddress, int securityLevel, int depth) {
-
- //System.out.println("Validate Merkle Signature");
- final TransactionViewModel merkleTx = bundleTransactionViewModels.get(securityLevel);
- int keyIndex = RoundViewModel.getRoundIndex(merkleTx); // get keyindex
-
- //System.out.println("Address: " + validationAddress);
- //System.out.println("Keyindex: " + keyIndex);
-
- //milestones sign the normalized hash of the sibling transaction. (why not bundle hash?)
- //TODO: check if its okay here to use bundle hash instead of tx hash
- byte[] bundleHash = Winternitz.normalizedBundle(merkleTx.getBundleHash().bytes());
-
- //validate leaf signature
- ByteBuffer bb = ByteBuffer.allocate(Sha3.HASH_LENGTH * securityLevel);
-
- for (int i = 0; i < securityLevel; i++) {
- byte[] bundleHashFragment = Arrays.copyOfRange(bundleHash, Winternitz.NORMALIZED_FRAGMENT_LENGTH * i, Winternitz.NORMALIZED_FRAGMENT_LENGTH * (i+1));
- byte[] digest = Winternitz.digest(mode, bundleHashFragment, bundleTransactionViewModels.get(i).getSignature());
- bb.put(digest);
- }
-
- byte[] digests = bb.array();
- byte[] address = Winternitz.address(mode, digests);
-
- //System.out.println("Public Key: " + Hex.toHexString(address));
-
- //validate Merkle path
- //System.out.println("Merkle Path: " + Hex.toHexString(merkleTx.getSignature()));
- byte[] merkleRoot = Merkle.getMerkleRoot(mode, address,
- merkleTx.getSignature(), 0, keyIndex, depth);
-
- //System.out.println("Recalculated Address: " + HashFactory.ADDRESS.create(merkleRoot));
-
- return HashFactory.ADDRESS.create(merkleRoot).equals(validationAddress);
- }
-
- public static List> readKeyfile(File keyfile) throws IOException {
- try (BufferedReader br = new BufferedReader(new FileReader(keyfile))) {
- String[] fields = br.readLine().split(" ");
- int depth = Integer.parseInt(fields[0]);
- List> result = new ArrayList<>(depth + 1);
- for (int i = 0; i <= depth; i++) {
- fields = br.readLine().split(" ");
- int leadingNulls = Integer.parseInt(fields[0]);
- List row = new ArrayList<>();
- for (int j = 0; j < leadingNulls; j++) {
- row.add(Hash.NULL_HASH);
- }
- for (int j = 0; j < fields[1].length() / 64; j++) {
- row.add(HashFactory.TRANSACTION.create(fields[1].substring(j * 64, (j+1) * 64)));
- }
- result.add(row);
- }
- return result;
- }
- }
-
- public static String getSeed(File keyfile) throws IOException {
- StringBuilder seedBuilder = new StringBuilder();
- try (BufferedReader br = new BufferedReader(new FileReader(keyfile))) {
- String[] fields = br.readLine().split(" ");
- seedBuilder.append(fields[1]);
- }
- return seedBuilder.toString();
- }
-
-
- public static void createKeyfile(List> merkleTree, byte[] seed, int pubkeyDepth, int keyIndex, int keyfileIndex, String filename) throws IOException {
- // fill buffer
- try (BufferedWriter bw = new BufferedWriter(new FileWriter(filename))) {
- // write pubkey depth and seed into buffer
- bw.write(pubkeyDepth + " " + Hex.toHexString(seed) + " " + keyfileIndex + " " + keyIndex);
- bw.newLine();
- writeKeys(bw, merkleTree.get(0));
- for (int i = 1; i < merkleTree.size(); i++) {
- writeKeys(bw, merkleTree.get(i));
- }
- }
- }
-
- private static void writeKeys(BufferedWriter bw, List keys) throws IOException {
- int leadingNulls = 0;
- while (keys.get(leadingNulls) == null) {
- leadingNulls++;
- }
- bw.write(leadingNulls + " ");
- for (int i = leadingNulls; i < keys.size(); i++) {
- if (keys.get(i) == null) {
- break;
- }
- bw.write(keys.get(i).toString());
- }
- bw.newLine();
- }
-
- public static byte[] padding(byte[] input, int length){
- if (input.length < length) {
- byte[] output = new byte[length];
- System.arraycopy(input, 0, output, length - input.length, input.length);
- return output;
- } else {
- if (input.length > length) {
- return Arrays.copyOfRange(input, 0, length);
- } else {
- return Arrays.copyOfRange(input, 0, input.length);
- }
-
- }
- }
-}
diff --git a/src/main/java/net/helix/pendulum/crypto/merkle/MerkleNode.java b/src/main/java/net/helix/pendulum/crypto/merkle/MerkleNode.java
new file mode 100644
index 00000000..1d6bd4fb
--- /dev/null
+++ b/src/main/java/net/helix/pendulum/crypto/merkle/MerkleNode.java
@@ -0,0 +1,9 @@
+package net.helix.pendulum.crypto.merkle;
+
+/**
+ * Used as node in merkle tree
+ */
+public interface MerkleNode {
+
+ byte[] bytes();
+}
diff --git a/src/main/java/net/helix/pendulum/crypto/merkle/MerkleOptions.java b/src/main/java/net/helix/pendulum/crypto/merkle/MerkleOptions.java
new file mode 100644
index 00000000..54e330a7
--- /dev/null
+++ b/src/main/java/net/helix/pendulum/crypto/merkle/MerkleOptions.java
@@ -0,0 +1,87 @@
+package net.helix.pendulum.crypto.merkle;
+
+import net.helix.pendulum.crypto.SpongeFactory;
+import net.helix.pendulum.model.Hash;
+
+public class MerkleOptions {
+
+ private boolean includeLeavesInTree;
+ private Hash milestoneHash;
+ private Hash address;
+ private SpongeFactory.Mode mode;
+ private int securityLevel;
+ private int depth;
+
+ /**
+ * Parameters needed fro validation
+ * @param mode
+ * @param address
+ * @param securityLevel
+ * @param depth
+ */
+ public MerkleOptions(SpongeFactory.Mode mode, Hash address, int securityLevel, int depth) {
+ this.address = address;
+ this.mode = mode;
+ this.securityLevel = securityLevel;
+ this.depth = depth;
+ this.includeLeavesInTree = true;
+ }
+
+ public MerkleOptions() {
+ }
+
+ public static MerkleOptions getDefault() {
+ MerkleOptions op = new MerkleOptions();
+ op.includeLeavesInTree = true;
+ op.mode = SpongeFactory.Mode.S256;
+ return op;
+ }
+
+ public boolean isIncludeLeavesInTree() {
+ return includeLeavesInTree;
+ }
+
+ public void setIncludeLeavesInTree(boolean includeLeavesInTree) {
+ this.includeLeavesInTree = includeLeavesInTree;
+ }
+
+ public Hash getMilestoneHash() {
+ return milestoneHash;
+ }
+
+ public void setMilestoneHash(Hash milestoneHash) {
+ this.milestoneHash = milestoneHash;
+ }
+
+ public Hash getAddress() {
+ return address;
+ }
+
+ public void setAddress(Hash address) {
+ this.address = address;
+ }
+
+ public SpongeFactory.Mode getMode() {
+ return mode;
+ }
+
+ public void setMode(SpongeFactory.Mode mode) {
+ this.mode = mode;
+ }
+
+ public int getSecurityLevel() {
+ return securityLevel;
+ }
+
+ public void setSecurityLevel(int securityLevel) {
+ this.securityLevel = securityLevel;
+ }
+
+ public int getDepth() {
+ return depth;
+ }
+
+ public void setDepth(int depth) {
+ this.depth = depth;
+ }
+}
diff --git a/src/main/java/net/helix/pendulum/crypto/merkle/MerkleTree.java b/src/main/java/net/helix/pendulum/crypto/merkle/MerkleTree.java
new file mode 100644
index 00000000..239a1842
--- /dev/null
+++ b/src/main/java/net/helix/pendulum/crypto/merkle/MerkleTree.java
@@ -0,0 +1,78 @@
+package net.helix.pendulum.crypto.merkle;
+
+import net.helix.pendulum.controllers.TransactionViewModel;
+import net.helix.pendulum.crypto.SpongeFactory;
+import net.helix.pendulum.model.Hash;
+
+import java.util.Arrays;
+import java.util.List;
+
+public interface MerkleTree {
+
+ /**
+ * Gets merkle path from the specified key index (Used in merkle transaction message
+ * @param merkleTree
+ * @param keyIndex
+ * @return
+ */
+ List getMerklePath(List> merkleTree, int keyIndex);
+
+ /**
+ * Build flat merkle tree
+ * @param leaves hash leaves
+ * @param options
+ * @return
+ */
+ List buildMerkle(List leaves, MerkleOptions options);
+
+ /**
+ * Build merkle tree as lists of lists
+ * @param leaves
+ * @param options
+ * @return
+ */
+ List> buildMerkleTree(List leaves, MerkleOptions options);
+
+ /**
+ * Gets merkle tree root for given leaves
+ * @param leaves
+ * @param options
+ * @return
+ */
+ byte[] getMerkleRoot(List leaves, MerkleOptions options);
+
+ /**
+ * Get merkle tree root, based on merkle path
+ * @param mode
+ * @param hash
+ * @param bytes merkle path bytes
+ * @param offset
+ * @param indexIn
+ * @param size of elements in path
+ * @return
+ */
+ byte[] getMerkleRoot(SpongeFactory.Mode mode, byte[] hash, byte[] bytes, int offset, int indexIn, int size);
+
+ /**
+ * Validate bundles which contains merkle transaction
+ * First it validates sender transactions signature
+ * @param bundleTransactionViewModels
+ * @param options
+ * @return
+ */
+ boolean validateMerkleSignature(List bundleTransactionViewModels, MerkleOptions options);
+
+ static byte[] padding(byte[] input, int length) {
+ if (input.length < length) {
+ byte[] output = new byte[length];
+ System.arraycopy(input, 0, output, length - input.length, input.length);
+ return output;
+ } else {
+ if (input.length > length) {
+ return Arrays.copyOfRange(input, 0, length);
+ } else {
+ return Arrays.copyOfRange(input, 0, input.length);
+ }
+ }
+ }
+}
diff --git a/src/main/java/net/helix/pendulum/crypto/merkle/impl/AbstractMerkleTree.java b/src/main/java/net/helix/pendulum/crypto/merkle/impl/AbstractMerkleTree.java
new file mode 100644
index 00000000..1471685e
--- /dev/null
+++ b/src/main/java/net/helix/pendulum/crypto/merkle/impl/AbstractMerkleTree.java
@@ -0,0 +1,219 @@
+package net.helix.pendulum.crypto.merkle.impl;
+
+import net.helix.pendulum.controllers.RoundViewModel;
+import net.helix.pendulum.controllers.TransactionViewModel;
+import net.helix.pendulum.crypto.Sha3;
+import net.helix.pendulum.crypto.Sponge;
+import net.helix.pendulum.crypto.SpongeFactory;
+import net.helix.pendulum.crypto.Winternitz;
+import net.helix.pendulum.crypto.merkle.MerkleNode;
+import net.helix.pendulum.crypto.merkle.MerkleOptions;
+import net.helix.pendulum.crypto.merkle.MerkleTree;
+import net.helix.pendulum.model.Hash;
+import net.helix.pendulum.model.HashFactory;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public abstract class AbstractMerkleTree implements MerkleTree {
+
+ protected abstract MerkleNode createMerkleNode(Hash hashParent, Hash h1, Hash h2, int row, long index, MerkleOptions options);
+
+ protected abstract List createMerkleNodes(List leaves, MerkleOptions options);
+
+ @Override
+ public byte[] getMerkleRoot(SpongeFactory.Mode mode, byte[] hash, byte[] bytes, int offset, final int indexIn, int size) {
+ int index = indexIn;
+ final Sponge sha3 = SpongeFactory.create(mode);
+ for (int i = 0; i < size; i++) {
+ sha3.reset();
+ if ((index & 1) == 0) {
+ sha3.absorb(hash, 0, hash.length);
+ sha3.absorb(bytes, offset + i * Sha3.HASH_LENGTH, Sha3.HASH_LENGTH);
+ } else {
+ sha3.absorb(bytes, offset + i * Sha3.HASH_LENGTH, Sha3.HASH_LENGTH);
+ sha3.absorb(hash, 0, hash.length);
+ }
+ sha3.squeeze(hash, 0, hash.length);
+ index >>= 1;
+ }
+ if (index != 0) {
+ return getDefaultMerkleHash().bytes();
+ }
+ return hash;
+ }
+
+ @Override
+ public List getMerklePath(List> merkleTree, int keyIndex) {
+ List merklePath = new ArrayList<>((merkleTree.size() - 1) * Hash.SIZE_IN_BYTES);
+ for (int i = 0; i < merkleTree.size() - 1; i++) {
+ MerkleNode subkey = merkleTree.get(i).get(keyIndex ^ 1);
+ merklePath.add(subkey == null ? getDefaultMerkleHash() : subkey);
+ keyIndex /= 2;
+ }
+ return merklePath;
+ }
+
+ @Override
+ public byte[] getMerkleRoot(List leaves, MerkleOptions options) {
+ List> merkleTree = buildMerkleTree(leaves, options);
+ return (merkleTree.get(merkleTree.size() - 1).get(0)).bytes();
+ }
+
+ @Override
+ public List buildMerkle(List leaves, MerkleOptions options) {
+ if (leaves.isEmpty()) {
+ leaves.add(getDefaultMerkleHash());
+ }
+ byte[] buffer;
+ Sponge sha3 = SpongeFactory.create(SpongeFactory.Mode.S256);
+ int row = 1;
+ addPaddingLeaves(leaves);
+ List merkleNodes = new ArrayList<>();
+ int depth = getTreeDepth(leaves.size());
+ while (leaves.size() > 1) {
+ List nextKeys = Arrays.asList(new Hash[getParentNodesSize(leaves)]);
+ for (int i = 0; i < nextKeys.size(); i++) {
+ if (areLeavesNull(leaves, i)) continue;
+ sha3.reset();
+ Hash k1 = getLeaves(leaves, i * 2);
+ Hash k2 = getLeaves(leaves, i * 2 + 1);
+ buffer = computeParentHash(sha3, k1, k2);
+ Hash parentHash = HashFactory.TRANSACTION.create(buffer);
+ nextKeys.set(i, parentHash);
+ merkleNodes.add(createMerkleNode(parentHash, k1, k2, row, getParentMerkleIndex(row, depth, i * 2), options));
+ }
+ leaves = nextKeys;
+ row++;
+ }
+ return merkleNodes;
+ }
+
+ @Override
+ public List> buildMerkleTree(List leaves, MerkleOptions options) {
+ if (leaves.isEmpty()) {
+ leaves.add(getDefaultMerkleHash());
+ }
+ byte[] buffer;
+ addPaddingLeaves(leaves);
+ Sponge sha3 = SpongeFactory.create(options.getMode());
+ int depth = getTreeDepth(leaves.size());
+ List> merkleTree = new ArrayList<>();
+
+ if (options.isIncludeLeavesInTree()) {
+ merkleTree.add(0, createMerkleNodes(leaves, options));
+ }
+ int row = 1;
+ while (leaves.size() > 1) {
+ List nextKeys = Arrays.asList(new Hash[getParentNodesSize(leaves)]);
+ for (int i = 0; i < nextKeys.size(); i++) {
+ if (areLeavesNull(leaves, i)) continue;
+ sha3.reset();
+ Hash k1 = getLeaves(leaves, i * 2);
+ Hash k2 = getLeaves(leaves, i * 2 + 1);
+ buffer = computeParentHash(sha3, k1, k2);
+ nextKeys.set(i, HashFactory.TRANSACTION.create(buffer));
+ }
+ leaves = nextKeys;
+ merkleTree.add(row++, createMerkleNodes(leaves, options));
+ }
+ return merkleTree;
+ }
+
+ public boolean validateMerkleSignature(List bundleTransactionViewModels, MerkleOptions options) {
+
+ final TransactionViewModel merkleTx = bundleTransactionViewModels.get(options.getSecurityLevel());
+ int keyIndex = RoundViewModel.getRoundIndex(merkleTx); // get keyindex
+
+ //milestones sign the normalized hash of the sibling transaction. (why not bundle hash?)
+ //TODO: check if its okay here to use bundle hash instead of tx hash
+ byte[] bundleHash = Winternitz.normalizedBundle(merkleTx.getBundleHash().bytes());
+
+ //validate leaf signature
+ ByteBuffer bb = ByteBuffer.allocate(Sha3.HASH_LENGTH * options.getSecurityLevel());
+
+ for (int i = 0; i < options.getSecurityLevel(); i++) {
+ byte[] bundleHashFragment = Arrays.copyOfRange(bundleHash, Winternitz.NORMALIZED_FRAGMENT_LENGTH * i, Winternitz.NORMALIZED_FRAGMENT_LENGTH * (i + 1));
+ byte[] digest = Winternitz.digest(options.getMode(), bundleHashFragment, bundleTransactionViewModels.get(i).getSignature());
+ bb.put(digest);
+ }
+
+ byte[] digests = bb.array();
+ byte[] address = Winternitz.address(options.getMode(), digests);
+
+ return validateMerklePath(merkleTx.getSignature(), keyIndex, address, options);
+ }
+
+ private boolean validateMerklePath(byte[] path, int keyIndex, byte[] address, MerkleOptions options) {
+ byte[] merkleRoot = getMerkleRoot(options.getMode(), address,
+ path, 0, keyIndex, options.getDepth());
+ return HashFactory.ADDRESS.create(merkleRoot).equals(options.getAddress());
+ }
+
+ protected void addPaddingLeaves(List leaves){
+ int closestPow = (int) Math.pow(2.0, getClosestPow(leaves.size()));
+ int leaveSize = leaves.size();
+ while(leaveSize < closestPow){
+ leaves.add(leaveSize++, Hash.NULL_HASH);
+ }
+ }
+
+ protected int getTreeDepth(int leavesNumber) {
+ return getClosestPow(leavesNumber);
+ }
+
+ protected int getClosestPow(int i) {
+ int j = 1;
+ int power = 0;
+ while (j < i) {
+ j = j << 1;
+ power++;
+ }
+ return power;
+ }
+
+ protected Hash getLeaves(List leaves, int index) {
+ return index < leaves.size() ? leaves.get(index) : getDefaultMerkleHash();
+ }
+
+ protected static long getParentMerkleIndex(int row, int depth, int i) {
+ if (row == depth) {
+ return 0;
+ }
+ long index = depth - row;
+ return (long) Math.pow(2, index) + i / 2 - 1;
+ }
+
+ protected int getParentNodesSize(List leaves) {
+ return leaves.size() % 2 == 0 ? (leaves.size() / 2) : (leaves.size() / 2 + 1);
+ }
+
+ private boolean areLeavesNull(List leaves, int i) {
+ if (leaves.get(i * 2) == null && leaves.get(i * 2 + 1) == null) {
+ return true;
+ }
+ return false;
+ }
+
+ private byte[] computeParentHash(Sponge sha3, byte[] k1, byte[] k2) {
+ byte[] buffer = new byte[Hash.SIZE_IN_BYTES];
+ sha3.absorb(k1, 0, k1.length);
+ sha3.absorb(k2, 0, k2.length);
+ sha3.squeeze(buffer, 0, buffer.length);
+ return buffer;
+ }
+
+ private byte[] computeParentHash(Sponge sha3, Hash k1, Hash k2) {
+ return computeParentHash(sha3, copyHash(k1), copyHash(k2));
+ }
+
+ private byte[] copyHash(Hash k2) {
+ return Arrays.copyOfRange(k2 == null ? getDefaultMerkleHash().bytes() : k2.bytes(), 0, Hash.SIZE_IN_BYTES);
+ }
+
+ private Hash getDefaultMerkleHash() {
+ return Hash.NULL_HASH;
+ }
+}
diff --git a/src/main/java/net/helix/pendulum/crypto/merkle/impl/MerkleTreeImpl.java b/src/main/java/net/helix/pendulum/crypto/merkle/impl/MerkleTreeImpl.java
new file mode 100644
index 00000000..abb9c96b
--- /dev/null
+++ b/src/main/java/net/helix/pendulum/crypto/merkle/impl/MerkleTreeImpl.java
@@ -0,0 +1,23 @@
+package net.helix.pendulum.crypto.merkle.impl;
+
+import net.helix.pendulum.crypto.merkle.MerkleNode;
+import net.helix.pendulum.crypto.merkle.MerkleOptions;
+import net.helix.pendulum.model.Hash;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MerkleTreeImpl extends AbstractMerkleTree {
+
+ @Override
+ protected MerkleNode createMerkleNode(Hash hashParent, Hash h1, Hash h2, int row, long index, MerkleOptions options) {
+ return hashParent;
+ }
+
+ @Override
+ protected List createMerkleNodes(List leaves, MerkleOptions options) {
+ List result = new ArrayList<>();
+ result.addAll(leaves);
+ return result;
+ }
+}
diff --git a/src/main/java/net/helix/pendulum/crypto/merkle/impl/TransactionMerkleTreeImpl.java b/src/main/java/net/helix/pendulum/crypto/merkle/impl/TransactionMerkleTreeImpl.java
new file mode 100644
index 00000000..d39246d4
--- /dev/null
+++ b/src/main/java/net/helix/pendulum/crypto/merkle/impl/TransactionMerkleTreeImpl.java
@@ -0,0 +1,35 @@
+package net.helix.pendulum.crypto.merkle.impl;
+
+import net.helix.pendulum.controllers.TransactionViewModel;
+import net.helix.pendulum.crypto.merkle.MerkleNode;
+import net.helix.pendulum.crypto.merkle.MerkleOptions;
+import net.helix.pendulum.model.Hash;
+import net.helix.pendulum.utils.bundle.BundleUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TransactionMerkleTreeImpl extends AbstractMerkleTree {
+
+ private static final Logger log = LoggerFactory.getLogger(TransactionMerkleTreeImpl.class);
+
+ @Override
+ protected MerkleNode createMerkleNode(Hash parentHash, Hash h1, Hash h2, int row, long index, MerkleOptions options) {
+ log.debug("New virtual transaction + " + parentHash + " for milestone: " + options.getMilestoneHash() + " with merkle index: " + index + " [" + h1 + ", " + h2 + " ]");
+ byte[] virtualTransaction = BundleUtils.createVirtualTransaction(h1, h2, index, options.getMilestoneHash().bytes(), options.getAddress());
+ return new TransactionViewModel(virtualTransaction, options.getMode());
+ }
+
+ @Override
+ protected List createMerkleNodes(List leaves, MerkleOptions options) {
+
+ int depth = getTreeDepth(leaves.size());
+ List result = new ArrayList<>();
+ for (int i = 0; i < leaves.size(); i++) {
+ result.add(createMerkleNode(leaves.get(i), Hash.NULL_HASH, Hash.NULL_HASH, 0, getParentMerkleIndex(0, depth, i), options));
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/net/helix/pendulum/model/Hash.java b/src/main/java/net/helix/pendulum/model/Hash.java
index 4fa078dc..e40de872 100644
--- a/src/main/java/net/helix/pendulum/model/Hash.java
+++ b/src/main/java/net/helix/pendulum/model/Hash.java
@@ -1,6 +1,7 @@
package net.helix.pendulum.model;
import net.helix.pendulum.crypto.Sha3;
+import net.helix.pendulum.crypto.merkle.MerkleNode;
import net.helix.pendulum.storage.Indexable;
@@ -9,7 +10,7 @@
* The model class contains a hash Hash , the size of the hash, lock
* and the inner classes ByteSafe
*/
-public interface Hash extends Indexable, HashId {
+public interface Hash extends Indexable, HashId, MerkleNode {
/**
* Creates a null transaction hash with from a byte array of length {@value Sha3#HASH_LENGTH}.
diff --git a/src/main/java/net/helix/pendulum/network/Node.java b/src/main/java/net/helix/pendulum/network/Node.java
index fe45302a..fa0c156c 100644
--- a/src/main/java/net/helix/pendulum/network/Node.java
+++ b/src/main/java/net/helix/pendulum/network/Node.java
@@ -13,6 +13,7 @@
import net.helix.pendulum.model.HashFactory;
import net.helix.pendulum.model.TransactionHash;
import net.helix.pendulum.service.milestone.MilestoneTracker;
+import net.helix.pendulum.service.milestone.VirtualTransactionService;
import net.helix.pendulum.service.snapshot.SnapshotProvider;
import net.helix.pendulum.storage.Tangle;
import org.apache.commons.lang3.StringUtils;
@@ -75,6 +76,7 @@ public class Node {
private final TipsViewModel tipsViewModel;
private final TransactionValidator transactionValidator;
private final TransactionRequester transactionRequester;
+ private final VirtualTransactionService virtualTransactionService;
private static final SecureRandom rnd = new SecureRandom();
@@ -109,7 +111,8 @@ public class Node {
* @param configuration Contains all the config.
*
*/
- public Node(final Tangle tangle, SnapshotProvider snapshotProvider, final TransactionValidator transactionValidator, final TransactionRequester transactionRequester, final TipsViewModel tipsViewModel, final MilestoneTracker milestoneTracker, final NodeConfig configuration
+ public Node(final Tangle tangle, SnapshotProvider snapshotProvider, final TransactionValidator transactionValidator, final TransactionRequester transactionRequester, final TipsViewModel tipsViewModel, final MilestoneTracker milestoneTracker,
+ final VirtualTransactionService virtualTransactionService, final NodeConfig configuration
) {
this.configuration = configuration;
this.tangle = tangle;
@@ -121,7 +124,7 @@ public Node(final Tangle tangle, SnapshotProvider snapshotProvider, final Transa
int packetSize = configuration.getTransactionPacketSize();
this.sendingPacket = new DatagramPacket(new byte[packetSize], packetSize);
this.tipRequestingPacket = new DatagramPacket(new byte[packetSize], packetSize);
-
+ this.virtualTransactionService = virtualTransactionService;
}
/**
@@ -313,6 +316,7 @@ public void preProcessReceivedData(byte[] receivedData, SocketAddress senderAddr
}
//if valid - add to receive queue (receivedTransactionViewModel, neighbor)
+ buildVirtualTransaction(receivedTransactionViewModel);
addReceivedDataToReceiveQueue(receivedTransactionViewModel, neighbor);
}
@@ -400,6 +404,12 @@ public void preProcessReceivedData(byte[] receivedData, SocketAddress senderAddr
}
}
+ public void buildVirtualTransaction(TransactionViewModel model){
+ if(model.isVirtual()){
+ virtualTransactionService.rebuildVirtualTransactionsIfPossible(model);
+ }
+ }
+
/**
* Adds incoming transactions to the {@link Node#receiveQueue} to be processed later.
*/
diff --git a/src/main/java/net/helix/pendulum/network/TransactionRequester.java b/src/main/java/net/helix/pendulum/network/TransactionRequester.java
index de522101..5c56a2e5 100644
--- a/src/main/java/net/helix/pendulum/network/TransactionRequester.java
+++ b/src/main/java/net/helix/pendulum/network/TransactionRequester.java
@@ -90,7 +90,8 @@ public void requestTransaction(Hash hash, boolean milestone) throws Exception {
protected void popEldestTransactionToRequest() {
Iterator iterator = transactionsToRequest.iterator();
if (iterator.hasNext()) {
- iterator.next();
+ Hash transactionHash = iterator.next();
+ log.debug("Transaction: " + transactionHash + " has been removed from requestList!");
iterator.remove();
}
}
diff --git a/src/main/java/net/helix/pendulum/service/API.java b/src/main/java/net/helix/pendulum/service/API.java
index 8251fdbd..2f26610b 100644
--- a/src/main/java/net/helix/pendulum/service/API.java
+++ b/src/main/java/net/helix/pendulum/service/API.java
@@ -10,14 +10,20 @@
import net.helix.pendulum.conf.APIConfig;
import net.helix.pendulum.conf.PendulumConfig;
import net.helix.pendulum.controllers.*;
-import net.helix.pendulum.crypto.*;
+import net.helix.pendulum.crypto.GreedyMiner;
+import net.helix.pendulum.crypto.Sha3;
+import net.helix.pendulum.crypto.Sponge;
+import net.helix.pendulum.crypto.SpongeFactory;
+import net.helix.pendulum.crypto.merkle.MerkleNode;
+import net.helix.pendulum.crypto.merkle.MerkleOptions;
+import net.helix.pendulum.crypto.merkle.impl.MerkleTreeImpl;
+import net.helix.pendulum.crypto.merkle.impl.TransactionMerkleTreeImpl;
import net.helix.pendulum.model.Hash;
import net.helix.pendulum.model.HashFactory;
import net.helix.pendulum.model.persistables.Transaction;
import net.helix.pendulum.network.Neighbor;
import net.helix.pendulum.network.Node;
import net.helix.pendulum.network.TransactionRequester;
-import net.helix.pendulum.service.validatomanager.CandidateTracker;
import net.helix.pendulum.service.dto.*;
import net.helix.pendulum.service.ledger.LedgerService;
import net.helix.pendulum.service.milestone.MilestoneTracker;
@@ -26,6 +32,8 @@
import net.helix.pendulum.service.spentaddresses.SpentAddressesService;
import net.helix.pendulum.service.tipselection.TipSelector;
import net.helix.pendulum.service.tipselection.impl.WalkValidatorImpl;
+import net.helix.pendulum.service.utils.RoundIndexUtil;
+import net.helix.pendulum.service.validatomanager.CandidateTracker;
import net.helix.pendulum.storage.Tangle;
import net.helix.pendulum.utils.Serializer;
import net.helix.pendulum.utils.bundle.BundleTypes;
@@ -115,6 +123,7 @@ public class API {
private final String[] features;
+
//endregion ////////////////////////////////////////////////////////////////////////////////////////////////////////
private final Gson gson = new GsonBuilder().create();
@@ -177,7 +186,6 @@ public API(ApiArgs args) {
commandRoute.put(ApiCommand.GET_MISSING_TRANSACTIONS, getMissingTransactions());
commandRoute.put(ApiCommand.CHECK_CONSISTENCY, checkConsistency());
commandRoute.put(ApiCommand.WERE_ADDRESSES_SPENT_FROM, wereAddressesSpentFrom());
- // commandRoute.put(ApiCommand.GET_MILESTONES, wereAddressesSpentFrom());
}
/**
@@ -590,15 +598,7 @@ private synchronized AbstractResponse getTipsStatement() throws Exception {
public void storeTransactionsStatement(final List txString) throws Exception {
final List elements = addValidTxvmToList(txString);
for (final TransactionViewModel transactionViewModel : elements) {
- //store transactions
- if(transactionViewModel.store(tangle, snapshotProvider.getInitialSnapshot())) {
- transactionViewModel.setArrivalTime(System.currentTimeMillis() / 1000L);
- if (transactionViewModel.isMilestoneBundle(tangle) == null) {
- transactionValidator.updateStatus(transactionViewModel);
- }
- transactionViewModel.updateSender("local");
- transactionViewModel.update(tangle, snapshotProvider.getInitialSnapshot(), "sender");
- }
+ transactionViewModel.storeTransactionLocal(tangle, snapshotProvider.getInitialSnapshot(), transactionValidator);
}
}
@@ -682,7 +682,7 @@ private AbstractResponse getNodeAPIConfigurationStatement() {
* @return {@link net.helix.pendulum.service.dto.GetInclusionStatesResponse}
* @throws Exception When a transaction cannot be loaded from hash
**/
- private AbstractResponse getInclusionStatesStatement(final List transactions, final List tips) throws Exception {
+ public AbstractResponse getInclusionStatesStatement(final List transactions, final List tips) throws Exception {
final List trans = transactions.stream()
.map(HashFactory.TRANSACTION::create)
@@ -1177,12 +1177,7 @@ public synchronized List attachToTangleStatement(final Hash trunkTransac
if(IntStream.range(TransactionViewModel.TAG_OFFSET, TransactionViewModel.TAG_OFFSET + TransactionViewModel.TAG_SIZE).allMatch(idx -> txBytes[idx] == ((byte) 0))) {
System.arraycopy(txBytes, TransactionViewModel.BUNDLE_NONCE_OFFSET, txBytes, TransactionViewModel.TAG_OFFSET, TransactionViewModel.TAG_SIZE);
}
- System.arraycopy(Serializer.serialize(timestamp),0, txBytes,TransactionViewModel.ATTACHMENT_TIMESTAMP_OFFSET,
- TransactionViewModel.ATTACHMENT_TIMESTAMP_SIZE);
- System.arraycopy(Serializer.serialize(0L),0,txBytes, TransactionViewModel.ATTACHMENT_TIMESTAMP_LOWER_BOUND_OFFSET,
- TransactionViewModel.ATTACHMENT_TIMESTAMP_LOWER_BOUND_SIZE);
- System.arraycopy(Serializer.serialize(MAX_TIMESTAMP_VALUE),0,txBytes,TransactionViewModel.ATTACHMENT_TIMESTAMP_UPPER_BOUND_OFFSET,
- TransactionViewModel.ATTACHMENT_TIMESTAMP_UPPER_BOUND_SIZE);
+ fillAttachmentTransactionFields(txBytes, timestamp);
if (!miner.mine(txBytes, minWeightMagnitude, 4)) {
transactionViewModels.clear();
@@ -1215,6 +1210,15 @@ public synchronized List attachToTangleStatement(final Hash trunkTransac
return elements;
}
+ private void fillAttachmentTransactionFields(byte[] txBytes, long timestamp) {
+ System.arraycopy(Serializer.serialize(timestamp),0, txBytes, TransactionViewModel.ATTACHMENT_TIMESTAMP_OFFSET,
+ TransactionViewModel.ATTACHMENT_TIMESTAMP_SIZE);
+ System.arraycopy(Serializer.serialize(0L),0,txBytes, TransactionViewModel.ATTACHMENT_TIMESTAMP_LOWER_BOUND_OFFSET,
+ TransactionViewModel.ATTACHMENT_TIMESTAMP_LOWER_BOUND_SIZE);
+ System.arraycopy(Serializer.serialize(MAX_TIMESTAMP_VALUE),0,txBytes,TransactionViewModel.ATTACHMENT_TIMESTAMP_UPPER_BOUND_OFFSET,
+ TransactionViewModel.ATTACHMENT_TIMESTAMP_UPPER_BOUND_SIZE);
+ }
+
/**
* Can be 0 or more, and is set to 0 every 100 requests.
* Each increase indicates another 2 tips send.
@@ -1450,10 +1454,10 @@ private void attachStoreAndBroadcast(final String address, final String message,
* @param txs transactions list
* @throws Exception if storing fails
*/
- private void storeAndBroadcast(Hash tip1, Hash tip2, int mwm, List txs) throws Exception{
+ private List attachAndStore(Hash tip1, Hash tip2, int mwm, List txs) throws Exception{
List powResult = attachToTangleStatement(tip1, tip2, mwm, txs);
storeTransactionsStatement(powResult);
- broadcastTransactionsStatement(powResult);
+ return powResult;
}
//
@@ -1498,10 +1502,87 @@ public void publish(BundleTypes type, final String address, final int minWeightM
* @param txToApprove transactions to approve
* @throws Exception if storing fails
*/
- private void storeCustomBundle(final Hash sndAddr, final Hash rcvAddr, List txToApprove, byte[] data, final long tag, final int mwm, boolean sign, int keyIdx, int maxKeyIdx, String keyfile, int security) throws Exception {
+ private void storeCustomBundle(final Hash sndAddr, final Hash rcvAddr, List txToApprove, byte[] data, final long tag, final int mwm, boolean sign, int keyIdx, int maxKeyIdx, String keyfile, int security, boolean createVirtualTransactions) throws Exception {
BundleUtils bundle = new BundleUtils(sndAddr, rcvAddr);
bundle.create(data, tag, sign, keyIdx, maxKeyIdx, keyfile, security);
- storeAndBroadcast(txToApprove.get(0), txToApprove.get(1), mwm, bundle.getTransactions());
+ List transactionStrings = attachAndStore(txToApprove.get(0), txToApprove.get(1), mwm, bundle.getTransactions());
+
+ if (createVirtualTransactions) {
+ createAndBroadcastVirtualTransactions(data, transactionStrings);
+ }
+ broadcastTransactionsStatement(transactionStrings);
+ }
+
+ /**
+ * Creates and broadcast virtual transactions
+ *
+ * @param tipsBytes
+ * @param milestoneBundle
+ */
+ private void createAndBroadcastVirtualTransactions(byte[] tipsBytes, List milestoneBundle) {
+ TransactionViewModel milestone = getFirstTransactionFromBundle(milestoneBundle);
+ if (milestone != null) {
+ List tips = extractTipsFromData(tipsBytes);
+ if (tips.size() == 0) {
+ return;
+ }
+ MerkleOptions options = MerkleOptions.getDefault();
+ options.setMilestoneHash(milestone.getHash());
+ options.setAddress(milestone.getAddressHash());
+
+ List merkleTransactions = new TransactionMerkleTreeImpl().buildMerkle(tips, options);
+
+ List virtualTransactions = merkleTransactions.stream()
+ .filter(tvm -> ((TransactionViewModel) tvm).getHash() == Hash.NULL_HASH).map(t -> {
+ try {
+ TransactionViewModel tvm = (TransactionViewModel) t;
+ fillAttachmentTransactionFields(tvm.getBytes(), RoundIndexUtil.getCurrentTime());
+ tvm.storeTransactionLocal(tangle, snapshotProvider.getInitialSnapshot(), transactionValidator);
+ return Hex.toHexString(tvm.getBytes());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ })
+ .filter(t -> t != null).collect(Collectors.toList());
+ broadcastTransactionsStatement(virtualTransactions);
+ }
+ }
+
+ /**
+ * Get transaction with index = 0 from a transaction list
+ *
+ * @param bundle
+ * @return TransactionViewModel with current index = 0, otherwise null
+ */
+ private TransactionViewModel getFirstTransactionFromBundle(List bundle) {
+ for (String transactionString : bundle) {
+ final TransactionViewModel transactionViewModel = transactionValidator.validateBytes(Hex.decode(transactionString),
+ transactionValidator.getMinWeightMagnitude());
+ if (transactionViewModel.getCurrentIndex() == 0) {
+ return transactionViewModel;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Extract tips list from data byte array
+ *
+ * @param data
+ * @return
+ */
+ private List extractTipsFromData(byte[] data) {
+ List tips = new ArrayList<>();
+ int i = 0;
+ while (i < data.length / Hash.SIZE_IN_BYTES) {
+ Hash tip = HashFactory.TRANSACTION.create(data, i++ * Hash.SIZE_IN_BYTES, Hash.SIZE_IN_BYTES);
+ if (tip.equals(Hash.NULL_HASH)) {
+ break;
+ }
+ tips.add(tip);
+ }
+ return tips;
}
// refactoring WIP (the following methods will be moved from API)
@@ -1521,7 +1602,8 @@ public void publishMilestone(final String address, final int minWeightMagnitude,
byte[] tipsBytes = Hex.decode(confirmedTips.stream().map(Hash::toString).collect(Collectors.joining()));
List txToApprove = addMilestoneReferences(confirmedTips, currentRoundIndex);
- storeCustomBundle(HashFactory.ADDRESS.create(address), Hash.NULL_HASH, txToApprove, tipsBytes, (long) currentRoundIndex, minWeightMagnitude, sign, keyIndex, maxKeyIndex, configuration.getValidatorKeyfile(), configuration.getValidatorSecurity());
+
+ storeCustomBundle(HashFactory.ADDRESS.create(address), Hash.NULL_HASH, txToApprove, tipsBytes, (long) currentRoundIndex, minWeightMagnitude, sign, keyIndex, maxKeyIndex, configuration.getValidatorKeyfile(), configuration.getValidatorSecurity(), true);
}
public void publishRegistration(final String address, final int minWeightMagnitude, boolean sign, int keyIndex, int maxKeyIndex, boolean join) throws Exception {
@@ -1530,7 +1612,7 @@ public void publishRegistration(final String address, final int minWeightMagnitu
List txToApprove = getTransactionToApproveTips(3, Optional.empty());
- storeCustomBundle(HashFactory.ADDRESS.create(address), configuration.getValidatorManagerAddress(), txToApprove, data, join ? 1L : -1L, minWeightMagnitude, sign, keyIndex, maxKeyIndex, configuration.getValidatorKeyfile(), configuration.getValidatorSecurity());
+ storeCustomBundle(HashFactory.ADDRESS.create(address), configuration.getValidatorManagerAddress(), txToApprove, data, join ? 1L : -1L, minWeightMagnitude, sign, keyIndex, maxKeyIndex, configuration.getValidatorKeyfile(), configuration.getValidatorSecurity(), false);
}
public void publishKeyChange(final String oldAddress, final Hash newAddress, final int minWeightMagnitude, boolean sign, int keyIndex, int maxKeyIndex) throws Exception {
@@ -1539,7 +1621,7 @@ public void publishKeyChange(final String oldAddress, final Hash newAddress, fin
List txToApprove = getTransactionToApproveTips(3, Optional.empty());
- storeCustomBundle(HashFactory.ADDRESS.create(oldAddress), configuration.getValidatorManagerAddress(), txToApprove, data, 0L, minWeightMagnitude, sign, keyIndex, maxKeyIndex, configuration.getValidatorKeyfile(), configuration.getValidatorSecurity());
+ storeCustomBundle(HashFactory.ADDRESS.create(oldAddress), configuration.getValidatorManagerAddress(), txToApprove, data, 0L, minWeightMagnitude, sign, keyIndex, maxKeyIndex, configuration.getValidatorKeyfile(), configuration.getValidatorSecurity(), false);
}
public void publishValidator(int startRoundDelay, final int minWeightMagnitude, Boolean sign, int keyIndex, int maxKeyIndex) throws Exception {
@@ -1551,7 +1633,7 @@ public void publishValidator(int startRoundDelay, final int minWeightMagnitude,
// get branch and trunk
List txToApprove = getTransactionToApproveTips(3, Optional.empty());
- storeCustomBundle(configuration.getValidatorManagerAddress(), Hash.NULL_HASH, txToApprove, validatorBytes, (long) startRoundIndex, minWeightMagnitude, sign, keyIndex, maxKeyIndex, configuration.getValidatorManagerKeyfile(), configuration.getValidatorManagerSecurity());
+ storeCustomBundle(configuration.getValidatorManagerAddress(), Hash.NULL_HASH, txToApprove, validatorBytes, (long) startRoundIndex, minWeightMagnitude, sign, keyIndex, maxKeyIndex, configuration.getValidatorManagerKeyfile(), configuration.getValidatorManagerSecurity(), false);
}
@@ -1574,7 +1656,7 @@ private List getConfirmedTips() throws Exception {
BundleValidator.validate(tangle, snapshotProvider.getInitialSnapshot(), txVM.getHash()).size() != 0) {
if (walkValidator.isValid(transaction)) {
confirmedTips.add(transaction);
- } else {
+ } else if(txVM.isSolid()){
log.warn("Inconsistent transaction has been removed from tips: " + transaction.toString());
tipsViewModel.removeTipHash(transaction);
}
@@ -1605,8 +1687,10 @@ private List addMilestoneReferences(List confirmedTips, int roundInd
txToApprove.add(previousRound.getMerkleRoot()); // merkle root of latest milestones
}
//branch
- List> merkleTreeTips = Merkle.buildMerkleTree(confirmedTips);
- txToApprove.add(merkleTreeTips.get(merkleTreeTips.size() - 1).get(0)); // merkle root of confirmed tips
+ Hash merkleRoot = HashFactory.TRANSACTION.create(new MerkleTreeImpl().getMerkleRoot(confirmedTips, MerkleOptions.getDefault()));
+ txToApprove.add(merkleRoot);
+
+ log.debug("Milestone future branch transaction hash: " + merkleRoot);
}
return txToApprove;
}
diff --git a/src/main/java/net/helix/pendulum/service/milestone/VirtualTransactionService.java b/src/main/java/net/helix/pendulum/service/milestone/VirtualTransactionService.java
new file mode 100644
index 00000000..699d3811
--- /dev/null
+++ b/src/main/java/net/helix/pendulum/service/milestone/VirtualTransactionService.java
@@ -0,0 +1,15 @@
+package net.helix.pendulum.service.milestone;
+
+import net.helix.pendulum.controllers.TransactionViewModel;
+
+
+public interface VirtualTransactionService {
+
+ /**
+ * It tries to rebuild the virtual parent transaction for given trasaction model, parent transaction could be build if the sibling transaction already exists,
+ * then it will try to rebuild the next parent transaction an so on.
+ * @param transactionViewModel
+ * @return number of the built transactions.
+ */
+ int rebuildVirtualTransactionsIfPossible(TransactionViewModel transactionViewModel);
+}
diff --git a/src/main/java/net/helix/pendulum/service/milestone/impl/MilestonePublisher.java b/src/main/java/net/helix/pendulum/service/milestone/impl/MilestonePublisher.java
index 45cb0c26..527fcad1 100644
--- a/src/main/java/net/helix/pendulum/service/milestone/impl/MilestonePublisher.java
+++ b/src/main/java/net/helix/pendulum/service/milestone/impl/MilestonePublisher.java
@@ -1,12 +1,13 @@
package net.helix.pendulum.service.milestone.impl;
import net.helix.pendulum.conf.PendulumConfig;
-import net.helix.pendulum.crypto.Merkle;
+import net.helix.pendulum.crypto.merkle.MerkleNode;
import net.helix.pendulum.model.Hash;
import net.helix.pendulum.model.HashFactory;
import net.helix.pendulum.service.API;
import net.helix.pendulum.service.utils.RoundIndexUtil;
import net.helix.pendulum.service.validatomanager.CandidateTracker;
+import net.helix.pendulum.utils.KeyfileUtil;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -73,8 +74,8 @@ private void initSeed(PendulumConfig configuration) {
}
private void writeKeyIndex() throws IOException {
- List> merkleTree = Merkle.readKeyfile(new File(keyfile));
- Merkle.createKeyfile(merkleTree, Hex.decode(seed), pubkeyDepth, currentKeyIndex, keyfileIndex, keyfile);
+ List> merkleTree = KeyfileUtil.readKeyfile(new File(keyfile));
+ KeyfileUtil.createKeyfile(merkleTree, Hex.decode(seed), pubkeyDepth, currentKeyIndex, keyfileIndex, keyfile);
}
private void readKeyfileMetadata() throws IOException {
@@ -87,7 +88,7 @@ private void readKeyfileMetadata() throws IOException {
seed = fields[1];
}
}
- List> merkleTree = Merkle.readKeyfile(new File(keyfile));
+ List> merkleTree = KeyfileUtil.readKeyfile(new File(keyfile));
address = HashFactory.ADDRESS.create(merkleTree.get(merkleTree.size() - 1).get(0).bytes());
}
@@ -106,7 +107,7 @@ private void doKeyChange() throws Exception {
// generate new keyfile
int newKeyfileIndex = keyfileIndex + 1;
log.debug("Generating Keyfile (idx: " + newKeyfileIndex + ")");
- List> merkleTree = Merkle.buildMerkleKeyTree(seed, pubkeyDepth, maxKeyIndex * newKeyfileIndex, maxKeyIndex, config.getValidatorSecurity());
+ List> merkleTree = KeyfileUtil.buildMerkleKeyTree(seed, pubkeyDepth, maxKeyIndex * newKeyfileIndex, maxKeyIndex, config.getValidatorSecurity());
Hash newAddress = HashFactory.ADDRESS.create(merkleTree.get(merkleTree.size()-1).get(0).bytes());
// send keyChange bundle to register new address
api.publishKeyChange(address.toString(), newAddress, mwm, sign, currentKeyIndex, maxKeyIndex);
@@ -114,13 +115,13 @@ private void doKeyChange() throws Exception {
keyfileIndex = newKeyfileIndex;
address = newAddress;
currentKeyIndex = maxKeyIndex * keyfileIndex;
- Merkle.createKeyfile(merkleTree, Hex.decode(seed), pubkeyDepth, 0, keyfileIndex, keyfile);
+ KeyfileUtil.createKeyfile(merkleTree, Hex.decode(seed), pubkeyDepth, 0, keyfileIndex, keyfile);
}
private void generateKeyfile(String seed) throws Exception {
log.debug("Generating Keyfile (idx: " + keyfileIndex + ")");
- List> merkleTree = Merkle.buildMerkleKeyTree(seed, pubkeyDepth, maxKeyIndex * keyfileIndex, maxKeyIndex, config.getValidatorSecurity());
- Merkle.createKeyfile(merkleTree, Hex.decode(seed), pubkeyDepth, 0, keyfileIndex, keyfile);
+ List> merkleTree = KeyfileUtil.buildMerkleKeyTree(seed, pubkeyDepth, maxKeyIndex * keyfileIndex, maxKeyIndex, config.getValidatorSecurity());
+ KeyfileUtil.createKeyfile(merkleTree, Hex.decode(seed), pubkeyDepth, 0, keyfileIndex, keyfile);
address = HashFactory.ADDRESS.create(merkleTree.get(merkleTree.size()-1).get(0).bytes());
}
diff --git a/src/main/java/net/helix/pendulum/service/milestone/impl/MilestoneServiceImpl.java b/src/main/java/net/helix/pendulum/service/milestone/impl/MilestoneServiceImpl.java
index b3ffd128..a927bf92 100644
--- a/src/main/java/net/helix/pendulum/service/milestone/impl/MilestoneServiceImpl.java
+++ b/src/main/java/net/helix/pendulum/service/milestone/impl/MilestoneServiceImpl.java
@@ -6,8 +6,9 @@
import net.helix.pendulum.conf.ConsensusConfig;
import net.helix.pendulum.controllers.RoundViewModel;
import net.helix.pendulum.controllers.TransactionViewModel;
-import net.helix.pendulum.crypto.Merkle;
import net.helix.pendulum.crypto.SpongeFactory;
+import net.helix.pendulum.crypto.merkle.MerkleOptions;
+import net.helix.pendulum.crypto.merkle.impl.MerkleTreeImpl;
import net.helix.pendulum.model.Hash;
import net.helix.pendulum.model.IntegerIndex;
import net.helix.pendulum.model.StateDiff;
@@ -183,7 +184,8 @@ public MilestoneValidity validateMilestone(TransactionViewModel transactionViewM
if (isMilestoneBundleStructureValid(bundleTransactionViewModels, securityLevel)) {
Hash senderAddress = tail.getAddressHash();
- boolean validSignature = Merkle.validateMerkleSignature(bundleTransactionViewModels, mode, senderAddress, securityLevel, config.getMilestoneKeyDepth());
+ boolean validSignature = new MerkleTreeImpl().validateMerkleSignature(bundleTransactionViewModels,
+ new MerkleOptions(mode, senderAddress, securityLevel, config.getMilestoneKeyDepth()));
//System.out.println("valid signature: " + validSignature);
if ((config.isTestnet() && config.isDontValidateTestnetMilestoneSig()) ||
diff --git a/src/main/java/net/helix/pendulum/service/milestone/impl/VirtualTransactionServiceImpl.java b/src/main/java/net/helix/pendulum/service/milestone/impl/VirtualTransactionServiceImpl.java
new file mode 100644
index 00000000..fab27f33
--- /dev/null
+++ b/src/main/java/net/helix/pendulum/service/milestone/impl/VirtualTransactionServiceImpl.java
@@ -0,0 +1,233 @@
+package net.helix.pendulum.service.milestone.impl;
+
+import net.helix.pendulum.TransactionValidator;
+import net.helix.pendulum.controllers.BundleNonceViewModel;
+import net.helix.pendulum.controllers.RoundViewModel;
+import net.helix.pendulum.controllers.TransactionViewModel;
+import net.helix.pendulum.crypto.SpongeFactory;
+import net.helix.pendulum.model.Hash;
+import net.helix.pendulum.service.milestone.VirtualTransactionService;
+import net.helix.pendulum.service.snapshot.SnapshotProvider;
+import net.helix.pendulum.storage.Tangle;
+import net.helix.pendulum.utils.bundle.BundleUtils;
+import org.apache.commons.collections4.CollectionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static net.helix.pendulum.controllers.TransactionViewModel.PREFILLED_SLOT;
+import static net.helix.pendulum.controllers.TransactionViewModel.fromHash;
+
+/**
+ * Creates a service instance that allows us to perform virtual transaction operations.
+ *
+ */
+public class VirtualTransactionServiceImpl implements VirtualTransactionService {
+
+ private static final Logger log = LoggerFactory.getLogger(VirtualTransactionServiceImpl.class);
+
+ /**
+ * Holds the tangle object which acts as a database interface.
+ */
+ private Tangle tangle;
+
+ /**
+ * Holds the snapshot provider which gives us access to the relevant snapshots.
+ */
+ private SnapshotProvider snapshotProvider;
+
+ /**
+ * Holds transaction validator.
+ */
+ private TransactionValidator transactionValidator;
+
+
+ /**
+ * This method initializes the instance and registers its dependencies.
+ *
+ * It simply stores the passed in values in their corresponding private properties.
+ *
+ * Note: Instead of handing over the dependencies in the constructor, we register them lazy. This allows us to have
+ * circular dependencies because the instantiation is separated from the dependency injection. To reduce the
+ * amount of code that is necessary to correctly instantiate this class, we return the instance itself which
+ * allows us to still instantiate, initialize and assign in one line - see Example:
+ *
+ * {@code milestoneService = new VirtualTransactionServiceImpl().init(...);}
+ *
+ * @param tangle Tangle object which acts as a database interface
+ * @param snapshotProvider snapshot provider which gives us access to the relevant snapshots
+ * @return the initialized instance itself to allow chaining
+ */
+ public VirtualTransactionServiceImpl init(Tangle tangle, SnapshotProvider snapshotProvider, TransactionValidator transactionValidator) {
+ this.tangle = tangle;
+ this.snapshotProvider = snapshotProvider;
+ this.transactionValidator = transactionValidator;
+ return this;
+ }
+
+ /**
+ * Rebuilds a new virtual transaction which is based on current virtual transaction and its sibling (it this transaction exists)
+ * Attached the new built transaction in local tangle
+ *
+ * @param transactionViewModel transaction view model of the child
+ * @return true if the new transaction is created, otherwise false.
+ */
+ @Override
+ public int rebuildVirtualTransactionsIfPossible(TransactionViewModel transactionViewModel) {
+ if (!transactionViewModel.isVirtual()) {
+ return 0;
+ }
+ int noOfRebuitTransactions = 0;
+ TransactionViewModel nextTransaction = transactionViewModel;
+ while (nextTransaction != null) {
+ nextTransaction = rebuildViirtualParentTransactionIfPossible(nextTransaction);
+ if (nextTransaction != null) {
+ noOfRebuitTransactions++;
+ }
+ }
+ return noOfRebuitTransactions;
+ }
+
+ private TransactionViewModel rebuildViirtualParentTransactionIfPossible(TransactionViewModel tvm) {
+ long currentIndex = tvm.getTagLongValue();
+ long siblingIndex = getSiblingIndex(currentIndex);
+ long parentIndex = getParentIndex(currentIndex);
+
+ Hash milestoneHash = tvm.getBundleNonceHash();
+ Map transactions = findByTagAndMilestone(milestoneHash, Arrays.asList(siblingIndex, parentIndex));
+
+ if (!transactions.containsKey(parentIndex) && transactions.containsKey(siblingIndex)) {
+ try {
+ TransactionViewModel siblingTransaction = fromHash(tangle, transactions.get(siblingIndex));
+ TransactionViewModel newTransaction = reconstructParent(tvm, siblingTransaction);
+ log.debug("Rebuild transaction virtual parent transaction {} for: {} ", newTransaction.getHash(), tvm.getHash());
+ return newTransaction;
+ } catch (Exception e) {
+ log.error("Error during generation of virtual transaction parent!");
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Reconstruct virtual parent based on 2 transactions which are consider the direct children of the new created transaction.
+ * Order of the t1 and t2 doesn't matter, they will be sorted according with their merkle index.
+ *
+ * @param t1 first child transaction
+ * @param t2 second child transactiopn
+ * @return
+ * @throws Exception
+ */
+ private TransactionViewModel reconstructParent(TransactionViewModel t1, TransactionViewModel t2) throws Exception {
+ if (validVirtualTransaction(Arrays.asList(t1, t2))) {
+ log.warn("Transactions: {} {} are not valid virtual transaction!", t1.getHash(), t2.getHash());
+ return null;
+ }
+ Hash trunk = RoundViewModel.getRoundIndex(t1) > RoundViewModel.getRoundIndex(t2) ? t2.getHash() : t1.getHash();
+ Hash branch = RoundViewModel.getRoundIndex(t1) < RoundViewModel.getRoundIndex(t2) ? t2.getHash() : t1.getHash();
+
+ byte[] newTransaction = BundleUtils.createVirtualTransaction(branch, trunk, getParentIndex(RoundViewModel.getRoundIndex(t1)),
+ t1.getSignature(), t1.getAddressHash());
+
+ TransactionViewModel newTransactionViewModel = new TransactionViewModel(newTransaction, SpongeFactory.Mode.S256);
+ newTransactionViewModel.storeTransactionLocal(tangle, snapshotProvider.getInitialSnapshot(), transactionValidator);
+ return newTransactionViewModel;
+ }
+
+ /**
+ * Get parent index of the current virtual transaction
+ *
+ * @param index merkle index of the current transaction
+ * @return merkle index of the parent transaction
+ */
+ private long getParentIndex(long index) {
+ return index % 2 == 0 ? index / 2 - 1 : (index - 1) / 2L;
+ }
+
+ /**
+ * Get sibling index, based on current transaction index.
+ *
+ * @param index of the current transaction
+ * @return the index of the other child (from merkle tree)
+ */
+ private long getSiblingIndex(long index) {
+ return index % 2 == 0 ? index - 1 : index + 1;
+ }
+
+
+ /**
+ * Virtual transactions validator. Call areVirtualTransaction and areSiblingVirtualTransactions validators.
+ *
+ * @param transactions, list of transactions that will be validated
+ * @return true if all transaction from povided list are valid virtual transactions.
+ */
+ private boolean validVirtualTransaction(List transactions) {
+ return areVirtualTransaction(transactions) &&
+ areSiblingVirtualTransactions(transactions);
+ }
+
+ /**
+ * Virtual transactions validator. Checks if all transactions are virtual.
+ *
+ * @param transactions, list of transactions that will be validated
+ * @return true if all transaction from povided list are valid virtual transactions.
+ */
+ private boolean areVirtualTransaction(List transactions) {
+ if (CollectionUtils.isEmpty(transactions)) {
+ return false;
+ }
+ return transactions.stream().filter(t -> !t.isVirtual()).collect(Collectors.toList()).size() == 0;
+ }
+
+ /**
+ * Virtual transactions validator. Checks if all transactions are siblings
+ *
+ * @param transactions, list of transactions that will be validated
+ * @return true if all transaction from povided list are valid virtual transactions.
+ */
+ private boolean areSiblingVirtualTransactions(List transactions) {
+ if (CollectionUtils.isEmpty(transactions)) {
+ return false;
+ }
+ Hash firstTag = transactions.get(0).getTagValue();
+ if (firstTag == null) {
+ return false;
+ }
+ return transactions.stream().filter(t -> firstTag.equals(t.getTagValue())).collect(Collectors.toList()).size() == 0;
+ }
+
+
+ /**
+ * Find all transaction which belongs to milestone and have tags from parameter.
+ *
+ * @param milestoneHash milestone hash
+ * @param tags List of longs which represents the index in merkle tree
+ * @return map of tag (long value) and transaction hashes
+ */
+ public Map findByTagAndMilestone(Hash milestoneHash, List tags) {
+ Map tagTransactionHash = new HashMap();
+ try {
+ Set transactionHashes = BundleNonceViewModel.load(tangle, milestoneHash)
+ .getHashes();
+ for (Hash t : transactionHashes) {
+ TransactionViewModel tvm = fromHash(tangle, t);
+ if (tvm.getType() == PREFILLED_SLOT) {
+ continue;
+ }
+ tags.stream().filter(tag -> !tagTransactionHash.keySet().contains(tag)).forEach(tag -> {
+ if (tag == tvm.getTagLongValue()) {
+ tagTransactionHash.put(tag, t);
+ }
+ });
+ if (tagTransactionHash.size() == tags.size()) {
+ break;
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return tagTransactionHash;
+ }
+}
diff --git a/src/main/java/net/helix/pendulum/service/validatomanager/impl/ValidatorManagerServiceImpl.java b/src/main/java/net/helix/pendulum/service/validatomanager/impl/ValidatorManagerServiceImpl.java
index 78362663..262e794b 100644
--- a/src/main/java/net/helix/pendulum/service/validatomanager/impl/ValidatorManagerServiceImpl.java
+++ b/src/main/java/net/helix/pendulum/service/validatomanager/impl/ValidatorManagerServiceImpl.java
@@ -3,8 +3,9 @@
import net.helix.pendulum.BundleValidator;
import net.helix.pendulum.conf.PendulumConfig;
import net.helix.pendulum.controllers.TransactionViewModel;
-import net.helix.pendulum.crypto.Merkle;
import net.helix.pendulum.crypto.SpongeFactory;
+import net.helix.pendulum.crypto.merkle.MerkleOptions;
+import net.helix.pendulum.crypto.merkle.impl.MerkleTreeImpl;
import net.helix.pendulum.model.Hash;
import net.helix.pendulum.service.snapshot.SnapshotProvider;
import net.helix.pendulum.service.snapshot.SnapshotService;
@@ -74,7 +75,9 @@ public CandidateValidity validateCandidate(TransactionViewModel transactionViewM
if (tail.getHash().equals(transactionViewModel.getHash()) && isCandidateBundleStructureValid(bundleTransactionViewModels, securityLevel)) {
Hash senderAddress = tail.getAddressHash();
- boolean validSignature = Merkle.validateMerkleSignature(bundleTransactionViewModels, mode, senderAddress, securityLevel, config.getMilestoneKeyDepth());
+
+ boolean validSignature = new MerkleTreeImpl().validateMerkleSignature(bundleTransactionViewModels,
+ new MerkleOptions(mode, senderAddress, securityLevel, config.getMilestoneKeyDepth()));
if ((config.isTestnet() && config.isDontValidateTestnetMilestoneSig()) || (validator.contains(senderAddress)) && validSignature) {
return VALID;
diff --git a/src/main/java/net/helix/pendulum/service/validator/impl/ValidatorServiceImpl.java b/src/main/java/net/helix/pendulum/service/validator/impl/ValidatorServiceImpl.java
index 36d5fa66..205dc0bf 100644
--- a/src/main/java/net/helix/pendulum/service/validator/impl/ValidatorServiceImpl.java
+++ b/src/main/java/net/helix/pendulum/service/validator/impl/ValidatorServiceImpl.java
@@ -3,8 +3,9 @@
import net.helix.pendulum.BundleValidator;
import net.helix.pendulum.conf.PendulumConfig;
import net.helix.pendulum.controllers.TransactionViewModel;
-import net.helix.pendulum.crypto.Merkle;
import net.helix.pendulum.crypto.SpongeFactory;
+import net.helix.pendulum.crypto.merkle.MerkleOptions;
+import net.helix.pendulum.crypto.merkle.impl.MerkleTreeImpl;
import net.helix.pendulum.model.Hash;
import net.helix.pendulum.service.snapshot.SnapshotProvider;
import net.helix.pendulum.service.snapshot.SnapshotService;
@@ -58,7 +59,8 @@ public ValidatorValidity validateValidators(TransactionViewModel transactionView
// validate signature
Hash senderAddress = tail.getAddressHash();
- boolean validSignature = Merkle.validateMerkleSignature(bundleTransactionViewModels, mode, senderAddress, securityLevel, config.getValidatorManagerKeyDepth());
+ boolean validSignature = new MerkleTreeImpl().validateMerkleSignature(bundleTransactionViewModels,
+ new MerkleOptions(mode, senderAddress, securityLevel, config.getMilestoneKeyDepth()));
//System.out.println("valid signature (validator): " + validSignature);
if ((config.isTestnet() && config.isDontValidateTestnetMilestoneSig()) || (config.getValidatorManagerAddress().equals(senderAddress) && validSignature)) {
diff --git a/src/main/java/net/helix/pendulum/utils/KeyfileUtil.java b/src/main/java/net/helix/pendulum/utils/KeyfileUtil.java
new file mode 100644
index 00000000..4ced4645
--- /dev/null
+++ b/src/main/java/net/helix/pendulum/utils/KeyfileUtil.java
@@ -0,0 +1,98 @@
+package net.helix.pendulum.utils;
+
+import net.helix.pendulum.crypto.Winternitz;
+import net.helix.pendulum.crypto.merkle.MerkleNode;
+import net.helix.pendulum.crypto.merkle.MerkleOptions;
+import net.helix.pendulum.crypto.merkle.MerkleTree;
+import net.helix.pendulum.crypto.merkle.impl.MerkleTreeImpl;
+import net.helix.pendulum.model.Hash;
+import net.helix.pendulum.model.HashFactory;
+import org.bouncycastle.util.encoders.Hex;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * It contains the required methods to manage key files (read, write, build)
+ */
+public class KeyfileUtil {
+
+ public static String getSeed(File keyfile) throws IOException {
+ StringBuilder seedBuilder = new StringBuilder();
+ try (BufferedReader br = new BufferedReader(new FileReader(keyfile))) {
+ String[] fields = br.readLine().split(" ");
+ seedBuilder.append(fields[1]);
+ }
+ return seedBuilder.toString();
+ }
+
+ public static byte[] getKeyTreeRoot(String seed, int pubkeyDepth, int firstIndex, int pubkeyCount, int security) {
+ List> tree = buildMerkleKeyTree(seed, pubkeyDepth,firstIndex,pubkeyCount,security);
+ return tree.get(tree.size()-1).get(0).bytes();
+
+ }
+ public static List> buildMerkleKeyTree(String seed, int pubkeyDepth, int firstIndex, int pubkeyCount, int security) {
+ List keys = new ArrayList<>(1 << pubkeyDepth);
+ for (int i = 0; i < pubkeyCount; i++) {
+ int idx = firstIndex + i;
+ keys.add(createKeyHash(seed, security, idx));
+ }
+ return (new MerkleTreeImpl()).buildMerkleTree(keys, MerkleOptions.getDefault());
+ }
+
+ public static void writeKeys(BufferedWriter bw, List keys) throws IOException {
+ int leadingNulls = 0;
+ while (keys.get(leadingNulls) == null) {
+ leadingNulls++;
+ }
+ bw.write(leadingNulls + " ");
+ for (int i = leadingNulls; i < keys.size(); i++) {
+ if (keys.get(i) == null) {
+ break;
+ }
+ bw.write(writeMerkleNode(keys.get(i)));
+ }
+ bw.newLine();
+ }
+
+ public static void createKeyfile(List> merkleTree, byte[] seed, int pubkeyDepth, int keyIndex, int keyfileIndex, String filename) throws IOException {
+ try (BufferedWriter bw = new BufferedWriter(new FileWriter(filename))) {
+ bw.write(pubkeyDepth + " " + Hex.toHexString(seed) + " " + keyfileIndex + " " + keyIndex);
+ bw.newLine();
+ writeKeys(bw, merkleTree.get(0));
+ for (int i = 1; i < merkleTree.size(); i++) {
+ writeKeys(bw, merkleTree.get(i));
+ }
+ }
+ }
+
+ public static List> readKeyfile(File keyfile) throws IOException {
+ try (BufferedReader br = new BufferedReader(new FileReader(keyfile))) {
+ String[] fields = br.readLine().split(" ");
+ int depth = Integer.parseInt(fields[0]);
+ List> result = new ArrayList<>(depth + 1);
+ for (int i = 0; i <= depth; i++) {
+ fields = br.readLine().split(" ");
+ int leadingNulls = Integer.parseInt(fields[0]);
+ List row = new ArrayList<>();
+ for (int j = 0; j < leadingNulls; j++) {
+ row.add(Hash.NULL_HASH);
+ }
+ for (int j = 0; j < fields[1].length() / 64; j++) {
+ row.add(HashFactory.ADDRESS.create(fields[1].substring(j * 64, (j + 1) * 64)));
+ }
+ result.add(row);
+ }
+ return result;
+ }
+ }
+
+ private static Hash createKeyHash(String seed, int security, int idx) {
+ return HashFactory.ADDRESS.create(Winternitz.generateAddress(Hex.decode(seed), idx, security));
+ }
+
+ private static String writeMerkleNode(MerkleNode key) {
+ return key.toString();
+ }
+}
diff --git a/src/main/java/net/helix/pendulum/utils/bundle/BundleUtils.java b/src/main/java/net/helix/pendulum/utils/bundle/BundleUtils.java
index df8134fa..642c1f44 100644
--- a/src/main/java/net/helix/pendulum/utils/bundle/BundleUtils.java
+++ b/src/main/java/net/helix/pendulum/utils/bundle/BundleUtils.java
@@ -1,11 +1,14 @@
package net.helix.pendulum.utils.bundle;
import net.helix.pendulum.controllers.TransactionViewModel;
-import net.helix.pendulum.crypto.Merkle;
import net.helix.pendulum.crypto.Sponge;
import net.helix.pendulum.crypto.SpongeFactory;
import net.helix.pendulum.crypto.Winternitz;
+import net.helix.pendulum.crypto.merkle.MerkleNode;
+import net.helix.pendulum.crypto.merkle.impl.MerkleTreeImpl;
import net.helix.pendulum.model.Hash;
+import net.helix.pendulum.service.utils.RoundIndexUtil;
+import net.helix.pendulum.utils.KeyfileUtil;
import net.helix.pendulum.utils.Serializer;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
@@ -20,7 +23,7 @@
public class BundleUtils {
- private final Logger log = LoggerFactory.getLogger(BundleUtils.class);
+ private static final Logger log = LoggerFactory.getLogger(BundleUtils.class);
private List senderTransactions = new ArrayList<>();
private byte[] merkleTransaction;
@@ -38,6 +41,9 @@ public BundleUtils(Hash senderAddress, Hash receiverAddress) {
this.receiverAddress = receiverAddress.toString();
}
+ public BundleUtils() {
+ }
+
/**
* @return transactions
*/
@@ -116,7 +122,7 @@ public void create(byte[] data, long tag, Boolean sign, int keyIndex, int maxKey
* @param tag tag
* @return transaction
*/
- private byte[] initTransaction(String address, int currentIndex, int lastIndex, long timestamp, long tag) {
+ public byte[] initTransaction(String address, int currentIndex, int lastIndex, long timestamp, long tag) {
byte[] transaction = new byte[TransactionViewModel.SIZE];
System.arraycopy(Hex.decode(address), 0, transaction, TransactionViewModel.ADDRESS_OFFSET, TransactionViewModel.ADDRESS_SIZE);
System.arraycopy(Serializer.serialize((long) currentIndex), 0, transaction, TransactionViewModel.CURRENT_INDEX_OFFSET, TransactionViewModel.CURRENT_INDEX_SIZE);
@@ -159,13 +165,13 @@ private byte[] addBundleHash(List bundle, SpongeFactory.Mode mode) {
private void signBundle(String filepath, byte[] merkleTransaction, List senderTransactions, byte[] bundleHash, int keyIndex, int maxKeyIndex) throws IOException {
// Get merkle path and store in signatureMessageFragment of Sibling Transaction
File keyfile = new File(filepath);
- List> merkleTree = Merkle.readKeyfile(keyfile);
- String seed = Merkle.getSeed(keyfile);
+ List> merkleTree = KeyfileUtil.readKeyfile(keyfile);
+ String seed = KeyfileUtil.getSeed(keyfile);
int security = senderTransactions.size();
// create merkle path from keyfile
- List merklePath = Merkle.getMerklePath(merkleTree, keyIndex % maxKeyIndex);
- byte[] path = Hex.decode(merklePath.stream().map(Hash::toString).collect(Collectors.joining()));
+ List merklePath = new MerkleTreeImpl().getMerklePath(merkleTree, keyIndex % maxKeyIndex);
+ byte[] path = Hex.decode(merklePath.stream().map(m -> m.toString()).collect(Collectors.joining()));
System.arraycopy(path, 0, merkleTransaction, TransactionViewModel.SIGNATURE_MESSAGE_FRAGMENT_OFFSET, path.length);
// sign bundle hash and store signature in Milestone Transaction
@@ -180,4 +186,42 @@ private void signBundle(String filepath, byte[] merkleTransaction, List
System.arraycopy(signature, 0, senderTransactions.get(i), TransactionViewModel.SIGNATURE_MESSAGE_FRAGMENT_OFFSET, TransactionViewModel.SIGNATURE_MESSAGE_FRAGMENT_SIZE);
}
}
+
+ /**
+ * Create a virtual transaction, the following fields are filled:
+ * - Branch and trunk transactions with information given in input parameters, should be the children of the current node in Merkle tree
+ * - Tag with merkle index from input parameters
+ * - Address with address from input parameters, should be the address of the validator
+ * - SignatureMessage with bundleNonce from input parameters, should be the hash of the milestone
+ * - BundleNonce with a default value for virtual transaction TransasctionViewModel.DEFAULT_VIRTUAL_BUNDLE_NONCE
+ * - Timestamp with current timestamp
+ * - Current index and last index are set as 0
+ *
+ * @param branchHash - branch transaction hash
+ * @param trunkHash - trunk transaction hash
+ * @param merkleIndex - index in merkle tree
+ * @param bundleNonce - list of bytes should contain the milestone hash
+ * @param address - current address of the validator
+ * @return
+ */
+ public static byte[] createVirtualTransaction(Hash branchHash, Hash trunkHash, long merkleIndex, byte[] bundleNonce, Hash address) {
+
+ if(bundleNonce.length > TransactionViewModel.BUNDLE_NONCE_SIZE){
+ log.error("Tried to create a virtual trasaction with bundleNonce greater than {}, for branch{}, and trunk {}.",
+ TransactionViewModel.BUNDLE_NONCE_SIZE, branchHash.toString(), trunkHash.toString());
+ return null;
+ }
+
+ BundleUtils bundleUtils = new BundleUtils();
+ byte[] transaction = bundleUtils.initTransaction(Hex.toHexString(address.bytes()), 0, 0, RoundIndexUtil.getCurrentTime(), merkleIndex);
+ System.arraycopy(bundleNonce, 0, transaction, TransactionViewModel.BUNDLE_NONCE_OFFSET, TransactionViewModel.BUNDLE_NONCE_SIZE);
+
+ // mark this transaction as virtual (all bundle nonce bits are one) and nonce is zero.
+ byte[] nonce = new byte[TransactionViewModel.NONCE_SIZE];
+ Arrays.fill(nonce, (byte) 0xff);
+ System.arraycopy(nonce, 0, transaction, TransactionViewModel.NONCE_OFFSET, TransactionViewModel.NONCE_SIZE);
+ System.arraycopy(branchHash.bytes(), 0, transaction, TransactionViewModel.BRANCH_TRANSACTION_OFFSET, TransactionViewModel.BRANCH_TRANSACTION_SIZE);
+ System.arraycopy(trunkHash.bytes(), 0, transaction, TransactionViewModel.TRUNK_TRANSACTION_OFFSET, TransactionViewModel.TRUNK_TRANSACTION_SIZE);
+ return transaction;
+ }
}
diff --git a/src/test/java/net/helix/pendulum/crypto/merkle/MerkleTreeTest.java b/src/test/java/net/helix/pendulum/crypto/merkle/MerkleTreeTest.java
new file mode 100644
index 00000000..83a9fec8
--- /dev/null
+++ b/src/test/java/net/helix/pendulum/crypto/merkle/MerkleTreeTest.java
@@ -0,0 +1,58 @@
+package net.helix.pendulum.crypto.merkle;
+
+import net.helix.pendulum.TransactionTestUtils;
+import net.helix.pendulum.controllers.TransactionViewModel;
+import net.helix.pendulum.crypto.SpongeFactory;
+import net.helix.pendulum.crypto.merkle.impl.MerkleTreeImpl;
+import net.helix.pendulum.model.Hash;
+import net.helix.pendulum.model.HashFactory;
+import net.helix.pendulum.model.TransactionHash;
+import net.helix.pendulum.utils.KeyfileUtil;
+import org.bouncycastle.util.encoders.Hex;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class MerkleTreeTest {
+
+ @Test
+ public void createMerkleTreeAndValidateIt() {
+ int keyDepth = 5;
+ int firstIndex = 0;
+ int security = 2;
+ int keyIndex = 0;
+ int maxKeyIndex = (int) Math.pow(2, keyDepth);
+ String seed = "aabbccdd00000000000000000000000000000000000000000000000000000000";
+
+ byte[] root = KeyfileUtil.getKeyTreeRoot(seed, keyDepth, firstIndex, maxKeyIndex, security);
+
+ Hash senderAddress = HashFactory.ADDRESS.create(root);
+ MerkleTree merkle = new MerkleTreeImpl();
+ MerkleOptions options = new MerkleOptions(SpongeFactory.Mode.S256, senderAddress, security, keyDepth);
+
+ List> merkleTree = KeyfileUtil.buildMerkleKeyTree(seed, keyDepth, maxKeyIndex * keyIndex, maxKeyIndex, security);
+
+ Assert.assertTrue(senderAddress.equals(HashFactory.ADDRESS.create(merkleTree.get(merkleTree.size() - 1).get(0).bytes())));
+
+ List merklePath = new MerkleTreeImpl().getMerklePath(merkleTree, keyIndex % maxKeyIndex);
+ byte[] path = Hex.decode(merklePath.stream().map(m -> m.toString()).collect(Collectors.joining()));
+ byte[] merkleTransactionSignature = new byte[TransactionViewModel.SIGNATURE_MESSAGE_FRAGMENT_SIZE];
+
+ System.arraycopy(path, 0, merkleTransactionSignature, 0, path.length);
+ byte[] merkleRootTest = merkle.getMerkleRoot(options.getMode(), senderAddress.bytes(),
+ merkleTransactionSignature, 0, keyIndex, options.getDepth());
+
+ Assert.assertTrue(HashFactory.ADDRESS.create(merkleRootTest).equals(options.getAddress()));
+ }
+
+ private List getHashes(int noOfLeaves) {
+ List transactions = new ArrayList();
+ for (int i = 0; i < noOfLeaves; i++) {
+ transactions.add(TransactionHash.calculate(SpongeFactory.Mode.S256, TransactionTestUtils.getTransactionBytes()));
+ }
+ return transactions;
+ }
+}
diff --git a/src/test/java/net/helix/pendulum/crypto/merkle/TransactionMerkleTreeTest.java b/src/test/java/net/helix/pendulum/crypto/merkle/TransactionMerkleTreeTest.java
new file mode 100644
index 00000000..d24329e6
--- /dev/null
+++ b/src/test/java/net/helix/pendulum/crypto/merkle/TransactionMerkleTreeTest.java
@@ -0,0 +1,51 @@
+package net.helix.pendulum.crypto.merkle;
+
+import net.helix.pendulum.TransactionTestUtils;
+import net.helix.pendulum.controllers.TransactionViewModel;
+import net.helix.pendulum.crypto.SpongeFactory;
+import net.helix.pendulum.crypto.merkle.impl.TransactionMerkleTreeImpl;
+import net.helix.pendulum.model.Hash;
+import net.helix.pendulum.model.TransactionHash;
+import org.junit.Assert;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+
+
+public class TransactionMerkleTreeTest {
+
+ private static final Logger log = LoggerFactory.getLogger(TransactionMerkleTreeTest.class);
+
+ @Test
+ public void testTreeTransactionGeneration() {
+ // 6 index is missing because null leaves are not added into merkle tree
+ checkMerkleVirtualIndexes(10, new HashSet(Arrays.asList(0L, 1L, 2L, 3L, 4L, 5L, 7L, 8L, 9L, 10L, 11L)));
+ }
+
+ private void checkMerkleVirtualIndexes(int noOfLeaves, Set expectedIndexes) {
+ TransactionHash milestoneHash = TransactionHash.calculate(SpongeFactory.Mode.S256, TransactionTestUtils.getTransactionBytes());
+ List transactions = new ArrayList();
+ for (int i = 0; i < noOfLeaves; i++) {
+ transactions.add(TransactionHash.calculate(SpongeFactory.Mode.S256, TransactionTestUtils.getTransactionBytes()));
+ }
+ MerkleOptions options = MerkleOptions.getDefault();
+ options.setMilestoneHash(milestoneHash);
+ options.setAddress(Hash.NULL_HASH);
+ List virtualTransactions = new TransactionMerkleTreeImpl().buildMerkle(transactions, options);
+ Assert.assertTrue(virtualTransactions.size() > 0);
+ Set merkleIndexes = new HashSet();
+ virtualTransactions.forEach(t -> {
+ try {
+ TransactionViewModel virtualTransaction = (TransactionViewModel)t;
+ Assert.assertTrue(virtualTransaction.isVirtual());
+ merkleIndexes.add(virtualTransaction.getTagLongValue());
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+ Assert.assertEquals(expectedIndexes, merkleIndexes);
+ }
+}
diff --git a/src/test/java/net/helix/pendulum/model/HashTest.java b/src/test/java/net/helix/pendulum/model/HashTest.java
index 8e26a449..4cc65f1b 100644
--- a/src/test/java/net/helix/pendulum/model/HashTest.java
+++ b/src/test/java/net/helix/pendulum/model/HashTest.java
@@ -1,6 +1,8 @@
package net.helix.pendulum.model;
+import net.helix.pendulum.TransactionTestUtils;
import net.helix.pendulum.controllers.TransactionViewModel;
+import net.helix.pendulum.crypto.Sponge;
import net.helix.pendulum.crypto.SpongeFactory;
import org.bouncycastle.util.encoders.Hex;
import org.junit.Assert;
@@ -110,4 +112,28 @@ public void compareToTest() throws Exception {
Assert.assertEquals(hash.compareTo(Hash.NULL_HASH), -Hash.NULL_HASH.compareTo(hash));
}
+ @Test
+ public void testHashAggregation() {
+ TransactionHash hash1 = TransactionHash.calculate(SpongeFactory.Mode.S256, TransactionTestUtils.getTransactionBytes());
+ TransactionHash hash2 = TransactionHash.calculate(SpongeFactory.Mode.S256, TransactionTestUtils.getTransactionBytes());
+
+ Sponge sha3 = SpongeFactory.create(SpongeFactory.Mode.S256);
+ byte[] result = new byte[Hash.SIZE_IN_BYTES];
+ sha3.reset();
+ sha3.absorb(hash1.bytes(), 0, hash1.getByteSize());
+ sha3.absorb(hash2.bytes(), 0, hash2.getByteSize());
+ sha3.squeeze(result, 0, Hash.SIZE_IN_BYTES);
+ TransactionHash resultHash = (TransactionHash) HashFactory.TRANSACTION.create(result, 0, Hash.SIZE_IN_BYTES);
+
+ byte[] buffer = new byte[2 * Hash.SIZE_IN_BYTES];
+ System.arraycopy(hash1.bytes(), 0, buffer, 0, hash1.getByteSize());
+ System.arraycopy(hash2.bytes(), 0, buffer, Hash.SIZE_IN_BYTES, hash2.getByteSize());
+
+ TransactionHash hash = TransactionHash.calculate(SpongeFactory.Mode.S256, buffer);
+
+ Assert.assertFalse(hash.equals(hash1));
+ Assert.assertTrue(hash.equals(resultHash));
+ Assert.assertFalse(hash.equals(Hash.NULL_HASH));
+ Assert.assertFalse(resultHash.equals(Hash.NULL_HASH));
+ }
}
diff --git a/src/test/java/net/helix/pendulum/network/NodeTest.java b/src/test/java/net/helix/pendulum/network/NodeTest.java
index d2cc9cd7..d4ed4f52 100644
--- a/src/test/java/net/helix/pendulum/network/NodeTest.java
+++ b/src/test/java/net/helix/pendulum/network/NodeTest.java
@@ -41,7 +41,7 @@ public void setUp() {
// set up class under test
nodeConfig = mock(NodeConfig.class);
- classUnderTest = new Node(null, null, null, null, null, null, nodeConfig);
+ classUnderTest = new Node(null, null, null, null, null, null, null, nodeConfig);
// verify config calls in Node constructor
verify(nodeConfig).getRequestHashSize();
@@ -71,7 +71,7 @@ public void spawnNeighborDNSRefresherThreadTest() {
@Test
public void whenProcessReceivedDataSetArrivalTimeToCurrentMillis() throws Exception {
- Node node = new Node(mock(Tangle.class), mock(SnapshotProvider.class), mock(TransactionValidator.class), null, null, null, mock(NodeConfig.class));
+ Node node = new Node(mock(Tangle.class), mock(SnapshotProvider.class), mock(TransactionValidator.class), null, null, null, null, mock(NodeConfig.class));
TransactionViewModel transaction = mock(TransactionViewModel.class);
// It is important to stub the getHash method here because processReceivedData will broadcast the transaction.
// This might sometimes (concurrency issue) lead to a NPE in the process receiver thread.