diff --git a/node/src/main/java/nl/johannisk/node/controller/ApiController.java b/node/src/main/java/nl/johannisk/node/controller/ApiController.java index 033d5e2..558ac1b 100755 --- a/node/src/main/java/nl/johannisk/node/controller/ApiController.java +++ b/node/src/main/java/nl/johannisk/node/controller/ApiController.java @@ -28,8 +28,8 @@ public ApiController(final BlockChainService blockChainService) { @RequestMapping(path = "/chain") public List chain() { - BlockChain blockChain = blockChainService.getChain(); - List mainChain = new LinkedList<>(); + final BlockChain blockChain = blockChainService.getChain(); + final List mainChain = new LinkedList<>(); TreeNode b = blockChain.getEndBlock(); do { mainChain.add(b.getData()); diff --git a/node/src/main/java/nl/johannisk/node/controller/NodeController.java b/node/src/main/java/nl/johannisk/node/controller/NodeController.java index fda3008..33adfc1 100755 --- a/node/src/main/java/nl/johannisk/node/controller/NodeController.java +++ b/node/src/main/java/nl/johannisk/node/controller/NodeController.java @@ -1,6 +1,5 @@ package nl.johannisk.node.controller; -import com.netflix.discovery.EurekaClient; import nl.johannisk.node.service.BlockChainService; import nl.johannisk.node.service.model.Block; import nl.johannisk.node.service.model.Message; diff --git a/node/src/main/java/nl/johannisk/node/hasher/JChainHasher.java b/node/src/main/java/nl/johannisk/node/hasher/JChainHasher.java index dcb91a2..7c998dd 100644 --- a/node/src/main/java/nl/johannisk/node/hasher/JChainHasher.java +++ b/node/src/main/java/nl/johannisk/node/hasher/JChainHasher.java @@ -9,23 +9,26 @@ import java.util.Set; public class JChainHasher { - private JChainHasher() { - } public static String hash(final String parentHash, final Set content, final String nonce) { - StringBuilder b = new StringBuilder(); - b.append(parentHash); - b.append(content.toString()); - b.append(nonce); - String blockData = b.toString(); - MessageDigest messageDigest = null; + final MessageDigest messageDigest = getMessageDigest(); + final String blockData = new StringBuilder() + .append(parentHash) + .append(content.toString()) + .append(nonce) + .toString(); + messageDigest.update(blockData.getBytes()); + return new String(Base64.getEncoder().encode(messageDigest.digest()), StandardCharsets.UTF_8); + } + + private static MessageDigest getMessageDigest() { try { - messageDigest = MessageDigest.getInstance("SHA-256"); + return MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { /* - * This excpetion may never be thrown. + * This exception may never be thrown. * * Every implementation of the Java platform is required to support the following standard MessageDigest algorithms: * MD5 @@ -34,20 +37,19 @@ public static String hash(final String parentHash, final Set content, f */ throw new RuntimeException("Java platform does not support standard encryption", e); } - messageDigest.update(blockData.getBytes()); - return new String(Base64.getEncoder().encode(messageDigest.digest()), StandardCharsets.UTF_8); } public static boolean isValidHash(final String hash) { - String lowered = hash; - int j = lowered.indexOf('J'); - int c = lowered.indexOf('C'); - int o = lowered.indexOf('o'); - int r = lowered.indexOf('r'); - int e = lowered.indexOf('e'); - return ((j != -1 && c != -1 && o != -1 && r != -1 && e != -1) && (j < c && - c < o && - o < r && - r < e)); + if (null == hash) { + return false; + } + final int jIndex = hash.indexOf('J'); + final int cIndex = hash.indexOf('C'); + final int oIndex = hash.indexOf('o'); + final int rIndex = hash.indexOf('r'); + final int eIndex = hash.indexOf('e'); + final boolean didFindJcoreCharacters = jIndex != -1 && cIndex != -1 && oIndex != -1 && rIndex != -1 && eIndex != -1; + final boolean jcoreCharactersInCorrectOrder = jIndex < cIndex && cIndex < oIndex && oIndex < rIndex && rIndex < eIndex; + return didFindJcoreCharacters && jcoreCharactersInCorrectOrder; } } diff --git a/node/src/main/java/nl/johannisk/node/service/BlockChainService.java b/node/src/main/java/nl/johannisk/node/service/BlockChainService.java index 0faf69d..b668d3e 100644 --- a/node/src/main/java/nl/johannisk/node/service/BlockChainService.java +++ b/node/src/main/java/nl/johannisk/node/service/BlockChainService.java @@ -14,30 +14,33 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; -import java.util.*; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; import java.util.stream.Collectors; @Service public class BlockChainService { @Value("${eureka.instance.instanceId}") - String instanceId; + private String instanceId; - final Set unhandledMessages; - final Set handledMessages; - final BlockChain chain; - private BlockCreatorService blockCreatorService; + private final Set unhandledMessages; + private final Set handledMessages; + private final BlockChain chain; + private final BlockCreatorService blockCreatorService; private final EurekaClient eurekaClient; - Random random; + private final Random random; @Autowired public BlockChainService(final BlockCreatorService blockCreatorService, final EurekaClient eurekaClient) { this.blockCreatorService = blockCreatorService; this.eurekaClient = eurekaClient; - unhandledMessages = new HashSet<>(); - handledMessages = new HashSet<>(); - chain = new BlockChain(); - random = new Random(); + this.unhandledMessages = new HashSet<>(); + this.handledMessages = new HashSet<>(); + this.chain = new BlockChain(); + this.random = new Random(); } public BlockChain getChain() { @@ -52,7 +55,7 @@ public void addMessage(final Message m) { if (!handledMessages.contains(m) && !unhandledMessages.contains(m)) { unhandledMessages.add(m); if (unhandledMessages.size() >= 5 && blockCreatorService.state == BlockCreatorService.State.READY) { - Set blockContent = pickMessagesForPotentialBlock(); + final Set blockContent = pickMessagesForPotentialBlock(); blockCreatorService.createBlock(chain.getEndBlock().getData(), blockContent, this::addCreatedBlock); } @@ -60,9 +63,9 @@ public void addMessage(final Message m) { } public void addBlock(final Block block) { - String hash = JChainHasher.hash(block.getParentHash(), block.getContent(), block.getNonce()); + final String hash = JChainHasher.hash(block.getParentHash(), block.getContent(), block.getNonce()); if (JChainHasher.isValidHash(hash) && block.getHash().equals(hash) && !chain.containsBlock(block)) { - String lastBlockHash = chain.getEndBlock().getData().getHash(); + final String lastBlockHash = chain.getEndBlock().getData().getHash(); chain.addBlock(block); if (!chain.getEndBlock().getData().getHash().equals(lastBlockHash)) { if (blockCreatorService.state == BlockCreatorService.State.RUNNING) { @@ -70,19 +73,18 @@ public void addBlock(final Block block) { } resetMessagesAccordingToChain(); if (unhandledMessages.size() >= 5 && blockCreatorService.state == BlockCreatorService.State.READY) { - Set blockContent = pickMessagesForPotentialBlock(); + final Set blockContent = pickMessagesForPotentialBlock(); blockCreatorService.createBlock(chain.getEndBlock().getData(), blockContent, this::addCreatedBlock); } - } } } - public void addCreatedBlock(final Block block) { + private void addCreatedBlock(final Block block) { if (chain.getEndBlock().getData().getHash().equals(block.getParentHash())) { chain.addBlock(block); - Application application = eurekaClient.getApplication("jchain-node"); - List instanceInfo = application.getInstances(); + final Application application = eurekaClient.getApplication("jchain-node"); + final List instanceInfo = application.getInstances(); for (InstanceInfo info : instanceInfo) { if (info.getInstanceId().equals(instanceId)) { continue; @@ -105,7 +107,7 @@ private void resetMessagesAccordingToChain() { } private Set pickMessagesForPotentialBlock() { - Set messageForNextBlock = unhandledMessages.stream() + final Set messageForNextBlock = unhandledMessages.stream() .limit(5) .collect(Collectors.toSet()); unhandledMessages.removeAll(messageForNextBlock); @@ -115,13 +117,13 @@ private Set pickMessagesForPotentialBlock() { @Async private void informNodeOfNewBlock(final String host, final Block block) { - int delay = random.nextInt(10000) + 3000; + final int delay = random.nextInt(10000) + 3000; try { Thread.sleep(delay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } - RestTemplate restTemplate = new RestTemplate(); + final RestTemplate restTemplate = new RestTemplate(); restTemplate.postForObject("http://localhost:" + host + "/node/block", block, Block.class); } } diff --git a/node/src/main/java/nl/johannisk/node/service/BlockCreatorService.java b/node/src/main/java/nl/johannisk/node/service/BlockCreatorService.java index 43180d3..2464841 100644 --- a/node/src/main/java/nl/johannisk/node/service/BlockCreatorService.java +++ b/node/src/main/java/nl/johannisk/node/service/BlockCreatorService.java @@ -19,16 +19,16 @@ public enum State { CANCELLED } - final Random random; - State state; + private final Random random; + private State state; public BlockCreatorService() { - random = new Random(); - state = State.READY; + this.random = new Random(); + this.state = State.READY; } @Async - public void createBlock(Block parentBlock, Set messages, Consumer consumer) { + void createBlock(Block parentBlock, Set messages, Consumer consumer) { state = State.RUNNING; String hash; String parentHash = parentBlock.getHash(); @@ -40,9 +40,9 @@ public void createBlock(Block parentBlock, Set messages, Consumer(), "0"); } @@ -44,11 +46,11 @@ public String getNonce() { @Override public String toString() { - return "Block{" + - "hash='" + hash + '\'' + - ", parentHash='" + parentHash + '\'' + - ", contents=" + contents + - ", nonce='" + nonce + '\'' + - '}'; + return MoreObjects.toStringHelper(this) + .add("hash", hash) + .add("parentHash", parentHash) + .add("contents", contents) + .add("nonce", nonce) + .toString(); } } diff --git a/node/src/main/java/nl/johannisk/node/service/model/BlockChain.java b/node/src/main/java/nl/johannisk/node/service/model/BlockChain.java index cd948ed..7879742 100644 --- a/node/src/main/java/nl/johannisk/node/service/model/BlockChain.java +++ b/node/src/main/java/nl/johannisk/node/service/model/BlockChain.java @@ -21,18 +21,16 @@ public BlockChain() { maxDepth = 0; } - public boolean addBlock(final Block block) { + public void addBlock(final Block block) { if(blocks.containsKey(block.getParentHash())) { - TreeNode parentBlock = blocks.get(block.getParentHash()); - TreeNode newNode = parentBlock.addChild(block); + final TreeNode parentBlock = blocks.get(block.getParentHash()); + final TreeNode newNode = parentBlock.addChild(block); if (newNode.getDepth() > maxDepth) { maxDepth = newNode.getDepth(); endBlock = newNode; } blocks.put(block.getHash(), newNode); - return true; } - return false; } public boolean containsBlock(final Block b) { @@ -44,7 +42,7 @@ public TreeNode getEndBlock() { } public List getOrphanedBlocks() { - List blocksInChain = new ArrayList<>(); + final List blocksInChain = new ArrayList<>(); TreeNode b = endBlock; do { blocksInChain.add(b.getData()); diff --git a/node/src/main/java/nl/johannisk/node/service/model/Message.java b/node/src/main/java/nl/johannisk/node/service/model/Message.java index ef7bef1..b7341aa 100644 --- a/node/src/main/java/nl/johannisk/node/service/model/Message.java +++ b/node/src/main/java/nl/johannisk/node/service/model/Message.java @@ -1,10 +1,11 @@ package nl.johannisk.node.service.model; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; -public class Message implements Comparable{ - final int index; - final String text; +public final class Message implements Comparable { + private final int index; + private final String text; public Message(@JsonProperty("index") final int index, @JsonProperty("text") final String text) { this.index = index; @@ -21,10 +22,14 @@ public String getText() { @Override public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || !getClass().equals(o.getClass())) { + return false; + } - Message message = (Message) o; + final Message message = (Message) o; return index == message.index; } @@ -36,16 +41,16 @@ public int hashCode() { @Override public String toString() { - return "Message{" + - "index=" + index + - ", text='" + text + '\'' + - '}'; + return MoreObjects.toStringHelper(this) + .add("index", index) + .add("text", text) + .toString(); } @Override - public int compareTo(Object o) { - if(o instanceof Message) { - Message m = (Message)o; + public int compareTo(final Object o) { + if (o instanceof Message) { + Message m = (Message) o; return this.getIndex() - m.getIndex(); } return 0; diff --git a/node/src/main/java/nl/johannisk/node/service/model/TreeNode.java b/node/src/main/java/nl/johannisk/node/service/model/TreeNode.java index d188949..2e3decb 100644 --- a/node/src/main/java/nl/johannisk/node/service/model/TreeNode.java +++ b/node/src/main/java/nl/johannisk/node/service/model/TreeNode.java @@ -5,8 +5,8 @@ public class TreeNode { private final T data; - private final List children = new ArrayList<>(); - private final TreeNode parent; + private final List> children = new ArrayList<>(); + private final TreeNode parent; private final int depth; public TreeNode(final TreeNode parent, final T data, final int depth) { @@ -21,11 +21,7 @@ public TreeNode addChild(final T data) { return newChild; } - public List getChildren() { - return children; - } - - public TreeNode getParent() { + public TreeNode getParent() { return parent; } diff --git a/node/src/test/java/nl/johannisk/node/hasher/JChainHasherTest.java b/node/src/test/java/nl/johannisk/node/hasher/JChainHasherTest.java new file mode 100644 index 0000000..7bac4ad --- /dev/null +++ b/node/src/test/java/nl/johannisk/node/hasher/JChainHasherTest.java @@ -0,0 +1,51 @@ +package nl.johannisk.node.hasher; + +import com.google.common.collect.ImmutableSet; +import org.apache.commons.codec.binary.Base64; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +public class JChainHasherTest { + @Test + public void testIsValidHashWithInvalidHashes() { + assertFalse(JChainHasher.isValidHash(null)); + assertFalse(JChainHasher.isValidHash("")); + assertFalse(JChainHasher.isValidHash("foo")); + } + + @Test + public void testIsValidHashWithInvalidlyCasedHashes() { + assertFalse(JChainHasher.isValidHash("JCoer")); + assertFalse(JChainHasher.isValidHash("jcore")); + assertFalse(JChainHasher.isValidHash("JCoRe")); + } + + @Test + public void testIsValidHashWithInvalidlyOrderedCharacterHashes() { + assertFalse(JChainHasher.isValidHash("JCoe JCore")); + } + + @Test + public void testIsValidHashWithValidHashes() { + assertTrue(JChainHasher.isValidHash("JCore")); + assertTrue(JChainHasher.isValidHash("JCor Core")); + assertTrue(JChainHasher.isValidHash("JCor Core")); + assertTrue(JChainHasher.isValidHash("f456J7yhgtC567uhogfdr4567e8ui")); + } + + @Test + public void testHashProducesNonEmptyString() { + final String hash = JChainHasher.hash(null, ImmutableSet.of(), null); + assertNotNull(hash); + assertFalse(hash.isEmpty()); + } + + @Test + public void testHashProducesBase64() { + final String hash = JChainHasher.hash(null, ImmutableSet.of(), null); + assertNotNull(hash); + assertTrue(Base64.isBase64(hash)); + } +} diff --git a/node/src/test/java/nl/johannisk/node/service/model/BlockChainTest.java b/node/src/test/java/nl/johannisk/node/service/model/BlockChainTest.java new file mode 100644 index 0000000..d379f92 --- /dev/null +++ b/node/src/test/java/nl/johannisk/node/service/model/BlockChainTest.java @@ -0,0 +1,68 @@ +package nl.johannisk.node.service.model; + +import static nl.johannisk.node.service.model.Block.ZERO; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.TreeSet; + +public class BlockChainTest { + private BlockChain impl; + + @Before + public void setup() { + impl = new BlockChain(); + } + + @Test + public void testContainsBlock() { + /* Arrange */ + final String hash = "foo"; + final String parentHash = ZERO.getHash(); + final Block addBlock = new Block(hash, parentHash, new TreeSet<>(), null); + final Block checkBlock = new Block(hash, parentHash, new TreeSet<>(), null); + final Block checkBlockNotInChain = new Block("bar", parentHash, new TreeSet<>(), null); + + /* Act & Assert */ + impl.addBlock(addBlock); + assertTrue(impl.containsBlock(checkBlock)); + assertFalse(impl.containsBlock(checkBlockNotInChain)); + } + + @Test + public void testThatChainHasCorrectHead() { + /* Arrange */ + final String hash = "foo"; + final String parentHash = ZERO.getHash(); + final Block addBlock = new Block(hash, parentHash, new TreeSet<>(), null); + final Block addBlockNotInChain = new Block("bar", parentHash, new TreeSet<>(), null); + /* Act & Assert */ + impl.addBlock(addBlock); + impl.addBlock(addBlockNotInChain); + + assertEquals(addBlock, impl.getEndBlock().getData()); + } + + @Test + public void testGetEndAndOrphanedBlock() { + /* Arrange */ + final Block a = new Block("a", ZERO.getHash(), new TreeSet<>(), null); + final Block bWithParentA = new Block("b", "a", new TreeSet<>(), null); + final Block cWithParentB = new Block("c", "b", new TreeSet<>(), null); + final Block dWithParentA = new Block("d", "a", new TreeSet<>(), null); + + /* Act & Assert */ + impl.addBlock(a); + impl.addBlock(bWithParentA); + assertEquals(bWithParentA, impl.getEndBlock().getData()); + impl.addBlock(cWithParentB); + assertEquals(cWithParentB, impl.getEndBlock().getData()); + impl.addBlock(dWithParentA); + assertEquals(cWithParentB, impl.getEndBlock().getData()); + assertEquals(Arrays.asList(dWithParentA), impl.getOrphanedBlocks()); + } +} diff --git a/node/src/test/java/nl/johannisk/node/service/model/BlockTest.java b/node/src/test/java/nl/johannisk/node/service/model/BlockTest.java new file mode 100644 index 0000000..aacd58f --- /dev/null +++ b/node/src/test/java/nl/johannisk/node/service/model/BlockTest.java @@ -0,0 +1,40 @@ +package nl.johannisk.node.service.model; + +import org.junit.Before; +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.*; + +public class BlockTest { + + private Block subject; + private Set messageSet; + + @Before + public void setup() { + messageSet = new HashSet<>(); + messageSet.add(new Message(1, "message")); + subject = new Block("hash", "parentHash", messageSet, "nonce"); + } + + @Test + public void testThatBlockReturnsCorrectValues() { + assertEquals("hash", subject.getHash()); + assertEquals("parentHash", subject.getParentHash()); + assertEquals(messageSet, subject.getContent()); + assertEquals("nonce", subject.getNonce()); + } + + @Test + public void testThatBlockIsImmutable() { + assertNotSame(messageSet, subject.getContent()); + } + + @Test + public void testThatToStringIsCorrect() { + assertEquals("Block{hash=hash, parentHash=parentHash, contents=[Message{index=1, text=message}], nonce=nonce}", subject.toString()); + } +} \ No newline at end of file diff --git a/node/src/test/java/nl/johannisk/node/service/model/MessageTest.java b/node/src/test/java/nl/johannisk/node/service/model/MessageTest.java new file mode 100644 index 0000000..5c0ac46 --- /dev/null +++ b/node/src/test/java/nl/johannisk/node/service/model/MessageTest.java @@ -0,0 +1,43 @@ +package nl.johannisk.node.service.model; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class MessageTest { + private Message subject; + + @Before + public void setup() { + subject = new Message(1, "test"); + } + + @Test + public void testThatMessageReturnsCorrectValues() { + assertEquals(1, subject.getIndex()); + assertEquals("test", subject.getText()); + } + + @Test + public void equalsContract() { + EqualsVerifier + .forClass(Message.class) + .withIgnoredFields("text") + .verify(); + } + + @Test + public void comparableContract() { + assertEquals(0, subject.compareTo(new Object())); + assertEquals(0, subject.compareTo(new Message(1, "test2"))); + assertTrue(subject.compareTo(new Message(0,"smallerMessage")) > 0); + assertTrue(subject.compareTo(new Message(2,"biggerMessage")) < 0); + } + + @Test + public void testThatToStringIsCorrect() { + assertEquals("Message{index=1, text=test}", subject.toString()); + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index e4cbc4d..166acf9 100755 --- a/pom.xml +++ b/pom.xml @@ -27,4 +27,26 @@ UTF-8 1.8 + + + + nl.jqno.equalsverifier + equalsverifier + 2.3.3 + test + + + + + + + org.pitest + pitest-maven + 1.2.4 + + false + + + + \ No newline at end of file