Commit 03691931 authored by Michel Schudel's avatar Michel Schudel

Big overhaul.

parent c1fe5910
......@@ -39,5 +39,6 @@ dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
compile 'commons-codec:commons-codec:1.10'
testCompile('org.springframework.boot:spring-boot-starter-test')
compile("org.springframework.boot:spring-boot-devtools")
compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.4'
}
......@@ -42,7 +42,20 @@ public class Block {
return transactions;
}
@Override
public String toString() {
return "Block{" +
"index=" + index +
", timestamp=" + timestamp +
", transactions=" + transactions +
", proof=" + proof +
", previousHash='" + previousHash + '\'' +
'}';
}
public String getPreviousHash() {
return previousHash;
}
}
......@@ -6,7 +6,6 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, isGetterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE)
public class BlockChain {
......@@ -34,14 +33,12 @@ public class BlockChain {
public long getIndexOfLastBlock() {
Optional<Block> block = getLastBlock();
return block
.map(block1 -> block1.getIndex() + 1)
.orElse(1L);
Block block = getLastBlock();
return block.getIndex();
}
public Optional<Block> getLastBlock() {
return chain.size() == 0 ? Optional.empty() : Optional.of(chain.get(chain.size() - 1));
public Block getLastBlock() {
return chain.size() == 0 ? null : chain.get(chain.size() - 1);
}
public long getBlockHeight() {
......@@ -71,4 +68,18 @@ public class BlockChain {
}
return true;
}
public boolean isNewBlockValid(Block newBlock) {
Block lastBlock = getLastBlock();
if (!newBlock.getPreviousHash().equals(HashUtil.hash(lastBlock))) {
return false;
}
if (!HashUtil.validProof(lastBlock.getProof(), newBlock.getProof())) {
return false;
}
return true;
}
}
package nl.craftsmen.blockchain.craftsmencoinnode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -25,6 +26,7 @@ public final class BlockChainRepository {
public BlockChain loadBlockChain() {
try {
File file = createBlockchainFileForThisNode();
LOGGER.info("trying to load blockchain for this node under filename {}", file.getName());
if (file.exists()) {
LOGGER.info("existing local blockchain found, loading...");
ObjectMapper objectMapper = new ObjectMapper();
......@@ -43,6 +45,7 @@ public final class BlockChainRepository {
try {
File file = createBlockchainFileForThisNode();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
LOGGER.info("saving blockchain");
objectMapper.writeValue(file, blockChain);
LOGGER.info("blockchain succesfully saved!");
......
......@@ -3,8 +3,10 @@ package nl.craftsmen.blockchain.craftsmencoinnode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
......@@ -15,12 +17,17 @@ public class BlockchainLogic {
private static final Logger LOGGER = LoggerFactory.getLogger(BlockchainLogic.class);
private static final BigDecimal CRAFTSCOIN_MINING_REWARD = BigDecimal.ONE;
private List<Transaction> currentTransactions = new ArrayList<>();
private BlockChain blockChain;
private Network network;
private BlockChainRepository blockChainRepository;
@Value("${miningWalletId}")
private String miningWalletId;
@Autowired
public BlockchainLogic(Network network, BlockChainRepository blockChainRepository) {
this.network = network;
......@@ -28,48 +35,52 @@ public class BlockchainLogic {
}
public BlockChain getBlockChain() {
@PostConstruct
public void init() {
network.connect();
initializeBlockchain();
resolveConflicts();
}
private void initializeBlockchain() {
this.blockChain = blockChainRepository.loadBlockChain();
if (this.blockChain == null) {
this.blockChain = blockChainRepository.loadBlockChain();
if (this.blockChain == null) {
this.blockChain = new BlockChain();
this.blockChain.createNewBlock(100, "1", this.currentTransactions);
blockChainRepository.saveBlockChain(this.blockChain);
}
this.blockChain = new BlockChain();
this.blockChain.createNewBlock(100, "1", this.currentTransactions);
blockChainRepository.saveBlockChain(this.blockChain);
}
return this.blockChain;
}
public BlockChain retrieveBlockChain() {
return getBlockChain();
return this.blockChain;
}
public long newTransaction(Transaction transaction) {
public long newTransaction(Transaction transaction, String sourcePeer) {
transaction.resolveTransactionId();
LOGGER.info("received new transaction: {}", transaction);
currentTransactions.add(transaction);
//notify the other nodes of this transaction
network.notifyNewTransaction(transaction);
return getBlockChain().getIndexOfLastBlock();
LOGGER.info("received transaction: {}", transaction);
if (!currentTransactions.contains(transaction)) {
LOGGER.info("transaction {} is new, adding it to the list of pending transactions.", transaction);
currentTransactions.add(transaction);
network.notifyNewTransaction(transaction, sourcePeer);
}
return this.blockChain.getIndexOfLastBlock();
}
public Block mine() {
long lastProof = getBlockChain().getLastBlock()
.get()
long lastProof = this.blockChain.getLastBlock()
.getProof();
long newProof = proofOfWork(lastProof);
//reward the miner
Transaction transaction = new Transaction("0", "this-node", BigDecimal.valueOf(1));
Transaction transaction = new Transaction("0", miningWalletId, CRAFTSCOIN_MINING_REWARD);
currentTransactions.add(transaction);
String previousHash = HashUtil.hash(getBlockChain().getLastBlock().get());
Block newBlock = getBlockChain().createNewBlock(newProof, previousHash, this.currentTransactions);
String previousHash = HashUtil.hash(this.blockChain.getLastBlock());
Block newBlock = this.blockChain.createNewBlock(newProof, previousHash, this.currentTransactions);
blockChainRepository.saveBlockChain(this.blockChain);
currentTransactions.clear();
network.notifyNewBlock(newBlock);
network.notifyNewBlock(newBlock, null);
return newBlock;
}
......@@ -86,26 +97,39 @@ public class BlockchainLogic {
public void resolveConflicts() {
List<BlockChain> otherChains = network.getBlockchains();
LOGGER.info("blockchains from peers received. Checking...");
for (BlockChain otherChain : otherChains) {
if (otherChain.getBlockHeight() > getBlockChain().getBlockHeight() && otherChain.isValid()) {
if (otherChain.getBlockHeight() > this.blockChain.getBlockHeight() && otherChain.isValid()) {
LOGGER.info("received a blockchain that is better than my currect chain. Replacing current chain.");
this.blockChain = otherChain;
blockChainRepository.saveBlockChain(blockChain);
break;
}
}
}
public void insertNewBlock(Block block) {
getBlockChain().addBlock(block);
blockChainRepository.saveBlockChain(blockChain);
public void notifyNewBlock(Block block, String sourcePeer) {
LOGGER.info("received new block: {}", block);
if (blockChain.isNewBlockValid(block)) {
LOGGER.info("block is valid, adding it to the blockchain.");
this.blockChain.addBlock(block);
this.network.notifyNewBlock(block, sourcePeer);
blockChainRepository.saveBlockChain(blockChain);
} else {
LOGGER.info("block is not valid with respect to my blockchain, discarding.");
}
}
public boolean isValid() {
return getBlockChain().isValid();
return this.blockChain.isValid();
}
public WalletDto getWallet(String walletName) {
BigDecimal balance = BigDecimal.ZERO;
List<Transaction> unconfirmedTransactions = new ArrayList<>();
List<Transaction> confirmedTransactions = getBlockChain().getTransactionsFor(walletName);
List<Transaction> confirmedTransactions = this.blockChain.getTransactionsFor(walletName);
for (Transaction transaction : confirmedTransactions) {
if (transaction.isRecepient(walletName)) {
balance = balance.add(transaction.getAmount());
......
......@@ -4,9 +4,9 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CraftsmenCoinApplication {
public class CraftsCoinApplication {
public static void main(String[] args) {
SpringApplication.run(CraftsmenCoinApplication.class, args);
SpringApplication.run(CraftsCoinApplication.class, args);
}
}
......@@ -4,9 +4,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Set;
@RestController
public class CraftsmenCoinRestController {
public class CraftsCoinRestController {
@Autowired
private BlockchainLogic blockchainLogic;
......@@ -16,8 +17,8 @@ public class CraftsmenCoinRestController {
private Network network;
@PostMapping("newtransaction")
public String newTransaction(@RequestBody Transaction transaction) {
long l = blockchainLogic.newTransaction(transaction);
public String newTransaction(@RequestHeader(value="peer", required = false) String peer, @RequestBody Transaction transaction) {
long l = blockchainLogic.newTransaction(transaction, peer);
return "{\"message\": \"new transaction will be added to block " + l + "\"}";
}
......@@ -27,9 +28,9 @@ public class CraftsmenCoinRestController {
}
@PostMapping("registernodes")
public void registerNode(@RequestBody String node) {
network.registerNewNode(node);
@PostMapping("registernode")
public Set<String> registerNode(@RequestBody String node) {
return network.registerNewNode(node);
}
@GetMapping("blockchain")
......@@ -42,8 +43,8 @@ public class CraftsmenCoinRestController {
return blockchainLogic.getPendingTransactions();
}
@PostMapping("newblock")
public void newBlock(@RequestBody Block block) {
blockchainLogic.insertNewBlock(block);
public void newBlock(@RequestHeader(value="peer", required = false) String peer, @RequestBody Block block) {
blockchainLogic.notifyNewBlock(block, peer);
}
@GetMapping("resolve")
......@@ -60,4 +61,9 @@ public class CraftsmenCoinRestController {
public WalletDto getWallet(@PathVariable String walletId) {
return blockchainLogic.getWallet(walletId);
}
@GetMapping("peers")
public Set<String> getPeers() {
return network.getPeers();
}
}
package nl.craftsmen.blockchain.craftsmencoinnode;
import org.springframework.boot.context.embedded.EmbeddedServletContainerInitializedEvent;
import org.springframework.context.ApplicationListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.net.Inet4Address;
import java.net.UnknownHostException;
@Component
public class InstanceInfo implements ApplicationListener<EmbeddedServletContainerInitializedEvent> {
public class InstanceInfo {
private static final Logger LOGGER = LoggerFactory.getLogger(InstanceInfo.class);
private ServerProperties serverProperties;
private String host;
private int port;
private String node;
private String hostnameNode;
@Autowired
public InstanceInfo(ServerProperties serverProperties) {
this.serverProperties = serverProperties;
}
@Override
public void onApplicationEvent(EmbeddedServletContainerInitializedEvent event) {
if (node == null) {
int thePort = event.getEmbeddedServletContainer().getPort();
try {
String host = Inet4Address
.getLocalHost()
.getHostAddress();
this.node = host + ":" + thePort;
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
@PostConstruct
public void init() {
try {
String host = Inet4Address
.getLocalHost()
.getHostAddress();
String hostName = Inet4Address
.getLocalHost().getHostName();
this.port = serverProperties.getPort();
this.node = host + ":" + serverProperties.getPort();
this.hostnameNode = hostName + ":" + serverProperties.getPort();
LOGGER.info("node name of this node is {}", node);
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
}
/**
* Gives the host and port the application is running on.
*
* @return the node address, for example: 192.168.1.250:8080
*/
public String getNode() {
return node;
}
public String getHostnameNode() {
return hostnameNode;
}
public int getPort() {
return port;
}
}
......@@ -3,14 +3,17 @@ package nl.craftsmen.blockchain.craftsmencoinnode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import java.util.stream.Collectors;
@Component
......@@ -18,56 +21,139 @@ public class Network {
private static final Logger LOGGER = LoggerFactory.getLogger(Network.class);
private String bootstrapNode = "localhost:8080";
@Value("${bootstrap.peer.host}")
private String bootstrapPeerHost;
private Set<String> nodes = new HashSet<>();
@Value("${bootstrap.peer.port}")
private int bootstrapPeerPort;
private RestTemplate restTemplate = new RestTemplate();
private Set<String> peers = new HashSet<>();
private RestTemplate restTemplate;
private InstanceInfo instanceInfo;
@Autowired
public Network(InstanceInfo instanceInfo) {
this.instanceInfo = instanceInfo;
restTemplate = new RestTemplate();
}
public void notifyNewTransaction(Transaction transaction) {
LOGGER.info("notifying the following nodes of transaction {}: {}", transaction, nodes);
for (String node : nodes) {
restTemplate.postForEntity(node + "/newtransaction", transaction, Object.class);
public void notifyNewTransaction(Transaction transaction, String sourePeer) {
Set<String> collect = peers.stream().filter(p -> !p.equals(sourePeer)).collect(Collectors.toSet());
LOGGER.info("notifying the following peers of transaction {}: {}", transaction, collect);
for (String node : collect) {
HttpEntity<Transaction> entity = new HttpEntity<>(transaction, createHttpHeaders());
post(node, "/newtransaction", entity, Object.class);
}
}
public void notifyNewBlock(Block newBlock) {
LOGGER.info("notifying the following nodes of new block {}: {}", newBlock, nodes);
for (String node : nodes) {
restTemplate.postForEntity(node + "newblock", newBlock, null);
private HttpHeaders createHttpHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("peer", instanceInfo.getNode());
return httpHeaders;
}
public void notifyNewBlock(Block newBlock, String sourcePeer) {
Set<String> collect = peers.stream().filter(p -> !p.equals(sourcePeer)).collect(Collectors.toSet());
LOGGER.info("notifying the following peers of new block {}: {}", newBlock, collect);
for (String node : peers.stream().filter(p -> !p.equals(sourcePeer)).collect(Collectors.toSet())) {
HttpEntity<Block> entity = new HttpEntity<>(newBlock, createHttpHeaders());
post(node, "newblock", entity, null);
}
}
public Set<String> registerNewNode(String newNode) {
if (!nodes.contains(newNode)) {
for (String node : nodes) {
ResponseEntity<Set> listResponseEntity = restTemplate.postForEntity(node + "registernode", newNode, Set.class, (Object) null);
Set<String> list = listResponseEntity.getBody();
nodes.addAll(list.stream().filter(e -> !e.equals(instanceInfo.getNode())).collect(Collectors.toSet()));
LOGGER.info("a new peer has connected to the network: {}", newNode);
if (!peers.contains(newNode)) {
LOGGER.info("peer {} is not previously known to this node. Peer registration will be forwarded to known peer: {}", newNode, peers);
peers.add(newNode);
for (String node : peers.stream().filter(p -> !p.equals(newNode)).collect(Collectors.toSet())) {
ResponseEntity<Set> listResponseEntity = post(node, "/registernode", newNode, Set.class);
if (listResponseEntity != null) {
Set<String> list = listResponseEntity.getBody();
peers.addAll(list.stream().filter(e -> !e.equals(instanceInfo.getNode())).collect(Collectors.toSet()));
}
}
nodes.add(newNode);
LOGGER.info("adding node {} to the list of known peers.", newNode);
} else {
LOGGER.info("peer {} is already known to this node. The node will not be forwarded to other peers.");
}
return nodes
Set<String> peersForRemote = peers
.stream()
.filter(e -> !e.equals(newNode))
.collect(Collectors.toSet());
peersForRemote.add(instanceInfo.getNode());
LOGGER.info("returning the following peers to the peer that just registered itself: {}", peersForRemote);
return peersForRemote;
}
public List<BlockChain> getBlockchains() {
List<BlockChain> blockchainList = new ArrayList<>();
LOGGER.info("asking the following nodes for their blockhain: {}", nodes);
for (String node : nodes) {
BlockChain otherChain = restTemplate.getForObject(node + "craftsmencoinnode", BlockChain.class);
LOGGER.info("asking the following peers for their blockhain: {}", peers);
for (String node : peers) {
BlockChain otherChain = restTemplate.getForObject("http://" + node + "/blockchain", BlockChain.class);
LOGGER.info("received blockchain from peer {}, adding it to the list.", node);
blockchainList.add(otherChain);
}
return blockchainList;
}
public void connect() {
if (peers.isEmpty()) {
LOGGER.info("no peers known yet, trying to register this node to the bootstrap peer: {}", createHostEndpoint());
try {
if (InetAddress.getByName(bootstrapPeerHost).isLoopbackAddress() && (instanceInfo.getPort() == 0 || instanceInfo.getPort() == bootstrapPeerPort)) {
LOGGER.info("This instance is the bootstrap node. It will not register as a peer to itself.");
} else {
registerThroughBootstrapNode();
}
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
} else {
LOGGER.info("registering this node to peers {}", peers);
for (String peer : peers) {
ResponseEntity<Set> listResponseEntity = post(peer, "/registernode", instanceInfo.getNode(), Set.class);
if (listResponseEntity != null) {
Set<String> list = listResponseEntity.getBody();
LOGGER.info("remote peer {} returned peer list: {}", peer, list);
peers.addAll(list);
}
}
}
}
private <T> ResponseEntity<T> post(String peer, String action, Object object, Class<T> clazz) {
try {
ResponseEntity<T> response = restTemplate.postForEntity("http://" + peer + action, object, clazz, (Object) null);
return response;
} catch (ResourceAccessException e) {
LOGGER.warn("peer {} not found, removing from peer list.", peer);
peers.remove(peer);
}
return null;
}
private String createHostEndpoint() {
return this.bootstrapPeerHost + ":" + bootstrapPeerPort;
}
private void registerThroughBootstrapNode() {
ResponseEntity<Set> listResponseEntity = post(createHostEndpoint(), "/registernode", instanceInfo.getNode(), Set.class);
if (listResponseEntity != null) {
Set<String> list = listResponseEntity.getBody();
LOGGER.info("received peer list: {}", list);
LOGGER.info("adding peers to the list of known peers: {}", list);
peers.addAll(list);
LOGGER.info("the following peers are now known to this node: {}", peers);
} else {
LOGGER.warn("bootstrap node not found. I'm operating this blockchain on my own right now!");
}
}
public Set<String> getPeers() {
return Collections.unmodifiableSet(peers);
}
}
......@@ -14,9 +14,9 @@ public class Transaction {
public Transaction() {
}
public Transaction(String to, String recepient, BigDecimal amount) {
public Transaction( String from, String to, BigDecimal amount) {
this.to = to;
this.from = recepient;
this.from = from;
this.amount = amount;
this.id = UUID.randomUUID();
}
......
server.port=8080
bootstrap.peer.host=localhost
bootstrap.peer.port=8080
miningWalletId=me
\ No newline at end of file
/ __ \ / _| | / __ \ (_)
| / \/_ __ __ _| |_| |_ ___ _ __ ___ ___ _ __ | / \/ ___ _ _ __
| | | '__/ _` | _| __/ __| '_ ` _ \ / _ \ '_ \| | / _ \| | '_ \
| \__/\ | | (_| | | | |_\__ \ | | | | | __/ | | | \__/\ (_) | | | | |
\____/_| \__,_|_| \__|___/_| |_| |_|\___|_| |_|\____/\___/|_|_| |_|
\ No newline at end of file
_____ __ _ _____ _
/ ____| / _| | / ____| (_)
| | _ __ __ _| |_| |_ ___| | ___ _ _ __
| | | '__/ _` | _| __/ __| | / _ \| | '_ \
| |____| | | (_| | | | |_\__ \ |___| (_) | | | | |
\_____|_| \__,_|_| \__|___/\_____\___/|_|_| |_|
\ No newline at end of file
......@@ -2,15 +2,30 @@ package nl.craftsmen.blockchain.craftsmencoinnode;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.net.Inet4Address;
import java.net.UnknownHostException;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class BlockchainLogicApplicationTests {
@Autowired
InstanceInfo instanceInfo;
@Test
public void contextLoads() {
public void nodeInformationIsAvailable() throws UnknownHostException {
String host = Inet4Address
.getLocalHost()
.getHostAddress();
//spring server properties aren't injected when running a springboot integration test so we check for 0.
assertThat(instanceInfo.getNode()).isEqualTo(host + ":" + 0);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment