diff --git a/src/main/java/net/helix/pendulum/Pendulum.java b/src/main/java/net/helix/pendulum/Pendulum.java
index bd1cb26b..d8959803 100644
--- a/src/main/java/net/helix/pendulum/Pendulum.java
+++ b/src/main/java/net/helix/pendulum/Pendulum.java
@@ -11,11 +11,8 @@
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.VirtualTransactionService;
+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;
@@ -23,16 +20,8 @@
import net.helix.pendulum.service.spentaddresses.SpentAddressesException;
import net.helix.pendulum.service.spentaddresses.impl.SpentAddressesProviderImpl;
import net.helix.pendulum.service.spentaddresses.impl.SpentAddressesServiceImpl;
-import net.helix.pendulum.service.tipselection.EntryPointSelector;
-import net.helix.pendulum.service.tipselection.RatingCalculator;
-import net.helix.pendulum.service.tipselection.TailFinder;
-import net.helix.pendulum.service.tipselection.TipSelector;
-import net.helix.pendulum.service.tipselection.Walker;
-import net.helix.pendulum.service.tipselection.impl.CumulativeWeightCalculator;
-import net.helix.pendulum.service.tipselection.impl.EntryPointSelectorImpl;
-import net.helix.pendulum.service.tipselection.impl.TailFinderImpl;
-import net.helix.pendulum.service.tipselection.impl.TipSelectorImpl;
-import net.helix.pendulum.service.tipselection.impl.WalkerAlpha;
+import net.helix.pendulum.service.tipselection.*;
+import net.helix.pendulum.service.tipselection.impl.*;
import net.helix.pendulum.service.transactionpruning.TransactionPruningException;
import net.helix.pendulum.service.transactionpruning.async.AsyncTransactionPruner;
import net.helix.pendulum.service.validatormanager.impl.CandidateSolidifierImpl;
@@ -105,6 +94,7 @@ public class Pendulum {
public final MilestoneSolidifierImpl milestoneSolidifier;
public final CandidateSolidifierImpl candidateSolidifier;
public final TransactionRequesterWorkerImpl transactionRequesterWorker;
+ public final VirtualTransactionService virtualTransactionService;
public final Tangle tangle;
public final TransactionValidator transactionValidator;
@@ -149,7 +139,7 @@ public Pendulum(PendulumConfig configuration) throws TransactionPruningException
? new AsyncTransactionPruner()
: null;
transactionRequesterWorker = new TransactionRequesterWorkerImpl();
-
+ virtualTransactionService = new VirtualTransactionServiceImpl();
// legacy code
bundleValidator = new BundleValidator();
tangle = new Tangle();
@@ -157,7 +147,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);
@@ -228,6 +218,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 66890dc2..334b560e 100644
--- a/src/main/java/net/helix/pendulum/TransactionValidator.java
+++ b/src/main/java/net/helix/pendulum/TransactionValidator.java
@@ -14,22 +14,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.LinkedList;
-import java.util.Queue;
-import java.util.Set;
+import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
-import static net.helix.pendulum.controllers.TransactionViewModel.PREFILLED_SLOT;
-import static net.helix.pendulum.controllers.TransactionViewModel.SIZE;
-import static net.helix.pendulum.controllers.TransactionViewModel.VALUE_OFFSET;
-import static net.helix.pendulum.controllers.TransactionViewModel.VALUE_SIZE;
-import static net.helix.pendulum.controllers.TransactionViewModel.VALUE_USABLE_SIZE;
-import static net.helix.pendulum.controllers.TransactionViewModel.fromHash;
-import static net.helix.pendulum.controllers.TransactionViewModel.updateSolidTransactions;
+import static net.helix.pendulum.controllers.TransactionViewModel.*;
public class TransactionValidator {
private static final Logger log = LoggerFactory.getLogger(TransactionValidator.class);
@@ -154,7 +142,7 @@ private boolean hasInvalidTimestamp(TransactionViewModel transactionViewModel) {
snapshotProvider.getInitialSnapshot().getTimestamp(),
snapshotProvider.getInitialSnapshot().hasSolidEntryPoint(transactionViewModel.getHash()));
- if (transactionViewModel.getAttachmentTimestamp() == 0) {
+ if (transactionViewModel.getAttachmentTimestamp() == 0) {
return transactionViewModel.getTimestamp() < snapshotProvider.getInitialSnapshot().getTimestamp() && !snapshotProvider.getInitialSnapshot().hasSolidEntryPoint(transactionViewModel.getHash())
|| transactionViewModel.getTimestamp() > (System.currentTimeMillis() / 1000) + MAX_TIMESTAMP_FUTURE;
}
@@ -162,6 +150,17 @@ private boolean hasInvalidTimestamp(TransactionViewModel transactionViewModel) {
|| transactionViewModel.getAttachmentTimestamp() > System.currentTimeMillis() + MAX_TIMESTAMP_FUTURE_MS;
}
+ private boolean isTransactionRequested(TransactionViewModel transactionViewModel) throws Exception {
+ if (transactionRequester.isTransactionRequested(transactionViewModel.getHash(), true)) {
+ if (TransactionViewModel.exists(tangle, transactionViewModel.getHash())) {
+ transactionRequester.clearTransactionRequest(transactionViewModel.getHash());
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
/**
* Runs the following validation checks on a transaction:
*
@@ -181,16 +180,23 @@ private boolean hasInvalidTimestamp(TransactionViewModel transactionViewModel) {
public void runValidation(TransactionViewModel transactionViewModel, final int minWeightMagnitude) {
transactionViewModel.setMetadata();
transactionViewModel.setAttachmentData();
+
+ try {
+ if (isTransactionRequested(transactionViewModel)) {
+ log.error("Waiting for transaction... " + transactionViewModel.getHash());
+ throw new IllegalStateException("Transaction is requested {} " + transactionViewModel.getHash());
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Transaction is requested {} " + transactionViewModel.getHash(), e);
+ }
+
if(hasInvalidTimestamp(transactionViewModel)) {
log.debug("Invalid timestamp for txHash/addressHash: {} {}", transactionViewModel.getHash().toString(), transactionViewModel.getAddressHash().toString());
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");
@@ -211,7 +217,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;
}
@@ -264,46 +271,38 @@ public boolean checkSolidity(Hash hash, boolean milestone) throws Exception {
* @throws Exception if anything goes wrong while trying to solidify the transaction
*/
public boolean checkSolidity(Hash hash, boolean milestone, int maxProcessedTransactions) throws Exception {
- if(fromHash(tangle, hash).isSolid()) {
+ if (fromHash(tangle, hash).isSolid()) {
return true;
}
Set analyzedHashes = new HashSet<>(snapshotProvider.getInitialSnapshot().getSolidEntryPoints().keySet());
- if(maxProcessedTransactions != Integer.MAX_VALUE) {
+ if (maxProcessedTransactions != Integer.MAX_VALUE) {
maxProcessedTransactions += analyzedHashes.size();
}
+ log.debug("Check solidity for hash " + hash);
boolean solid = true;
final Queue nonAnalyzedTransactions = new LinkedList<>(Collections.singleton(hash));
Hash hashPointer;
while ((hashPointer = nonAnalyzedTransactions.poll()) != null) {
if (analyzedHashes.add(hashPointer)) {
- if(analyzedHashes.size() >= maxProcessedTransactions) {
+ if (analyzedHashes.size() >= maxProcessedTransactions) {
return false;
}
final TransactionViewModel transaction = fromHash(tangle, hashPointer);
- if(!transaction.isSolid() && !snapshotProvider.getInitialSnapshot().hasSolidEntryPoint(hashPointer)) {
+ if (!transaction.isSolid() && !snapshotProvider.getInitialSnapshot().hasSolidEntryPoint(hashPointer)) {
if (transaction.getType() == PREFILLED_SLOT) {
solid = false;
if (!transactionRequester.isTransactionRequested(hashPointer, milestone)) {
transactionRequester.requestTransaction(hashPointer, milestone);
+ solid = false;
break;
}
} else {
- // transaction of milestone bundle
- TransactionViewModel milestoneTx;
- if ((milestoneTx = transaction.isMilestoneBundle(tangle)) != null){
- Set parents = RoundViewModel.getMilestoneTrunk(tangle, transaction, milestoneTx);
- parents.addAll(RoundViewModel.getMilestoneBranch(tangle, transaction, milestoneTx, config.getValidatorSecurity()));
- for (Hash parent : parents){
- nonAnalyzedTransactions.offer(parent);
- }
- }
- // normal transaction
- else {
- nonAnalyzedTransactions.offer(transaction.getTrunkTransactionHash());
- nonAnalyzedTransactions.offer(transaction.getBranchTransactionHash());
- }
+ nonAnalyzedTransactions.offer(transaction.getTrunkTransactionHash());
+ log.debug("Check solidity for hash:" + transaction.getHash() + " trunk " + transaction.getTrunkTransactionHash());
+ nonAnalyzedTransactions.offer(transaction.getBranchTransactionHash());
+ log.debug("Check solidity for hash:" + transaction.getHash() + " branch " + transaction.getBranchTransactionHash());
}
}
}
@@ -410,20 +409,12 @@ protected void propagateSolidTransactions() {
//what transaction we gossip.
public void updateStatus(TransactionViewModel transactionViewModel) throws Exception {
transactionRequester.clearTransactionRequest(transactionViewModel.getHash());
- if(transactionViewModel.getApprovers(tangle).size() == 0) {
+
+ if (transactionViewModel.getApprovers(tangle).size() == 0 && !transactionViewModel.isVirtual()) {
tipsViewModel.addTipHash(transactionViewModel.getHash());
} else {
- TransactionViewModel milestoneTx;
- if ((milestoneTx = transactionViewModel.isMilestoneBundle(tangle)) != null){
- Set parents = RoundViewModel.getMilestoneTrunk(tangle, transactionViewModel, milestoneTx);
- parents.addAll(RoundViewModel.getMilestoneBranch(tangle, transactionViewModel, milestoneTx, config.getValidatorSecurity()));
- for (Hash parent : parents){
- tipsViewModel.removeTipHash(parent);
- }
- } else {
- tipsViewModel.removeTipHash(transactionViewModel.getTrunkTransactionHash());
- tipsViewModel.removeTipHash(transactionViewModel.getBranchTransactionHash());
- }
+ tipsViewModel.removeTipHash(transactionViewModel.getTrunkTransactionHash());
+ tipsViewModel.removeTipHash(transactionViewModel.getBranchTransactionHash());
}
if(quickSetSolid(transactionViewModel)) {
diff --git a/src/main/java/net/helix/pendulum/conf/BasePendulumConfig.java b/src/main/java/net/helix/pendulum/conf/BasePendulumConfig.java
index b6763176..9c46eeeb 100644
--- a/src/main/java/net/helix/pendulum/conf/BasePendulumConfig.java
+++ b/src/main/java/net/helix/pendulum/conf/BasePendulumConfig.java
@@ -937,6 +937,11 @@ protected void setSaveLogXMLFile(String saveLogXMLFile) {
this.saveLogXMLFile = saveLogXMLFile;
}
+ @Override
+ public double getConfirmationQuorumPercentage() {
+ return Defaults.CONFIRMATION_QUORUM_PERCENTAGE;
+ }
+
public interface Defaults {
//API
int API_PORT = 8085;
@@ -1059,5 +1064,8 @@ public interface Defaults {
boolean SAVELOG_ENABLED = false;
String SAVELOG_BASE_PATH = "./logs/";
String SAVELOG_XML_FILE = "/logback-save.xml";
+
+ //Consensus
+ double CONFIRMATION_QUORUM_PERCENTAGE = 2.0 / 3.0;
}
}
diff --git a/src/main/java/net/helix/pendulum/conf/ConsensusConfig.java b/src/main/java/net/helix/pendulum/conf/ConsensusConfig.java
index 1626e3d2..c38d63eb 100644
--- a/src/main/java/net/helix/pendulum/conf/ConsensusConfig.java
+++ b/src/main/java/net/helix/pendulum/conf/ConsensusConfig.java
@@ -7,4 +7,16 @@
* the current code base and will be changed in the future
*/
public interface ConsensusConfig extends SnapshotConfig, MilestoneConfig, ValidatorManagerConfig {
+
+ /**
+ * @return {@value PoWConfig.Descriptions#POW_THREADS}
+ */
+ double getConfirmationQuorumPercentage();
+
+ /**
+ * Field descriptions
+ */
+ interface Descriptions {
+ String CONFIRMATION_QUORUM_PERCENTAGE = "Confirmation quorum percentage";
+ }
}
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..ad5fe0f4
--- /dev/null
+++ b/src/main/java/net/helix/pendulum/controllers/BundleNonceViewModel.java
@@ -0,0 +1,156 @@
+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.Objects;
+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;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ BundleNonceViewModel that = (BundleNonceViewModel) o;
+ return Objects.equals(self, that.self) &&
+ Objects.equals(hash, that.hash);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(self, hash);
+ }
+}
diff --git a/src/main/java/net/helix/pendulum/controllers/RoundViewModel.java b/src/main/java/net/helix/pendulum/controllers/RoundViewModel.java
index 8d72113d..8437df46 100644
--- a/src/main/java/net/helix/pendulum/controllers/RoundViewModel.java
+++ b/src/main/java/net/helix/pendulum/controllers/RoundViewModel.java
@@ -2,12 +2,20 @@
import net.helix.pendulum.TransactionValidator;
import net.helix.pendulum.conf.BasePendulumConfig;
-import net.helix.pendulum.crypto.Merkle;
+import net.helix.pendulum.conf.PendulumConfig;
+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;
import net.helix.pendulum.model.persistables.Round;
+import net.helix.pendulum.service.ledger.LedgerException;
+import net.helix.pendulum.service.ledger.LedgerService;
import net.helix.pendulum.service.milestone.MilestoneTracker;
+import net.helix.pendulum.service.snapshot.Snapshot;
+import net.helix.pendulum.service.snapshot.SnapshotException;
+import net.helix.pendulum.service.snapshot.SnapshotProvider;
import net.helix.pendulum.storage.Indexable;
import net.helix.pendulum.storage.Persistable;
import net.helix.pendulum.storage.Tangle;
@@ -242,72 +250,76 @@ public static int getRoundIndex(TransactionViewModel milestoneTransaction) {
}
// todo this may be very inefficient
- public static Set getMilestoneTrunk(Tangle tangle, TransactionViewModel transaction, TransactionViewModel milestoneTx) throws Exception{
+ public static Set getMilestoneTrunk(Tangle tangle, TransactionViewModel transaction, TransactionViewModel milestoneTx) throws Exception {
Set trunk = new HashSet<>();
- int round = RoundViewModel.getRoundIndex(milestoneTx);
// idx = n: milestone merkle root in trunk
if (transaction.getCurrentIndex() == transaction.lastIndex()) {
- // add previous milestones to non analyzed transactions
- RoundViewModel prevMilestone = RoundViewModel.get(tangle, round-1);
- if (prevMilestone == null) {
- if (transaction.getBranchTransactionHash().equals(Hash.NULL_HASH)) {
- trunk.add(Hash.NULL_HASH);
- }
- } else {
- Set prevMilestones = prevMilestone.getHashes();
- List> merkleTree = Merkle.buildMerkleTree(new ArrayList<>(prevMilestones));
- if (transaction.getTrunkTransactionHash().equals(merkleTree.get(merkleTree.size() - 1).get(0))) {
- if (prevMilestones.isEmpty()) {
- trunk.add(Hash.NULL_HASH);
- } else {
- trunk.addAll(prevMilestones);
- }
- }
- }
- }
- else {
+ trunk.addAll(splitMilestonesMerkleRoot(tangle, transaction.getTrunkTransactionHash(), milestoneTx));
+ } else {
// idx = 0 - (n-1): merkle root in branch, trunk is normal tx hash
trunk.add(transaction.getTrunkTransactionHash());
}
return trunk;
}
- public static Set getMilestoneBranch(Tangle tangle, TransactionViewModel transaction, TransactionViewModel milestoneTx, int security) throws Exception{
+
+ public static Set getMilestoneBranch(Tangle tangle, TransactionViewModel transaction, TransactionViewModel milestoneTx, int security) throws Exception {
Set branch = new HashSet<>();
- int round = RoundViewModel.getRoundIndex(milestoneTx);
// idx = n: milestone merkle root in trunk and tips merkle root in branch
if (transaction.getCurrentIndex() == transaction.lastIndex()) {
- // tips merkle root
- Set confirmedTips = getTipSet(tangle, milestoneTx.getHash(), security);
- List> merkleTree = Merkle.buildMerkleTree(new ArrayList<>(confirmedTips));
- if (transaction.getBranchTransactionHash().equals(merkleTree.get(merkleTree.size()-1).get(0))) {
- if (confirmedTips.isEmpty()){
- branch.add(Hash.NULL_HASH);
+ branch.addAll(splitTipsMerkleRoot(tangle, transaction, milestoneTx, security));
+ } else {
+ branch.addAll(splitMilestonesMerkleRoot(tangle, transaction.getBranchTransactionHash(), milestoneTx));
+ }
+
+ if (log.isTraceEnabled()) {
+ log.trace("Milestone branch: {}", PendulumUtils.logHashList(branch, 8));
+ }
+ return branch;
+ }
+
+
+ private static Set splitMilestonesMerkleRoot(Tangle tangle, Hash expectedMilestoneMerkleRoot, TransactionViewModel milestoneTx) throws Exception {
+ Set result = new HashSet<>();
+ int round = RoundViewModel.getRoundIndex(milestoneTx);
+ RoundViewModel prevMilestone = RoundViewModel.get(tangle, round - 1);
+ if (prevMilestone == null) {
+ if (Hash.NULL_HASH.equals(expectedMilestoneMerkleRoot)) {
+ result.add(Hash.NULL_HASH);
+ }
+ } else {
+ Set prevMilestones = prevMilestone.getHashes();
+ MerkleTree merkle = new MerkleTreeImpl();
+ Hash merkleTreeRoot = HashFactory.TRANSACTION.create(merkle.getMerkleRoot(new ArrayList<>(prevMilestones), MerkleOptions.getDefault()));
+ if (expectedMilestoneMerkleRoot.equals(merkleTreeRoot)) {
+ if (prevMilestones.isEmpty()) {
+ result.add(Hash.NULL_HASH);
} else {
- branch.addAll(confirmedTips);
+ result.addAll(prevMilestones);
}
+ } else {
+ log.error("Milestone merkle tree can not be splitted into milestones, milestone merkle root {} for milestone {}, computed merkle root based on previous round milestones: {}", expectedMilestoneMerkleRoot, milestoneTx.getHash(), merkleTreeRoot);
}
}
- else {
- // add previous milestones to non analyzed transactions
- RoundViewModel prevMilestone = RoundViewModel.get(tangle, round-1);
- if (prevMilestone == null) {
- if (transaction.getBranchTransactionHash().equals(Hash.NULL_HASH)) {
- branch.add(Hash.NULL_HASH);
- }
+ return result;
+ }
+
+ private static Set splitTipsMerkleRoot(Tangle tangle, TransactionViewModel transaction, TransactionViewModel milestoneTx, int security) throws Exception {
+ Set result = new HashSet<>();
+ Set confirmedTips = getTipSet(tangle, milestoneTx.getHash(), security);
+ Hash root = HashFactory.TRANSACTION.create(new MerkleTreeImpl().getMerkleRoot(new ArrayList<>(confirmedTips), MerkleOptions.getDefault()));
+ if (transaction.getBranchTransactionHash().equals(root)) {
+ if (confirmedTips.isEmpty()) {
+ result.add(Hash.NULL_HASH);
} else {
- Set prevMilestones = prevMilestone.getHashes();
- List> merkleTree = Merkle.buildMerkleTree(new ArrayList<>(prevMilestones));
- if (transaction.getBranchTransactionHash().equals(merkleTree.get(merkleTree.size() - 1).get(0))) {
- if (prevMilestones.isEmpty()) {
- branch.add(Hash.NULL_HASH);
- } else {
- branch.addAll(prevMilestones);
- }
- }
+ result.addAll(confirmedTips);
}
+ } else {
+ //TODO check this
+ //confirmedTips.add(transaction.getBranchTransactionHash());
+ log.error("Tips merkle tree can not be splitted into tips, tips merkle root: {} for milestone {} ", transaction.getBranchTransactionHash(), milestoneTx.getHash());
}
- return branch;
+ return result;
}
public static Set getTipSet(Tangle tangle, Hash milestone, int security) throws Exception {
@@ -396,17 +408,17 @@ public Set getReferencedTransactions(Tangle tangle, Set tips) throws
// we can add the tx to confirmed transactions, because it is a parent of confirmedTips
transactions.add(hashPointer);
// traverse parents and add new candidates to queue
- if(!seenTransactions.contains(transaction.getTrunkTransactionHash())){
+ if (!seenTransactions.contains(transaction.getTrunkTransactionHash())) {
seenTransactions.add(transaction.getTrunkTransactionHash());
nonAnalyzedTransactions.offer(transaction.getTrunkTransactionHash());
}
- if(!seenTransactions.contains(transaction.getBranchTransactionHash())){
+ if (!seenTransactions.contains(transaction.getBranchTransactionHash())) {
seenTransactions.add(transaction.getBranchTransactionHash());
nonAnalyzedTransactions.offer(transaction.getBranchTransactionHash());
}
- // roundIndex already set, i.e. tx is already confirmed.
+ // roundIndex already set, i.e. tx is already confirmed.
} else {
continue;
}
@@ -414,6 +426,43 @@ public Set getReferencedTransactions(Tangle tangle, Set tips) throws
return transactions;
}
+ /**
+ * Returns a maps of milesteons with a set of confirmed tips for each milestone
+ * @param tangle
+ * @param config Pendulum config
+ * @return maps of milestones
+ * @throws Exception
+ */
+ public Map> getConfirmedTipsPerMilestone(Tangle tangle, PendulumConfig config) throws Exception {
+
+ Map occurrences = new HashMap<>();
+ Map> milestoneHashes = new HashMap<>();
+
+ int quorum = (int)(config.getConfirmationQuorumPercentage() * BasePendulumConfig.Defaults.NUMBER_OF_ACTIVE_VALIDATORS);
+
+ for (Hash milestoneHash : getHashes()) {
+ Set tips = getTipSet(tangle, milestoneHash, config.getValidatorSecurity());
+
+ for (Hash tip : tips) {
+ if (occurrences.containsKey(tip)) {
+ occurrences.put(tip, occurrences.get(tip) + 1);
+ } else {
+ occurrences.put(tip, 1);
+ }
+ }
+ milestoneHashes.put(milestoneHash, tips);
+ }
+ Set tips = occurrences.entrySet().stream()
+ .filter(entry -> entry.getValue() >= quorum)
+ .map(entry -> entry.getKey())
+ .collect(Collectors.toSet());
+
+ milestoneHashes.keySet().forEach(k -> {
+ milestoneHashes.get(k).stream().filter(h -> tips.contains(h));
+ });
+ return milestoneHashes;
+ }
+
public Hash getRandomMilestone(Tangle tangle) throws Exception {
Set confirmingMilestones = getHashes(); // todo getConfirmingMilestones(tangle);
if (!confirmingMilestones.isEmpty()) {
@@ -460,6 +509,11 @@ public boolean addMilestone(Hash milestoneHash) {
return getHashes().add(milestoneHash);
}
+ public boolean removeMilestone(Hash milestoneHash) {
+ round.inconsistentMilestones.add(milestoneHash);
+ return getHashes().remove(milestoneHash);
+ }
+
public void update(Tangle tangle) throws Exception {
tangle.update(round, round.index, "round");
}
@@ -474,10 +528,41 @@ public Integer index() {
return round.index.getValue();
}
+ /**
+ * Return merkle root for accepted milestone from this round.
+ *
+ * @return hash of the merkle root.
+ */
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()));
+ }
+
+ public Hash getMerkleRoot(Tangle tangle, SnapshotProvider snapshotProvider, LedgerService ledgerService) {
+ List nonConflictualMilestones = getMilestonesToApprove(tangle, snapshotProvider, ledgerService);
+ return HashFactory.TRANSACTION.create( new MerkleTreeImpl().getMerkleRoot(new LinkedList<>(nonConflictualMilestones), MerkleOptions.getDefault()));
+ }
+
+ public List getMilestonesToApprove(Tangle tangle, SnapshotProvider snapshotProvider, LedgerService ledgerService) {
+
+ List targetRoundMilestones = TransactionViewModel.fromHashes(getHashes(), tangle);
+ Snapshot snapshot = snapshotProvider.getInitialSnapshot();
+
+ if (snapshot.isConsistent()) {
+ try {
+ Set approvedMilestones = new HashSet();
+ ledgerService.getSnapshotService().replayMilestones(snapshot, index() - 1);
+ ledgerService.applyRoundToLedger(this, snapshot, approvedMilestones, true);
+ targetRoundMilestones.stream().filter(m -> !approvedMilestones.contains(m)).forEach(m -> {
+ removeMilestone(m.getHash());
+ });
+ update(tangle);
+ } catch (Exception e) {
+ log.error("Error during appling milestone states simultaneously", e);
+ }
+ }
+
+ return new ArrayList<>(getHashes());
}
/**
diff --git a/src/main/java/net/helix/pendulum/controllers/TransactionViewModel.java b/src/main/java/net/helix/pendulum/controllers/TransactionViewModel.java
index 5079801c..6fd2e1da 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,8 +12,12 @@
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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.*;
+import java.util.stream.Collectors;
/**
@@ -18,8 +25,9 @@
* 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 static final Logger log = LoggerFactory.getLogger(TransactionViewModel.class);
private final Transaction transaction;
public static final int SIZE = 768;
@@ -148,6 +156,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 +187,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
@@ -178,9 +209,9 @@ public void update(Tangle tangle, Snapshot initialSnapshot, String item) throws
getBundleNonceHash();
setAttachmentData();
setMetadata();
- if (initialSnapshot.hasSolidEntryPoint(hash)) {
- return;
- }
+// if (initialSnapshot.hasSolidEntryPoint(hash)) {
+// return;
+// }
tangle.update(transaction, hash, item);
}
@@ -314,6 +345,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 +430,10 @@ public byte[] getBytes() {
return transaction.bytes;
}
+ public byte[] bytes(){
+ return getBytes();
+ }
+
public Hash getHash() {
return hash;
}
@@ -487,6 +535,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.
@@ -685,6 +742,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);
@@ -746,6 +813,18 @@ public String getSender() {
return transaction.sender;
}
+ public static List fromHashes(Set hashes, Tangle tangle) {
+ return hashes.stream().map(
+ h -> {
+ try {
+ return TransactionViewModel.fromHash(tangle, h);
+ } catch (Exception e) {
+ log.error("Could not get transaction for hash " + h, e);
+ }
+ return null;
+ }).filter(t -> t != null).collect(Collectors.toList());
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
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 aea7ffc2..00000000
--- a/src/main/java/net/helix/pendulum/crypto/Merkle.java
+++ /dev/null
@@ -1,193 +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) {
-
- final TransactionViewModel merkleTx = bundleTransactionViewModels.get(securityLevel);
- 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 * 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);
-
- //validate Merkle path
- byte[] merkleRoot = Merkle.getMerkleRoot(mode, address,
- merkleTx.getSignature(), 0, keyIndex, depth);
- 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..593bc8fa
--- /dev/null
+++ b/src/main/java/net/helix/pendulum/crypto/merkle/impl/AbstractMerkleTree.java
@@ -0,0 +1,229 @@
+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.Comparator;
+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);
+ sortLeaves(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;
+ }
+
+ private void sortLeaves(List leaves) {
+ leaves.sort(Comparator.comparing((Hash m) -> m.toString()));
+ }
+
+ @Override
+ public List> buildMerkleTree(List leaves, MerkleOptions options) {
+ if (leaves.isEmpty()) {
+ leaves.add(getDefaultMerkleHash());
+ }
+ byte[] buffer;
+ addPaddingLeaves(leaves);
+ sortLeaves(leaves);
+ Sponge sha3 = SpongeFactory.create(options.getMode());
+ 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/model/persistables/Round.java b/src/main/java/net/helix/pendulum/model/persistables/Round.java
index 84ee065f..d4d17fb0 100644
--- a/src/main/java/net/helix/pendulum/model/persistables/Round.java
+++ b/src/main/java/net/helix/pendulum/model/persistables/Round.java
@@ -1,15 +1,21 @@
package net.helix.pendulum.model.persistables;
+import net.helix.pendulum.model.Hash;
import net.helix.pendulum.model.IntegerIndex;
import net.helix.pendulum.utils.Serializer;
import org.apache.commons.lang3.ArrayUtils;
- /**
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
* The Round model class consists of a set of milestone hashes and a corresponding index.
*/
public class Round extends Hashes {
public IntegerIndex index;
+ public Set inconsistentMilestones = new LinkedHashSet<>();
+
@Override
public byte[] bytes() {
return ArrayUtils.addAll(index.bytes(), super.bytes());
diff --git a/src/main/java/net/helix/pendulum/network/Node.java b/src/main/java/net/helix/pendulum/network/Node.java
index 20440e53..f72d5632 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 449cd9e7..cec6a324 100644
--- a/src/main/java/net/helix/pendulum/service/API.java
+++ b/src/main/java/net/helix/pendulum/service/API.java
@@ -11,7 +11,14 @@
import net.helix.pendulum.conf.BasePendulumConfig;
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;
@@ -26,6 +33,7 @@
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.validatormanager.CandidateTracker;
import net.helix.pendulum.storage.Tangle;
import net.helix.pendulum.utils.Serializer;
@@ -116,6 +124,7 @@ public class API {
private final String[] features;
+
//endregion ////////////////////////////////////////////////////////////////////////////////////////////////////////
private final Gson gson = new GsonBuilder().create();
@@ -178,7 +187,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());
}
/**
@@ -403,7 +411,8 @@ private AbstractResponse checkConsistencyStatement(List transactionsList
* @return false if we received at least a solid milestone, otherwise true
*/
public boolean invalidSubtangleStatus() {
- return (snapshotProvider.getLatestSnapshot().getIndex() == snapshotProvider.getInitialSnapshot().getIndex());
+ return (snapshotProvider.getLatestSnapshot().getIndex() == 0 &&
+ snapshotProvider.getLatestSnapshot().getIndex() == snapshotProvider.getInitialSnapshot().getIndex());
}
/**
* Returns the set of neighbors you are connected with, as well as their activity statistics (or counters).
@@ -591,19 +600,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());
- if (transactionViewModel.isMilestoneBundle(tangle) == null) {
- transactionValidator.updateStatus(transactionViewModel);
- }
- transactionViewModel.updateSender("local");
- transactionViewModel.update(tangle, snapshotProvider.getInitialSnapshot(), "sender");
- if (tangle != null) {
- tangle.publish("vis %s %s %s", transactionViewModel.getHash(), transactionViewModel.getTrunkTransactionHash(), transactionViewModel.getBranchTransactionHash());
- }
- log.trace("Stored_txhash = {}", transactionViewModel.getHash().toString());
- }
+ transactionViewModel.storeTransactionLocal(tangle, snapshotProvider.getInitialSnapshot(), transactionValidator);
}
}
@@ -702,7 +699,7 @@ private AbstractResponse getConfirmationStatesStatement(final List trans
log.trace("tx_confirmations {}:[{}:{}]", transaction.getHash().toString(), transaction.getConfirmations(), (double) transaction.getConfirmations() / n);
// is transaction finalized
- if(((double)transaction.getConfirmations() / n) > threshold) {
+ if(transaction.getRoundIndex() > 0 && ((double)transaction.getConfirmations() / n) > threshold) {
confirmationStates[count] = 1;
}
// not finalized yet
@@ -787,7 +784,7 @@ private AbstractResponse getConfirmationStatesStatementObsolete(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)
@@ -1128,6 +1125,14 @@ private AbstractResponse getBalancesStatement(List addresses, int thresh
snapshotProvider.getLatestSnapshot().lockRead();
final int index = snapshotProvider.getLatestSnapshot().getIndex();
+ if (tips == null || tips.size() == 0) {
+ RoundViewModel round = RoundViewModel.get(tangle, index);
+ hashes = round != null ? new LinkedList<>(round.getHashes()) : new ArrayList<>();
+ } else {
+ hashes = tips.stream()
+ .map(tip -> (HashFactory.TRANSACTION.create(tip)))
+ .collect(Collectors.toCollection(LinkedList::new));
+ }
try {
// Get the balance for each address at the last snapshot
for (final Hash address : addressList) {
@@ -1247,12 +1252,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();
@@ -1285,6 +1285,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.
@@ -1520,11 +1529,11 @@ 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);
log.trace("tips = [{}, {}]", tip1.toString(), tip2.toString());
storeTransactionsStatement(powResult);
- broadcastTransactionsStatement(powResult);
+ return powResult;
}
//
@@ -1569,10 +1578,90 @@ 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());
+
+ broadcastTransactionsStatement(transactionStrings);
+ if (createVirtualTransactions) {
+ createAndBroadcastVirtualTransactions(data, transactionStrings);
+ }
+ }
+
+ /**
+ * Creates and broadcast virtual transactions
+ *
+ * @param tipsBytes
+ * @param milestoneBundle
+ */
+ private void createAndBroadcastVirtualTransactions(byte[] tipsBytes, List milestoneBundle) throws Exception {
+ 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());
+
+ RoundViewModel previousRound = RoundViewModel.get(tangle, (int)milestone.getTagLongValue() - 1);
+
+ List merkleTransactions = new TransactionMerkleTreeImpl().buildMerkle(tips, options);
+ if(previousRound != null) {
+ merkleTransactions.addAll(new TransactionMerkleTreeImpl().buildMerkle(new ArrayList<>(previousRound.getHashes()), options));
+ }
+
+ List virtualTransactions = merkleTransactions.stream().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) {
+ log.error("Could not generated virtual transaction for " + ((TransactionViewModel) t).getHash());
+ }
+ return 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)
@@ -1592,7 +1681,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 {
@@ -1601,7 +1691,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 {
@@ -1610,7 +1700,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 {
@@ -1622,7 +1712,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);
}
//
@@ -1641,17 +1731,24 @@ private List getConsistentTips() throws Exception {
snapshotProvider.getLatestSnapshot().lockRead();
try {
WalkValidatorImpl walkValidator = new WalkValidatorImpl(tangle, snapshotProvider, ledgerService, configuration);
- for (Hash transaction : tipsViewModel.getTips()) {
- TransactionViewModel txVM = TransactionViewModel.fromHash(tangle, transaction);
+
+ List transactionViewModelStream = TransactionViewModel.fromHashes(tipsViewModel.getTips(), tangle);
+
+ transactionViewModelStream.sort(
+ Comparator.comparing((TransactionViewModel m) -> m.getTimestamp()));
+
+ for (TransactionViewModel txVM : transactionViewModelStream) {
if (txVM.getType() != TransactionViewModel.PREFILLED_SLOT &&
txVM.getCurrentIndex() == 0 &&
txVM.isSolid() &&
BundleValidator.validate(tangle, snapshotProvider.getInitialSnapshot(), txVM.getHash()).size() != 0) {
- if (walkValidator.isValid(transaction)) {
- confirmedTips.add(transaction);
- } else {
- log.warn("Inconsistent transaction has been removed from tips: " + transaction.toString());
- tipsViewModel.removeTipHash(transaction);
+ if (walkValidator.isValid(txVM.getHash())) {
+ confirmedTips.add(txVM.getHash());
+ } else if(txVM.isSolid()){
+ log.warn("Inconsistent transaction has been removed from tips: {} ", txVM.getHash());
+ txVM.setConfirmations(-1);
+ txVM.update(tangle, snapshotProvider.getInitialSnapshot(), "confirmation");
+ tipsViewModel.removeTipHash(txVM.getHash());
}
}
}
@@ -1675,11 +1772,14 @@ private List addMilestoneReferences(List confirmedTips, int roundInd
if (previousRound == null){
txToApprove.add(Hash.NULL_HASH);
} else {
- txToApprove.add(previousRound.getMerkleRoot()); // merkle root of latest milestones
+ txToApprove.add(previousRound.getMerkleRoot(tangle, snapshotProvider, ledgerService)); // 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/ledger/LedgerService.java b/src/main/java/net/helix/pendulum/service/ledger/LedgerService.java
index 6ac309ca..c8fbdc34 100644
--- a/src/main/java/net/helix/pendulum/service/ledger/LedgerService.java
+++ b/src/main/java/net/helix/pendulum/service/ledger/LedgerService.java
@@ -2,6 +2,8 @@
import net.helix.pendulum.controllers.RoundViewModel;
import net.helix.pendulum.model.Hash;
+import net.helix.pendulum.service.snapshot.Snapshot;
+import net.helix.pendulum.service.snapshot.SnapshotService;
import java.util.List;
import java.util.Map;
@@ -42,6 +44,8 @@ public interface LedgerService {
*/
boolean applyRoundToLedger(RoundViewModel round) throws LedgerException;
+ boolean applyRoundToLedger(RoundViewModel round, Snapshot snaphshot, Set nonConflictualMilestones, boolean simulation) throws LedgerException;
+
/**
* Checks the consistency of the combined balance changes of the given tips.
*
@@ -90,4 +94,9 @@ public interface LedgerService {
*/
Map generateBalanceDiff(Set visitedTransactions, Set startTransactions, int milestoneIndex)
throws LedgerException;
+
+ boolean updateDiff(Set approvedHashes, final Map diff, Hash tip); // temporary
+
+ SnapshotService getSnapshotService();
+
}
diff --git a/src/main/java/net/helix/pendulum/service/ledger/impl/LedgerServiceImpl.java b/src/main/java/net/helix/pendulum/service/ledger/impl/LedgerServiceImpl.java
index abe258bb..f4fb3d3b 100644
--- a/src/main/java/net/helix/pendulum/service/ledger/impl/LedgerServiceImpl.java
+++ b/src/main/java/net/helix/pendulum/service/ledger/impl/LedgerServiceImpl.java
@@ -5,15 +5,19 @@
import net.helix.pendulum.controllers.RoundViewModel;
import net.helix.pendulum.controllers.StateDiffViewModel;
import net.helix.pendulum.controllers.TransactionViewModel;
+import net.helix.pendulum.crypto.Sponge;
+import net.helix.pendulum.crypto.SpongeFactory;
import net.helix.pendulum.model.Hash;
import net.helix.pendulum.service.ledger.LedgerException;
import net.helix.pendulum.service.ledger.LedgerService;
import net.helix.pendulum.service.milestone.MilestoneService;
+import net.helix.pendulum.service.snapshot.Snapshot;
import net.helix.pendulum.service.snapshot.SnapshotException;
import net.helix.pendulum.service.snapshot.SnapshotProvider;
import net.helix.pendulum.service.snapshot.SnapshotService;
import net.helix.pendulum.service.snapshot.impl.SnapshotStateDiffImpl;
import net.helix.pendulum.storage.Tangle;
+import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -94,9 +98,14 @@ public void restoreLedgerState() throws LedgerException {
@Override
public boolean applyRoundToLedger(RoundViewModel round) throws LedgerException {
- if(generateStateDiff(round)) {
+ return applyRoundToLedger(round, snapshotProvider.getLatestSnapshot(), new HashSet<>(), false);
+ }
+
+ @Override
+ public boolean applyRoundToLedger(RoundViewModel round, Snapshot snapshot, Set nonConflictualMilestones, boolean simulation) throws LedgerException {
+ if(generateStateDiff(round, nonConflictualMilestones, simulation)) {
try {
- snapshotService.replayMilestones(snapshotProvider.getLatestSnapshot(), round.index());
+ snapshotService.replayMilestones(snapshot, round.index());
} catch (SnapshotException e) {
throw new LedgerException("failed to apply the balance changes to the ledger state", e);
}
@@ -220,15 +229,7 @@ public Map generateBalanceDiff(Set visitedTransactions, Set parents = RoundViewModel.getMilestoneBranch(tangle, transactionViewModel, milestoneTx, config.getValidatorSecurity());
- for (Hash parent : parents) {
- nonAnalyzedTransactions.offer(parent);
- }
- } else {
- nonAnalyzedTransactions.offer(transactionViewModel.getBranchTransactionHash());
- }
+ nonAnalyzedTransactions.offer(transactionViewModel.getBranchTransactionHash());
}
}
}
@@ -258,49 +259,99 @@ public Map generateBalanceDiff(Set visitedTransactions, Set successfullyProcessedMilestones, boolean simulation) throws LedgerException {
try {
- /*TransactionViewModel transactionViewModel = TransactionViewModel.fromHash(tangle, round.getHash());
-
- if (!transactionViewModel.isSolid()) {
- return false;
+ if (round.getHashes().isEmpty()) {
+ return true;
}
-
- final int transactionSnapshotIndex = transactionViewModel.snapshotIndex();
- boolean successfullyProcessed = transactionSnapshotIndex == round.index();
- if (!successfullyProcessed) {
- // if the snapshotIndex of our transaction was set already, we have processed our milestones in
- // the wrong order (i.e. while rescanning the db)
- if (transactionSnapshotIndex != 0) {
- milestoneService.resetCorruptedRound(round.index());
- }*/
-
snapshotProvider.getLatestSnapshot().lockRead();
- boolean successfullyProcessed;
+
+ boolean successfullyProcessed = true;
try {
- Set confirmedTips = milestoneService.getConfirmedTips(round.index());
- //todo remove: System.out.println("round.index(): " + round.index() + ", " + confirmedTips);
- Map balanceChanges = generateBalanceDiff(new HashSet<>(), confirmedTips == null? new HashSet<>() : confirmedTips,
- snapshotProvider.getLatestSnapshot().getIndex() + 1);
- successfullyProcessed = balanceChanges != null;
- if (successfullyProcessed) {
- successfullyProcessed = snapshotProvider.getLatestSnapshot().patchedState(
- new SnapshotStateDiffImpl(balanceChanges)).isConsistent();
- if (successfullyProcessed) {
- milestoneService.updateRoundIndexOfMilestoneTransactions(round.index());
+ Set processedTips = new HashSet<>();
+ Map totalBalanceChanges = new HashMap<>();
+
+ Map> confirmedTips = round.getConfirmedTipsPerMilestone(tangle, config);
+ List targetRoundMilestones = getSortedMilestones(round, confirmedTips);
+ Snapshot localSnapshot = snapshotProvider.getLatestSnapshot();
+
+ for (int i = 0; i < targetRoundMilestones.size(); i++) {
+
+ Hash milestone = targetRoundMilestones.get(i).getHash();
+ Set newTipsFromMilestone = confirmedTips.get(milestone).stream().filter(h ->
+ !processedTips.contains(h)).collect(Collectors.toSet());
+
+ if (newTipsFromMilestone.isEmpty()) {
+ successfullyProcessedMilestones.add(milestone);
+ continue;
+ }
+
+ Set testTransactions = (new HashSet<>(processedTips));
+ testTransactions.addAll(newTipsFromMilestone);
+
+ Map balanceChanges = generateBalanceDiff(new HashSet<>(), testTransactions, localSnapshot.getIndex() + 1);
+
+ if (balanceChanges != null) {
+ boolean localProcessed = localSnapshot.patchedState(new SnapshotStateDiffImpl(balanceChanges)).isConsistent();
+ if (localProcessed) {
+ processedTips.addAll(newTipsFromMilestone);
+ successfullyProcessedMilestones.add(milestone);
if (!balanceChanges.isEmpty()) {
- new StateDiffViewModel(balanceChanges, round.index()).store(tangle);
+ totalBalanceChanges.putAll(balanceChanges);
}
}
+ successfullyProcessed = successfullyProcessed && localProcessed;
+ }
+ }
+
+ if (!simulation && !processedTips.isEmpty()) {
+
+ TransactionViewModel.fromHashes(processedTips, tangle).forEach(tvm -> {
+ try {
+ tvm.setRoundIndex(tvm.getRoundIndex() == 0 ? round.index() : tvm.getRoundIndex());
+ tvm.update(tangle, snapshotProvider.getInitialSnapshot(), "roundIndex");
+ } catch (Exception e){
+ log.error("Error during transaction round index update: " + tvm.getHash(), e);
+ }
+ });
+
+ milestoneService.updateRoundIndexOfMilestoneTransactions(round.index());
+ if (!totalBalanceChanges.isEmpty()) {
+ new StateDiffViewModel(totalBalanceChanges, round.index()).store(tangle);
}
- } finally {
- snapshotProvider.getLatestSnapshot().unlockRead();
}
+ } finally {
+ snapshotProvider.getLatestSnapshot().unlockRead();
+ }
return successfullyProcessed;
} catch (Exception e) {
throw new LedgerException("unexpected error while generating the StateDiff for Round" + round.index(), e);
}
}
+
+ private List getSortedMilestones(RoundViewModel round, Map> confirmedTips) {
+ Set milestoneHashes = confirmedTips.keySet();
+
+ List targetRoundMilestones = TransactionViewModel.fromHashes(milestoneHashes, tangle);
+ Sponge sponge = SpongeFactory.create(SpongeFactory.Mode.S256);
+
+ targetRoundMilestones.sort(
+ Comparator.comparing((TransactionViewModel m) -> Hex.toHexString(getHash(sponge,
+ (m.getSignature().toString() + round.index()).getBytes()))));
+ return targetRoundMilestones;
+ }
+
+ public SnapshotService getSnapshotService(){
+ return this.snapshotService;
+ }
+
+ private byte[] getHash(Sponge sponge, byte[] data) {
+ byte[] result = new byte[Sponge.HASH_LENGTH];
+ sponge.reset();
+ sponge.absorb(data, 0, data.length);
+ sponge.squeeze(result, 0, Sponge.HASH_LENGTH);
+ return result;
+ }
}
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/LatestSolidMilestoneTrackerImpl.java b/src/main/java/net/helix/pendulum/service/milestone/impl/LatestSolidMilestoneTrackerImpl.java
index 05675696..ec5645b0 100644
--- a/src/main/java/net/helix/pendulum/service/milestone/impl/LatestSolidMilestoneTrackerImpl.java
+++ b/src/main/java/net/helix/pendulum/service/milestone/impl/LatestSolidMilestoneTrackerImpl.java
@@ -175,12 +175,14 @@ public void trackLatestSolidMilestones() throws MilestoneException {
//syncValidatorTracker();
//syncLatestMilestoneTracker(nextRound.index());
applyRoundToLedger(nextRound);
- logChange(currentSolidRoundIndex);
currentSolidRoundIndex = snapshotProvider.getLatestSnapshot().getIndex();
+ if(nextRound.index() == currentSolidRoundIndex){
+ logChange(currentSolidRoundIndex);
tangle.publish("ctx %s %d", nextRound.getReferencedTransactions(tangle, nextRound.getConfirmedTips(tangle, BasePendulumConfig.Defaults.VALIDATOR_SECURITY)), nextRound.index());
log.delegate().trace("ctx= " + nextRound.getReferencedTransactions(tangle, nextRound.getConfirmedTips(tangle, BasePendulumConfig.Defaults.VALIDATOR_SECURITY)) + ", round=" + nextRound.index());
}
}
+ }
} catch (Exception e) {
throw new MilestoneException("unexpected error while checking for new latest solid milestones", e);
}
@@ -290,10 +292,7 @@ private void stopRepair() {
private void logChange(int prevSolidRoundIndex) {
Snapshot latestSnapshot = snapshotProvider.getLatestSnapshot();
int latestRoundIndex = latestSnapshot.getIndex();
-
- if (prevSolidRoundIndex != latestRoundIndex) {
- log.debug("Round #" + latestRoundIndex + " is SOLID");
- }
+ log.debug("Round #" + latestRoundIndex + " is SOLID");
}
/**
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 6f518758..5f01036e 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,11 +1,12 @@
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.utils.KeyfileUtil;
import net.helix.pendulum.service.validatormanager.CandidateTracker;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
@@ -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());
}
@@ -203,7 +204,7 @@ private Runnable getRunnablePublishMilestone() {
try {
publishMilestone();
} catch (Exception e) {
- e.printStackTrace();
+ log.error("Error during processing milestone", e);
}
};
}
@@ -214,7 +215,7 @@ public void shutdown() {
try {
writeKeyIndex();
} catch (Exception e) {
- e.printStackTrace();
+ log.error("Shut down milestone publisher", e);
}
scheduledExecutorService.shutdown();
}
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 cae1f9a4..9f6c7a11 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,8 +184,10 @@ public MilestoneValidity validateMilestone(TransactionViewModel transactionViewM
if (isMilestoneBundleStructureValid(bundleTransactionViewModels, securityLevel)) {
Hash senderAddress = tail.getAddressHash();
- boolean validSignature = Merkle.validateMerkleSignature(bundleTransactionViewModels, mode, senderAddress, securityLevel, config.getMilestoneKeyDepth());
- log.trace("valid signature: {}", validSignature);
+ boolean validSignature = new MerkleTreeImpl().validateMerkleSignature(bundleTransactionViewModels,
+ new MerkleOptions(mode, senderAddress, securityLevel, config.getMilestoneKeyDepth()));
+ //System.out.println("valid signature: " + validSignature);
+
if ((config.isTestnet() && config.isDontValidateTestnetMilestoneSig()) ||
(validatorAddresses.contains(senderAddress)) && validSignature) {
@@ -357,18 +360,19 @@ private boolean wasRoundAppliedToLedger(RoundViewModel round) throws Exception {
* @throws MilestoneException if anything unexpected happens while updating the milestone index
* @param processedTransactions a set of transactions that have been processed already (for the recursive calls)
*/
- private void updateRoundIndexOfMilestoneTransactions(int correctIndex, int newIndex,
+ public void updateRoundIndexOfMilestoneTransactions(int correctIndex, int newIndex,
Set processedTransactions) throws MilestoneException {
Set inconsistentMilestones = new HashSet<>();
try {
// update milestones
RoundViewModel round = RoundViewModel.get(tangle, newIndex);
- if(round != null) {
- for (Hash milestoneHash : round.getHashes()) {
+ if(round == null) {
+ return;
+ }
+ for (Hash milestoneHash : round.getHashes()) {
TransactionViewModel milestoneTx = TransactionViewModel.fromHash(tangle, milestoneHash);
updateRoundIndexOfSingleTransaction(milestoneTx, newIndex);
- }
}
// update confirmed transactions
final Queue transactionsToUpdate = new LinkedList<>(getConfirmedTips(newIndex));
diff --git a/src/main/java/net/helix/pendulum/service/milestone/impl/MilestoneTrackerImpl.java b/src/main/java/net/helix/pendulum/service/milestone/impl/MilestoneTrackerImpl.java
index 07bc196b..ff71b70a 100644
--- a/src/main/java/net/helix/pendulum/service/milestone/impl/MilestoneTrackerImpl.java
+++ b/src/main/java/net/helix/pendulum/service/milestone/impl/MilestoneTrackerImpl.java
@@ -168,8 +168,8 @@ public MilestoneTrackerImpl init(Tangle tangle, SnapshotProvider snapshotProvide
private void publishMilestoneRefs(TransactionViewModel transaction) throws Exception {
BundleViewModel bundle = BundleViewModel.load(tangle, transaction.getBundleHash());
- for (Hash tx: bundle.getHashes()) {
- tangle.publish("lmr %s %s %s", tx, "Branch " + RoundViewModel.getMilestoneBranch(tangle, TransactionViewModel.fromHash(tangle, tx), transaction, config.getValidatorSecurity()), "Trunk " + RoundViewModel.getMilestoneTrunk(tangle, TransactionViewModel.fromHash(tangle, tx), transaction));
+ for (Hash tx : bundle.getHashes()) {
+ tangle.publish("lmr %s %s %s", tx, "Branch " + transaction.getBranchTransactionHash(), "Trunk " + transaction.getTrunkTransactionHash(), transaction);
}
}
@@ -184,7 +184,16 @@ public void addMilestoneToRoundLog(Hash milestoneHash, int roundIndex, int numbe
tangle.publish("lmi %s %d", milestoneHash, roundIndex);
// todo: temporarily log hardcoded number of _active_ validators instead of numberOfValidators
log.delegate().debug("New milestone {} ({}/{}) added to round #{}", milestoneHash, numberOfMilestones, BasePendulumConfig.Defaults.NUMBER_OF_ACTIVE_VALIDATORS, roundIndex);
+ Set tips = null;
+ try {
+ tips = RoundViewModel.getTipSet(tangle, milestoneHash, config.getValidatorSecurity());
+ } catch (Exception e) {
+ log.error("Add milestone to round log!", e);
+ }
+ tips.forEach(t ->
+ log.delegate().debug("Valid tips transaction: {}, from milesltone {} ", t, milestoneHash)
+ );
}
/**
@@ -201,8 +210,6 @@ public void setRoundIndexAndConfirmations(RoundViewModel currentRVM, Transaction
// The confirmation counter should be incremented with each milestone reference
for (Hash tx : referencedTipSet) {
TransactionViewModel txvm = TransactionViewModel.fromHash(tangle, tx);
- txvm.setRoundIndex(txvm.getRoundIndex() == 0 ? roundIndex : txvm.getRoundIndex());
- txvm.update(tangle, snapshotProvider.getInitialSnapshot(), "roundIndex");
txvm.setConfirmations(txvm.getConfirmations() + 1);
txvm.update(tangle, snapshotProvider.getInitialSnapshot(), "confirmation");
}
@@ -335,7 +342,11 @@ public boolean processMilestoneCandidate(TransactionViewModel transaction) throw
setRoundIndexAndConfirmations(currentRoundViewModel, transaction, roundIndex);
publishMilestoneRefs(transaction);
} else {
+ log.delegate().debug("Failed to validate milestone {} in round #{}", transaction.getHash(), roundIndex);
+ Set tips = RoundViewModel.getTipSet(tangle, transaction.getHash(), config.getValidatorSecurity());
+ tips.forEach(t -> log.delegate().debug("Missed transaction: {}, from skipped milesltone {} ", t, transaction.getHash()));
tracer.trace("round is not active");
+
}
if (!transaction.isSolid()) {
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..bdbd232e
--- /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/tipselection/impl/WalkValidatorImpl.java b/src/main/java/net/helix/pendulum/service/tipselection/impl/WalkValidatorImpl.java
index 51764765..b191bb22 100644
--- a/src/main/java/net/helix/pendulum/service/tipselection/impl/WalkValidatorImpl.java
+++ b/src/main/java/net/helix/pendulum/service/tipselection/impl/WalkValidatorImpl.java
@@ -59,6 +59,9 @@ public WalkValidatorImpl(Tangle tangle, SnapshotProvider snapshotProvider, Ledge
public boolean isValid(Hash transactionHash) throws Exception {
TransactionViewModel transactionViewModel = TransactionViewModel.fromHash(tangle, transactionHash);
+ if(Hash.NULL_HASH.equals(transactionHash)){
+ return true;
+ }
if (transactionViewModel.getType() == TransactionViewModel.PREFILLED_SLOT) {
log.trace("tx_hash: {} ", transactionViewModel.getHash());
log.trace("type: {} ", transactionViewModel.getType());
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 36780497..315b6840 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,9 @@ 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()));
if ((config.isTestnet() && config.isDontValidateTestnetMilestoneSig()) || (config.getValidatorManagerAddress().equals(senderAddress) && validSignature)) {
return VALID;
diff --git a/src/main/java/net/helix/pendulum/service/validatormanager/impl/ValidatorManagerServiceImpl.java b/src/main/java/net/helix/pendulum/service/validatormanager/impl/ValidatorManagerServiceImpl.java
index ca25e285..43322cc0 100644
--- a/src/main/java/net/helix/pendulum/service/validatormanager/impl/ValidatorManagerServiceImpl.java
+++ b/src/main/java/net/helix/pendulum/service/validatormanager/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/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..92fb6fef 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,45 @@ 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);
+
+ bundleUtils.addBundleHash(Arrays.asList(transaction), SpongeFactory.Mode.S256);
+
+ 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.