-
Notifications
You must be signed in to change notification settings - Fork 6
Part four #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Part four #8
Changes from all commits
fe8e821
ddac0ff
7078dff
ee39a82
849cbc3
7bc0f21
ae3b3ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package blockchain; | ||
|
|
||
| import java.io.Serializable; | ||
|
|
||
| public class Block implements Serializable { | ||
|
|
||
| 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 String data; | ||
|
|
||
|
|
||
| public Block(Integer id, Long minerId, String hashPreviousBlock, String hash, Long timestamp, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the constructor takes a lot of parameters. You may try to use the Builder pattern or combine some parameters together in a single entity |
||
| Integer magicNumber, String data) { | ||
| this.id = id; | ||
| this.minerId = minerId; | ||
| this.hashPreviousBlock = hashPreviousBlock; | ||
| this.hash = hash; | ||
| this.timestamp = timestamp; | ||
| this.magicNumber = magicNumber; | ||
| this.data = data; | ||
| } | ||
|
|
||
| public Integer getId() { | ||
| return id; | ||
| } | ||
|
|
||
| public String getHashPreviousBlock() { | ||
| return hashPreviousBlock; | ||
| } | ||
|
|
||
| public String getHash() { | ||
| return hash; | ||
| } | ||
|
|
||
| public Long getTimestamp() { | ||
| return timestamp; | ||
| } | ||
|
|
||
| public String getData() { | ||
| return data; | ||
| } | ||
|
|
||
| public void setData(String data) { | ||
| this.data = data; | ||
| } | ||
|
|
||
| public Integer getMagicNumber() { | ||
| return magicNumber; | ||
| } | ||
|
|
||
| public Long getMinerId() { | ||
| return minerId; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package blockchain; | ||
|
|
||
| public class BlockBuilder { | ||
|
|
||
| private Integer id; | ||
| private Long minerId; | ||
| private String hashPreviousBlock; | ||
| private String hash; | ||
| private Long timestamp; | ||
| private Integer magicNumber; | ||
| private String data; | ||
|
|
||
| public BlockBuilder withId(Integer id) { | ||
| this.id = id; | ||
| return this; | ||
| } | ||
|
|
||
| public BlockBuilder withMinerId(Long minerId) { | ||
| this.minerId = minerId; | ||
| return this; | ||
| } | ||
|
|
||
| public BlockBuilder withHashPreviousBlock(String hashPreviousBlock) { | ||
| this.hashPreviousBlock = hashPreviousBlock; | ||
| return this; | ||
| } | ||
|
|
||
| public BlockBuilder withHash(String hash) { | ||
| this.hash = hash; | ||
| return this; | ||
| } | ||
|
|
||
| public BlockBuilder withTimestamp(Long timestamp) { | ||
| this.timestamp = timestamp; | ||
| return this; | ||
| } | ||
|
|
||
| public BlockBuilder withMagicNumber(Integer magicNumber) { | ||
| this.magicNumber = magicNumber; | ||
| return this; | ||
| } | ||
|
|
||
| public BlockBuilder withData(String data) { | ||
| this.data = data; | ||
| return this; | ||
| } | ||
|
|
||
| public Block build() { | ||
| return new Block(id, minerId, hashPreviousBlock, hash, timestamp, magicNumber, data); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| package blockchain; | ||
|
|
||
| import blockchain.data.format.DataFormatter; | ||
| import blockchain.io.Persister; | ||
| import java.util.Collections; | ||
| import java.util.Date; | ||
| import java.util.LinkedList; | ||
| import java.util.List; | ||
| import java.util.Objects; | ||
| import java.util.StringJoiner; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.concurrent.locks.ReentrantReadWriteLock; | ||
| import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; | ||
| import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; | ||
| import java.util.stream.Stream; | ||
|
|
||
| public class Blockchain<T> { | ||
|
|
||
| private static final Long ACCEPTABLE_TIME = TimeUnit.SECONDS.toMillis(5); | ||
|
|
||
| private final ReentrantReadWriteLock readWritelock; | ||
|
|
||
| private final List<Block> blocks; | ||
| private final List<T> pendingMessages; | ||
| private final Persister persister; | ||
|
|
||
| private int prefixLength; | ||
| private DataFormatter<T> dataFormatter; | ||
|
|
||
| public Blockchain(Persister persister, DataFormatter<T> dataFormatter) { | ||
| this.readWritelock = new ReentrantReadWriteLock(); | ||
| this.persister = persister; | ||
| this.dataFormatter = dataFormatter; | ||
| this.prefixLength = 0; | ||
| List<Block> blocks = persister.load(); | ||
| if (!blocks.isEmpty() && validateAllChain(blocks)) { | ||
| this.blocks = blocks; | ||
| } else { | ||
| new BlockBuilder().build(); | ||
| this.blocks = new LinkedList<>(Collections.singleton( | ||
| new BlockBuilder() | ||
| .withId(1) | ||
| .withMinerId(0L) | ||
| .withHashPreviousBlock("") | ||
| .withHash("0") | ||
| .withMagicNumber(0) | ||
| .withTimestamp(new Date().getTime()).build())); | ||
| persister.save(blocks); | ||
| } | ||
| pendingMessages = new LinkedList<>(); | ||
| } | ||
|
|
||
| public void accept(Block newBlock) { | ||
| WriteLock writeLock = readWritelock.writeLock(); | ||
| try { | ||
| writeLock.lock(); | ||
| if (isValid(newBlock)) { | ||
| newBlock.setData(pendingData()); | ||
| outputAndAdjust(newBlock); | ||
| blocks.add(newBlock); | ||
| pendingMessages.clear(); | ||
| persister.save(blocks); | ||
| } | ||
| } finally { | ||
| writeLock.unlock(); | ||
| } | ||
| } | ||
|
|
||
| public void appendData(T data) { | ||
| WriteLock writeLock = readWritelock.writeLock(); | ||
| try { | ||
| writeLock.lock(); | ||
| pendingMessages.add(data); | ||
| } finally { | ||
| writeLock.unlock(); | ||
| } | ||
| } | ||
|
|
||
| public String getPrefix() { | ||
| ReadLock readLock = readWritelock.readLock(); | ||
| try { | ||
| readLock.lock(); | ||
| return Stream.iterate("0", x -> "0") | ||
| .limit(prefixLength) | ||
| .reduce("", (x, y) -> x + y); | ||
| } finally { | ||
| readLock.unlock(); | ||
| } | ||
| } | ||
|
|
||
| public String pendingData() { | ||
| ReadLock readLock = readWritelock.readLock(); | ||
| try { | ||
| readLock.lock(); | ||
| StringJoiner stringJoiner = new StringJoiner("\n"); | ||
| if (pendingMessages.isEmpty()) { | ||
| return "no messages"; | ||
| } | ||
| pendingMessages.stream() | ||
| .map(dataFormatter::format) | ||
| .forEach(stringJoiner::add); | ||
| return stringJoiner.toString(); | ||
| } finally { | ||
| readLock.unlock(); | ||
| } | ||
| } | ||
|
|
||
| public Block tail() { | ||
| ReadLock readLock = readWritelock.readLock(); | ||
| try { | ||
| readLock.lock(); | ||
| return blocks.get(blocks.size() - 1); | ||
| } finally { | ||
| readLock.unlock(); | ||
| } | ||
| } | ||
|
|
||
| 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 newBlock) { | ||
| outputStats(newBlock); | ||
| adjustComplexity(newBlock); | ||
| } | ||
|
|
||
| private void outputStats(Block 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()); | ||
| 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 newBlock) { | ||
| if (!withinAcceptable(newBlock)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is good idea to encapsulate the condition's logic in a separated method |
||
| 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> blocks) { | ||
| for (int i = 1; i < blocks.size(); i++) { | ||
| Block prev = blocks.get(i - 1); | ||
| Block cur = blocks.get(i); | ||
| if (!Objects.equals(prev.getHash(), cur.getHashPreviousBlock())) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,46 @@ | ||
| package blockchain; | ||
|
|
||
| import blockchain.data.Message; | ||
| import blockchain.data.format.PlanMessageFormat; | ||
| import blockchain.io.FilePersister; | ||
| 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 static AtomicLong ids = new AtomicLong(1); | ||
|
|
||
| public static void main(String[] args) throws InterruptedException { | ||
|
|
||
| Persister persister = new FilePersister(OUTPUT_FILE_NAME); | ||
| Blockchain<Message> blockchain = new Blockchain<>(persister, | ||
| new PlanMessageFormat()); | ||
|
|
||
| List<Thread> miners = Stream | ||
| .generate(() -> new Thread(new Miner(blockchain, ids.getAndIncrement()))) | ||
| .limit(NUMBER_OF_MINERS) | ||
| .collect(Collectors.toList()); | ||
|
|
||
| miners.forEach(Thread::start); | ||
|
|
||
| Thread.sleep(1000); | ||
| blockchain.appendData(new Message("Tom", "Hey, I'm first!")); | ||
| blockchain.appendData(new Message("Sarah", "It's not fair!")); | ||
| Thread.sleep(2000); | ||
| blockchain.appendData( | ||
| new Message("Sarah", "You always will be first because it is your blockchain!")); | ||
| Thread.sleep(1000); | ||
| blockchain.appendData(new Message("Sarah", "Anyway, thank you for this amazing chat.")); | ||
| Thread.sleep(2000); | ||
| blockchain.appendData(new Message("Tom", "You're welcome :)")); | ||
| blockchain.appendData(new Message("Nick", "Hey Tom, nice chat")); | ||
|
|
||
| miners.forEach(Thread::interrupt); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package blockchain.data; | ||
|
|
||
| public class Message { | ||
|
|
||
| private final String author; | ||
| private final String text; | ||
|
|
||
| public Message(String author, String text) { | ||
| this.author = author; | ||
| this.text = text; | ||
| } | ||
|
|
||
| public String getAuthor() { | ||
| return author; | ||
| } | ||
|
|
||
| public String getText() { | ||
| return text; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package blockchain.data.format; | ||
|
|
||
| public interface DataFormatter<T> { | ||
|
|
||
| String format(T message); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package blockchain.data.format; | ||
|
|
||
| import blockchain.data.Message; | ||
|
|
||
| public class PlanMessageFormat implements DataFormatter<Message> { | ||
|
|
||
| @Override | ||
| public String format(Message message) { | ||
| return String.format("%s: %s", message.getAuthor(), message.getText()); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code looks good and readable. Convenient separation into classes and methods.