Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added keys/privateKey
Binary file not shown.
Binary file added keys/publicKey
Binary file not shown.
59 changes: 59 additions & 0 deletions src/blockchain/Block.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package blockchain;

import blockchain.data.SignedData;
import java.io.Serializable;
import java.util.List;

public class Block<T extends SignedData & Serializable> implements Serializable {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good abstraction


private final Integer id;
private final Long minerId;
private final String hashPreviousBlock;
private final String hash;
private final Long timestamp;
private final Integer magicNumber;
private List<T> data = List.of();


public Block(Integer id, Long minerId, String hashPreviousBlock, String hash,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I don’t really like constructors with a lot of arguments (3-4+) of the same (String, String) or almost the same types (Integer, Long). But you use builder below which can be added inside this class to hide the constructor.

Long timestamp, Integer magicNumber) {
this.id = id;
this.minerId = minerId;
this.hashPreviousBlock = hashPreviousBlock;
this.hash = hash;
this.timestamp = timestamp;
this.magicNumber = magicNumber;
}

public Integer getId() {
return id;
}

public String getHashPreviousBlock() {
return hashPreviousBlock;
}

public String getHash() {
return hash;
}

public Long getTimestamp() {
return timestamp;
}

public Integer getMagicNumber() {
return magicNumber;
}

public Long getMinerId() {
return minerId;
}

public List<T> getData() {
return data;
}

public void setData(List<T> data) {
this.data = data;
}
}
48 changes: 48 additions & 0 deletions src/blockchain/BlockBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package blockchain;

import blockchain.data.SignedData;
import java.io.Serializable;

public class BlockBuilder<T extends SignedData & Serializable> {

private Integer id;
private Long minerId;
private String hashPreviousBlock;
private String hash;
private Long timestamp;
private Integer magicNumber;

public BlockBuilder<T> withId(Integer id) {
this.id = id;
return this;
}

public BlockBuilder<T> withMinerId(Long minerId) {
this.minerId = minerId;
return this;
}

public BlockBuilder<T> withHashPreviousBlock(String hashPreviousBlock) {
this.hashPreviousBlock = hashPreviousBlock;
return this;
}

public BlockBuilder<T> withHash(String hash) {
this.hash = hash;
return this;
}

public BlockBuilder<T> withTimestamp(Long timestamp) {
this.timestamp = timestamp;
return this;
}

public BlockBuilder<T> withMagicNumber(Integer magicNumber) {
this.magicNumber = magicNumber;
return this;
}

public Block<T> build() {
return new Block<>(id, minerId, hashPreviousBlock, hash, timestamp, magicNumber);
}
}
220 changes: 220 additions & 0 deletions src/blockchain/Blockchain.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package blockchain;

import blockchain.crypto.SignValidator;
import blockchain.data.SignedData;
import blockchain.data.format.DataFormatter;
import blockchain.io.Persister;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Blockchain<T extends SignedData & Serializable> {

private static final Long ACCEPTABLE_TIME = TimeUnit.SECONDS.toMillis(5);

private final ReentrantReadWriteLock readWritelock;

private final List<Block<T>> blocks;
private final List<T> pendingMessages;
private final Persister persister;

private int prefixLength;
private DataFormatter<T> dataFormatter;

private AtomicInteger idGenerator;

public Blockchain(Persister<T> persister, DataFormatter<T> dataFormatter) {
this.readWritelock = new ReentrantReadWriteLock();
this.persister = persister;
this.dataFormatter = dataFormatter;
this.prefixLength = 0;
List<Block<T>> fileBlocks = persister.load();
if (!fileBlocks.isEmpty() && validateAllChain(fileBlocks)) {
this.blocks = fileBlocks;
} else {
this.blocks = new LinkedList<>(Collections.singleton(
new BlockBuilder<T>()
.withId(1)
.withMinerId(0L)
.withHashPreviousBlock("")
.withHash("0")
.withMagicNumber(0)
.withTimestamp(new Date().getTime()).build()));
persister.save(fileBlocks);
}
pendingMessages = new LinkedList<>();
this.idGenerator = new AtomicInteger(blocks.get(blocks.size() - 1).getId() + 1);
}

public void accept(Block<T> newBlock) {
WriteLock writeLock = readWritelock.writeLock();
try {
writeLock.lock();
if (isValid(newBlock)) {
newBlock.setData(new ArrayList<>(pendingMessages));
outputAndAdjust(newBlock);
blocks.add(newBlock);
pendingMessages.clear();
persister.save(blocks);
}
} finally {
writeLock.unlock();
}
}

public boolean appendData(T data) {
WriteLock writeLock = readWritelock.writeLock();
try {
writeLock.lock();
if (isSigned(data) && dataIdentityValid(data)) {
return pendingMessages.add(data);
}
} finally {
writeLock.unlock();
}
return false;
}

public String getPrefix() {
ReadLock readLock = readWritelock.readLock();
try {
readLock.lock();
return Stream.iterate("0", x -> "0")
.limit(prefixLength)
.reduce("", (x, y) -> x + y);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a reduce-like function joining for strings. You may try to use it.

} finally {
readLock.unlock();
}
}

public Block<T> tail() {
ReadLock readLock = readWritelock.readLock();
try {
readLock.lock();
return blocks.get(blocks.size() - 1);
} finally {
readLock.unlock();
}
}

public Integer uniqueIdentity() {
return idGenerator.incrementAndGet();
}

private boolean isSigned(T data) {
try {
SignValidator signValidator = new SignValidator(data.publicKey());
return signValidator.verifySignature(data.raw(), data.dataSignature());
} catch (Exception e) {
return false;
}
}

private boolean isValid(Block newBlock) {
Block tailBlock = blocks.get(blocks.size() - 1);
if (!newBlock.getHash().startsWith(getPrefix()) || !Objects
.equals(newBlock.getHashPreviousBlock(), tailBlock.getHash())) {
return false;
}
return true;
}

private void outputAndAdjust(Block<T> newBlock) {
outputStats(newBlock);
adjustComplexity(newBlock);
}

private void outputStats(Block<T> newBlock) {
System.out.printf("Block:\n");
System.out.printf("Id: %s\n", newBlock.getId());
System.out.printf("Created by miner # %s\n", newBlock.getMinerId());
System.out.printf("Timestamp: %s\n", newBlock.getTimestamp());
System.out.printf("Magic number: %s\n", newBlock.getMagicNumber());
System.out.printf("Hash of the previous block:\n%s\n", newBlock.getHashPreviousBlock());
System.out.printf("Block data: \n%s\n", newBlock.getData().stream().map(
x -> dataFormatter.format(x)
).collect(Collectors.joining("\n")));
System.out.printf("Magic number: %s\n", newBlock.getMagicNumber());
System.out
.printf("Block was generating for: %s seconds\n", getGenerationTime(newBlock) / 1000);
}

private void adjustComplexity(Block<T> newBlock) {
if (!withinAcceptable(newBlock)) {
if (ACCEPTABLE_TIME < getGenerationTime(newBlock)) {
if (prefixLength > 0) {
prefixLength--;
}
System.out.printf("N was decreased to %s\n\n", prefixLength);
} else {
prefixLength++;
System.out.printf("N was increased to %s\n\n", prefixLength);
}
} else {
System.out.printf("N stays the same\n\n");
}
}

private Long getGenerationTime(Block<?> newBlock) {
return tail() == null ? 0 : (newBlock.getTimestamp() - tail().getTimestamp());
}

private boolean withinAcceptable(Block<?> newBlock) {
return Math.abs(getGenerationTime(newBlock) - ACCEPTABLE_TIME) <= 1000;
}

private boolean validateAllChain(List<Block<T>> blocks) {
for (int i = 1; i < blocks.size(); i++) {
Block<T> prev = blocks.get(i - 1);
Block<T> cur = blocks.get(i);
Stream<T> concatData = Stream.concat(
Objects.requireNonNullElse(prev.getData(), new ArrayList<T>()).stream(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Objects.requireNonNullElse(cur.getData(), new ArrayList<T>()).stream());
if (!Objects.equals(prev.getHash(), cur.getHashPreviousBlock())
&& dataIdentityValid(concatData.collect(Collectors.toList()))) {
return false;
}
}
return true;
}

private boolean dataIdentityValid(List<T> jointBlocks) {
for (int i = 1; i < jointBlocks.size(); i++) {
if (Objects.compare(jointBlocks.get(i - 1), jointBlocks.get(i),
Comparator.comparing(T::id)) > 0) {
return false;
}
}
return true;
}

private boolean dataIdentityValid(T data) {
Optional<T> lastEntry = getLastEntry();
return lastEntry
.map(t -> t.id() < data.id())
.orElse(true);
}

private Optional<T> getLastEntry() {
return blocks.stream()
.map(Block::getData)
.filter(List::isEmpty)
.flatMap(Collection::stream)
.limit(1)
.findFirst();
}
}
50 changes: 47 additions & 3 deletions src/blockchain/Main.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,51 @@
package blockchain;

import blockchain.crypto.Sign;
import blockchain.data.Message;
import blockchain.data.format.PlainMessageFormat;
import blockchain.io.FilePersister;
import blockchain.io.Messenger;
import blockchain.io.Persister;
import blockchain.miners.Miner;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Main {
public static void main(String[] args) {
System.out.print("Hello world!");
}

private static final String OUTPUT_FILE_NAME = "blockchain.ser";
private static final int NUMBER_OF_MINERS = 10;
private final static AtomicLong minersIdGenerator = new AtomicLong(1);

public static void main(String[] args) throws Exception {

Sign sign = new Sign("keys/privateKey", "keys/publicKey");

Persister<Message> persister = new FilePersister<>(OUTPUT_FILE_NAME);
Blockchain<Message> blockchain = new Blockchain<>(persister,
new PlainMessageFormat());

List<Thread> miners = Stream
.generate(() -> new Thread(new Miner<>(blockchain, minersIdGenerator.getAndIncrement())))
.limit(NUMBER_OF_MINERS)
.collect(Collectors.toList());

miners.forEach(Thread::start);

Messenger messenger = new Messenger(blockchain, sign);

Thread.sleep(1000);
messenger.sendMessage("Tom", "Hey, I'm first!");
messenger.sendMessage("Sarah", "It's not fair!");
Thread.sleep(2000);
messenger.sendMessage("Sarah", "You always will be first because it is your blockchain!");
Thread.sleep(1000);
messenger.sendMessage("Sarah", "Anyway, thank you for this amazing chat.");
Thread.sleep(2000);
messenger.sendMessage("Tom", "You're welcome :)");
messenger.sendMessage("Nick", "Hey Tom, nice chat");

miners.forEach(Thread::interrupt);
}
}
Loading