Part2 :使用 Java 创建你的第一个区块链[译]
本篇是该系列的第二篇,你可以在这里找到第一篇。原文链接在此。
在第本篇,我们将
- 创建一个简单的钱包
- 在我们的区块链上签发一个交易。
上面的这些过程,其实就产生了我们自己的加密货币。
在上一篇文章中,我们有了一个可验证的、基本的区块链。但是,我们的链中仅仅存储了一些无用的信息。今天,我们将把这些无用的信息替换为交易数据。这允许我们可以创建一个简单的加密货币,我们称之为 “NoobCoin”。
本文中,还将使用 Bouncy Castle 和 GSON 库。
1. 准备钱包
在加密货币中,货币的所有权在区块链上以交易的形式流转
,交易者拥有一个可以地址可以转入转出。因此,对于一个钱包而言,至少需要能够存储这些地址。更多的,钱包也可以作为一个软件用以在区块链上产生新的交易。
现在,让我们俩创建一个持有我们公钥和私钥的 Wallet
类:
package noobchain;
import java.security.PrivateKey;
import java.security.PublicKey;
public class Wallet {
public PrivateKey privateKey;
public PublicKey publicKey ;
}
公钥和私钥有什么用?
对于我们的加密货币 noobcoin
来说,公钥扮演者我们的钱包地址的角色,我们可以随意分享自己的公钥。私钥,是用来 签署(sign) 交易的,没有人能花费我们的 noobcoin,除非,他拥有我们的私钥。所以,用户的私钥一定要被保管好。公钥部分通常会和交易一起发送的,用以验证交易前面是否合法,交易内容是否被篡改。
公钥和私钥通过 KeyPair 生成,接下来,我们将使用 椭圆曲线加密算法 来生成密钥对。
public class Wallet {
public PrivateKey privateKey;
public PublicKey publicKey ;
public Wallet(){
generateKeyPair();
}
public void generateKeyPair() {
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("ECDSA","BC");
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
ECGenParameterSpec ecSpec = new ECGenParameterSpec("prime192v1");
// Initialize the key generator and generate a KeyPair
keyGen.initialize(ecSpec, random); //256 bytes provides an acceptable security level
KeyPair keyPair = keyGen.generateKeyPair();
// Set the public and private keys from the keyPair
privateKey = keyPair.getPrivate();
publicKey = keyPair.getPublic();
}catch(Exception e) {
throw new RuntimeException(e);
}
}
}
现在,我们的钱包类差不多了,接下来,让我们来看看交易。
cash.gif
2. 交易和签名
每一个交易,都要携带一定的数据:
- 发送者资金的公钥(译注:相当发送者的地址)
- 接受者资金的公钥(译注:相当于接受者的地址)
- 交易资金的数目
- inputs,证明发送者有足够的币发送
- outputs,接收地址收到的总金额
- 加密签名,保证发送者签署的交易不会被恶意篡改
现在,我们来创建一个 Transaction 类:
import java.security.*;
import java.util.ArrayList;
public class Transaction {
public String transactionId; // this is also the hash of the transaction.
public PublicKey sender; // senders address/public key.
public PublicKey reciepient; // Recipients address/public key.
public float value;
public byte[] signature; // this is to prevent anybody else from spending funds in our wallet.
public ArrayList<TransactionInput> inputs = new ArrayList<TransactionInput>();
public ArrayList<TransactionOutput> outputs = new ArrayList<TransactionOutput>();
private static int sequence = 0; // a rough count of how many transactions have been generated.
// Constructor:
public Transaction(PublicKey from, PublicKey to, float value, ArrayList<TransactionInput> inputs) {
this.sender = from;
this.reciepient = to;
this.value = value;
this.inputs = inputs;
}
// This Calculates the transaction hash (which will be used as its Id)
private String calulateHash() {
sequence++; //increase the sequence to avoid 2 identical transactions having the same hash
return StringUtil.applySha256(
StringUtil.getStringFromKey(sender) +
StringUtil.getStringFromKey(reciepient) +
Float.toString(value) + sequence
);
}
}
此时,inputs 和 outputs 都是空的,后面我们会使用到它们。这个 Transaction 类还将包含生成、验证签名、验证交易等相关方法。但是,签名的目的是是什么?它们是如何工作的?
签名的目的和工作原理
签名
在区块链中执行了两个非常重要的任务:首先,允许拥有者们消费他们的币,它会放置交易信息被篡改。
私钥被用来签署数据,公钥被用来验证数据的完整性。
举个栗子:Bob 想向 Sally 转 2 个 NoobCoin,因此,钱包软件会生成这个交易,然后将这个交易提交给矿工,以将该数据包含连接到区块链中。如果矿工企图将这 2 个币的接收人改为 John。然而,幸运的是 Bob 使用私钥签署了这个交易数据,并且允许任何人使用他的公钥验证该交易的完整性、合法性。
从上面的代码中,我们可以看到,所谓签名实际上一个 byte 数组,因此,下面让我们来生成它。首先,这里需要一个 StringUtil
类:
//Applies ECDSA Signature and returns the result ( as bytes ).
public static byte[] applyECDSASig(PrivateKey privateKey, String input) {
Signature dsa;
byte[] output = new byte[0];
try {
dsa = Signature.getInstance("ECDSA", "BC");
dsa.initSign(privateKey);
byte[] strByte = input.getBytes();
dsa.update(strByte);
byte[] realSig = dsa.sign();
output = realSig;
} catch (Exception e) {
throw new RuntimeException(e);
}
return output;
}
//Verifies a String signature
public static boolean verifyECDSASig(PublicKey publicKey, String data, byte[] signature) {
try {
Signature ecdsaVerify = Signature.getInstance("ECDSA", "BC");
ecdsaVerify.initVerify(publicKey);
ecdsaVerify.update(data.getBytes());
return ecdsaVerify.verify(signature);
}catch(Exception e) {
throw new RuntimeException(e);
}
}
public static String getStringFromKey(Key key) {
return Base64.getEncoder().encodeToString(key.getEncoded());
}
现在,在 Transaction
类的 generateSignature() 和 verifySignature() 方法中运用该签名方法。
//Signs all the data we dont wish to be tampered with.
public void generateSignature(PrivateKey privateKey) {
String data = StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient) + Float.toString(value) ;
signature = StringUtil.applyECDSASig(privateKey,data);
}
//Verifies the data we signed hasnt been tampered with
public boolean verifiySignature() {
String data = StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient) + Float.toString(value) ;
return StringUtil.verifyECDSASig(sender, data, signature);
}
当该交易被矿工添加到区块链上的一个新块的时候,这个签名将会被验证。
check.gif
3.测试钱包和签名
现在,我们基本上已经完成了一半的工作。在 NoobChain
这个类中,添加一些新的变量,并替换到 main 方法中的一些方法。
import java.security.Security;
import java.util.ArrayList;
import java.util.Base64;
import com.google.gson.GsonBuilder;
public class NoobChain {
public static ArrayList<Block> blockchain = new ArrayList<Block>();
public static int difficulty = 5;
public static Wallet walletA;
public static Wallet walletB;
public static void main(String[] args) {
//Setup Bouncey castle as a Security Provider
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
//Create the new wallets
walletA = new Wallet();
walletB = new Wallet();
//Test public and private keys
System.out.println("Private and public keys:");
System.out.println(StringUtil.getStringFromKey(walletA.privateKey));
System.out.println(StringUtil.getStringFromKey(walletA.publicKey));
//Create a test transaction from WalletA to walletB
Transaction transaction = new Transaction(walletA.publicKey, walletB.publicKey, 5, null);
transaction.generateSignature(walletA.privateKey);
//Verify the signature works and verify it from the public key
System.out.println("Is signature verified");
System.out.println(transaction.verifiySignature());
}
现在,我们创建了两个钱包,walletA 和 walletB,并且打印了 walletA 的私钥和公钥。你能看到的大概是这样子的
image
回过头来看,现在我们需要创建/验证 outputs 和 inputs,并且将他们存储到区块链上的交易上。
4. Inputs 和 Outputs
1. 加密货币的归属
如果你要拥有一个 bitcoin,首先你要收到一个 bitcoin。这个过程,并非是在总账单上将你的 bitcoin 加一 ,将发送者的 bitcoin 减一的过程。事实上,发送者肯定是前面也接收到的一个 bitcoin,然后才能将其发送到你的地址上。
钱包的余额,是所有跟你地址(公钥)相关的未花费出去的交易输出的总和。(译注:后面将会看到,实际上这个链会维护一个由 publicKey 做 key,TransactionOutput 做 value 的 HashMap,这个 map 是所有交易输出的记录,通过 publicKey 可以查找到关于其拥有者的 bitcoin 数量)
接下来,我们遵从 bitcoin 惯例,将未花费出去的交易输出命名为:UTXO.
现在,我们来创建 TransactionInput 类:
public class TransactionInput {
public String transactionOutputId; //Reference to TransactionOutputs -> transactionId
public TransactionOutput UTXO; //Contains the Unspent transaction output
public TransactionInput(String transactionOutputId) {
this.transactionOutputId = transactionOutputId;
}
}
然后,创建 TransactionOutputs 类:
import java.security.PublicKey;
public class TransactionOutput {
public String id;
public PublicKey reciepient; //also known as the new owner of these coins.
public float value; //the amount of coins they own
public String parentTransactionId; //the id of the transaction this output was created in
//Constructor
public TransactionOutput(PublicKey reciepient, float value, String parentTransactionId) {
this.reciepient = reciepient;
this.value = value;
this.parentTransactionId = parentTransactionId;
this.id = StringUtil.applySha256(StringUtil.getStringFromKey(reciepient)+Float.toString(value)+parentTransactionId);
}
//Check if coin belongs to you
public boolean isMine(PublicKey publicKey) {
return (publicKey == reciepient);
}
}
通过 Transaction outputs 可以获取到交易双方通过交易获取到的各自 bitcoin 的总数。因此,它也可以作为新交易的 inputs,以证明你有足够多的币用以交易。
done.gif
2. 处理交易
链上的区块,可能包含了很多交易,区块链可能会很长很长很长很长,也因此,在处理新块的时候,可能会花费很长很长的时间,因为我们需要查找并检查它的输入。为了绕过这一点,我们将使用一个额外的集合,以保存未花费的交易。在 NoobChain
中,我们通过 UTXO
表示:
public class NoobChain {
public static ArrayList<Block> blockchain = new ArrayList<Block>();
public static HashMap<String,TransactionOutput> UTXOs = new HashMap<String,TransactionOutput>(); //list of all unspent transactions.
public static int difficulty = 5;
public static Wallet walletA;
public static Wallet walletB;
public static void main(String[] args) {
......
ok,现在是时候来揭晓事情的真相了。
让我们把所有事情集中起来,在 Transactoin
处理:
//Returns true if new transaction could be created.
public boolean processTransaction() {
if(verifiySignature() == false) {
System.out.println("#Transaction Signature failed to verify");
return false;
}
//gather transaction inputs (Make sure they are unspent):
for(TransactionInput i : inputs) {
i.UTXO = NoobChain.UTXOs.get(i.transactionOutputId);
}
//check if transaction is valid:
if(getInputsValue() < NoobChain.minimumTransaction) {
System.out.println("#Transaction Inputs to small: " + getInputsValue());
return false;
}
//generate transaction outputs:
float leftOver = getInputsValue() - value; //get value of inputs then the left over change:
transactionId = calulateHash();
outputs.add(new TransactionOutput( this.reciepient, value,transactionId)); //send value to recipient
outputs.add(new TransactionOutput( this.sender, leftOver,transactionId)); //send the left over 'change' back to sender
//add outputs to Unspent list
for(TransactionOutput o : outputs) {
NoobChain.UTXOs.put(o.id , o);
}
//remove transaction inputs from UTXO lists as spent:
for(TransactionInput i : inputs) {
if(i.UTXO == null) continue; //if Transaction can't be found skip it
NoobChain.UTXOs.remove(i.UTXO.id);
}
return true;
}
//returns sum of inputs(UTXOs) values
public float getInputsValue() {
float total = 0;
for(TransactionInput i : inputs) {
if(i.UTXO == null) continue; //if Transaction can't be found skip it
total += i.UTXO.value;
}
return total;
}
//returns sum of outputs:
public float getOutputsValue() {
float total = 0;
for(TransactionOutput o : outputs) {
total += o.value;
}
return total;
}
使用这个方法,我们执行一些检查,确保交易的合法性,接着,收集输入,并产生输出。
很重要的一点,在结尾处,我们从 UTXO 中删除了 Inputs。这意味着 transaction output 仅有一次机会作为输入...所有输入值,都将在本次交易中被使用,如果没有使用完,剩余的部分会返回到自身中。
最后,让我们来更新下钱包:
- 计算余额(通过对 UTXO 循环,计算属于“我”的余额,判断是否有足够的余额进行交易 )。
- 产生新的 transaction(交易)。
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class Wallet {
public PrivateKey privateKey;
public PublicKey publicKey;
public HashMap<String,TransactionOutput> UTXOs = new HashMap<String,TransactionOutput>(); //only UTXOs owned by this wallet.
public Wallet() {...
public void generateKeyPair() {...
//returns balance and stores the UTXO's owned by this wallet in this.UTXOs
public float getBalance() {
float total = 0;
for (Map.Entry<String, TransactionOutput> item: NoobChain.UTXOs.entrySet()){
TransactionOutput UTXO = item.getValue();
if(UTXO.isMine(publicKey)) { //if output belongs to me ( if coins belong to me )
UTXOs.put(UTXO.id,UTXO); //add it to our list of unspent transactions.
total += UTXO.value ;
}
}
return total;
}
//Generates and returns a new transaction from this wallet.
public Transaction sendFunds(PublicKey _recipient,float value ) {
if(getBalance() < value) { //gather balance and check funds.
System.out.println("#Not Enough funds to send transaction. Transaction Discarded.");
return null;
}
//create array list of inputs
ArrayList<TransactionInput> inputs = new ArrayList<TransactionInput>();
float total = 0;
for (Map.Entry<String, TransactionOutput> item: UTXOs.entrySet()){
TransactionOutput UTXO = item.getValue();
total += UTXO.value;
inputs.add(new TransactionInput(UTXO.id));
if(total > value) break;
}
Transaction newTransaction = new Transaction(publicKey, _recipient , value, inputs);
newTransaction.generateSignature(privateKey);
for(TransactionInput input: inputs){
UTXOs.remove(input.transactionOutputId);
}
return newTransaction;
}
}
6. 将交易添加到区块中
现在,我们有了一个可以工作的交易系统了,接下来,需要将其实现到区块链上。现在,我们可以将以前那些无用的数据替换为 ArrayList of transactions(交易列表),同时,使用根哈希的方式,计算区块的哈希值。
下面,在 StringUtil 中增加一个获取根哈希(译注:可以参考 维基百科)的方法:
//Tacks in array of transactions and returns a merkle root.
public static String getMerkleRoot(ArrayList<Transaction> transactions) {
int count = transactions.size();
ArrayList<String> previousTreeLayer = new ArrayList<String>();
for(Transaction transaction : transactions) {
previousTreeLayer.add(transaction.transactionId);
}
ArrayList<String> treeLayer = previousTreeLayer;
while(count > 1) {
treeLayer = new ArrayList<String>();
for(int i=1; i < previousTreeLayer.size(); i++) {
treeLayer.add(applySha256(previousTreeLayer.get(i-1) + previousTreeLayer.get(i)));
}
count = treeLayer.size();
previousTreeLayer = treeLayer;
}
String merkleRoot = (treeLayer.size() == 1) ? treeLayer.get(0) : "";
return merkleRoot;
}
再接下来,进行 Block 中的修改:
import java.util.ArrayList;
import java.util.Date;
public class Block {
public String hash;
public String previousHash;
public String merkleRoot;
public ArrayList<Transaction> transactions = new ArrayList<Transaction>(); //our data will be a simple message.
public long timeStamp; //as number of milliseconds since 1/1/1970.
public int nonce;
//Block Constructor.
public Block(String previousHash ) {
this.previousHash = previousHash;
this.timeStamp = new Date().getTime();
this.hash = calculateHash(); //Making sure we do this after we set the other values.
}
//Calculate new hash based on blocks contents
public String calculateHash() {
String calculatedhash = StringUtil.applySha256(
previousHash +
Long.toString(timeStamp) +
Integer.toString(nonce) +
merkleRoot
);
return calculatedhash;
}
//Increases nonce value until hash target is reached.
public void mineBlock(int difficulty) {
merkleRoot = StringUtil.getMerkleRoot(transactions);
String target = StringUtil.getDificultyString(difficulty); //Create a string with difficulty * "0"
while(!hash.substring( 0, difficulty).equals(target)) {
nonce ++;
hash = calculateHash();
}
System.out.println("Block Mined!!! : " + hash);
}
//Add transactions to this block
public boolean addTransaction(Transaction transaction) {
//process transaction and check if valid, unless block is genesis block then ignore.
if(transaction == null) return false;
if((previousHash != "0")) {
if((transaction.processTransaction() != true)) {
System.out.println("Transaction failed to process. Discarded.");
return false;
}
}
transactions.add(transaction);
System.out.println("Transaction Successfully added to Block");
return true;
}
}
注意,我们更新了 Block 的构造器,不在传入一个字符串,并且添加了用于计算哈希值的 merkle root(根哈希)属性。
addTransaction 方法将返回一个 boolean,以表示交易是否成功。
7. 华丽的落幕
最后,我们还需要测试从钱包中消费 noobcoin ,更新区块链合法性检测。但是,首先,我们还是需要引入这些新的 coins。其实,还是有很多方式引入新的 coins,比如在比特币区块链上,新币可以作为对矿工挖到矿的奖励。在本文中,我们将采用直接在创世区块中释放所有币的方式。
我们来更新下 NoobChain
类:
- 创世区块将向 walletA 发放 100 个 Noobcoins.
- 当交易发生后,检测区块链的合法性
- 通过一些交易,测试是否一切工作 ok
public class NoobChain {
public static ArrayList<Block> blockchain = new ArrayList<Block>();
public static HashMap<String,TransactionOutput> UTXOs = new HashMap<String,TransactionOutput>();
public static int difficulty = 3;
public static float minimumTransaction = 0.1f;
public static Wallet walletA;
public static Wallet walletB;
public static Transaction genesisTransaction;
public static void main(String[] args) {
//add our blocks to the blockchain ArrayList:
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); //Setup Bouncey castle as a Security Provider
//Create wallets:
walletA = new Wallet();
walletB = new Wallet();
Wallet coinbase = new Wallet();
//create genesis transaction, which sends 100 NoobCoin to walletA:
genesisTransaction = new Transaction(coinbase.publicKey, walletA.publicKey, 100f, null);
genesisTransaction.generateSignature(coinbase.privateKey); //manually sign the genesis transaction
genesisTransaction.transactionId = "0"; //manually set the transaction id
genesisTransaction.outputs.add(new TransactionOutput(genesisTransaction.reciepient, genesisTransaction.value, genesisTransaction.transactionId)); //manually add the Transactions Output
UTXOs.put(genesisTransaction.outputs.get(0).id, genesisTransaction.outputs.get(0)); //its important to store our first transaction in the UTXOs list.
System.out.println("Creating and Mining Genesis block... ");
Block genesis = new Block("0");
genesis.addTransaction(genesisTransaction);
addBlock(genesis);
//testing
Block block1 = new Block(genesis.hash);
System.out.println("\nWalletA's balance is: " + walletA.getBalance());
System.out.println("\nWalletA is Attempting to send funds (40) to WalletB...");
block1.addTransaction(walletA.sendFunds(walletB.publicKey, 40f));
addBlock(block1);
System.out.println("\nWalletA's balance is: " + walletA.getBalance());
System.out.println("WalletB's balance is: " + walletB.getBalance());
Block block2 = new Block(block1.hash);
System.out.println("\nWalletA Attempting to send more funds (1000) than it has...");
block2.addTransaction(walletA.sendFunds(walletB.publicKey, 1000f));
addBlock(block2);
System.out.println("\nWalletA's balance is: " + walletA.getBalance());
System.out.println("WalletB's balance is: " + walletB.getBalance());
Block block3 = new Block(block2.hash);
System.out.println("\nWalletB is Attempting to send funds (20) to WalletA...");
block3.addTransaction(walletB.sendFunds( walletA.publicKey, 20));
System.out.println("\nWalletA's balance is: " + walletA.getBalance());
System.out.println("WalletB's balance is: " + walletB.getBalance());
isChainValid();
}
public static Boolean isChainValid() {
Block currentBlock;
Block previousBlock;
String hashTarget = new String(new char[difficulty]).replace('\0', '0');
HashMap<String,TransactionOutput> tempUTXOs = new HashMap<String,TransactionOutput>(); //a temporary working list of unspent transactions at a given block state.
tempUTXOs.put(genesisTransaction.outputs.get(0).id, genesisTransaction.outputs.get(0));
//loop through blockchain to check hashes:
for(int i=1; i < blockchain.size(); i++) {
currentBlock = blockchain.get(i);
previousBlock = blockchain.get(i-1);
//compare registered hash and calculated hash:
if(!currentBlock.hash.equals(currentBlock.calculateHash()) ){
System.out.println("#Current Hashes not equal");
return false;
}
//compare previous hash and registered previous hash
if(!previousBlock.hash.equals(currentBlock.previousHash) ) {
System.out.println("#Previous Hashes not equal");
return false;
}
//check if hash is solved
if(!currentBlock.hash.substring( 0, difficulty).equals(hashTarget)) {
System.out.println("#This block hasn't been mined");
return false;
}
//loop thru blockchains transactions:
TransactionOutput tempOutput;
for(int t=0; t <currentBlock.transactions.size(); t++) {
Transaction currentTransaction = currentBlock.transactions.get(t);
if(!currentTransaction.verifiySignature()) {
System.out.println("#Signature on Transaction(" + t + ") is Invalid");
return false;
}
if(currentTransaction.getInputsValue() != currentTransaction.getOutputsValue()) {
System.out.println("#Inputs are note equal to outputs on Transaction(" + t + ")");
return false;
}
for(TransactionInput input: currentTransaction.inputs) {
tempOutput = tempUTXOs.get(input.transactionOutputId);
if(tempOutput == null) {
System.out.println("#Referenced input on Transaction(" + t + ") is Missing");
return false;
}
if(input.UTXO.value != tempOutput.value) {
System.out.println("#Referenced input Transaction(" + t + ") value is Invalid");
return false;
}
tempUTXOs.remove(input.transactionOutputId);
}
for(TransactionOutput output: currentTransaction.outputs) {
tempUTXOs.put(output.id, output);
}
if( currentTransaction.outputs.get(0).reciepient != currentTransaction.reciepient) {
System.out.println("#Transaction(" + t + ") output reciepient is not who it should be");
return false;
}
if( currentTransaction.outputs.get(1).reciepient != currentTransaction.sender) {
System.out.println("#Transaction(" + t + ") output 'change' is not sender.");
return false;
}
}
}
System.out.println("Blockchain is valid");
return true;
}
public static void addBlock(Block newBlock) {
newBlock.mineBlock(difficulty);
blockchain.add(newBlock);
}
}
你可以在 Github 上下载到这个项目。