Commit 757b3607 authored by Michel Schudel's avatar Michel Schudel

Implemented first version of signed transactions.

parent 583ec209
......@@ -120,7 +120,7 @@ Deze method creert een nieuwe blockchain met een eerste block. De vulling van di
Nu we de code voor een initiele blockchain hebben, dienen we deze daadwerklijk te creeren.
Maak in de Spring component `BlockChainService` een nieuw field `blockchain` van type `BlockChain`. Initialiseer in de `init` method (die middels een `@PostConstruct` annotatie afgaat wanneer je de spring container start) nu het field door middel van een aanroep naar de static `create` factory method.
Maak in de Spring component `BlockChainService` een nieuw field `blockchain` van tyckChain`. Initialiseer in de `init` method (die middels een `@PostConstruct` annotatie afgaat wanneer je de spring container start) nu het field door middel van een aanroep naar de static `create` factory method.
Laat de functie `retrieveBlockChain` het blockchain field retourneren.
......
......@@ -3,4 +3,6 @@ export interface Transaction {
from: string;
to: string;
amount: number;
signature: string;
publicKey: string;
}
......@@ -20,6 +20,8 @@
<span class="badge badge-primary">from {{ transaction.from }}</span>
<span class="badge badge-secondary">to {{ transaction.to }}</span>
<span class="badge badge-info">{{ transaction.amount | currency: 'EUR' }}</span>
<span class="badge badge-light">{{ transaction.signature}}</span>
<span class="badge badge-light">{{ transaction.publicKey}}</span>
</div>
</ng-container>
</div>
......
......@@ -13,7 +13,9 @@ export class CreateTransactionComponent {
id: '',
from: '',
to: '',
amount: 0
amount: 0,
signature: '',
publicKey: ''
};
constructor(private transactionService: TransactionService, private router: Router) { }
......
......@@ -13,12 +13,16 @@
<th>From</th>
<th>To</th>
<th>Amount</th>
<th>Signature</th>
<th>Public key</th>
</tr>
<tr *ngFor="let transaction of transactions; trackBy: trackByFn">
<td>{{ transaction.id }}</td>
<td>{{ transaction.from }}</td>
<td>{{ transaction.to }}</td>
<td>{{ transaction.amount | currency: 'EUR' }}</td>
<td>{{ transaction.signature}}</td>
<td>{{ transaction.publicKey}}</td>
</tr>
</table>
</div>
......@@ -12,6 +12,8 @@
<span class="badge badge-primary">from {{ transaction.from }}</span>
<span class="badge badge-secondary">to {{ transaction.to }}</span>
<span class="badge badge-success">{{ transaction.amount | currency: 'EUR' }}</span>
<span class="badge badge-success">{{ transaction.signature}}</span>
<span class="badge badge-success">{{ transaction.publicKey}}</span>
<span class="badge badge-danger float-right">Unconfirmed</span>
</p>
</li>
......@@ -22,6 +24,8 @@
<span class="badge badge-primary">from {{ transaction.from }}</span>
<span class="badge badge-secondary">to {{ transaction.to }}</span>
<span class="badge badge-success">{{ transaction.amount | currency: 'EUR' }}</span>
<span class="badge badge-success">{{ transaction.signature}}</span>
<span class="badge badge-success">{{ transaction.publicKey}}</span>
</p>
</li>
</ul>
......
......@@ -2,6 +2,7 @@ package nl.craftsmen.blockchain.craftscoinnode.blockchain;
import nl.craftsmen.blockchain.craftscoinnode.CraftsCoinConfigurationProperties;
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.InstanceInfo;
......@@ -27,6 +28,7 @@ public class BlockchainService {
private static final BigDecimal CRAFTSCOIN_MINING_REWARD = BigDecimal.TEN;
private final Network network;
private final SignatureService signatureService;
private final BlockchainRepository blockchainRepository;
private final TransactionPool transactionPool;
private final InstanceInfo instanceInfo;
......@@ -36,8 +38,9 @@ public class BlockchainService {
@Autowired
public BlockchainService(Network network, BlockchainRepository blockchainRepository, TransactionPool transactionPool, InstanceInfo instanceInfo, CraftsCoinConfigurationProperties configuration) {
public BlockchainService(Network network, SignatureService signatureService, BlockchainRepository blockchainRepository, TransactionPool transactionPool, InstanceInfo instanceInfo, CraftsCoinConfigurationProperties configuration) {
this.network = network;
this.signatureService = signatureService;
this.blockchainRepository = blockchainRepository;
this.transactionPool = transactionPool;
this.instanceInfo = instanceInfo;
......@@ -84,7 +87,8 @@ public class BlockchainService {
*/
public long createTransaction(String from, String to, BigDecimal amount) {
LOGGER.info("creating new transaction: from: {}, to: {}, amount: {}", from, to, amount);
Transaction transaction = new Transaction(from, to, amount);
String signature = signatureService.sign(from + to + amount);
Transaction transaction = new Transaction(from, to, amount, signature, signatureService.getPublicKey());
transactionPool.addTransaction(transaction);
network.notifyPeersOfNewTransaction(transaction, instanceInfo.getNode());
return this.blockchain.getIndexOfNextBlock();
......@@ -109,7 +113,8 @@ public class BlockchainService {
throw new RuntimeException("no pending transactions, nothing to mine.");
}
//reward the miner
Transaction transaction = new Transaction("0", miningWalletId, CRAFTSCOIN_MINING_REWARD);
String signature = signatureService.sign("0" + miningWalletId + CRAFTSCOIN_MINING_REWARD);
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);
......@@ -148,7 +153,13 @@ public class BlockchainService {
LOGGER.info("received transaction: {} from source peer {}", transaction, sourcePeer);
if (!transactionPool.getAllTransactions().contains(transaction)) {
LOGGER.info("transaction {} is new, adding it to the list of pending transactions.", transaction);
transactionPool.addTransaction(transaction);
boolean validTransaction = signatureService.verify(transaction.getFrom() + transaction.getTo() + transaction.getAmount(), transaction.getSignature(), transaction.getPublicKey());
if (validTransaction) {
LOGGER.info("transaction is valid, adding to the transaction pool.");
transactionPool.addTransaction(transaction);
} else {
LOGGER.error("transaction has no valid signature, discarding.");
}
network.notifyPeersOfNewTransaction(transaction, sourcePeer);
}
}
......
package nl.craftsmen.blockchain.craftscoinnode.transaction;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import nl.craftsmen.blockchain.craftscoinnode.util.InstanceInfo;
......@@ -11,24 +8,20 @@ import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Optional;
@Component
public class KeyRepository {
class KeyRepository {
private InstanceInfo instanceInfo;
@Autowired
public KeyRepository(InstanceInfo instanceInfo) {
KeyRepository(InstanceInfo instanceInfo) {
this.instanceInfo = instanceInfo;
}
public void saveKeys(KeyPair keyPair) {
void saveKeys(Keys keys) {
try {
Keys keys = new Keys(new String(Base64.getEncoder().encode(keyPair.getPrivate().getEncoded())), new String(Base64.getEncoder().encode(keyPair.getPublic().getEncoded())));
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
File file = createKeyPairFileForThisNode();
......@@ -38,29 +31,17 @@ public class KeyRepository {
}
}
public KeyPair loadKeys() {
Optional<Keys> loadKeys() {
File file = createKeyPairFileForThisNode();
if (file.exists()) {
try {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
Keys keys = objectMapper.readValue(file, Keys.class);
KeyFactory kf = KeyFactory.getInstance("ECDSA", "BC");
byte[] bytePrivateKey = Base64.getDecoder().decode(keys.getPrivateKey().getBytes());
PKCS8EncodedKeySpec pkcs8privateKey = new PKCS8EncodedKeySpec(bytePrivateKey);
PrivateKey privateKey = kf.generatePrivate(pkcs8privateKey);
byte[] bytePublicKey = Base64.getDecoder().decode(keys.getPublicKey().getBytes());
X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(bytePublicKey);
PublicKey publicKey = kf.generatePublic(X509publicKey);
return new KeyPair(publicKey, privateKey);
} catch (GeneralSecurityException | IOException e) {
return Optional.of(objectMapper.readValue(file, Keys.class));
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
return null;
return Optional.empty();
}
}
......@@ -73,29 +54,4 @@ public class KeyRepository {
return node.replace(".", "").replace(":", "") + "-keypair.json";
}
@JsonPropertyOrder(alphabetic = true)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
public static class Keys {
private String privateKey;
private String publicKey;
//default public constructor, needed for json deserialization
@SuppressWarnings({"unused", "WeakerAccess"})
public Keys() {
}
Keys(String privateKey, String publicKey) {
this.privateKey = privateKey;
this.publicKey = publicKey;
}
String getPrivateKey() {
return privateKey;
}
String getPublicKey() {
return publicKey;
}
}
}
package nl.craftsmen.blockchain.craftscoinnode.transaction;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
@Component
public class KeyService {
private KeyRepository keyRepository;
private KeyPair keyPair;
@Autowired
public KeyService(KeyRepository keyRepository) {
this.keyRepository = keyRepository;
}
@PostConstruct
public void init() throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
Security.addProvider(new BouncyCastleProvider());
keyPair = keyRepository.loadKeys();
if (keyPair == null) {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("ECDSA", "BC");
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
ECGenParameterSpec ecSpec = new ECGenParameterSpec("prime192v1");
keyGen.initialize(ecSpec, random);
keyPair = keyGen.generateKeyPair();
keyRepository.saveKeys(keyPair);
}
}
public KeyPair getKeyPair() {
return keyPair;
}
}
package nl.craftsmen.blockchain.craftscoinnode.transaction;
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 Keys {
private String privateKey;
private String publicKey;
//default public constructor, needed for json deserialization
@SuppressWarnings({"unused", "WeakerAccess"})
public Keys() {
}
Keys(String privateKey, String publicKey) {
this.privateKey = privateKey;
this.publicKey = publicKey;
}
String getPrivateKey() {
return privateKey;
}
String getPublicKey() {
return publicKey;
}
}
package nl.craftsmen.blockchain.craftscoinnode.transaction;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Optional;
@Component
public class SignatureService {
private static final Logger LOGGER = LoggerFactory.getLogger(SignatureService.class);
private static final String ALGORITHM = "ECDSA";
private static final String BC = "BC";
private KeyRepository keyRepository;
private KeyPair keyPair;
@Autowired
public SignatureService(KeyRepository keyRepository) {
this.keyRepository = keyRepository;
}
@PostConstruct
public void init() throws GeneralSecurityException {
Security.addProvider(new BouncyCastleProvider());
Optional<Keys> keys = keyRepository.loadKeys();
if (keys.isPresent()) {
LOGGER.info("existing key material found, loading...");
loadExistingKeyData(keys.get());
} else {
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);
}
}
private void loadExistingKeyData(Keys keys) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
KeyFactory kf = KeyFactory.getInstance(ALGORITHM, BC);
byte[] bytePrivateKey = Base64.getDecoder().decode(keys.getPrivateKey().getBytes());
PKCS8EncodedKeySpec pkcs8privateKey = new PKCS8EncodedKeySpec(bytePrivateKey);
PrivateKey privateKey = kf.generatePrivate(pkcs8privateKey);
byte[] bytePublicKey = Base64.getDecoder().decode(keys.getPublicKey().getBytes());
X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(bytePublicKey);
PublicKey publicKey = kf.generatePublic(X509publicKey);
this.keyPair = new KeyPair(publicKey, privateKey);
}
private void generateNewKeyPair() throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM, BC);
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
ECGenParameterSpec ecSpec = new ECGenParameterSpec("prime192v1");
keyGen.initialize(ecSpec, random);
this.keyPair = keyGen.generateKeyPair();
}
public String sign(String payload) {
try {
LOGGER.info("signing payload...");
Signature dsa = Signature.getInstance(ALGORITHM, BC);
dsa.initSign(keyPair.getPrivate());
dsa.update(payload.getBytes());
return Base64.getEncoder().encodeToString(dsa.sign());
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public boolean verify(String payload, String signature, String publicKey) {
try {
LOGGER.info("verifying signature...");
Signature dsa = Signature.getInstance(ALGORITHM, BC);
byte[] bytePublicKey = Base64.getDecoder().decode(publicKey.getBytes());
KeyFactory kf = KeyFactory.getInstance(ALGORITHM, BC);
X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(bytePublicKey);
PublicKey incomingPublicKey = kf.generatePublic(X509publicKey);
dsa.initVerify(incomingPublicKey);
dsa.update(payload.getBytes());
return dsa.verify(Base64.getDecoder().decode(signature.getBytes()));
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public String getPublicKey() {
return new String(Base64.getEncoder().encode(keyPair.getPublic().getEncoded()));
}
}
......@@ -17,17 +17,22 @@ public class Transaction {
private String to;
private BigDecimal amount;
private String signature;
private String publicKey;
//default public constructor, needed for json deserialization
@SuppressWarnings({"unused", "WeakerAccess"})
public Transaction() {
}
public Transaction(String from, String to, BigDecimal amount) {
public Transaction(String from, String to, BigDecimal amount, String signature, String publicKey) {
this.id = UUID.randomUUID();
this.to = to;
this.from = from;
this.amount = amount;
this.signature = signature;
this.publicKey = publicKey;
}
public String getFrom() {
......@@ -42,6 +47,14 @@ public class Transaction {
return amount;
}
public String getPublicKey() {
return publicKey;
}
public String getSignature() {
return signature;
}
public boolean hasParticipant(String walletId) {
return isRecepient(walletId) || isSender(walletId);
}
......@@ -65,6 +78,8 @@ public class Transaction {
}
@Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
......
package nl.craftsmen.blockchain.craftscoinnode.transaction;
import nl.craftsmen.blockchain.craftscoinnode.blockchain.BlockchainService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
......@@ -16,7 +15,7 @@ import java.util.Set;
@Component
public class TransactionPool {
private static final Logger LOGGER = LoggerFactory.getLogger(BlockchainService.class);
private static final Logger LOGGER = LoggerFactory.getLogger(TransactionPool.class);
private final Set<Transaction> transactions = new HashSet<>();
......
......@@ -127,7 +127,7 @@ public class NetworkTest {
public void notifyPeerOfNewTransactionShouldPropagateTheTransactionToThePeers() {
when(peersRepository.loadPeers()).thenReturn(Stream.of("alreadyKnownPeer:8080").collect(Collectors.toSet()));
String sourcePeer = "sourcePeer:8080";
Transaction transaction = new Transaction("michel", "jan", BigDecimal.TEN);
Transaction transaction = new Transaction("michel", "jan", BigDecimal.TEN, "", "");
network.init();
network.notifyPeersOfNewTransaction(transaction, sourcePeer);
verify(restTemplate).exchange(eq("http://alreadyKnownPeer:8080/api/addtransaction"), eq(HttpMethod.POST), Matchers.<HttpEntity<Transaction>>any(),
......
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