Refactor serialization and added message validation
This commit is contained in:
@@ -1,18 +1,20 @@
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.IntStream;
|
||||
import model.BlockChain;
|
||||
import model.ChatClient;
|
||||
import util.ChatClient;
|
||||
import model.Miner;
|
||||
import util.FileManagement;
|
||||
import util.Security;
|
||||
|
||||
public final class Main {
|
||||
|
||||
public static void main(final String[] args) throws InterruptedException {
|
||||
public static void main(final String[] args) throws InterruptedException, IOException {
|
||||
|
||||
final var nThreads = Runtime.getRuntime().availableProcessors();
|
||||
final var blockChain = new BlockChain();
|
||||
blockChain.load();
|
||||
final var blockChain = BlockChain.getInstance();
|
||||
|
||||
// Cryptographic keys management
|
||||
final File publicKey = new File(Security.PUBLIC_KEY);
|
||||
@@ -24,15 +26,13 @@ public final class Main {
|
||||
|
||||
final var chatExecutor = Executors.newScheduledThreadPool(nThreads);
|
||||
|
||||
// Mocks 3 chat clients who send messages to the blockchain
|
||||
IntStream.range(0, 3)
|
||||
.mapToObj(chatClientId -> new ChatClient(chatClientId, blockChain))
|
||||
.forEach(e -> chatExecutor.scheduleAtFixedRate(e, 0, 100, TimeUnit.MILLISECONDS));
|
||||
// Mocks a chat client who send messages to the blockchain
|
||||
chatExecutor.scheduleAtFixedRate(new ChatClient(blockChain), 0, 200, TimeUnit.MILLISECONDS);
|
||||
|
||||
final var minerExecutor = Executors.newFixedThreadPool(nThreads);
|
||||
|
||||
// Creation of 10 miners
|
||||
IntStream.range(0, 10)
|
||||
// Creation of 5 miners
|
||||
IntStream.range(0, 5)
|
||||
.mapToObj(minerId -> new Miner(minerId, blockChain))
|
||||
.forEach(minerExecutor::submit);
|
||||
|
||||
@@ -48,8 +48,8 @@ public final class Main {
|
||||
chatExecutor.shutdownNow();
|
||||
}
|
||||
|
||||
blockChain.save();
|
||||
}
|
||||
FileManagement.saveBlockChain(blockChain);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.stream.Collectors;
|
||||
public class Block implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String previousBlockHash;
|
||||
private final String blockHash;
|
||||
private final int minerId;
|
||||
@@ -24,7 +25,7 @@ public class Block implements Serializable {
|
||||
final int id,
|
||||
final int magicNumber,
|
||||
final float generationSecs,
|
||||
final List<Message> chatMessages
|
||||
final List<Message> blockMessages
|
||||
) {
|
||||
this.previousBlockHash = previousBlockHash;
|
||||
this.blockHash = blockHash;
|
||||
@@ -32,7 +33,7 @@ public class Block implements Serializable {
|
||||
this.id = id;
|
||||
this.magicNumber = magicNumber;
|
||||
this.generationSecs = generationSecs;
|
||||
this.chatMessages = chatMessages;
|
||||
this.chatMessages = blockMessages;
|
||||
this.timeStamp = new Date().getTime();
|
||||
}
|
||||
|
||||
@@ -58,8 +59,8 @@ public class Block implements Serializable {
|
||||
}
|
||||
|
||||
public String messagesToString() {
|
||||
return chatMessages.stream()
|
||||
.map(m -> m.getText().concat("\n"))
|
||||
return chatMessages.isEmpty() ? "Empty block\n" : chatMessages.stream()
|
||||
.map(m -> String.valueOf(m.getId()).concat(" - ").concat(m.getText()).concat("\n"))
|
||||
.collect(Collectors.joining());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package model;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SignatureException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import util.FileManagement;
|
||||
import util.HashFunction;
|
||||
import util.Security;
|
||||
@@ -9,20 +16,42 @@ import java.util.stream.Collectors;
|
||||
|
||||
import static java.lang.String.valueOf;
|
||||
|
||||
public class BlockChain {
|
||||
public class BlockChain implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final float LOWER_LIMIT_SECS = 0.1F;
|
||||
private static final float UPPER_LIMIT_SECS = 0.5F;
|
||||
|
||||
private final Random random = new Random();
|
||||
private final List<Block> blockList = new LinkedList<>();
|
||||
private final List<Message> chatMessages = new ArrayList<>();
|
||||
private final List<Message> incomingChatMessages = new ArrayList<>();
|
||||
private final AtomicInteger lastMessageId = new AtomicInteger(1);
|
||||
|
||||
private int hashZeroes;
|
||||
private int magicNumber;
|
||||
private float generationSecs = 0;
|
||||
|
||||
public static BlockChain getInstance() {
|
||||
try {
|
||||
final BlockChain blockChain = (BlockChain) FileManagement.loadBlockChain();
|
||||
if (!blockChain.validateBlockchain()) {
|
||||
System.out.println("Blockchain not valid! Creating new one");
|
||||
return new BlockChain();
|
||||
} else {
|
||||
return blockChain;
|
||||
}
|
||||
} catch (final ClassNotFoundException | IOException e) {
|
||||
return new BlockChain();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void addBlock(final int minerId) {
|
||||
final int nextId = blockList.size();
|
||||
final String previousBlockHash = (nextId > 0) ? blockList.get(nextId - 1).getBlockHash() : "0";
|
||||
final String blockHash = calculateBlockHash(minerId);
|
||||
|
||||
incomingChatMessages.sort(Comparator.comparingInt(Message::getId));
|
||||
|
||||
final var block = new Block(
|
||||
previousBlockHash,
|
||||
blockHash,
|
||||
@@ -30,16 +59,16 @@ public class BlockChain {
|
||||
nextId,
|
||||
magicNumber,
|
||||
generationSecs,
|
||||
chatMessages
|
||||
incomingChatMessages
|
||||
);
|
||||
blockList.add(block);
|
||||
System.out.println(block);
|
||||
this.chatMessages.clear();
|
||||
incomingChatMessages.clear();
|
||||
|
||||
if (generationSecs < 1) {
|
||||
if (generationSecs < LOWER_LIMIT_SECS) {
|
||||
hashZeroes += 1;
|
||||
System.out.println("N was increased to " + hashZeroes +"\n");
|
||||
} else if (generationSecs > 10) {
|
||||
} else if (generationSecs > UPPER_LIMIT_SECS) {
|
||||
hashZeroes -= 1;
|
||||
System.out.println("N was decreased by 1\n");
|
||||
} else {
|
||||
@@ -48,42 +77,52 @@ public class BlockChain {
|
||||
|
||||
}
|
||||
|
||||
public void acceptMessage(Message message) {
|
||||
if (!blockList.isEmpty() || message.getId() > getLastMessageId()) {
|
||||
chatMessages.add(message);
|
||||
}
|
||||
public int getNextMessageId() {
|
||||
return lastMessageId.getAndIncrement();
|
||||
}
|
||||
|
||||
public int getLastMessageId() {
|
||||
return blockList.stream()
|
||||
.map(Block::getMessages)
|
||||
.flatMap(Collection::stream)
|
||||
.mapToInt(Message::getId)
|
||||
.max().orElse(0);
|
||||
public void acceptText(final int nextMessageId, final String name, final String text)
|
||||
throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, IOException, InvalidKeySpecException {
|
||||
incomingChatMessages.add(new Message(
|
||||
nextMessageId,
|
||||
name,
|
||||
text,
|
||||
Security.getPrivate(),
|
||||
Security.getPublic()
|
||||
));
|
||||
}
|
||||
|
||||
public void load() {
|
||||
FileManagement.loadBlockchain(blockList);
|
||||
}
|
||||
|
||||
public void save() {
|
||||
FileManagement.saveBlockchain(blockList);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
if (!validateBlockchain()) {
|
||||
System.out.println("Blockchain invalid!");
|
||||
}
|
||||
return blockList.stream().map(Block::toString).collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
private boolean validateBlockchain() {
|
||||
public boolean validateBlockchain() {
|
||||
if (blockList.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//TODO implement message validation
|
||||
// Check message security
|
||||
if (!blockList.stream()
|
||||
.map(Block::getMessages)
|
||||
.flatMap(Collection::stream)
|
||||
.allMatch(Security::messageIsValid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check messages Id ordering
|
||||
if (!blockList.stream()
|
||||
.map(Block::getMessages)
|
||||
.flatMap(Collection::stream)
|
||||
.map(Message::getId)
|
||||
.sorted()
|
||||
.collect(Collectors.toList())
|
||||
.equals(blockList.stream()
|
||||
.map(Block::getMessages)
|
||||
.flatMap(Collection::stream)
|
||||
.map(Message::getId)
|
||||
.collect(Collectors.toList())
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check block hashes ordering
|
||||
final List<String> previousHashes = blockList.stream()
|
||||
.map(Block::getPreviousBlockHash)
|
||||
.collect(Collectors.toList());
|
||||
@@ -100,11 +139,6 @@ public class BlockChain {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean messageIsValid(Message msg) {
|
||||
//TODO correct signature issues
|
||||
return Security.verifySignature(msg.getId() + msg.getText(), msg.getSignature(), msg.getPublicKey());
|
||||
}
|
||||
|
||||
private String calculateBlockHash(final int minerId) {
|
||||
final long start = System.currentTimeMillis();
|
||||
var hash = "";
|
||||
@@ -113,7 +147,7 @@ public class BlockChain {
|
||||
+ valueOf(new Date().getTime())
|
||||
+ calculateMagicNumber()
|
||||
);
|
||||
} while (!hash.matches("(?s)0{" + hashZeroes + "}([^0].*)?"));
|
||||
} while (!hash.startsWith("0".repeat(hashZeroes)));
|
||||
final long end = System.currentTimeMillis();
|
||||
generationSecs = (end - start) / 1000F;
|
||||
return hash;
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package model;
|
||||
|
||||
import util.Security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SignatureException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Random;
|
||||
|
||||
public class ChatClient implements Runnable {
|
||||
|
||||
private final int clientId;
|
||||
private final BlockChain blockChain;
|
||||
private static final Random random = new Random();
|
||||
|
||||
public ChatClient(final int chatClientId, final BlockChain blockChain) {
|
||||
this.clientId = chatClientId;
|
||||
this.blockChain = blockChain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
blockChain.acceptMessage(createMessage());
|
||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IOException | InvalidKeySpecException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private Message createMessage()
|
||||
throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, IOException, InvalidKeySpecException {
|
||||
return new Message(
|
||||
blockChain.getLastMessageId() + 1,
|
||||
clientId,
|
||||
generateRandomAlphabeticString(),
|
||||
Security.getPrivate(),
|
||||
Security.getPublic()
|
||||
);
|
||||
}
|
||||
|
||||
private String generateRandomAlphabeticString() {
|
||||
final int leftLimit = 97; // letter 'a'
|
||||
final int rightLimit = 122; // letter 'z'
|
||||
final int targetStringLength = 10;
|
||||
|
||||
return random.ints(leftLimit, rightLimit + 1)
|
||||
.limit(targetStringLength)
|
||||
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
|
||||
.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,15 +8,16 @@ import java.security.*;
|
||||
public class Message implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final int id;
|
||||
private final String text;
|
||||
private final byte[] signature;
|
||||
private final PublicKey publicKey;
|
||||
|
||||
public Message(final int id, final int clientId, final String text, final PrivateKey privateKey, final PublicKey publicKey)
|
||||
public Message(final int id, final String name, final String text, final PrivateKey privateKey, final PublicKey publicKey)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||
this.id = id;
|
||||
this.text = "Client " + clientId + " says: " + text;
|
||||
this.text = "Chatter " + name + " says: " + text;
|
||||
this.signature = Security.sign(text, privateKey);
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
53
src/util/ChatClient.java
Normal file
53
src/util/ChatClient.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package util;
|
||||
|
||||
import model.BlockChain;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SignatureException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Random;
|
||||
|
||||
public class ChatClient implements Runnable {
|
||||
|
||||
private static final Random random = new Random();
|
||||
|
||||
private final BlockChain blockChain;
|
||||
|
||||
public ChatClient(final BlockChain blockChain) {
|
||||
this.blockChain = blockChain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
blockChain.acceptText(blockChain.getNextMessageId(), generateRandomName(), generateRandomText());
|
||||
} catch (final NoSuchAlgorithmException | SignatureException | InvalidKeyException | IOException | InvalidKeySpecException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private String generateRandomName() {
|
||||
final int leftLimit = 97; // letter 'a'
|
||||
final int rightLimit = 122; // letter 'z'
|
||||
final int targetStringLength = 5;
|
||||
|
||||
return random.ints(leftLimit, rightLimit + 1)
|
||||
.limit(targetStringLength)
|
||||
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
|
||||
.toString();
|
||||
}
|
||||
|
||||
private String generateRandomText() {
|
||||
final int leftLimit = 97; // letter 'a'
|
||||
final int rightLimit = 122; // letter 'z'
|
||||
final int targetStringLength = 20;
|
||||
|
||||
return random.ints(leftLimit, rightLimit + 1)
|
||||
.limit(targetStringLength)
|
||||
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
|
||||
.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,43 +1,37 @@
|
||||
package util;
|
||||
|
||||
import model.Block;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.List;
|
||||
|
||||
public final class FileManagement {
|
||||
|
||||
private static final String BLOCKCHAIN = "blockChain.txt";
|
||||
|
||||
public static void saveBlockchain(final List<Block> blockList) {
|
||||
final var blockChainFile = new File(BLOCKCHAIN);
|
||||
try (final var fileOut = new FileOutputStream(blockChainFile);
|
||||
final var objectOut = new ObjectOutputStream(fileOut)) {
|
||||
for (final var block : blockList) {
|
||||
objectOut.writeObject(block);
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
private FileManagement() {
|
||||
throw new IllegalStateException("FileManagement class");
|
||||
}
|
||||
|
||||
public static void loadBlockchain(final List<Block> blockList) {
|
||||
final var blockChainFile = new File(BLOCKCHAIN);
|
||||
if (blockChainFile.exists()) {
|
||||
try (final var fileIn = new FileInputStream(blockChainFile);
|
||||
final var objectIn = new ObjectInputStream(fileIn)) {
|
||||
while (fileIn.available() > 0) {
|
||||
final var object = objectIn.readObject();
|
||||
blockList.add((Block) object);
|
||||
}
|
||||
} catch (final IOException | ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
public static void saveBlockChain(final Object obj) throws IOException {
|
||||
final FileOutputStream fos = new FileOutputStream(BLOCKCHAIN);
|
||||
final BufferedOutputStream bos = new BufferedOutputStream(fos);
|
||||
final ObjectOutputStream oos = new ObjectOutputStream(bos);
|
||||
oos.writeObject(obj);
|
||||
oos.close();
|
||||
}
|
||||
|
||||
public static Object loadBlockChain() throws IOException, ClassNotFoundException {
|
||||
final FileInputStream fis = new FileInputStream(BLOCKCHAIN);
|
||||
final BufferedInputStream bis = new BufferedInputStream(fis);
|
||||
final ObjectInputStream ois = new ObjectInputStream(bis);
|
||||
final Object obj = ois.readObject();
|
||||
ois.close();
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static void saveKey(final String path, final byte[] key) throws IOException {
|
||||
|
||||
@@ -4,6 +4,11 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
public final class HashFunction {
|
||||
|
||||
private HashFunction() {
|
||||
throw new IllegalStateException("HashFunction class");
|
||||
}
|
||||
|
||||
/* Applies Sha256 to a string and returns a hash. */
|
||||
public static String applySha256(final String input) throws RuntimeException {
|
||||
try {
|
||||
@@ -22,4 +27,5 @@ public final class HashFunction {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,12 +7,17 @@ import java.security.*;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import model.Message;
|
||||
|
||||
public final class Security {
|
||||
|
||||
public static final String PRIVATE_KEY = "KeyPair/privateKey";
|
||||
public static final String PUBLIC_KEY = "KeyPair/publicKey";
|
||||
|
||||
private Security() {
|
||||
throw new IllegalStateException("Security class");
|
||||
}
|
||||
|
||||
public static PublicKey getPublic()
|
||||
throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
|
||||
final byte[] keyBytes = Files.readAllBytes(new File(PUBLIC_KEY).toPath());
|
||||
@@ -37,6 +42,10 @@ public final class Security {
|
||||
return rsa.sign();
|
||||
}
|
||||
|
||||
public static boolean messageIsValid(final Message msg) {
|
||||
return Security.verifySignature(msg.getId() + msg.getText(), msg.getSignature(), msg.getPublicKey());
|
||||
}
|
||||
|
||||
public static boolean verifySignature(final String data, final byte[] signature, final PublicKey publicKey) {
|
||||
try {
|
||||
final Signature verifier = Signature.getInstance("SHA1withRSA");
|
||||
|
||||
Reference in New Issue
Block a user