diff --git a/src/main/java/crypto/Sha3256Hasher.java b/src/main/java/crypto/Sha3256Hasher.java index 4a954990..3f50e4ff 100644 --- a/src/main/java/crypto/Sha3256Hasher.java +++ b/src/main/java/crypto/Sha3256Hasher.java @@ -5,14 +5,21 @@ import model.codec.EncodedEntity; import model.crypto.Sha3256Hash; +import model.lightchain.Identifier; /** * Implements SHA3-256 hashing functionality. */ public class Sha3256Hasher implements Hasher { - private static final String HASH_ALG_SHA_3_256 = "SHA3-256"; + private static byte[] concat(final byte[] e1, final byte[] e2) { + byte[] result = new byte[e1.length + e2.length]; + System.arraycopy(e1, 0, result, 0, e1.length); + System.arraycopy(e2, 0, result, e1.length, e2.length); + return result; + } + /** * Computes hash of the given encoded entity. * @@ -21,12 +28,52 @@ public class Sha3256Hasher implements Hasher { */ @Override public Sha3256Hash computeHash(EncodedEntity e) { + return this.computeHash(e.getBytes()); + } + + /** + * Computes hash of the given identifier. + * + * @param id input identifier + * @return SHA3-256 hash object of the entity + */ + public Sha3256Hash computeHash(Identifier id) { + return this.computeHash(id.getBytes()); + } + + /** + * Computes hash of the given bytes. + * + * @param bytes input bytes. + * @return SHA3-256 hash object of the given bytes. + */ + public Sha3256Hash computeHash(byte[] bytes) { try { MessageDigest md = MessageDigest.getInstance(HASH_ALG_SHA_3_256); - byte[] hashValue = md.digest(e.getBytes()); + byte[] hashValue = md.digest(bytes); return new Sha3256Hash(hashValue); } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException(HASH_ALG_SHA_3_256 + "algorithm not found.", ex); } } + + /** + * Commutative hashing of two given byte arrays. + * + * @param b1 first byte array. + * @param b2 second byte array. + * @return SHA3-256 hash object of the commutative concatenation of the two byte arrays. + */ + public Sha3256Hash computeHash(byte[] b1, byte[] b2) { + try { + MessageDigest md = MessageDigest.getInstance(HASH_ALG_SHA_3_256); + return new Sha3256Hash(md.digest(concat(b1, b2))); + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException(HASH_ALG_SHA_3_256 + "algorithm not found.", ex); + } + } + + public Sha3256Hash computeHash(Sha3256Hash h1, Sha3256Hash h2) { + return computeHash(h1.getBytes(), h2.getBytes()); + } } diff --git a/src/main/java/modules/ads/AuthenticatedDataStructure.java b/src/main/java/modules/ads/AuthenticatedDataStructure.java index 8fde6250..8b360017 100644 --- a/src/main/java/modules/ads/AuthenticatedDataStructure.java +++ b/src/main/java/modules/ads/AuthenticatedDataStructure.java @@ -10,4 +10,6 @@ public interface AuthenticatedDataStructure { AuthenticatedEntity put(Entity e); AuthenticatedEntity get(Identifier id); + + int size(); } diff --git a/src/main/java/modules/ads/AuthenticatedEntity.java b/src/main/java/modules/ads/AuthenticatedEntity.java index d66d35a8..449aa167 100644 --- a/src/main/java/modules/ads/AuthenticatedEntity.java +++ b/src/main/java/modules/ads/AuthenticatedEntity.java @@ -7,7 +7,7 @@ * that entity against a root identifier. */ public abstract class AuthenticatedEntity extends Entity { - abstract Entity getEntity(); + public abstract Entity getEntity(); - abstract MembershipProof getMembershipProof(); + public abstract MembershipProof getMembershipProof(); } diff --git a/src/main/java/modules/ads/MembershipProof.java b/src/main/java/modules/ads/MembershipProof.java index 50078bd9..724792ff 100644 --- a/src/main/java/modules/ads/MembershipProof.java +++ b/src/main/java/modules/ads/MembershipProof.java @@ -1,6 +1,8 @@ package modules.ads; -import model.lightchain.Identifier; +import java.util.ArrayList; + +import model.crypto.Sha3256Hash; /** * Represents a Merkle Proof of membership against a certain root identifier. @@ -9,15 +11,16 @@ public interface MembershipProof { /** * Root of the authenticated data structure that this proof belongs to. * - * @return root identifier. + * @return hash value of the root node. */ - Identifier getRoot(); + Sha3256Hash getRoot(); /** - * Sibling of the given identifier on the membership Merkle Proof path to the root. + * Returns the path of the proof of membership. * - * @param identifier identifier of the entity. - * @return sibling of given identifier. + * @return path of the proof of membership. */ - Identifier getSiblingOf(Identifier identifier); + ArrayList getPath(); + + ArrayList getIsLeftNode(); } diff --git a/src/main/java/modules/ads/merkletree/MerkleNode.java b/src/main/java/modules/ads/merkletree/MerkleNode.java new file mode 100644 index 00000000..4551b1bf --- /dev/null +++ b/src/main/java/modules/ads/merkletree/MerkleNode.java @@ -0,0 +1,117 @@ +package modules.ads.merkletree; + +import crypto.Sha3256Hasher; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import model.Entity; +import model.crypto.Sha3256Hash; + +/** + * A node in the Merkle tree. + */ +public class MerkleNode { + private static final Sha3256Hasher hasher = new Sha3256Hasher(); + private MerkleNode left; + private MerkleNode right; + private MerkleNode parent; + private boolean isLeft; + private Sha3256Hash hash; + + /** + * Default constructor. + */ + public MerkleNode() { + this.left = null; + this.right = null; + this.parent = null; + this.isLeft = false; + this.hash = null; + } + + /** + * Constructor with entity and isLeft. + * + * @param e input entity + * @param isLeft boolean that specifies if the node is left child or not + */ + public MerkleNode(Entity e, boolean isLeft) { + this.left = null; + this.right = null; + this.parent = null; + this.isLeft = isLeft; + this.hash = hasher.computeHash(e.id()); + } + + /** + * Constructor with hash of the entity. + * + * @param hash input hash of the entity corresponding to that node + */ + public MerkleNode(Sha3256Hash hash) { + this.left = null; + this.right = null; + this.parent = null; + this.isLeft = false; + this.hash = hash; + } + + /** + * Constructor of a parent node. + * + * @param hash input hash of the entity corresponding to that node + * @param left left child of the node + * @param right right child of the node + */ + @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "left and right are intentionally mutable externally") + public MerkleNode(Sha3256Hash hash, MerkleNode left, MerkleNode right) { + this.left = left; + this.right = right; + this.parent = null; + this.isLeft = false; + this.hash = hash; + } + + @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "internal representation is intentionally returned") + public MerkleNode getLeft() { + return left; + } + + @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "internal representation is intentionally returned") + public MerkleNode getRight() { + return right; + } + + @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "internal representation is intentionally returned") + public MerkleNode getParent() { + return parent; + } + + @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "parent is intentionally mutable externally") + public void setParent(MerkleNode parent) { + this.parent = parent; + } + + public Sha3256Hash getHash() { + return hash; + } + + public boolean isLeft() { + return isLeft; + } + + public void setLeft(boolean isLeft) { + this.isLeft = isLeft; + } + + /** + * Returns the sibling of the node. + * + * @return the sibling of the node + */ + public MerkleNode getSibling() { + if (isLeft()) { + return parent.getRight(); + } else { + return parent.getLeft(); + } + } +} diff --git a/src/main/java/modules/ads/merkletree/MerkleProof.java b/src/main/java/modules/ads/merkletree/MerkleProof.java new file mode 100644 index 00000000..c0b3e8c4 --- /dev/null +++ b/src/main/java/modules/ads/merkletree/MerkleProof.java @@ -0,0 +1,71 @@ +package modules.ads.merkletree; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import model.crypto.Sha3256Hash; +import modules.ads.MembershipProof; + +/** + * A proof of membership in a Merkle tree. + */ +public class MerkleProof implements MembershipProof { + private ArrayList path; + private final ArrayList isLeftNode; + private final Sha3256Hash root; + + /** + * Constructs a proof from a list of hashes and a root. + * + * @param path the list of hashes + * @param root the root + * @param isLeftNode the list of isLeft Boolean values of the hashes + */ + public MerkleProof(ArrayList path, Sha3256Hash root, ArrayList isLeftNode) { + this.path = new ArrayList<>(path); + this.root = root; + this.isLeftNode = new ArrayList<>(isLeftNode); + } + + @Override + public ArrayList getPath() { + return new ArrayList<>(path); + } + + public void setPath(ArrayList path) { + this.path = new ArrayList<>(path); + } + + @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "internal representation is intentionally returned") + public ArrayList getIsLeftNode() { + return isLeftNode; + } + + public Sha3256Hash getRoot() { + return root; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MerkleProof proof = (MerkleProof) o; + for (int i = 0; i < path.size(); i++) { + if (!Arrays.equals(path.get(i).getBytes(), proof.path.get(i).getBytes())) { + return false; + } + } + return root.equals(proof.root); + } + + @Override + public int hashCode() { + return Objects.hash(path, root); + } +} diff --git a/src/main/java/modules/ads/merkletree/MerkleTree.java b/src/main/java/modules/ads/merkletree/MerkleTree.java new file mode 100644 index 00000000..2c194c4a --- /dev/null +++ b/src/main/java/modules/ads/merkletree/MerkleTree.java @@ -0,0 +1,136 @@ +package modules.ads.merkletree; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import crypto.Sha3256Hasher; +import model.Entity; +import model.crypto.Sha3256Hash; +import model.lightchain.Identifier; +import modules.ads.AuthenticatedDataStructure; + +/** + * Implementation of an in-memory Authenticated Skip List + * that is capable of storing and retrieval of LightChain entities. + */ +public class MerkleTree implements AuthenticatedDataStructure { + private static final Sha3256Hasher hasher = new Sha3256Hasher(); + private final ReentrantReadWriteLock lock; + private final ArrayList leafNodes; + private final Map leafNodesHashTable; + private final Map entityHashTable; + private int size; + private MerkleNode root; + + /** + * Default constructor for a Merkle Tree. + */ + public MerkleTree() { + this.size = 0; + this.root = new MerkleNode(); + this.leafNodes = new ArrayList<>(); + this.lock = new ReentrantReadWriteLock(); + this.leafNodesHashTable = new HashMap<>(); + this.entityHashTable = new HashMap<>(); + } + + @Override + public modules.ads.AuthenticatedEntity put(Entity e) throws IllegalArgumentException { + try { + lock.writeLock().lock(); + if (e == null) { + throw new IllegalArgumentException("entity cannot be null"); + } + Sha3256Hash hash = new Sha3256Hash(e.id().getBytes()); + Integer idx = leafNodesHashTable.get(hash); + if (idx == null) { + leafNodes.add(new MerkleNode(e, false)); + leafNodesHashTable.put(hash, size); + entityHashTable.put(e.id(), e); + size++; + buildMerkleTree(); + MerkleProof proof = getProof(e.id()); + return new MerkleTreeAuthenticatedEntity(proof, e.type(), e); + } else { + MerkleProof proof = getProof(e.id()); + return new MerkleTreeAuthenticatedEntity(proof, e.type(), e); + } + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public modules.ads.AuthenticatedEntity get(Identifier id) throws IllegalArgumentException { + MerkleProof proof; + if (id == null) { + throw new IllegalArgumentException("identifier cannot be null"); + } + try { + lock.readLock().lock(); + proof = getProof(id); + Entity e = entityHashTable.get(id); + return new MerkleTreeAuthenticatedEntity(proof, e.type(), e); + } finally { + lock.readLock().unlock(); + } + } + + private MerkleProof getProof(Identifier id) throws IllegalArgumentException { + ArrayList isLeftNode = new ArrayList<>(); + Sha3256Hash hash = new Sha3256Hash(id.getBytes()); + Integer idx = leafNodesHashTable.get(hash); + if (idx == null) { + throw new IllegalArgumentException("identifier not found"); + } + ArrayList path = new ArrayList<>(); + MerkleNode currentNode = leafNodes.get(idx); + while (currentNode != root) { + path.add(currentNode.getSibling().getHash()); + isLeftNode.add(currentNode.isLeft()); + currentNode = currentNode.getParent(); + } + return new MerkleProof(path, root.getHash(), isLeftNode); + } + + private void buildMerkleTree() { + // keeps nodes of the current level of the merkle tree + // will be updated bottom up + // initialized with leaves + ArrayList currentLevelNodes = new ArrayList<>(leafNodes); + + // keeps nodes of the next level of merkle tree + // used as an intermediary data structure. + ArrayList nextLevelNodes = new ArrayList<>(); + + while (currentLevelNodes.size() > 1) { // more than one current node, means we have not yet reached root. + for (int i = 0; i < currentLevelNodes.size(); i += 2) { + // pairs up current level nodes as siblings for next level. + MerkleNode left = currentLevelNodes.get(i); + left.setLeft(true); + + MerkleNode right; + if (i + 1 < currentLevelNodes.size()) { + right = currentLevelNodes.get(i + 1); // we have a right node + } else { + // TODO: edge case need to get fixed. + right = new MerkleNode(left.getHash()); + } + Sha3256Hash hash = hasher.computeHash(left.getHash().getBytes(), right.getHash().getBytes()); + MerkleNode parent = new MerkleNode(hash, left, right); + left.setParent(parent); + right.setParent(parent); + nextLevelNodes.add(parent); + } + currentLevelNodes = nextLevelNodes; + nextLevelNodes = new ArrayList<>(); + } + root = currentLevelNodes.get(0); + } + + public int size() { + return this.size; + } +} diff --git a/src/main/java/modules/ads/merkletree/MerkleTreeAuthenticatedEntity.java b/src/main/java/modules/ads/merkletree/MerkleTreeAuthenticatedEntity.java new file mode 100644 index 00000000..a6aa2f33 --- /dev/null +++ b/src/main/java/modules/ads/merkletree/MerkleTreeAuthenticatedEntity.java @@ -0,0 +1,41 @@ +package modules.ads.merkletree; + +import model.Entity; +import modules.ads.MembershipProof; + +/** + * An entity with its membership proof and type. + */ +public class MerkleTreeAuthenticatedEntity extends modules.ads.AuthenticatedEntity { + private final MembershipProof membershipProof; + private final String type; + private final Entity entity; + + /** + * Constructor of an authenticated entity. + * + * @param proof the membership proof + * @param type the type of the entity + * @param e the entity + */ + public MerkleTreeAuthenticatedEntity(MerkleProof proof, String type, Entity e) { + this.membershipProof = new MerkleProof(proof.getPath(), proof.getRoot(), proof.getIsLeftNode()); + this.type = type; + this.entity = e; + } + + @Override + public String type() { + return type; + } + + @Override + public Entity getEntity() { + return entity; + } + + @Override + public MembershipProof getMembershipProof() { + return new MerkleProof(membershipProof.getPath(), membershipProof.getRoot(), membershipProof.getIsLeftNode()); + } +} diff --git a/src/main/java/modules/ads/merkletree/MerkleTreeAuthenticatedEntityVerifier.java b/src/main/java/modules/ads/merkletree/MerkleTreeAuthenticatedEntityVerifier.java new file mode 100644 index 00000000..bdea1990 --- /dev/null +++ b/src/main/java/modules/ads/merkletree/MerkleTreeAuthenticatedEntityVerifier.java @@ -0,0 +1,45 @@ +package modules.ads.merkletree; + +import java.util.ArrayList; +import java.util.Arrays; + +import crypto.Sha3256Hasher; +import model.crypto.Sha3256Hash; +import modules.ads.AuthenticatedEntity; +import modules.ads.MembershipProof; + +/** + * Verifies the AuthenticatedEntity against its self-contained proof. + */ +public class MerkleTreeAuthenticatedEntityVerifier implements modules.ads.AuthenticatedEntityVerifier { + + /** + * Verifies the AuthenticatedEntity against its self-contained proof. + * + * @param authenticatedEntity the AuthenticatedEntity to verify. + * @return true if entity contains a valid Merkle Proof against its root identifier, false otherwise. + */ + @Override + public boolean verify(AuthenticatedEntity authenticatedEntity) { + Sha3256Hasher hasher = new Sha3256Hasher(); + MembershipProof proof = authenticatedEntity.getMembershipProof(); + ArrayList isLeftNode = proof.getIsLeftNode(); + ArrayList proofPath = proof.getPath(); + + Sha3256Hash initialHash = hasher.computeHash(authenticatedEntity.getEntity().id()); + Sha3256Hash currentHash; + if (isLeftNode.get(0)) { + currentHash = hasher.computeHash(initialHash, proofPath.get(0)); + } else { + currentHash = hasher.computeHash(proofPath.get(0), initialHash); + } + for (int i = 1; i < proofPath.size(); i++) { + if (isLeftNode.get(i)) { + currentHash = hasher.computeHash(currentHash, proofPath.get(i)); + } else { + currentHash = hasher.computeHash(proofPath.get(i), currentHash); + } + } + return Arrays.equals(proof.getRoot().getBytes(), currentHash.getBytes()); + } +} \ No newline at end of file diff --git a/src/main/java/modules/ads/skiplist/SkipList.java b/src/main/java/modules/ads/skiplist/SkipList.java deleted file mode 100644 index 6a77ecdb..00000000 --- a/src/main/java/modules/ads/skiplist/SkipList.java +++ /dev/null @@ -1,22 +0,0 @@ -package modules.ads.skiplist; - -import model.Entity; -import model.lightchain.Identifier; -import modules.ads.AuthenticatedDataStructure; -import modules.ads.AuthenticatedEntity; - -/** - * Implementation of an in-memory Authenticated Skip List - * that is capable of storing and retrieval of LightChain entities. - */ -public class SkipList implements AuthenticatedDataStructure { - @Override - public AuthenticatedEntity put(Entity e) { - return null; - } - - @Override - public AuthenticatedEntity get(Identifier id) { - return null; - } -} diff --git a/src/test/java/modules/ads/MerkleTreeTest.java b/src/test/java/modules/ads/MerkleTreeTest.java new file mode 100644 index 00000000..95a290dc --- /dev/null +++ b/src/test/java/modules/ads/MerkleTreeTest.java @@ -0,0 +1,271 @@ +package modules.ads; + +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import model.Entity; +import model.crypto.Sha3256Hash; +import model.lightchain.Identifier; +import modules.ads.merkletree.MerkleProof; +import modules.ads.merkletree.MerkleTree; +import modules.ads.merkletree.MerkleTreeAuthenticatedEntity; +import modules.ads.merkletree.MerkleTreeAuthenticatedEntityVerifier; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import unittest.fixtures.EntityFixture; +import unittest.fixtures.MerkleTreeFixture; +import unittest.fixtures.Sha3256HashFixture; + +/** + * Encapsulates tests for an authenticated and concurrent implementation of MerkleTree ADS. + */ +public class MerkleTreeTest { + + /** + * A basic test for one sequential put and get operations. + */ + @Test + public void testVerification() { + MerkleTree merkleTree = MerkleTreeFixture.createMerkleTree(5); + Assertions.assertEquals(merkleTree.size(), 5); // fixture sanity check. + + Entity entity = new EntityFixture(); + merkleTree.put(entity); + Assertions.assertEquals(merkleTree.size(), 6); + + AuthenticatedEntity authenticatedEntity = merkleTree.get(entity.id()); + MerkleTreeAuthenticatedEntityVerifier verifier = new MerkleTreeAuthenticatedEntityVerifier(); + Assertions.assertTrue(verifier.verify(authenticatedEntity)); + } + + /** + * Tests both putting and getting the same entity gives same proof + * and putting another entity gives different proofs. + */ + @Test + public void testPutGetSameProof() { + MerkleTree merkleTree = MerkleTreeFixture.createMerkleTree(5); + Assertions.assertEquals(merkleTree.size(), 5); // fixture sanity check. + Entity e1 = new EntityFixture(); + + // putting e1 + AuthenticatedEntity authenticatedEntityPut = merkleTree.put(e1); + MembershipProof proofPutE1 = authenticatedEntityPut.getMembershipProof(); + Assertions.assertEquals(merkleTree.size(), 6); + + // getting e1 + AuthenticatedEntity authE1Get = merkleTree.get(e1.id()); + MembershipProof proofGetE1 = authE1Get.getMembershipProof(); + + // putting e2 + Entity e2 = new EntityFixture(); + AuthenticatedEntity authE2Put = merkleTree.put(e2); + Assertions.assertEquals(merkleTree.size(), 7); + + // getting e2 + MembershipProof proofPutE2 = authE2Put.getMembershipProof(); + + // proofs for putting and getting e1 should be the same. + Assertions.assertEquals(proofPutE1, proofGetE1); + + // proofs for putting e1 and e2 must be different. + Assertions.assertNotEquals(proofPutE1, proofPutE2); + } + + /** + * Tests putting an existing entity does not change the proof. + */ + @Test + public void testPutExistingEntity() { + MerkleTree merkleTree = MerkleTreeFixture.createMerkleTree(5); + Assertions.assertEquals(merkleTree.size(), 5); // fixture sanity check. + Entity entity = new EntityFixture(); + + // first time put + AuthenticatedEntity authenticatedEntityPut = merkleTree.put(entity); + MembershipProof proofPut = authenticatedEntityPut.getMembershipProof(); + Assertions.assertEquals(merkleTree.size(), 6); + + // second attempt + AuthenticatedEntity authenticatedEntityPutAgain = merkleTree.put(entity); + MembershipProof proofPutAgain = authenticatedEntityPutAgain.getMembershipProof(); + + // proofs must be equal. + Assertions.assertEquals(proofPut, proofPutAgain); + Assertions.assertEquals(merkleTree.size(), 6); // duplicate entity should not change the size. + } + + /** + * Concurrently puts and gets entities and checks their proofs are correct (thread safety check). + */ + @Test + public void testConcurrentPutGet() { + int concurrencyDegree = 100; + ArrayList entities = new ArrayList<>(); + ArrayList ids = new ArrayList<>(); + + AtomicInteger threadError = new AtomicInteger(); + CountDownLatch putDone = new CountDownLatch(concurrencyDegree); + CountDownLatch getDone = new CountDownLatch(concurrencyDegree); + + Thread[] putThreads = new Thread[concurrencyDegree]; + Thread[] getThreads = new Thread[concurrencyDegree]; + + MerkleTree merkleTree = MerkleTreeFixture.createMerkleTree(0); + Assertions.assertEquals(merkleTree.size(), 0); // fixture sanity check. + + for (int i = 0; i < concurrencyDegree; i++) { + Entity entity = new EntityFixture(); + entities.add(entity); + ids.add(entity.id()); + } + + // put + for (int i = 0; i < concurrencyDegree; i++) { + Entity entity = entities.get(i); + putThreads[i] = new Thread(() -> { + try { + merkleTree.put(entity); + putDone.countDown(); + } catch (Exception e) { + threadError.getAndIncrement(); + } + }); + } + for (Thread t : putThreads) { + t.start(); + } + try { + boolean doneOneTimePut = putDone.await(60, TimeUnit.SECONDS); + Assertions.assertTrue(doneOneTimePut); + } catch (InterruptedException e) { + Assertions.fail(); + } + + // get + for (int i = 0; i < concurrencyDegree; i++) { + Identifier id = ids.get(i); + getThreads[i] = new Thread(() -> { + try { + AuthenticatedEntity authenticatedEntity = merkleTree.get(id); + MerkleTreeAuthenticatedEntityVerifier verifier = new MerkleTreeAuthenticatedEntityVerifier(); + if (!verifier.verify(authenticatedEntity)) { + threadError.getAndIncrement(); + } + getDone.countDown(); + } catch (Exception e) { + threadError.getAndIncrement(); + } + }); + } + for (Thread t : getThreads) { + t.start(); + } + try { + boolean doneOneTimeGet = getDone.await(60, TimeUnit.SECONDS); + Assertions.assertTrue(doneOneTimeGet); + } catch (InterruptedException e) { + Assertions.fail(); + } + Assertions.assertEquals(0, threadError.get()); + Assertions.assertEquals(concurrencyDegree, merkleTree.size()); + } + + /** + * Tests getting an entity that does not exist in the merkle tree throws IllegalArgumentException. + */ + @Test + public void testGetNonExistingEntity() { + MerkleTree merkleTree = MerkleTreeFixture.createMerkleTree(5); + Assertions.assertEquals(merkleTree.size(), 5); // fixture sanity check. + Entity entity = new EntityFixture(); + + Assertions.assertThrows(IllegalArgumentException.class, () -> { + merkleTree.get(entity.id()); + }); + } + + /** + * Tests inserting null throws IllegalArgumentException. + */ + @Test + public void testNullInsertion() { + MerkleTree merkleTree = MerkleTreeFixture.createMerkleTree(5); + Assertions.assertEquals(merkleTree.size(), 5); // fixture sanity check. + Assertions.assertThrows(IllegalArgumentException.class, () -> { + merkleTree.put(null); + }); + } + + /** + * Tests the proof verification fails when root is changed. + */ + @Test + public void testManipulatedRoot() { + MerkleTree merkleTree = MerkleTreeFixture.createMerkleTree(5); + Assertions.assertEquals(merkleTree.size(), 5); // fixture sanity check. + Entity entity = new EntityFixture(); + AuthenticatedEntity authenticatedEntity = merkleTree.put(entity); + MembershipProof proof = authenticatedEntity.getMembershipProof(); + + // creates a tampered proof with random root. + MerkleProof tamperedProof = new MerkleProof(proof.getPath(), new Sha3256Hash(new byte[32]), proof.getIsLeftNode()); + AuthenticatedEntity tamperedAuthenticatedEntity = new MerkleTreeAuthenticatedEntity( + tamperedProof, + authenticatedEntity.type(), + authenticatedEntity.getEntity()); + + MerkleTreeAuthenticatedEntityVerifier verifier = new MerkleTreeAuthenticatedEntityVerifier(); + // authenticated entity must be verified. + Assertions.assertTrue(verifier.verify(authenticatedEntity)); + // tampered authenticated entity must be failed. + Assertions.assertFalse(verifier.verify(tamperedAuthenticatedEntity)); + } + + /** + * Tests the proof verification fails when entity is changed. + */ + @Test + public void testManipulatedEntity() { + MerkleTree merkleTree = MerkleTreeFixture.createMerkleTree(5); + Assertions.assertEquals(merkleTree.size(), 5); // fixture sanity check. + Entity entity = new EntityFixture(); + AuthenticatedEntity authenticatedEntity = merkleTree.put(entity); + + AuthenticatedEntity tamperedEntity = new MerkleTreeAuthenticatedEntity( + (MerkleProof) authenticatedEntity.getMembershipProof(), + authenticatedEntity.type(), + new EntityFixture()); + + MerkleTreeAuthenticatedEntityVerifier verifier = new MerkleTreeAuthenticatedEntityVerifier(); + Assertions.assertTrue(verifier.verify(authenticatedEntity)); // original authenticated entity passes verification. + Assertions.assertFalse(verifier.verify(tamperedEntity)); // tampered entity fails verification. + } + + /** + * Tests the proof fails verification when proof part of authenticated entity is changed. + */ + @Test + public void testManipulatedProof() { + MerkleTree merkleTree = MerkleTreeFixture.createMerkleTree(5); + Assertions.assertEquals(merkleTree.size(), 5); // fixture sanity check. + + Entity entity = new EntityFixture(); + AuthenticatedEntity authenticatedEntity = merkleTree.put(entity); + MembershipProof proof = authenticatedEntity.getMembershipProof(); + + AuthenticatedEntity tamperedEntity = new MerkleTreeAuthenticatedEntity( + new MerkleProof(Sha3256HashFixture.newSha3256HashArrayList( + proof.getPath().size()), + proof.getRoot(), + proof.getIsLeftNode()), + authenticatedEntity.type(), + authenticatedEntity.getEntity()); + + MerkleTreeAuthenticatedEntityVerifier verifier = new MerkleTreeAuthenticatedEntityVerifier(); + Assertions.assertTrue(verifier.verify(authenticatedEntity)); // original authenticated entity passes verification. + Assertions.assertFalse(verifier.verify(tamperedEntity)); // tampered entity fails verification. + } +} diff --git a/src/test/java/modules/ads/SkipListTest.java b/src/test/java/modules/ads/SkipListTest.java deleted file mode 100644 index 7b48990a..00000000 --- a/src/test/java/modules/ads/SkipListTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package modules.ads; - -/** - * Encapsulates tests for an authenticated and concurrent implementation of SkipList ADS. - */ -public class SkipListTest { - // TODO: writing tests to cover - // 1. When putting a unique entity into skip list, we can recover it. - // 2. Proof of membership for putting and getting an entity is the same. - // 3. Putting an already existing entity does not change its membership proof. - // 4. Putting 100 distinct entities concurrently inserts all of them into skip list with correct membership proofs, - // and also, makes them all retrievable with correct membership proofs. - // 5. Getting non-existing identifiers returns null. - // 7. Putting null returns null. - // 8. Tampering with root identifier of an authenticated entity fails its verification. - // 9. Tampering with entity of an authenticated entity fails its verification. - // 10. Tampering with proof of an authenticated entity fails its verification. -} diff --git a/src/test/java/unittest/fixtures/MerkleTreeFixture.java b/src/test/java/unittest/fixtures/MerkleTreeFixture.java new file mode 100644 index 00000000..ebf0347c --- /dev/null +++ b/src/test/java/unittest/fixtures/MerkleTreeFixture.java @@ -0,0 +1,22 @@ +package unittest.fixtures; + +import modules.ads.merkletree.MerkleTree; + +/** + * Creates a new randomly looking MerkleTree. + */ +public class MerkleTreeFixture { + /** + * Creates a new skip list with n random elements. + * + * @param n number of elements to create + * @return a new merkle tree with n random elements. + */ + public static MerkleTree createMerkleTree(int n) { + MerkleTree merkleTree = new MerkleTree(); + for (int i = 0; i < n; i++) { + merkleTree.put(new EntityFixture()); + } + return merkleTree; + } +} diff --git a/src/test/java/unittest/fixtures/Sha3256HashFixture.java b/src/test/java/unittest/fixtures/Sha3256HashFixture.java index 2a0bc7e6..41c5bbc7 100644 --- a/src/test/java/unittest/fixtures/Sha3256HashFixture.java +++ b/src/test/java/unittest/fixtures/Sha3256HashFixture.java @@ -1,5 +1,7 @@ package unittest.fixtures; +import java.util.ArrayList; + import model.crypto.Sha3256Hash; /** @@ -28,4 +30,18 @@ public static model.crypto.Sha3256Hash[] newSha3256HashArray() { } return hashArray; } + + /** + * Generates an ArrayList of random looking SHA3-256 hash values. + * + * @param size size of array list. + * @return an array filled with randomly generated SHA3-256 hash values. + */ + public static ArrayList newSha3256HashArrayList(int size) { + ArrayList hashArray = new ArrayList<>(); + for (int i = 0; i < size; i++) { + hashArray.add(newSha3256Hash()); + } + return hashArray; + } }