diff --git a/keys/privateKey b/keys/privateKey new file mode 100644 index 0000000..d6b6bf7 Binary files /dev/null and b/keys/privateKey differ diff --git a/keys/publicKey b/keys/publicKey new file mode 100644 index 0000000..3d0ed97 Binary files /dev/null and b/keys/publicKey differ diff --git a/src/blockchain/Block.java b/src/blockchain/Block.java new file mode 100644 index 0000000..8a4a22c --- /dev/null +++ b/src/blockchain/Block.java @@ -0,0 +1,59 @@ +package blockchain; + +import blockchain.data.SignedData; +import java.io.Serializable; +import java.util.List; + +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 List data = List.of(); + + + public Block(Integer id, Long minerId, String hashPreviousBlock, String hash, + 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 getData() { + return data; + } + + public void setData(List data) { + this.data = data; + } +} diff --git a/src/blockchain/BlockBuilder.java b/src/blockchain/BlockBuilder.java new file mode 100644 index 0000000..d75fad7 --- /dev/null +++ b/src/blockchain/BlockBuilder.java @@ -0,0 +1,48 @@ +package blockchain; + +import blockchain.data.SignedData; +import java.io.Serializable; + +public class BlockBuilder { + + private Integer id; + private Long minerId; + private String hashPreviousBlock; + private String hash; + private Long timestamp; + private Integer magicNumber; + + 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 Block build() { + return new Block<>(id, minerId, hashPreviousBlock, hash, timestamp, magicNumber); + } +} \ No newline at end of file diff --git a/src/blockchain/Blockchain.java b/src/blockchain/Blockchain.java new file mode 100644 index 0000000..ea685ce --- /dev/null +++ b/src/blockchain/Blockchain.java @@ -0,0 +1,239 @@ +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.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class Blockchain { + + private static final Long ACCEPTABLE_TIME = TimeUnit.SECONDS.toMillis(5); + + private final ReentrantReadWriteLock readWritelock; + + private final List> blocks; + private final List pendingMessages; + private final Persister persister; + + private int prefixLength; + private DataFormatter dataFormatter; + + private AtomicInteger idGenerator; + private BiFunction> systemFeedback; + + public Blockchain(Persister persister, DataFormatter dataFormatter, + SystemFeedback systemFeedback) { + this.systemFeedback = systemFeedback; + this.readWritelock = new ReentrantReadWriteLock(); + this.persister = persister; + this.dataFormatter = dataFormatter; + this.prefixLength = 0; + List> fileBlocks = persister.load(); + if (!fileBlocks.isEmpty() && validateAllChain(fileBlocks)) { + this.blocks = fileBlocks; + } else { + this.blocks = new LinkedList<>(Collections.singleton( + new BlockBuilder() + .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 newBlock) { + WriteLock writeLock = readWritelock.writeLock(); + try { + writeLock.lock(); + if (isValid(newBlock)) { + applyFeedback(String.format("miner%d", newBlock.getMinerId())); + 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); + } finally { + readLock.unlock(); + } + } + + public Block tail() { + ReadLock readLock = readWritelock.readLock(); + try { + readLock.lock(); + return blocks.get(blocks.size() - 1); + } finally { + readLock.unlock(); + } + } + + public Integer uniqueIdentity() { + return idGenerator.incrementAndGet(); + } + + public Stream data() { + return new ArrayList<>(blocks).stream() + .map(Block::getData) + .filter(List::isEmpty) + .flatMap(Collection::stream); + } + + 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 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().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 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> blocks) { + for (int i = 1; i < blocks.size(); i++) { + Block prev = blocks.get(i - 1); + Block cur = blocks.get(i); + Stream concatData = Stream.concat( + Objects.requireNonNullElse(prev.getData(), new ArrayList()).stream(), + Objects.requireNonNullElse(cur.getData(), new ArrayList()).stream()); + if (!Objects.equals(prev.getHash(), cur.getHashPreviousBlock()) + && dataIdentityValid(concatData.collect(Collectors.toList()))) { + return false; + } + } + return true; + } + + private boolean dataIdentityValid(List 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 lastEntry = getLastEntry(); + return lastEntry + .map(t -> t.id() < data.id()) + .orElse(true); + } + + private Optional getLastEntry() { + return blocks.stream() + .map(Block::getData) + .filter(List::isEmpty) + .flatMap(Collection::stream) + .limit(1) + .findFirst(); + } + + private void applyFeedback(String miner) { + Optional feedback = systemFeedback.apply(uniqueIdentity(), miner); + feedback.ifPresent( + this::appendData + ); + } +} diff --git a/src/blockchain/Main.java b/src/blockchain/Main.java index adf1d02..22dc653 100644 --- a/src/blockchain/Main.java +++ b/src/blockchain/Main.java @@ -1,7 +1,59 @@ package blockchain; +import blockchain.crypto.Sign; +import blockchain.data.Transaction; +import blockchain.data.format.TransactionPrintFormat; +import blockchain.io.FilePersister; +import blockchain.io.Persister; +import blockchain.io.Wallet; +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 persister = new FilePersister<>(OUTPUT_FILE_NAME); + Blockchain blockchain = new Blockchain<>(persister, new TransactionPrintFormat(), + new PayForMining(sign)); + + List miners = Stream + .generate(() -> new Thread(new Miner<>(blockchain, minersIdGenerator.getAndIncrement()))) + .limit(NUMBER_OF_MINERS) + .collect(Collectors.toList()); + + miners.forEach(Thread::start); + + Wallet wallet = new Wallet(blockchain, sign); + + wallet.sendMoney("miner9", "miner1", 30); + wallet.sendMoney("miner9", "miner2", 30); + wallet.sendMoney("miner9", "Nick", 30); + Thread.sleep(2000); + wallet.sendMoney("miner9", "Bob", 10); + wallet.sendMoney("miner7", "Alice", 10); + wallet.sendMoney("Nick", "ShoesShop", 1); + wallet.sendMoney("Nick", "FastFood", 2); + wallet.sendMoney("Nick", "CarShop", 15); + wallet.sendMoney("miner7", "CarShop", 90); + Thread.sleep(3000); + wallet.sendMoney("CarShop", "Worker1", 10); + wallet.sendMoney("CarShop", "Worker2", 10); + wallet.sendMoney("CarShop", "Worker3", 10); + wallet.sendMoney("CarShop", "Director1", 30); + wallet.sendMoney("CarShop", "CarPartsShop", 45); + wallet.sendMoney("Bob", "GamingShop", 5); + wallet.sendMoney("Alice", "BeautyShop", 5); + + miners.forEach(Thread::interrupt); + } } \ No newline at end of file diff --git a/src/blockchain/PayForMining.java b/src/blockchain/PayForMining.java new file mode 100644 index 0000000..e27d50d --- /dev/null +++ b/src/blockchain/PayForMining.java @@ -0,0 +1,27 @@ +package blockchain; + +import blockchain.crypto.Sign; +import blockchain.data.Transaction; +import java.util.Optional; + +public class PayForMining implements SystemFeedback { + + private Sign sign; + + public PayForMining(Sign sign) { + + this.sign = sign; + } + + @Override + public Optional apply(Integer id, String x) { + try { + return Optional.of(new + Transaction(id, "System", x, 100.0, + sign.sign(String.format("%s,%s,%s", "System", x, 100.0) + id), + sign.getPublicKeyBytes())); + } catch (Exception e) { + return Optional.empty(); + } + } +} diff --git a/src/blockchain/SystemFeedback.java b/src/blockchain/SystemFeedback.java new file mode 100644 index 0000000..11e9375 --- /dev/null +++ b/src/blockchain/SystemFeedback.java @@ -0,0 +1,8 @@ +package blockchain; + +import java.util.Optional; +import java.util.function.BiFunction; + +public interface SystemFeedback extends BiFunction> { + +} diff --git a/src/blockchain/crypto/Sign.java b/src/blockchain/crypto/Sign.java new file mode 100644 index 0000000..e818899 --- /dev/null +++ b/src/blockchain/crypto/Sign.java @@ -0,0 +1,33 @@ +package blockchain.crypto; + +import java.io.File; +import java.nio.file.Files; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.spec.PKCS8EncodedKeySpec; + +public class Sign { + + private final byte[] publicKeyBytes; + private final PrivateKey privateKey; + + public Sign(String privateKeyPath, String publicKeyFilePath) throws Exception { + byte[] keyBytes = Files.readAllBytes(new File(privateKeyPath).toPath()); + this.publicKeyBytes = Files.readAllBytes(new File(publicKeyFilePath).toPath()); + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance("RSA"); + this.privateKey = kf.generatePrivate(spec); + } + + public byte[] sign(String text) throws Exception { + Signature rsa = Signature.getInstance("SHA1withRSA"); + rsa.initSign(privateKey); + rsa.update(text.getBytes()); + return rsa.sign(); + } + + public byte[] getPublicKeyBytes() { + return publicKeyBytes; + } +} diff --git a/src/blockchain/crypto/SignValidator.java b/src/blockchain/crypto/SignValidator.java new file mode 100644 index 0000000..832036e --- /dev/null +++ b/src/blockchain/crypto/SignValidator.java @@ -0,0 +1,30 @@ +package blockchain.crypto; + +import java.io.File; +import java.nio.file.Files; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.X509EncodedKeySpec; + +public class SignValidator { + + private PublicKey publicKey; + + public SignValidator(String publicKeyFilePath) throws Exception { + this(Files.readAllBytes(new File(publicKeyFilePath).toPath())); + } + + public SignValidator(byte[] publicKeyBytes) throws Exception { + X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKeyBytes); + KeyFactory kf = KeyFactory.getInstance("RSA"); + publicKey = kf.generatePublic(spec); + } + + public boolean verifySignature(byte[] data, byte[] signature) throws Exception { + Signature sig = Signature.getInstance("SHA1withRSA"); + sig.initVerify(publicKey); + sig.update(data); + return sig.verify(signature); + } +} diff --git a/src/blockchain/data/Message.java b/src/blockchain/data/Message.java new file mode 100644 index 0000000..e82ecc5 --- /dev/null +++ b/src/blockchain/data/Message.java @@ -0,0 +1,48 @@ +package blockchain.data; + +import java.io.Serializable; + +public class Message implements SignedData, Serializable { + + private final Integer id; + private final String author; + private final String text; + private final byte[] dataSignature; + private final byte[] publicKey; + + public Message(Integer id, String author, String text, byte[] dataSignature, byte[] publicKey) { + this.id = id; + this.author = author; + this.text = text; + this.dataSignature = dataSignature; + this.publicKey = publicKey; + } + + public String getAuthor() { + return author; + } + + public String getText() { + return text; + } + + @Override + public Integer id() { + return id; + } + + @Override + public byte[] raw() { + return (getText() + id()).getBytes(); + } + + @Override + public byte[] dataSignature() { + return dataSignature; + } + + @Override + public byte[] publicKey() { + return publicKey; + } +} diff --git a/src/blockchain/data/SignedData.java b/src/blockchain/data/SignedData.java new file mode 100644 index 0000000..8297fd2 --- /dev/null +++ b/src/blockchain/data/SignedData.java @@ -0,0 +1,12 @@ +package blockchain.data; + +public interface SignedData { + + Integer id(); + + byte [] raw(); + + byte[] dataSignature(); + + byte[] publicKey(); +} diff --git a/src/blockchain/data/Transaction.java b/src/blockchain/data/Transaction.java new file mode 100644 index 0000000..3573430 --- /dev/null +++ b/src/blockchain/data/Transaction.java @@ -0,0 +1,55 @@ +package blockchain.data; + +import java.io.Serializable; + +public class Transaction implements SignedData, Serializable { + + private Integer id; + private String from; + private String to; + private Double amount; + private byte[] dataSignature; + private byte[] publicKey; + + public Transaction(Integer id, String from, String to, Double amount, byte[] dataSignature, + byte[] publicKey) { + this.id = id; + this.from = from; + this.to = to; + this.amount = amount; + this.dataSignature = dataSignature; + this.publicKey = publicKey; + } + + @Override + public Integer id() { + return id; + } + + @Override + public byte[] raw() { + return (String.format("%s,%s,%s", from, to, amount) + id()).getBytes(); + } + + @Override + public byte[] dataSignature() { + return dataSignature; + } + + @Override + public byte[] publicKey() { + return publicKey; + } + + public String getFrom() { + return from; + } + + public String getTo() { + return to; + } + + public Double getAmount() { + return amount; + } +} diff --git a/src/blockchain/data/format/DataFormatter.java b/src/blockchain/data/format/DataFormatter.java new file mode 100644 index 0000000..0945a27 --- /dev/null +++ b/src/blockchain/data/format/DataFormatter.java @@ -0,0 +1,6 @@ +package blockchain.data.format; + +public interface DataFormatter { + + String format(T message); +} diff --git a/src/blockchain/data/format/PlainMessageFormat.java b/src/blockchain/data/format/PlainMessageFormat.java new file mode 100644 index 0000000..cfa7bf2 --- /dev/null +++ b/src/blockchain/data/format/PlainMessageFormat.java @@ -0,0 +1,11 @@ +package blockchain.data.format; + +import blockchain.data.Message; + +public class PlainMessageFormat implements DataFormatter { + + @Override + public String format(Message message) { + return String.format("%s: %s", message.getAuthor(), message.getText()); + } +} diff --git a/src/blockchain/data/format/TransactionPrintFormat.java b/src/blockchain/data/format/TransactionPrintFormat.java new file mode 100644 index 0000000..10537fe --- /dev/null +++ b/src/blockchain/data/format/TransactionPrintFormat.java @@ -0,0 +1,13 @@ +package blockchain.data.format; + +import blockchain.data.Transaction; + +public class TransactionPrintFormat implements DataFormatter { + + @Override + public String format(Transaction transaction) { + return String + .format("%s sent %s VC to %s", transaction.getFrom(), transaction.getAmount(), + transaction.getTo()); + } +} diff --git a/src/blockchain/io/FilePersister.java b/src/blockchain/io/FilePersister.java new file mode 100644 index 0000000..451f60b --- /dev/null +++ b/src/blockchain/io/FilePersister.java @@ -0,0 +1,49 @@ +package blockchain.io; + +import blockchain.Block; +import blockchain.data.SignedData; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +public class FilePersister implements Persister { + + private final String filename; + + public FilePersister(String filename) { + this.filename = filename; + } + + @Override + public void save(List> blockchain) { + try (FileOutputStream fileOutputStream = new FileOutputStream(filename); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) { + objectOutputStream.writeObject(blockchain); + } catch (IOException e) { + throw new RuntimeException(); + } + } + + @Override + public List> load() { + if (fileExists()) { + try (FileInputStream fileInputStream = new FileInputStream(filename); + ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) { + return (List>) objectInputStream.readObject(); + } catch (IOException | ClassNotFoundException e) { + return List.of(); + } + } + return List.of(); + } + + private boolean fileExists() { + return Files.exists(Paths.get(filename)); + } +} diff --git a/src/blockchain/io/Messenger.java b/src/blockchain/io/Messenger.java new file mode 100644 index 0000000..3976f75 --- /dev/null +++ b/src/blockchain/io/Messenger.java @@ -0,0 +1,29 @@ +package blockchain.io; + +import blockchain.Blockchain; +import blockchain.crypto.Sign; +import blockchain.data.Message; + +public class Messenger { + + private Blockchain blockchain; + private Sign sign; + + public Messenger(Blockchain blockchain, Sign sign) { + this.blockchain = blockchain; + this.sign = sign; + } + + public boolean sendMessage(String user, String text) { + try { + Integer id = blockchain.uniqueIdentity(); + Message message = new Message(id, user, text, sign.sign(text + id), + sign.getPublicKeyBytes()); + return blockchain.appendData(message); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Message was not sent"); + } + return false; + } +} diff --git a/src/blockchain/io/Persister.java b/src/blockchain/io/Persister.java new file mode 100644 index 0000000..de31cce --- /dev/null +++ b/src/blockchain/io/Persister.java @@ -0,0 +1,14 @@ +package blockchain.io; + +import blockchain.Block; +import blockchain.data.SignedData; +import java.io.Serializable; +import java.util.List; + +public interface Persister { + + void save(List> blockchain); + + List> load(); +} + diff --git a/src/blockchain/io/Wallet.java b/src/blockchain/io/Wallet.java new file mode 100644 index 0000000..a120533 --- /dev/null +++ b/src/blockchain/io/Wallet.java @@ -0,0 +1,49 @@ +package blockchain.io; + +import blockchain.Blockchain; +import blockchain.crypto.Sign; +import blockchain.data.Transaction; +import java.util.Objects; + +public class Wallet { + + private static final double INIT_SUM = 100.0; + + private Blockchain blockchain; + private Sign sign; + + public Wallet(Blockchain blockchain, Sign sign) { + this.blockchain = blockchain; + this.sign = sign; + } + + public boolean sendMoney(String from, String to, double amount) { + try { + if (!isEnoughMoney(from, amount)) { + return false; + } + Integer id = blockchain.uniqueIdentity(); + Transaction message = new Transaction(id, from, to, amount, + sign.sign(String.format("%s,%s,%s", + from, to, amount) + id), + sign.getPublicKeyBytes()); + return blockchain.appendData(message); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Message was not sent"); + } + return false; + } + + private boolean isEnoughMoney(String from, double amount) { + double sentSum = -blockchain.data().filter( + d -> Objects.equals(d.getFrom(), from)) + .mapToDouble(Transaction::getAmount) + .sum(); + double recvSum = blockchain.data().filter( + d -> Objects.equals(d.getTo(), from)) + .mapToDouble(Transaction::getAmount) + .sum(); + return INIT_SUM + sentSum + recvSum >= amount; + } +} diff --git a/src/blockchain/miners/Miner.java b/src/blockchain/miners/Miner.java new file mode 100644 index 0000000..76e3e43 --- /dev/null +++ b/src/blockchain/miners/Miner.java @@ -0,0 +1,54 @@ +package blockchain.miners; + +import blockchain.Block; +import blockchain.BlockBuilder; +import blockchain.Blockchain; +import blockchain.data.SignedData; +import blockchain.utils.StringUtil; +import java.io.Serializable; +import java.util.Date; +import java.util.Random; + +public class Miner implements Runnable { + + private final Blockchain blockchain; + private final Long id; + private final Random random; + + public Miner(Blockchain blockchain, Long id) { + this.blockchain = blockchain; + this.id = id; + this.random = new Random(); + } + + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + Block prev = blockchain.tail(); + blockchain.accept( + generateNextBlock(prev.getId() + 1, id, prev.getHash()) + ); + } + } + + private Block generateNextBlock(int blockId, long minerId, String previousHash) { + BlockBuilder blockBuilder = new BlockBuilder() + .withId(blockId) + .withMinerId(minerId) + .withHashPreviousBlock(previousHash); + + Integer magicNumber; + String hash; + String prefix; + do { + prefix = blockchain.getPrefix(); + magicNumber = random.nextInt(); + hash = StringUtil.applySha256(previousHash + magicNumber); + blockBuilder = blockBuilder.withHash(hash) + .withMagicNumber(magicNumber) + .withTimestamp(new Date().getTime()); + } while (!hash.startsWith(prefix)); + + return blockBuilder.build(); + } +} diff --git a/src/blockchain/utils/StringUtil.java b/src/blockchain/utils/StringUtil.java new file mode 100644 index 0000000..b883ebd --- /dev/null +++ b/src/blockchain/utils/StringUtil.java @@ -0,0 +1,28 @@ +package blockchain.utils; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +public class StringUtil { + + /** + * Applies Sha256 to a string and returns a hash. + */ + public static String applySha256(String input) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + for (byte elem : hash) { + String hex = Integer.toHexString(0xff & elem); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/crypto/KeyPairGen.java b/src/crypto/KeyPairGen.java new file mode 100644 index 0000000..1c730ec --- /dev/null +++ b/src/crypto/KeyPairGen.java @@ -0,0 +1,59 @@ +package crypto; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; + +public class KeyPairGen { + + private static final String PUBLIC_KEY_PATH = "keys/publicKey"; + private static final String PRIVATE_KEY_PATH = "keys/privateKey"; + + private final KeyPairGenerator keyGen; + + private PrivateKey privateKey; + private PublicKey publicKey; + + private KeyPairGen(int keyLen) throws NoSuchAlgorithmException { + this.keyGen = KeyPairGenerator.getInstance("RSA"); + this.keyGen.initialize(keyLen); + } + + + private KeyPairGen genPair() { + KeyPair pair = keyGen.generateKeyPair(); + privateKey = pair.getPrivate(); + publicKey = pair.getPublic(); + return this; + } + + private PrivateKey getPrivateKey() { + return privateKey; + } + + private PublicKey getPublicKey() { + return publicKey; + } + + public static void main(String[] args) throws Exception { + KeyPairGen gen = new KeyPairGen(1024) + .genPair(); + writeToFile(PUBLIC_KEY_PATH, gen.getPublicKey().getEncoded()); + writeToFile(PRIVATE_KEY_PATH, gen.getPrivateKey().getEncoded()); + } + + private static void writeToFile(String path, byte[] key) throws IOException { + Path keyPath = Paths.get(path); + Files.createDirectories(keyPath.getParent()); + try (OutputStream fos = Files.newOutputStream(keyPath)) { + fos.write(key); + } + } +}