翻译—区块链(二):用Java创建你的第一个区块链—交易
(PS:part3 2月份儿后就见不到影子了…)
第二篇文章我们将做以下:
· 创建一个简单的钱包
· 使用我们的区块链发送已签名的交易
· 感受更加酷的东西
以上将会产生我们自己的加密货币(sorta)
继续我们上篇的,我们已经有了基本的可校验的区块链。但是我们目前的链仅仅存储了相当无用的信息。今天我们打算用交易代替这些无用信息(我们的区块可以容纳多种交易),允许我们创建一个非常简单的加密货币。我们把它取一个新货币的名字"Noob"币。
· Dependencies:你需要导入包bounceycastle
准备一个钱包
在加密货币中,硬币的所有权被转移到了区块链上用来交易,参与者有一个可以进行虚拟货币交易的网址。在他们的基本形式中,钱包仅仅能存储这些网址,但大多数钱包也是一种软件,能够在区块链上做新的交易。
image.png然后让我们创建一个Wallet类来支持我们的公钥和私钥:
package noobchain;
import java.security.*;
public class Wallet {
public PrivateKey privateKey;
public PublicKey publicKey;
}
用公钥和私钥来做什么呢?
对于我们的"Noob"币来说,公钥将作为我们的地址。在与其他人交易的时候分享公钥是可以的。我们的私钥被用来签名我们的交易,来保证没有人可以用他们的私钥来花费我们的"Noob"币。用户必须保存他们自己的私钥!我们还将公钥和交易一起发送,他可以用来验证我们的签名是否有效、数据是否被修改。
image.png我们使用KeyPair类来生成我们的公钥和私钥。使用Elliptic-curve cryptography来生成KeyPairs。让我们在Wallet类中添加generateKeyPair()方法并在构造器中调用它。
package noobchain;
import java.security.*;
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);
}
}
}
现在我们的Wallet有一个轮廓了,让我们在交易中看看。
交易 & 签名
每个交易都携带以下数据:
· 发送者基金的公钥地址
· 接受者基金的公钥地址
· 要交易的价值/基金数量
· 输入:是交易之前的参考(证明发送方有基金去交易)
· 输出:表明相关地址接受到的交易量/价值。(这些输出会在一个新的交易中被作为输入)
· 一个加密的签名,证明地址的所有者发送此交易只有一次,数据没有修改过(例如:防止第三方修改交易的数量)
接下来创建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
);
}
}
我们还应该有空的TransactionInput类和TransactionOutput类。不用担心我们将在之后补充它们。
我们的Transaction类还包含了相关的生成/验证签名的方法和验证交易的方法。
但是……?
签名的目的是什么?它怎样工作的?
签名有两个非常重要的任务在我们的区块链中:第一,它只允许所有者消费他们的虚拟币,第二,他们在新的区块被挖掘出之前防止其他人篡改已经提交的交易。(在切入点?at the point of entry)。
例如:Bob想要发送2个币给Sally,然后他们的钱包软件会生成这个交易,把它提交给矿工并传入到下一个区块中。矿工试图把2个币的收件人改成John。然而,幸运的是,Bob已经用他的私钥签名了交易数据,允许任何人验证交易数据是否被Bob用他的公钥修改了。(因为没有其他人的公钥就可以验证交易)。
我们可以从之前的Block类中看到,我们签名是一串字节数组,所以,让我们创建一个生成他们的方法。首先,我们需要一些辅助功能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类中,添加generateSianature()和verifySignature()方法。
当一个新当交易被添加到区块中到时候,签名将会由矿工去验证。
测试 Wallets 和 Signatures
现在我们几乎做了一半让我们测试一些东西。在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());
}
我们创建了两个wallets类walletA和walletB,打印walletA的私钥和公钥。生成一个交易并使用它的私钥进行签名。
你的输出应该是以下结果:
image.png(Time to pat your self on the back.)现在我们仅仅需要创建/验证输出和输入,然后把交易存储在区块链中。
如何拥有加密货币…?
为了拥有1个比特币,你必须收到1个比特币。账簿不是真的给你添加了1个比特币并且给发送者减少了1个比特币,然后交易输出1个比特币被发送到你的地址了。(交易本次输入参考于上一个交易的输出)。
你的钱包余额是发给你的所有未消费的交易输出的总和。
从这一点开始,我们将遵循比特币约定并调用未消费的交易输出: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;
}
}
TransactionOutput类:
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);
}
}
交易输出将显示发送给每一方后的最终金额。当在新交易中作为输入饮用时,将会作为你要发送硬币的证明。
输入 & 输出2:进行交易…
区块链可能接受许多交易并且这条链可能是非常非常长的。它可能需要很长的时间来处理新的交易,因为我们必须找到并检查它的输入。为了解决这个问题,我们额外收集所有可用作输入的为消费的交易。在NoobChain类中添加如下UTXO集合:
public class NoobChain {
public static ArrayList<Block> blockchain = new ArrayList<Block>();
public static HashMap<String,TransactionOutputs> UTXOs = new HashMap<String,TransactionOutputs>(); //list of all unspent transactions.
public static int difficulty = 5;
public static Wallet walletA;
public static Wallet walletB;
public static void main(String[] args) {
是时候深入了解细节了…
好的,是时候让我们把所有东西放在一起来处理交易了,在我们的Transaction类中使用boolean类型的processTransaction方法来处理细节:
//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列表中丢弃输入,意味着交易输出只能用作输入一次。因此必须使用输入的完整值,所以发送者发送"change"反馈给自己。
image.png最后我们修改以下Wallet类:
· 收集我们的余额(通过循环UTXO列表检查交易输出isMine)
· 生成我们的交易…
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;
}
}
给我们的区块添加交易
现在我们有一个可用的交易系统了,我们需要把它结合在我们的区块链中。我们应用一个交易的集合替换掉无用的数据。但是在单个区块中可能有1000个交易,我们的hash计算包含的内容太多了…不过不用担心,我们可以使用交易的merkleroot。
让我们在StringUtil类中添加一个helper方法去生成merkleroot。
//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类中做一些修改(merkroot的修改):
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类构造器,因为我们不再需要String类型的data,并在计算hash的方法中添加上merkleroot。
我们有一个boolean的addTransaction方法添加交易并返回这个交易是否添加成功。
最后一步
我们应该测试一下用钱包发送币,并修改我们区块链的校验方法。首先我们需要一个方法把新币引入进来。有很多办法去创造新币,例如比特币中:矿工把交易包含在内作为每个开采新区块的奖励。
就目前而言,我们将在第一个区块(创世块)中释放我们所希望拥有的所有货币。就像比特币一样,我们会对创世块进行硬编码。
修改我们的NoobChain类为他所需要的:
· 一个创世块释放了100个Noob币给walletA
· 考虑到交易的账户修改链的校验
· 用一些交易测试来看看每个东西是不是在正常运行
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);
}
}
输出为以下这样:
image.png在你的区块链中钱包现在可以安全的发送货币了,只要他们有资金发送即可。这意味着你拥有资金的本地加密货币了。
恭喜你完成了区块链上的交易
你已经成功的创建了自己的加密货币(有点!sort of!),你的区块链现在的状态是:
· 允许用户用new wallet()创建钱包。
· 提供有用Elliptic-Curve加密的公钥和私钥的钱包。
· 通过使用数字签名算法证明所有权,确认资金转移。
· 允许用户在你的区块链上使用交易:Block.addTransaction(walletA.sendFunds( walletB.publicKey, 20))
你可以在这里下载作者的源码