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