Commit 60f9dbc7 authored by Schudel, MJ (Michel)'s avatar Schudel, MJ (Michel)

Generified repository functionality.

parent 0b79983f
package nl.craftsmen.blockchain.craftscoinnode.blockchain;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import nl.craftsmen.blockchain.craftscoinnode.util.InstanceInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
/**
* Persistence functionality for the blockchain.
*/
@Component
final class BlockchainRepository {
private final InstanceInfo instanceInfo;
private static final Logger LOGGER = LoggerFactory.getLogger(BlockchainRepository.class);
@Autowired
BlockchainRepository(InstanceInfo instanceInfo) {
this.instanceInfo = instanceInfo;
}
/**
* attempts the load the blockchain from disk.
* @return the blockchain, or null if no blockchain was found on disk.
*/
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();
Blockchain blockchain = objectMapper.readValue(file, Blockchain.class);
LOGGER.info("blockchain succesfully loaded!");
return blockchain;
} else {
return null;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Saves the blockchain to disk.
* @param blockchain the blockchain.
*/
void saveBlockChain(Blockchain blockchain) {
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!");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private File createBlockchainFileForThisNode() {
return new File(System.getProperty("user.dir"), createBlockChainFileName(instanceInfo.getNode()));
}
private String createBlockChainFileName(String node) {
return node.replace(".", "").replace(":", "") + "-blockchain.json";
}
}
......@@ -5,6 +5,7 @@ import nl.craftsmen.blockchain.craftscoinnode.network.Network;
import nl.craftsmen.blockchain.craftscoinnode.transaction.SignatureService;
import nl.craftsmen.blockchain.craftscoinnode.transaction.Transaction;
import nl.craftsmen.blockchain.craftscoinnode.transaction.TransactionPool;
import nl.craftsmen.blockchain.craftscoinnode.util.GenericRepository;
import nl.craftsmen.blockchain.craftscoinnode.util.InstanceInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -14,6 +15,7 @@ import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
......@@ -28,7 +30,7 @@ public class BlockchainService {
private final Network network;
private final SignatureService signatureService;
private final BlockchainRepository blockchainRepository;
private final GenericRepository genericRepository;
private final TransactionPool transactionPool;
private final InstanceInfo instanceInfo;
private final String miningWalletId;
......@@ -37,10 +39,10 @@ public class BlockchainService {
@Autowired
public BlockchainService(Network network, SignatureService signatureService, BlockchainRepository blockchainRepository, TransactionPool transactionPool, InstanceInfo instanceInfo, CraftsCoinConfigurationProperties configuration) {
public BlockchainService(Network network, SignatureService signatureService, GenericRepository genericRepository, TransactionPool transactionPool, InstanceInfo instanceInfo, CraftsCoinConfigurationProperties configuration) {
this.network = network;
this.signatureService = signatureService;
this.blockchainRepository = blockchainRepository;
this.genericRepository = genericRepository;
this.transactionPool = transactionPool;
this.instanceInfo = instanceInfo;
this.miningWalletId = configuration.getMiningWalletId();
......@@ -58,10 +60,10 @@ public class BlockchainService {
private void initializeBlockchain() {
if (this.blockchain == null) {
this.blockchain = blockchainRepository.loadBlockChain();
if (this.blockchain == null) {
Optional<Blockchain> blockchainOptional = genericRepository.load(Blockchain.class);
if (!blockchainOptional.isPresent()) {
this.blockchain = Blockchain.create();
blockchainRepository.saveBlockChain(this.blockchain);
genericRepository.save(this.blockchain);
}
}
}
......@@ -115,7 +117,7 @@ public class BlockchainService {
Transaction transaction = new Transaction("0", miningWalletId, CRAFTSCOIN_MINING_REWARD, signature, signatureService.getPublicKey());
transactionPool.addTransaction(transaction);
Block newBlock = this.blockchain.mineNewBlock(transactionPool.getAllTransactions());
blockchainRepository.saveBlockChain(this.blockchain);
genericRepository.save(this.blockchain);
transactionPool.clearTransactions();
network.notifyPeersOfNewBlock(newBlock, null);
return newBlock;
......@@ -127,17 +129,18 @@ public class BlockchainService {
*/
private void reachConsensus() {
List<Blockchain> otherChains = network.retrieveBlockchainsFromPeers();
LOGGER.info("blockchains from peers received. Checking...");
LOGGER.info("{} blockchains from peers received. Checking...", otherChains.size());
for (Blockchain otherChain : otherChains) {
if (this.blockchain == null || this.blockchain.isInferiorTo(otherChain)) {
LOGGER.info("received a blockchain that is better than my currect chain. Replacing current chain.");
this.blockchain = otherChain;
clearConfirmedTransactions();
blockchainRepository.saveBlockChain(blockchain);
genericRepository.save(this.blockchain);
break;
}
}
LOGGER.info("Finished reaching consensus.");
}
/**
......@@ -174,7 +177,7 @@ public class BlockchainService {
LOGGER.info("block is valid, adding it to the blockchain.");
this.blockchain.addBlock(block);
this.network.notifyPeersOfNewBlock(block, sourcePeer);
blockchainRepository.saveBlockChain(blockchain);
genericRepository.save(this.blockchain);
clearConfirmedTransactions();
} else {
......
......@@ -4,6 +4,7 @@ import nl.craftsmen.blockchain.craftscoinnode.CraftsCoinConfigurationProperties;
import nl.craftsmen.blockchain.craftscoinnode.blockchain.Block;
import nl.craftsmen.blockchain.craftscoinnode.blockchain.Blockchain;
import nl.craftsmen.blockchain.craftscoinnode.transaction.Transaction;
import nl.craftsmen.blockchain.craftscoinnode.util.GenericRepository;
import nl.craftsmen.blockchain.craftscoinnode.util.InstanceInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -31,18 +32,18 @@ public class Network {
private final int bootstrapPeerPort;
private final Set<String> peers = new HashSet<>();
private Peers peers;
private final RestTemplate restTemplate;
private final InstanceInfo instanceInfo;
private final PeersRepository peersRepository;
private final GenericRepository genericRepository;
@Autowired
public Network(InstanceInfo instanceInfo, PeersRepository peersRepository, RestTemplate restTemplate, CraftsCoinConfigurationProperties configuration) {
public Network(InstanceInfo instanceInfo, GenericRepository genericRepository, RestTemplate restTemplate, CraftsCoinConfigurationProperties configuration) {
this.instanceInfo = instanceInfo;
this.peersRepository = peersRepository;
this.genericRepository = genericRepository;
this.restTemplate = restTemplate;
this.bootstrapPeerHost = configuration.getBootstrapPeerHost();
this.bootstrapPeerPort = configuration.getBootstrapPeerPort();
......@@ -50,7 +51,7 @@ public class Network {
}
public void init() {
peers.addAll(peersRepository.loadPeers());
this.peers = genericRepository.load(Peers.class).orElse(new Peers());
}
/**
......@@ -59,7 +60,7 @@ public class Network {
* @return the list of known peers.
*/
public Set<String> getPeers() {
return Collections.unmodifiableSet(peers);
return Collections.unmodifiableSet(peers.getPeers());
}
/**
......@@ -82,7 +83,7 @@ public class Network {
String newPeer = instanceInfo.getNode();
propagateNewPeerAndCollectPeers(newPeer);
peersRepository.savePeers(peers);
genericRepository.save(peers);
}
}
......@@ -98,8 +99,8 @@ public class Network {
newPeers.addAll(list.stream().filter(e -> !e.equals(instanceInfo.getNode())).collect(Collectors.toSet()));
}
}
peers.removeAll(peersToRemove);
peers.addAll(newPeers);
peers.getPeers().removeAll(peersToRemove);
peers.getPeers().addAll(newPeers);
}
private String createHostEndpoint() {
......@@ -113,8 +114,8 @@ public class Network {
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);
peersRepository.savePeers(peers);
peers.getPeers().addAll(list);
genericRepository.save(peers);
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!");
......@@ -129,12 +130,12 @@ public class Network {
*/
public Set<String> registerNewPeer(String newPeer) {
LOGGER.info("a new peer has connected to the network: {}", newPeer);
if (!peers.contains(newPeer)) {
if (!peers.getPeers().contains(newPeer)) {
LOGGER.info("peer {} is not previously known to this node. Peer registration will be forwarded to known peer: {}", newPeer, peers);
propagateNewPeerAndCollectPeers(newPeer);
peers.add(newPeer);
peersRepository.savePeers(peers);
peers.getPeers().add(newPeer);
genericRepository.save(peers);
LOGGER.info("adding node {} to the list of known peers.", newPeer);
} else {
LOGGER.info("peer {} is already known to this node. The node will not be forwarded to other peers.");
......@@ -175,11 +176,11 @@ public class Network {
post(node, "/add" + artifactType, entity, new ParameterizedTypeReference<Object>() {
}, peersToRemove);
}
peers.removeAll(peersToRemove);
peers.getPeers().removeAll(peersToRemove);
}
private Set<String> peersWithout(String sourcePeer) {
return peers.stream().filter(p -> !p.equals(sourcePeer)).collect(Collectors.toSet());
return peers.getPeers().stream().filter(p -> !p.equals(sourcePeer)).collect(Collectors.toSet());
}
private HttpHeaders createHttpHeaders() {
......@@ -197,7 +198,7 @@ public class Network {
List<Blockchain> blockchainList = new ArrayList<>();
LOGGER.info("asking the following peers for their blockhain: {}", peers);
Set<String> peersToRemove = new HashSet<>();
for (String node : peers) {
for (String node : peers.getPeers()) {
try {
Blockchain otherChain = restTemplate.getForObject("http://" + node + "/api/blockchain", Blockchain.class);
LOGGER.info("received blockchain from peer {}, adding it to the list.", node);
......@@ -206,9 +207,9 @@ public class Network {
LOGGER.info("could not connect to peer, remvong... {}", node);
peersToRemove.add(node);
}
peers.removeAll(peersToRemove);
peers.getPeers().removeAll(peersToRemove);
}
peersRepository.savePeers(peers);
genericRepository.save(peers);
return blockchainList;
}
......
package nl.craftsmen.blockchain.craftscoinnode.network;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import java.util.HashSet;
import java.util.Set;
@JsonPropertyOrder(alphabetic = true)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
class Peers {
private Set<String> peers = new HashSet<>();
Set<String> getPeers() {
return peers;
}
void setPeers(Set<String> peers) {
this.peers = peers;
}
boolean isEmpty() {
return peers.isEmpty();
}
}
package nl.craftsmen.blockchain.craftscoinnode.network;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import nl.craftsmen.blockchain.craftscoinnode.util.InstanceInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
/**
* Persistence functionality for the list of peers.
*/
@Component
class PeersRepository {
private final InstanceInfo instanceInfo;
private final ObjectMapper objectMapper = new ObjectMapper();
private static final Logger LOGGER = LoggerFactory.getLogger(PeersRepository.class);
@Autowired
PeersRepository(InstanceInfo instanceInfo) {
this.instanceInfo = instanceInfo;
}
/**
* Loads all known peers from disk.
* @return the set of known peers.
*/
Set<String> loadPeers() {
try {
File file = createPeersFileForThisNode();
LOGGER.info("trying to load peers for this node under filename {}", file.getName());
if (file.exists()) {
LOGGER.info("existing peers file found, loading...");
TypeReference<Set<String>> typeRef
= new TypeReference<Set<String>>() {
};
Set<String> peers = objectMapper.readValue(file, typeRef);
LOGGER.info("peers succesfully loaded!");
return peers;
} else {
LOGGER.info("no peers file found, returning empty peers list.");
return new HashSet<>();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Saves known peers to disk.
* @param peers the set of known peers.
*/
void savePeers(Set<String> peers) {
try {
File file = createPeersFileForThisNode();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
LOGGER.info("saving peers");
objectMapper.writeValue(file, peers);
LOGGER.info("peers succesfully saved!");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private File createPeersFileForThisNode() {
return new File(System.getProperty("user.dir"), createPeersChainFileName(instanceInfo.getNode()));
}
private String createPeersChainFileName(String node) {
return node.replace(".", "").replace(":", "") + "-peers.json";
}
}
package nl.craftsmen.blockchain.craftscoinnode.transaction;
import nl.craftsmen.blockchain.craftscoinnode.util.GenericRepository;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -21,18 +22,18 @@ public class SignatureService {
private static final String ALGORITHM = "ECDSA";
private static final String BC = "BC";
private final KeyRepository keyRepository;
private final GenericRepository genericRepository;
private KeyPair keyPair;
@Autowired
public SignatureService(KeyRepository keyRepository) {
this.keyRepository = keyRepository;
public SignatureService(GenericRepository genericRepository) {
this.genericRepository = genericRepository;
}
public void init() {
try {
Security.addProvider(new BouncyCastleProvider());
Optional<Keys> keys = keyRepository.loadKeys();
Optional<Keys> keys = genericRepository.load(Keys.class);
if (keys.isPresent()) {
LOGGER.info("existing key material found, loading...");
loadExistingKeyData(keys.get());
......@@ -41,7 +42,7 @@ public class SignatureService {
LOGGER.info("no key material found, generating new keypair.");
generateNewKeyPair();
Keys serializedKeys = new Keys(new String(Base64.getEncoder().encode(keyPair.getPrivate().getEncoded())), new String(Base64.getEncoder().encode(keyPair.getPublic().getEncoded())));
keyRepository.saveKeys(serializedKeys);
genericRepository.save(serializedKeys);
LOGGER.info("new keypair generated and saved.");
}
} catch (GeneralSecurityException e) {
......
package nl.craftsmen.blockchain.craftscoinnode.transaction;
package nl.craftsmen.blockchain.craftscoinnode.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import nl.craftsmen.blockchain.craftscoinnode.util.InstanceInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import nl.craftsmen.blockchain.craftscoinnode.network.Network;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
import java.io.File;
import java.io.IOException;
import java.util.Optional;
@Component
class KeyRepository {
@Repository
public class GenericRepository {
private static final Logger LOGGER = LoggerFactory.getLogger(GenericRepository.class);
private final InstanceInfo instanceInfo;
@Autowired
KeyRepository(InstanceInfo instanceInfo) {
public GenericRepository(InstanceInfo instanceInfo) {
this.instanceInfo = instanceInfo;
}
void saveKeys(Keys keys) {
try {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
File file = createKeyPairFileForThisNode();
objectMapper.writeValue(file, keys);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Optional<Keys> loadKeys() {
File file = createKeyPairFileForThisNode();
public <T> Optional<T> load(Class<T> requiredType) {
File file = createFileForThisNode(requiredType.getSimpleName().toLowerCase());
LOGGER.info("try to access file: {}", file);
if (file.exists()) {
LOGGER.info("File {} found, reading object of type: {} ", file, requiredType);
try {
ObjectMapper objectMapper = new ObjectMapper();
return Optional.of(objectMapper.readValue(file, Keys.class));
T t = objectMapper.readValue(file, requiredType);
LOGGER.info("sucessully parsed object {}", t);
return Optional.of(t);
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
LOGGER.info("File {} not found.");
return Optional.empty();
}
}
private String getFileSuffix(Object object) {
return object.getClass().getSimpleName().toLowerCase();
}
private File createKeyPairFileForThisNode() {
return new File(System.getProperty("user.dir"), createKeyPairFileName(instanceInfo.getNode()));
public void save(Object object) {
try {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
File file = createFileForThisNode(getFileSuffix(object));
objectMapper.writeValue(file, object);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String createKeyPairFileName(String node) {
return node.replace(".", "").replace(":", "") + "-keypair.json";
private File createFileForThisNode(String suffix) {
return new File(System.getProperty("user.dir"), createKeyPairFileName(instanceInfo.getNode(), suffix));
}
private String createKeyPairFileName(String node, String suffix) {
return node.replace(".", "").replace(":", "") + "-" + suffix + ".json";
}
}
package nl.craftsmen.blockchain.craftscoinnode;
import nl.craftsmen.blockchain.craftscoinnode.util.InstanceInfo;
import org.junit.After;
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.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CraftsCoinApplicationIT {
@Autowired
private InstanceInfo instanceInfo;
@Test
public void allSpringBootWiringIsOk() {
}
@After
public void cleanup() throws IOException {
String node = instanceInfo.getNode();
Files.delete(Paths.get(System.getProperty("user.dir"), normalize(node) + "-blockchain.json"));
Files.delete(Paths.get(System.getProperty("user.dir"), normalize(node) + "-keys.json"));
Files.delete(Paths.get(System.getProperty("user.dir"), normalize(node) + "-peers.json"));
}
private String normalize(String node) {
return node.replace(":", "").replace(".", "");
}
}
package nl.craftsmen.blockchain.craftscoinnode.blockchain;
import nl.craftsmen.blockchain.craftscoinnode.util.InstanceInfo;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class BlockchainRepositoryTest {
private static final String THISNODE = "thistestnode:8080";
private static final String THISNODE_OTHER = "thistestnodeother:8080";
private InstanceInfo instanceInfo = mock(InstanceInfo.class);
private BlockchainRepository blockchainRepository;
@Before
public void setup() {
blockchainRepository = new BlockchainRepository(instanceInfo);
}
@Test
public void loadingNotExistantFileProducesNull() {
when(instanceInfo.getNode()).thenReturn(THISNODE_OTHER);
assertThat(blockchainRepository.loadBlockChain()).isNull();
}
@Test
public void blockchainCanBeSavedAndLoaded() {
when(instanceInfo.getNode()).thenReturn(THISNODE);
Blockchain blockchain = Blockchain.create();
blockchainRepository.saveBlockChain(blockchain);
Blockchain loadedBlockchain = blockchainRepository.loadBlockChain();
assertThat(loadedBlockchain).isNotNull();
}
@After
public void cleanup() throws IOException {
Path path = Paths.get(System.getProperty("user.dir"), "thistestnode8080-blockchain.json");
Files.deleteIfExists(path);
}
}
\ No newline at end of file
......@@ -17,7 +17,7 @@ public class TransactionTest {
public void testIfTransactionHasCorrectFormat() throws JsonProcessingException {
Transaction transaction = new Transaction();
String transactionAsJson = objectMapper.writeValueAsString(transaction);
assertThat(transactionAsJson).isEqualTo("{\"amount\":null,\"from\":null,\"id\":null,\"to\":null}");
assertThat(transactionAsJson).isEqualTo("{\"amount\":null,\"from\":null,\"id\":null,\"publicKey\":null,\"signature\":null,\"to\":null}");
}
@Test
......
package nl.craftsmen.blockchain.craftscoinnode.util;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@JsonPropertyOrder(alphabetic = true)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
class DataClass {
String text;
}
\ No newline at end of file