diff --git a/src/main/java/model/crypto/Signature.java b/src/main/java/model/crypto/Signature.java index 6b0c50bf..dbeea7db 100644 --- a/src/main/java/model/crypto/Signature.java +++ b/src/main/java/model/crypto/Signature.java @@ -1,17 +1,18 @@ package model.crypto; +import java.io.Serializable; + import model.Entity; import model.lightchain.Identifier; /** * Represents abstract data type for the cryptographic digital signature used in LightChain. */ -public abstract class Signature extends Entity { +public abstract class Signature extends Entity implements Serializable { /** * The signature value in bytes. */ private final byte[] bytes; - /** * Identifier of node that signed transaction. */ diff --git a/src/main/java/model/crypto/ecdsa/EcdsaSignature.java b/src/main/java/model/crypto/ecdsa/EcdsaSignature.java index 73b85fad..8623d61b 100644 --- a/src/main/java/model/crypto/ecdsa/EcdsaSignature.java +++ b/src/main/java/model/crypto/ecdsa/EcdsaSignature.java @@ -1,5 +1,7 @@ package model.crypto.ecdsa; +import java.io.Serializable; + import model.codec.EntityType; import model.crypto.Signature; import model.lightchain.Identifier; @@ -7,7 +9,7 @@ /** * ECDSA signature implementation with signer ID. */ -public class EcdsaSignature extends Signature { +public class EcdsaSignature extends Signature implements Serializable { public static final String ELLIPTIC_CURVE = "EC"; public static final String SIGN_ALG_SHA_3_256_WITH_ECDSA = "SHA3-256withECDSA"; diff --git a/src/main/java/model/lightchain/Block.java b/src/main/java/model/lightchain/Block.java index 0001db36..50b9c549 100644 --- a/src/main/java/model/lightchain/Block.java +++ b/src/main/java/model/lightchain/Block.java @@ -1,12 +1,14 @@ package model.lightchain; +import java.io.Serializable; + import model.codec.EntityType; import model.crypto.Signature; /** * Represents a LightChain Block that encapsulates set of ValidatedTransaction(s). */ -public class Block extends model.Entity { +public class Block extends model.Entity implements Serializable { /** * Reference to the hash value of another block as its parent. */ @@ -72,6 +74,24 @@ public Block(Identifier previousBlockId, this.height = height; } + @Override + public int hashCode() { + return this.id().hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Block)) { + return false; + } + Block that = (Block) o; + + return this.id().equals(that.id()); + } + /** * Type of this entity. * diff --git a/src/main/java/model/lightchain/Identifiers.java b/src/main/java/model/lightchain/Identifiers.java index 10e858d4..93e060c3 100644 --- a/src/main/java/model/lightchain/Identifiers.java +++ b/src/main/java/model/lightchain/Identifiers.java @@ -1,11 +1,12 @@ package model.lightchain; +import java.io.Serializable; import java.util.ArrayList; /** * Represents an aggregated type for identifiers. */ -public class Identifiers { +public class Identifiers implements Serializable { private final ArrayList identifiers; public Identifiers() { diff --git a/src/main/java/model/lightchain/Transaction.java b/src/main/java/model/lightchain/Transaction.java index ef0aab57..902cc2a1 100644 --- a/src/main/java/model/lightchain/Transaction.java +++ b/src/main/java/model/lightchain/Transaction.java @@ -1,12 +1,14 @@ package model.lightchain; +import java.io.Serializable; + import model.codec.EntityType; import model.crypto.Signature; /** * Represents a LightChain transaction in form of a token transfer between a sender and receiver. */ -public class Transaction extends model.Entity { +public class Transaction extends model.Entity implements Serializable { /** * The identifier of a finalized block that this transaction refers to its snapshot. */ @@ -51,6 +53,35 @@ public Transaction(Identifier referenceBlockId, Identifier sender, Identifier re this.amount = amount; } + /** + * Return the HashCode. + * + * @return the hashcode. + */ + @Override + public int hashCode() { + return this.id().hashCode(); + } + + /** + * Returns true if objects are equal. + * + * @param o an transaction object. + * @return true if objects equal. + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Transaction)) { + return false; + } + Transaction that = (Transaction) o; + + return this.id().equals(that.id()); + } + /** * Type of this entity. * diff --git a/src/main/java/model/lightchain/ValidatedBlock.java b/src/main/java/model/lightchain/ValidatedBlock.java index c47d5ef6..1443a5a8 100644 --- a/src/main/java/model/lightchain/ValidatedBlock.java +++ b/src/main/java/model/lightchain/ValidatedBlock.java @@ -1,5 +1,7 @@ package model.lightchain; +import java.util.Arrays; + import model.codec.EntityType; import model.crypto.Signature; @@ -36,6 +38,28 @@ public Signature[] getCertificates() { return certificates.clone(); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ValidatedBlock)) { + return false; + } + if (!super.equals(o)) { + return false; + } + ValidatedBlock that = (ValidatedBlock) o; + return Arrays.equals(getCertificates(), that.getCertificates()); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Arrays.hashCode(getCertificates()); + return result; + } + @Override public String type() { return EntityType.TYPE_VALIDATED_TRANSACTION; diff --git a/src/main/java/model/lightchain/ValidatedTransaction.java b/src/main/java/model/lightchain/ValidatedTransaction.java index e60c932c..33f41c98 100644 --- a/src/main/java/model/lightchain/ValidatedTransaction.java +++ b/src/main/java/model/lightchain/ValidatedTransaction.java @@ -1,5 +1,8 @@ package model.lightchain; +import java.io.Serializable; +import java.util.Arrays; + import model.codec.EntityType; import model.crypto.Signature; @@ -7,7 +10,7 @@ * A ValidatedTransaction is a wrapper around a Transaction that carries a proof of assigned validators that attests * the transaction passed local validation of validators. */ -public class ValidatedTransaction extends Transaction { +public class ValidatedTransaction extends Transaction implements Serializable { /** * Represents the signatures of assigned validators to this transaction. */ @@ -36,6 +39,28 @@ public Signature[] getCertificates() { return certificates.clone(); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ValidatedTransaction)) { + return false; + } + if (!super.equals(o)) { + return false; + } + ValidatedTransaction that = (ValidatedTransaction) o; + return Arrays.equals(getCertificates(), that.getCertificates()); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Arrays.hashCode(getCertificates()); + return result; + } + @Override public String type() { return EntityType.TYPE_VALIDATED_TRANSACTION; diff --git a/src/main/java/storage/Blocks.java b/src/main/java/storage/Blocks.java index 037242ba..aafdda53 100644 --- a/src/main/java/storage/Blocks.java +++ b/src/main/java/storage/Blocks.java @@ -9,6 +9,8 @@ * Persistent module for storing blocks on the disk. */ public interface Blocks { + // TODO: refactor blocks to keep validated blocks. + /** * Checks existence of block on the database. * diff --git a/src/main/java/storage/mapdb/BlocksMapDb.java b/src/main/java/storage/mapdb/BlocksMapDb.java new file mode 100644 index 00000000..3c72086c --- /dev/null +++ b/src/main/java/storage/mapdb/BlocksMapDb.java @@ -0,0 +1,179 @@ +package storage.mapdb; + +import java.util.ArrayList; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import model.lightchain.Block; +import model.lightchain.Identifier; +import org.mapdb.*; +import storage.Blocks; + +/** + * Implementation of Transactions interface. + */ +public class BlocksMapDb implements Blocks { + private final DB dbId; + private final DB dbHeight; + private final ReentrantReadWriteLock lock; + private static final String MAP_NAME_ID = "blocks_map_id"; + private static final String MAP_NAME_HEIGHT = "blocks_map_height"; + private final HTreeMap blocksIdMap; + private final HTreeMap> blocksHeightMap; + + /** + * Creates blocks mapdb. + * + * @param filePathId of id,block mapdb. + * @param filePathHeight of height,id mapdb. + */ + public BlocksMapDb(String filePathId, String filePathHeight) { + // TODO: file paths consolidated. + this.dbId = DBMaker.fileDB(filePathId).make(); + this.lock = new ReentrantReadWriteLock(); + blocksIdMap = this.dbId.hashMap(MAP_NAME_ID) + .keySerializer(Serializer.BYTE_ARRAY) + .createOrOpen(); + this.dbHeight = DBMaker.fileDB(filePathHeight).make(); + blocksHeightMap = (HTreeMap>) this.dbHeight.hashMap(MAP_NAME_HEIGHT) + .createOrOpen(); + } + + /** + * Checks existence of block on the database. + * + * @param blockId Identifier of block. + * @return true if a block with that identifier exists, false otherwise. + */ + @Override + public boolean has(Identifier blockId) { + boolean hasBoolean; + try { + lock.readLock().lock(); + hasBoolean = blocksIdMap.containsKey(blockId.getBytes()); + } finally { + lock.readLock().unlock(); + } + return hasBoolean; + } + + /** + * Adds block to the database. + * + * @param block given block to be added. + * @return true if block did not exist on the database, false if block is already in + * database. + */ + @Override + public boolean add(Block block) { + boolean addBooleanId; + Integer integer = block.getHeight(); + try { + lock.writeLock().lock(); + addBooleanId = blocksIdMap.putIfAbsentBoolean(block.id().getBytes(), block); + if (addBooleanId) { + blocksHeightMap.compute(integer, (key, value) -> { + final ArrayList newBlockArray; + if (value == null) { + newBlockArray = new ArrayList<>(); + } else { + newBlockArray = new ArrayList<>(value); + } + newBlockArray.add(block.id()); + return newBlockArray; + }); + } + } finally { + lock.writeLock().unlock(); + } + return addBooleanId; + } + + /** + * Removes block with given identifier. + * + * @param blockId identifier of the block. + * @return true if block exists on database and removed successfully, false if block does not exist on + * database. + */ + @Override + public boolean remove(Identifier blockId) { + boolean removeBoolean; + try { + lock.writeLock().lock(); + Block block = byId(blockId); + removeBoolean = blocksIdMap.remove(blockId.getBytes(), block); + if (removeBoolean) { + ArrayList identifierArrayList = blocksHeightMap.get(block.getHeight()); + if (identifierArrayList != null) { + identifierArrayList.remove(blockId); + } + } + } finally { + lock.writeLock().unlock(); + } + return removeBoolean; + } + + /** + * Returns the block with given identifier. + * t + * + * @param blockId identifier of the block. + * @return the block itself if exists and null otherwise. + */ + @Override + public Block byId(Identifier blockId) { + Block block; + try { + lock.readLock().lock(); + block = (Block) blocksIdMap.get(blockId.getBytes()); + } finally { + lock.readLock().unlock(); + } + return block; + } + + /** + * Returns the block with the given height. + * + * @param height height of the block. + * @return the block itself if exists and null otherwise. + */ + @Override + public Block atHeight(int height) { + Block block = null; + try { + lock.readLock().lock(); + ArrayList identifierArrayList = blocksHeightMap.get(height); + if (identifierArrayList != null) { + Identifier identifier = identifierArrayList.get(0); + block = byId(identifier); + } + } finally { + lock.readLock().unlock(); + } + return block; + } + + /** + * Returns all blocks stored in database. + * + * @return all stored blocks in database. + */ + @Override + public ArrayList all() { + ArrayList allBlocks = new ArrayList<>(); + for (Object block : blocksIdMap.values()) { + allBlocks.add((Block) block); + } + return allBlocks; + } + + /** + * Close the db. + */ + public void closeDb() { + dbId.close(); + dbHeight.close(); + } +} diff --git a/src/main/java/storage/mapdb/TransactionsMapDb.java b/src/main/java/storage/mapdb/TransactionsMapDb.java new file mode 100644 index 00000000..78135de1 --- /dev/null +++ b/src/main/java/storage/mapdb/TransactionsMapDb.java @@ -0,0 +1,125 @@ +package storage.mapdb; + +import java.util.ArrayList; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import model.lightchain.Identifier; +import model.lightchain.Transaction; +import org.mapdb.*; +import storage.Transactions; + +/** + * Implementation of Transactions interface. + */ +public class TransactionsMapDb implements Transactions { + private final DB db; + private final ReentrantReadWriteLock lock; + private static final String MAP_NAME = "transactions_map"; + private final HTreeMap transactionsMap; + + /** + * Creates TransactionsMapDb. + * + * @param filePath the path of the file. + */ + public TransactionsMapDb(String filePath) { + this.db = DBMaker.fileDB(filePath).make(); + this.lock = new ReentrantReadWriteLock(); + transactionsMap = this.db.hashMap(MAP_NAME) + .keySerializer(Serializer.BYTE_ARRAY) + .createOrOpen(); + } + + /** + * Checks existence of a transaction on the database. + * + * @param transactionId Identifier of transaction. + * @return true if a transaction with that identifier exists, false otherwise. + */ + @Override + public boolean has(Identifier transactionId) { + boolean hasBoolean; + try { + lock.readLock().lock(); + hasBoolean = transactionsMap.containsKey(transactionId.getBytes()); + } finally { + lock.readLock().unlock(); + } + return hasBoolean; + } + + /** + * Adds transaction to the database. + * + * @param transaction given transaction to be added. + * @return true if transaction did not exist on the database, false if transaction is already in + * database. + */ + @Override + public boolean add(Transaction transaction) { + boolean addBoolean; + try { + lock.writeLock().lock(); + addBoolean = transactionsMap.putIfAbsentBoolean(transaction.id().getBytes(), transaction); + } finally { + lock.writeLock().unlock(); + } + return addBoolean; + } + + /** + * Removes transaction with given identifier. + * + * @param transactionId identifier of the transaction. + * @return true if transaction exists on database and removed successfully, false if transaction does not exist on + * database. + */ + @Override + public boolean remove(Identifier transactionId) { + boolean removeBoolean; + try { + lock.writeLock().lock(); + Transaction transaction = get(transactionId); + removeBoolean = transactionsMap.remove(transactionId.getBytes(), transaction); + } finally { + lock.writeLock().unlock(); + } + return removeBoolean; + } + + /** + * Returns the transaction with given identifier. + * + * @param transactionId identifier of the transaction. + * @return the transaction itself if exists and null otherwise. + */ + @Override + public Transaction get(Identifier transactionId) { + + lock.readLock().lock(); + Transaction transaction = (Transaction) transactionsMap.get(transactionId.getBytes()); + lock.readLock().unlock(); + return transaction; + } + + /** + * Returns all transactions stored in database. + * + * @return all transactions stored tranin database. + */ + @Override + public ArrayList all() { + ArrayList allTransactions = new ArrayList<>(); + for (Object transaction : transactionsMap.values()) { + allTransactions.add((Transaction) transaction); + } + return allTransactions; + } + + /** + * It closes the database. + */ + public void closeDb() { + db.close(); + } +} diff --git a/src/test/java/storage/BlocksTest.java b/src/test/java/storage/BlocksTest.java index afcefb39..86d3b33a 100644 --- a/src/test/java/storage/BlocksTest.java +++ b/src/test/java/storage/BlocksTest.java @@ -1,36 +1,525 @@ package storage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import model.lightchain.Block; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.shaded.org.apache.commons.io.FileUtils; +import storage.mapdb.BlocksMapDb; +import unittest.fixtures.BlockFixture; + /** * Encapsulates tests for block database. */ public class BlocksTest { - // TODO: implement a unit test for each of the following scenarios: - // IMPORTANT NOTE: each test must have a separate instance of database, and the database MUST only created on a - // temporary directory. - // In following tests by a "new" block, we mean a block that already does not exist in the database, - // and by a "duplicate" block, we mean one that already exists in the database. - // 1. When adding 10 new blocks sequentially, the Add method must return true for all of them. Moreover, after - // adding blocks is done, querying the Has method for each of the block should return true. After adding all blocks - // are done, each block must be retrievable using both its id (byId) as well as its height (byHeight). Also, when - // querying All method, list of all 10 block must be returned. - // 2. Repeat test case 1 for concurrently adding blocks as well as concurrently querying the database for has, byId, - // and byHeight. - // 3. Add 10 new blocks sequentially, check that they are added correctly, i.e., while adding each block - // Add must return - // true, Has returns true for each of them, each block is retrievable by both its height and its identifier, - // and All returns list of all of them. Then Remove the first 5 blocks sequentially. - // While Removing each of them, the Remove should return true. Then query all 10 blocks using has, byId, - // and byHeight. - // Has should return false for the first 5 blocks have been removed, - // and byId and byHeight should return null. But for the last 5 blocks, has should return true, and byId - // and byHeight should successfully retrieve the exact block. Also, All should return only the last 5 blocks. - // 4. Repeat test case 3 for concurrently adding and removing blocks as well as concurrently querying the - // database for has, byId, and byHeight. - // 5. Add 10 new blocks and check that all of them are added correctly, i.e., while adding each block - // Add must return true, has returns true for each of them, and All returns list of all of them. Moreover, each - // block is retrievable using its identifier (byId) and height (byHeight). Then try Adding all of them again, and - // Add should return false for each of them, while has should still return true, and byId and byHeight should be - // able to retrieve the block. - // 6. Repeat test case 5 for concurrently adding blocks as well as concurrently querying the - // database for has, byId, and byHeight. + + private static final String TEMP_DIR = "tempdir"; + private static final String TEMP_FILE_ID = "tempfileID.db"; + private static final String TEMP_FILE_HEIGHT = "tempfileHEIGHT.db"; + private Path tempdir; + private ArrayList allBlocks; + private BlocksMapDb db; + + /** + * Initializes database. + * + * @throws IOException if creating temporary directory faces unhappy path. + */ + @BeforeEach + void setup() throws IOException { + Path currentRelativePath = Paths.get(""); + tempdir = Files.createTempDirectory(currentRelativePath, TEMP_DIR); + db = new BlocksMapDb(tempdir.toAbsolutePath() + "/" + TEMP_FILE_ID, + tempdir.toAbsolutePath() + "/" + TEMP_FILE_HEIGHT); + allBlocks = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + allBlocks.add(BlockFixture.newBlock()); + } + } + + /** + * Closes database. + * + * @throws IOException if deleting temporary directory faces unhappy path. + */ + @AfterEach + void cleanup() throws IOException { + db.closeDb(); + FileUtils.deleteDirectory(new File(tempdir.toString())); + } + + /** + * When adding 10 new blocks SEQUENTIALLY, the Add method must return true for all of them. Moreover, after + * adding blocks is done, querying the Has method for each of the block should return true. After adding all blocks + * are done, each block must be retrievable using both its id (byId) as well as its height (byHeight). Also, when + * querying All method, list of all 10 block must be returned. + */ + @Test + void sequentialAddTest() { + for (Block block : allBlocks) { + Assertions.assertTrue(db.add(block)); + } + for (Block block : allBlocks) { + Assertions.assertTrue(db.has(block.id())); + } + for (Block block : allBlocks) { + Assertions.assertEquals(block, db.atHeight(block.getHeight())); + Assertions.assertEquals(block.id(), db.atHeight(block.getHeight()).id()); + } + for (Block block : allBlocks) { + Assertions.assertEquals(block, db.byId(block.id())); + Assertions.assertEquals(block.id(), db.byId(block.id()).id()); + } + ArrayList all = db.all(); + Assertions.assertEquals(all.size(), 10); + for (Block block : all) { + Assertions.assertTrue(allBlocks.contains(block)); + } + } + + /** + * When adding 10 new blocks CONCURRENTLY, the Add method must return true for all of them. Moreover, after + * adding blocks is done, querying the Has method for each of the block should return true. After adding all blocks + * are done, each block must be retrievable using both its id (byId) as well as its height (byHeight). Also, when + * querying All method, list of all 10 block must be returned. + */ + @Test + void concurrentAddTest() { + /* + Adding all blocks concurrently. + */ + this.addAllBlocksConcurrently(true); + + /* + All blocks should be retrievable + */ + this.checkForHasConcurrently(0); + this.checkForByIdConcurrently(0); + this.checkForByHeightConcurrently(0); + this.checkForAllConcurrently(0); + } + + /** + * Add 10 new blocks SEQUENTIALLY, check that they are added correctly, i.e., while adding each block + * Add must return + * true, Has returns true for each of them, each block is retrievable by both its height and its identifier, + * and All returns list of all of them. Then Remove the first 5 blocks sequentially. + * While Removing each of them, the Remove should return true. Then query all 10 blocks using has, byId, + * and byHeight. + */ + @Test + void removeFirstFiveTest() { + for (Block block : allBlocks) { + Assertions.assertTrue(db.add(block)); + } + for (Block block : allBlocks) { + Assertions.assertTrue(db.has(block.id())); + } + for (Block block : allBlocks) { + Assertions.assertEquals(block, db.atHeight(block.getHeight())); + } + for (Block block : allBlocks) { + Assertions.assertEquals(block, db.byId(block.id())); + } + ArrayList all = db.all(); + Assertions.assertEquals(all.size(), 10); + for (Block block : all) { + Assertions.assertTrue(allBlocks.contains(block)); + } + for (int i = 0; i < 5; i++) { + Assertions.assertTrue(db.remove(allBlocks.get(i).id())); + } + for (int i = 0; i < 10; i++) { + Block block = allBlocks.get(i); + if (i < 5) { + Assertions.assertFalse(db.has(block.id())); + Assertions.assertFalse(db.all().contains(block)); + Assertions.assertNull(db.byId(block.id())); + } else { + Assertions.assertTrue(db.has(block.id())); + Assertions.assertTrue(db.all().contains(allBlocks.get(i))); + Assertions.assertEquals(block, db.atHeight(block.getHeight())); + Assertions.assertEquals(block, db.byId(allBlocks.get(i).id())); + } + } + } + + /** + * Add 10 new blocks SEQUENTIALLY, check that they are added correctly, i.e., while adding each block + * Add must return + * true, Has returns true for each of them, each block is retrievable by both its height and its identifier, + * and All returns list of all of them. Then Remove the first 5 blocks sequentially. + * While Removing each of them, the Remove should return true. Then query all 10 blocks using has, byId, + * and byHeight. + */ + @Test + void concurrentRemoveFirstFiveTest() { + + /* + Adding all blocks concurrently. + */ + this.addAllBlocksConcurrently(true); + + /* + All blocks should be retrievable using their id or height. + */ + this.checkForByIdConcurrently(0); + this.checkForByHeightConcurrently(0); + this.checkForHasConcurrently(0); + this.checkForAllConcurrently(0); + + /* + Removing first 5 concurrently + */ + this.removeBlocksTill(5); + + /* + first five blocks must not be retrievable, + the rest must be. + */ + this.checkForByHeightConcurrently(5); + this.checkForByIdConcurrently(5); + this.checkForHasConcurrently(5); + this.checkForAllConcurrently(5); + } + + /** + * Add 10 new blocks SEQUENTIALLY and check that all of them are added correctly, i.e., while adding each block + * Add must return true, has returns true for each of them, and All returns list of all of them. Moreover, each + * block is retrievable using its identifier (byId) and height (byHeight). + * Then try Adding all of them again, and Add should return false for each of them, + * while has should still return true, and byId and byHeight should be + * able to retrieve the block. + */ + @Test + void duplicationTest() { + for (Block block : allBlocks) { + Assertions.assertTrue(db.add(block)); + } + for (Block block : allBlocks) { + Assertions.assertTrue(db.has(block.id())); + } + + for (Block block : allBlocks) { + Assertions.assertTrue(allBlocks.contains(db.atHeight(block.getHeight()))); + } + for (Block block : allBlocks) { + Assertions.assertTrue(allBlocks.contains(db.byId(block.id()))); + } + ArrayList all = db.all(); + Assertions.assertEquals(all.size(), 10); + for (Block block : all) { + Assertions.assertTrue(allBlocks.contains(block)); + } + for (Block block : allBlocks) { + Assertions.assertFalse(db.add(block)); + } + /* + After trying duplication, check again. + */ + for (Block block : allBlocks) { + Assertions.assertTrue(db.has(block.id())); + } + + for (Block block : allBlocks) { + Assertions.assertTrue(allBlocks.contains(db.atHeight(block.getHeight()))); + } + for (Block block : allBlocks) { + Assertions.assertTrue(allBlocks.contains(db.byId(block.id()))); + } + } + + /** + * Add 10 new blocks CONCURRENTLY and check that all of them are added correctly, i.e., while adding each block + * Add must return true, has returns true for each of them, and All returns list of all of them. Moreover, each + * block is retrievable using its identifier (byId) and height (byHeight). + * Then try Adding all of them again, and Add should return false for each of them, + * while has should still return true, and byId and byHeight should be + * able to retrieve the block. + */ + @Test + void concurrentDuplicationTest() { + /* + Adding all blocks concurrently. + */ + this.addAllBlocksConcurrently(true); + + /* + All blocks should be retrievable using their id or height. + */ + this.checkForByIdConcurrently(0); + this.checkForByHeightConcurrently(0); + this.checkForHasConcurrently(0); + this.checkForAllConcurrently(0); + + /* + Adding all blocks again concurrently, all should fail due to duplication. + */ + this.addAllBlocksConcurrently(false); + + /* + Again, all blocks should be retrievable using their id or height. + */ + this.checkForByIdConcurrently(0); + this.checkForByHeightConcurrently(0); + this.checkForHasConcurrently(0); + this.checkForAllConcurrently(0); + } + + /** + * Removes blocks from blocks storage database till the given index concurrently. + * + * @param till exclusive index of the last block being removed. + */ + private void removeBlocksTill(int till) { + AtomicInteger threadError = new AtomicInteger(); + CountDownLatch doneRemove = new CountDownLatch(till); + Thread[] removeThreads = new Thread[till]; + for (int i = 0; i < till; i++) { + int finalI = i; + removeThreads[i] = new Thread(() -> { + if (!db.remove(allBlocks.get(finalI).id())) { + threadError.getAndIncrement(); + } + doneRemove.countDown(); + }); + } + + for (Thread t : removeThreads) { + t.start(); + } + try { + boolean doneOneTime = doneRemove.await(60, TimeUnit.SECONDS); + Assertions.assertTrue(doneOneTime); + } catch (InterruptedException e) { + Assertions.fail(); + } + Assertions.assertEquals(0, threadError.get()); + } + + /** + * Adds all blocks to the block storage database till the given index concurrently. + * + * @param expectedResult expected boolean result after each insertion; true means block added successfully, + * false means block was not added successfully. + */ + private void addAllBlocksConcurrently(boolean expectedResult) { + AtomicInteger threadError = new AtomicInteger(); + CountDownLatch addDone = new CountDownLatch(allBlocks.size()); + Thread[] addThreads = new Thread[allBlocks.size()]; + /* + Adding all blocks concurrently. + */ + for (int i = 0; i < allBlocks.size(); i++) { + int finalI = i; + addThreads[i] = new Thread(() -> { + if (db.add(allBlocks.get(finalI)) != expectedResult) { + threadError.getAndIncrement(); + } + addDone.countDown(); + }); + } + for (Thread t : addThreads) { + t.start(); + } + try { + boolean doneOneTime = addDone.await(60, TimeUnit.SECONDS); + Assertions.assertTrue(doneOneTime); + } catch (InterruptedException e) { + Assertions.fail(); + } + + Assertions.assertEquals(0, threadError.get()); + } + + /** + * Checks existence of blocks in the block storage database starting from the given index. + * + * @param from inclusive index of the first block to check. + */ + private void checkForHasConcurrently(int from) { + AtomicInteger threadError = new AtomicInteger(); + CountDownLatch hasDone = new CountDownLatch(allBlocks.size()); + Thread[] hasThreads = new Thread[allBlocks.size()]; + for (int i = 0; i < allBlocks.size(); i++) { + int finalI = i; + Block block = allBlocks.get(i); + + hasThreads[i] = new Thread(() -> { + if (finalI < from) { + // blocks should not exist + if (this.db.has(block.id())) { + threadError.incrementAndGet(); + } + } else { + // block should exist + if (!this.db.has(block.id())) { + threadError.getAndIncrement(); + } + } + + hasDone.countDown(); + }); + } + + for (Thread t : hasThreads) { + t.start(); + } + try { + boolean doneOneTime = hasDone.await(60, TimeUnit.SECONDS); + Assertions.assertTrue(doneOneTime); + } catch (InterruptedException e) { + Assertions.fail(); + } + + Assertions.assertEquals(0, threadError.get()); + } + + /** + * Checks retrievability of blocks from the block storage database by identifier starting from the given index. + * + * @param from inclusive index of the first block to check. + */ + private void checkForByIdConcurrently(int from) { + AtomicInteger threadError = new AtomicInteger(); + CountDownLatch getDone = new CountDownLatch(allBlocks.size()); + Thread[] getThreads = new Thread[allBlocks.size()]; + for (int i = 0; i < allBlocks.size(); i++) { + int finalI = i; + Block block = allBlocks.get(i); + + getThreads[i] = new Thread(() -> { + Block got = db.byId(block.id()); + if (finalI < from) { + // blocks should not exist + if (got != null) { + threadError.incrementAndGet(); + } + } else { + // block should be retrievable + if (!block.equals(got)) { + threadError.getAndIncrement(); + } + if (!block.id().equals(got.id())) { + threadError.getAndIncrement(); + } + } + getDone.countDown(); + }); + } + + for (Thread t : getThreads) { + t.start(); + } + try { + boolean doneOneTime = getDone.await(60, TimeUnit.SECONDS); + Assertions.assertTrue(doneOneTime); + } catch (InterruptedException e) { + Assertions.fail(); + } + + Assertions.assertEquals(0, threadError.get()); + } + + /** + * Checks retrievability of blocks from the block storage database by height starting from the given index. + * + * @param from inclusive index of the first block to check. + */ + private void checkForByHeightConcurrently(int from) { + AtomicInteger threadError = new AtomicInteger(); + CountDownLatch heightDone = new CountDownLatch(allBlocks.size()); + Thread[] heightThreats = new Thread[allBlocks.size()]; + for (int i = 0; i < allBlocks.size(); i++) { + final int finalI = i; + Block block = allBlocks.get(i); + + heightThreats[i] = new Thread(() -> { + Block got = db.atHeight(block.getHeight()); + if (finalI < from) { + // blocks should not exist + if (got != null) { + threadError.incrementAndGet(); + } + } else { + // block should be retrievable + if (!block.equals(got)) { + threadError.getAndIncrement(); + } + if (!block.id().equals(got.id())) { + threadError.getAndIncrement(); + } + } + + heightDone.countDown(); + }); + } + + for (Thread t : heightThreats) { + t.start(); + } + try { + boolean doneOneTime = heightDone.await(60, TimeUnit.SECONDS); + Assertions.assertTrue(doneOneTime); + } catch (InterruptedException e) { + Assertions.fail(); + } + + Assertions.assertEquals(0, threadError.get()); + } + + /** + * Checks retrievability of blocks from the block storage database starting from the given index. + * + * @param from inclusive index of the first block to check. + */ + private void checkForAllConcurrently(int from) { + AtomicInteger threadError = new AtomicInteger(); + CountDownLatch doneAll = new CountDownLatch(allBlocks.size()); + Thread[] allThreads = new Thread[allBlocks.size()]; + ArrayList all = db.all(); + + for (int i = 0; i < allBlocks.size(); i++) { + int finalI = i; + final Block block = allBlocks.get(i); + + allThreads[i] = new Thread(() -> { + if (finalI < from) { + // blocks should not exist + if (all.contains(block)) { + threadError.incrementAndGet(); + } + } else { + // block should exist + if (!all.contains(block)) { + threadError.getAndIncrement(); + } + } + doneAll.countDown(); + }); + } + + for (Thread t : allThreads) { + t.start(); + } + try { + boolean doneOneTime = doneAll.await(60, TimeUnit.SECONDS); + Assertions.assertTrue(doneOneTime); + } catch (InterruptedException e) { + Assertions.fail(); + } + + Assertions.assertEquals(0, threadError.get()); + } } diff --git a/src/test/java/storage/TransactionsTest.java b/src/test/java/storage/TransactionsTest.java index 8b2257d5..d88f321e 100644 --- a/src/test/java/storage/TransactionsTest.java +++ b/src/test/java/storage/TransactionsTest.java @@ -1,34 +1,453 @@ package storage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import model.lightchain.Transaction; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.shaded.org.apache.commons.io.FileUtils; +import storage.mapdb.TransactionsMapDb; +import unittest.fixtures.TransactionFixture; + /** * Encapsulates tests for transactions database. */ public class TransactionsTest { - // TODO: implement a unit test for each of the following scenarios: - // IMPORTANT NOTE: each test must have a separate instance of database, and the database MUST only created on a - // temporary directory. - // In following tests by a "new" transaction, we mean transaction that already does not exist in the database, - // and by a "duplicate" transaction, we mean one that already exists in the database. - // 1. When adding 10 new transactions sequentially, the Add method must return true for all of them. Moreover, after - // adding transactions is done, querying the Has method for each of the transaction should return true. Also, when - // querying All method, list of all 10 transactions must be returned. Moreover, all transactions should be - // retrievable through get method. - // 2. Repeat test case 1 for concurrently adding transactions as well as concurrently querying the database for has, - // and get. - // 3. Add 10 new transactions, check that they are added correctly, i.e., while adding each transactions - // Add must return true, Has returns true for each of them, and All returns list of all of them, and get must - // return the transaction. Then Remove the first - // 5 transactions. While Removing each of them, the Remove should return true. - // Then query all 10 transactions using Has. Has should return false for the first 5 transactions - // that have been removed, and get should return null for them. But for the last 5 transactions has - // should return true, and get should return the transaction. Also, All should return only the last 5 transactions. - // 4. Repeat test case 3 for concurrently adding and removing transactions as well as concurrently querying the - // database for has and get. - // 5. Add 10 new transactions and check that all of them are added correctly, i.e., while adding each transaction - // Add must return true, Has returns true for each of them, get should return the transaction, - // and All returns list of all of them. Then try Adding all of them again, and - // Add should return false for each of them, while has should still return true, and get should be able to - // able to retrieve the transaction. - // 6. Repeat test case 5 for concurrently adding transactions as well as concurrently querying the - // database for has, and get. + + private static final String TEMP_DIR = "tempdir"; + private static final String TEMP_FILE = "tempfile.db"; + private Path tempdir; + private ArrayList allTransactions; + private TransactionsMapDb db; + + /** + * Initializes database. + */ + @BeforeEach + void setup() throws IOException { + Path currentRelativePath = Paths.get(""); + tempdir = Files.createTempDirectory(currentRelativePath, TEMP_DIR); + db = new TransactionsMapDb(tempdir.toAbsolutePath() + "/" + TEMP_FILE); + allTransactions = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + allTransactions.add(TransactionFixture.newTransaction(10)); + } + } + + /** + * Closes database. + */ + @AfterEach + void cleanup() throws IOException { + db.closeDb(); + FileUtils.deleteDirectory(new File(tempdir.toString())); + } + + /** + * When adding 10 new transactions sequentially, the Add method must return true for all of them. Moreover, after + * adding transactions is done, querying the Has method for each of the transaction should return true. Also, when + * querying All method, list of all 10 transactions must be returned. Moreover, all transactions should be + * retrievable through get method. + */ + @Test + void sequentialAddTest() { + for (Transaction transaction : allTransactions) { + Assertions.assertTrue(db.add(transaction)); + } + for (Transaction transaction : allTransactions) { + Assertions.assertTrue(db.has(transaction.id())); + } + for (Transaction transaction : allTransactions) { + Assertions.assertEquals(transaction, db.get(transaction.id())); + } + ArrayList all = db.all(); + Assertions.assertEquals(all.size(), 10); + for (Transaction transaction : all) { + Assertions.assertTrue(allTransactions.contains(transaction)); + } + } + + /** + * When adding 10 new transactions concurrently, the Add method must return true for all of them. Moreover, after + * adding transactions is done, querying the Has method for each of the transaction should return true. Also, when + * querying All method, list of all 10 transactions must be returned. Moreover, all transactions should be + * retrievable through get method. + */ + @Test + void concurrentAddTest() { + /* + Adding all transactions concurrently. + */ + this.addAllTransactionsConcurrently(true); + + /* + All blocks should be retrievable + */ + this.checkHasConcurrently(0); + this.checkForGetConcurrently(0); + this.checkForAllConcurrently(0); + } + + /** + * Add 10 new transactions SEQUENTIALLY, check that they are added correctly, i.e., while adding each transaction + * Add must return + * true, Has returns true for each of them, each transaction is retrievable by identifier, + * and All returns list of all of them. Then Remove the first 5 transactions sequentially. + * While Removing each of them, the Remove should return true. Then query all 10 transactions using has, get. + */ + @Test + void removeFirstFiveTest() { + for (Transaction transaction : allTransactions) { + Assertions.assertTrue(db.add(transaction)); + } + for (Transaction transaction : allTransactions) { + Assertions.assertTrue(db.has(transaction.id())); + } + for (Transaction transaction : allTransactions) { + Assertions.assertEquals(transaction, db.get(transaction.id())); + } + + ArrayList all = db.all(); + Assertions.assertEquals(all.size(), 10); + for (Transaction transaction : all) { + Assertions.assertTrue(allTransactions.contains(transaction)); + } + + // removing first five + for (int i = 0; i < 5; i++) { + Assertions.assertTrue(db.remove(allTransactions.get(i).id())); + } + for (int i = 0; i < 10; i++) { + Transaction transaction = allTransactions.get(i); + if (i < 5) { + Assertions.assertFalse(db.has(transaction.id())); + Assertions.assertFalse(db.all().contains(transaction)); + Assertions.assertNull(db.get(transaction.id())); + } else { + Assertions.assertTrue(db.has(transaction.id())); + Assertions.assertTrue(db.all().contains(transaction)); + Assertions.assertEquals(transaction, db.get(transaction.id())); + } + } + } + + /** + * Add 10 new transactions CONCURRENTLY, check that they are added correctly, i.e., while adding each transaction + * Add must return + * true, Has returns true for each of them, each transaction is retrievable by identifier, + * and All returns list of all of them. Then Remove the first 5 transactions sequentially. + * While Removing each of them, the Remove should return true. Then query all 10 transactions using has, get. + */ + @Test + void concurrentRemoveFirstFiveTest() { + + /* + Adding all transactions concurrently. + */ + this.addAllTransactionsConcurrently(true); + + /* + All transactions should be retrievable using their id or height. + */ + this.checkForGetConcurrently(0); + this.checkHasConcurrently(0); + this.checkForAllConcurrently(0); + + /* + Removing first 5 concurrently + */ + this.removeBlocksTill(5); + + /* + first five transactions must not be retrievable, + the rest must be. + */ + this.checkForGetConcurrently(5); + this.checkHasConcurrently(5); + this.checkForAllConcurrently(5); + } + + /** + * Add 10 new transactions SEQUENTIALLY and check that all of them are added correctly, i.e., while adding + * each transaction + * Add must return true, has returns true for each of them, and All returns list of all of them. Moreover, each + * transaction is retrievable using get. + * Then try Adding all of them again, and Add should return false for each of them, + * while has should still return true, and get should be + * able to retrieve the transaction. + */ + @Test + void duplicationTest() { + for (Transaction transaction : allTransactions) { + Assertions.assertTrue(db.add(transaction)); + } + for (Transaction transaction : allTransactions) { + Assertions.assertTrue(db.has(transaction.id())); + } + for (Transaction transaction : allTransactions) { + Assertions.assertEquals(transaction, db.get(transaction.id())); + } + ArrayList all = db.all(); + Assertions.assertEquals(all.size(), 10); + for (Transaction transaction : all) { + Assertions.assertTrue(allTransactions.contains(transaction)); + } + for (Transaction transaction : allTransactions) { + Assertions.assertTrue(allTransactions.contains(db.get(transaction.id()))); + } + + // adding all again + for (Transaction transaction : allTransactions) { + Assertions.assertFalse(db.add(transaction)); + } + /* + After trying duplication, check again. + */ + for (Transaction transaction : allTransactions) { + Assertions.assertTrue(db.has(transaction.id())); + } + for (Transaction transaction : allTransactions) { + Assertions.assertTrue(allTransactions.contains(db.get(transaction.id()))); + } + } + + /** + * Add 10 new transactions CONCURRENTLY and check that all of them are added correctly, i.e., while adding + * each transaction + * Add must return true, has returns true for each of them, and All returns list of all of them. Moreover, each + * transaction is retrievable using get. + * Then try Adding all of them again, and Add should return false for each of them, + * while has should still return true, and get should be + * able to retrieve the transaction. + */ + @Test + void concurrentDuplicationTest() { + /* + Adding all transactions concurrently. + */ + this.addAllTransactionsConcurrently(true); + + /* + All transactions should be retrievable using their id or height. + */ + this.checkForGetConcurrently(0); + this.checkHasConcurrently(0); + this.checkForAllConcurrently(0); + + /* + Adding all transactions again concurrently, all should fail due to duplication. + */ + this.addAllTransactionsConcurrently(false); + + /* + Again, all transactions should be retrievable using their id or height. + */ + this.checkHasConcurrently(0); + this.checkForGetConcurrently(0); + this.checkForAllConcurrently(0); + } + + /** + * Adds all transactions to the transaction storage. + * + * @param expectedResult expected boolean result after each insertion; true means transaction added successfully, + * false means transaction was not added successfully. + */ + private void addAllTransactionsConcurrently(boolean expectedResult) { + AtomicInteger threadError = new AtomicInteger(); + CountDownLatch addDone = new CountDownLatch(allTransactions.size()); + Thread[] addThreads = new Thread[allTransactions.size()]; + /* + Adding all transactions concurrently. + */ + for (int i = 0; i < allTransactions.size(); i++) { + int finalI = i; + addThreads[i] = new Thread(() -> { + if (db.add(allTransactions.get(finalI)) != expectedResult) { + threadError.getAndIncrement(); + } + addDone.countDown(); + }); + } + for (Thread t : addThreads) { + t.start(); + } + try { + boolean doneOneTime = addDone.await(60, TimeUnit.SECONDS); + Assertions.assertTrue(doneOneTime); + } catch (InterruptedException e) { + Assertions.fail(); + } + Assertions.assertEquals(0, threadError.get()); + } + + /** + * Checks existence of transactions in the transactions storage database starting from the given index. + * + * @param from inclusive index of the first transactions to check. + */ + private void checkHasConcurrently(int from) { + AtomicInteger threadError = new AtomicInteger(); + CountDownLatch hasDone = new CountDownLatch(allTransactions.size()); + Thread[] hasThreads = new Thread[allTransactions.size()]; + for (int i = 0; i < allTransactions.size(); i++) { + int finalI = i; + Transaction transaction = allTransactions.get(i); + hasThreads[i] = new Thread(() -> { + + if (finalI < from) { + // transaction should not exist + if (this.db.has(transaction.id())) { + threadError.incrementAndGet(); + } + } else { + // transaction should exist + if (!this.db.has(transaction.id())) { + threadError.getAndIncrement(); + } + } + hasDone.countDown(); + }); + } + for (Thread t : hasThreads) { + t.start(); + } + try { + boolean doneOneTime = hasDone.await(60, TimeUnit.SECONDS); + Assertions.assertTrue(doneOneTime); + } catch (InterruptedException e) { + Assertions.fail(); + } + Assertions.assertEquals(0, threadError.get()); + } + + /** + * Checks retrievability of transactions from the transaction storage database starting from the given index. + * + * @param from inclusive index of the first transaction to check. + */ + private void checkForGetConcurrently(int from) { + AtomicInteger threadError = new AtomicInteger(); + CountDownLatch getDone = new CountDownLatch(allTransactions.size()); + Thread[] getThreads = new Thread[allTransactions.size()]; + + for (int i = 0; i < allTransactions.size(); i++) { + int finalI = i; + Transaction transaction = allTransactions.get(i); + getThreads[i] = new Thread(() -> { + Transaction got = db.get(transaction.id()); + if (finalI < from) { + // transaction should not exist + if (got != null) { + threadError.incrementAndGet(); + } + } else { + // transaction should exist + if (!transaction.equals(got)) { + threadError.getAndIncrement(); + } + if (!transaction.id().equals(got.id())) { + threadError.getAndIncrement(); + } + } + + getDone.countDown(); + }); + } + + for (Thread t : getThreads) { + t.start(); + } + try { + boolean doneOneTime = getDone.await(60, TimeUnit.SECONDS); + Assertions.assertTrue(doneOneTime); + } catch (InterruptedException e) { + Assertions.fail(); + } + Assertions.assertEquals(0, threadError.get()); + } + + /** + * Checks retrievability of transactions from the transaction storage database starting from the given index. + * + * @param from inclusive index of the first transaction to check. + */ + private void checkForAllConcurrently(int from) { + AtomicInteger threadError = new AtomicInteger(); + CountDownLatch doneAll = new CountDownLatch(allTransactions.size()); + Thread[] allThreads = new Thread[allTransactions.size()]; + ArrayList all = db.all(); + + for (int i = 0; i < allTransactions.size(); i++) { + int finalI = i; + final Transaction transaction = allTransactions.get(i); + allThreads[i] = new Thread(() -> { + if (finalI < from) { + // transaction should not exist + if (all.contains(transaction)) { + threadError.incrementAndGet(); + } + } else { + // transaction should exist + if (!all.contains(transaction)) { + threadError.getAndIncrement(); + } + } + doneAll.countDown(); + }); + } + + for (Thread t : allThreads) { + t.start(); + } + try { + boolean doneOneTime = doneAll.await(60, TimeUnit.SECONDS); + Assertions.assertTrue(doneOneTime); + } catch (InterruptedException e) { + Assertions.fail(); + } + Assertions.assertEquals(0, threadError.get()); + } + + /** + * Removes transactions from blocks transaction database till the given index concurrently. + * + * @param till exclusive index of the last transaction being removed. + */ + private void removeBlocksTill(int till) { + AtomicInteger threadError = new AtomicInteger(); + CountDownLatch doneRemove = new CountDownLatch(till); + Thread[] removeThreads = new Thread[till]; + + for (int i = 0; i < till; i++) { + int finalI = i; + removeThreads[i] = new Thread(() -> { + if (!db.remove(allTransactions.get(finalI).id())) { + threadError.getAndIncrement(); + } + doneRemove.countDown(); + }); + } + + for (Thread t : removeThreads) { + t.start(); + } + try { + boolean doneOneTime = doneRemove.await(60, TimeUnit.SECONDS); + Assertions.assertTrue(doneOneTime); + } catch (InterruptedException e) { + Assertions.fail(); + } + Assertions.assertEquals(0, threadError.get()); + } }