本文我们将会做以下事情:
1、创建一个钱包(wallet)。
2、使用我们的前面创建的区块链发送一笔签名的交易出去。
3、还有其他更叼的事情等等。
听起来是不是就让人心动。
最后的结果就是我们有了自己的加密货币,是的,crypto coin。
前面我们已经构建了一个基本的区块链。但目前这个区块链的区块中的message是一些没有什么实际用途和意义的数据。本文我们就尝试让区块中能够存储一些交易数据(一个区块中可以存储多笔交易数据),这样我们就可以创建自己的加密货币(当然还是一个简单的),这里给我们的货币起个名字叫:“NoobCoin”。
1、创建钱包
在加密货币(crypto-currencies)中,货币所有权被作为交易(transaction)在区块链上进行转移,参与者有一个收发资金的地址。
好,现在让我们创建一个钱包(Wallet)来持有pubkey和private key:
import java.security.*;
public class Wallet { public PrivateKey privateKey; public PublicKey publicKey; }
公钥和私钥的用途是什么?
对于我们的“noobcoin”,公钥(public key)就是我们的一个地址,address。
可以与其他人共享这个公钥,来接受支付。我们的私钥是用来签署(sign)我们的交易(transaction),所以除了私钥(private key)的所有者,没有人可以花我们的钱。用户将不得不对自己的私钥保密!我们还将公钥与交易(transaction)一起发送,它可以用来验证我们的签名是否有效,并且数据没有被篡改。
私钥用于对我们不希望被篡改的数据进行签名。公钥用于验证签名。
我们在一个KeyPair中生成我们的私钥和公钥。这里使用
Elliptic-curve加密来生成KeyPair。现在我们就去Wallet类中添加一个方法generateKeyPair(),然后在构造函数中调用它:
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 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); } } }
这个方法就是负责生成公钥和私钥。具体就是通过Java.security.KeyPairGenerator来生成Elliptic Curve key对。然后把这个方法加入到Wallet的构造函数中。
现在我们已经有了一个大体的钱包类。接下来我们看看交易(transaction)类。
2. 交易和签名(Transactions & Signatures)
每笔交易将会携带如下数据:
1、资金发送方的公钥(地址)。
2、资金接收方的公钥(地址)。
3、要转移的资金金额。
4、输入(Inputs)。这个输入是对以前交易的引用,这些交易证明发件人拥有要发送的资金。
5、输出(Outputs),显示交易中收到的相关地址量。(这些输出作为新交易中的输入引用)
6、一个加密签名。证明地址的所有者是发起该交易的人,并且数据没有被更改。(例如:防止第三方更改发送的金额)
让我们创建交易类吧:
import java.security.*; import java.util.ArrayList; public class Transaction { public String transactionId; //Contains a hash of transaction* public PublicKey sender; //Senders address/public key. public PublicKey reciepient; //Recipients address/public key. public float value; //Contains the amount we wish to send to the recipient. 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; } 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)类还应该包含生成/验证签名和验证交易的相关方法。
注意这里,既有验证签名的方法,也有验证交易的方法。
但是,稍等...
先来说说签名的目的是什么?它们是如何工作的?
签名在我们的区块链上执行两个非常重要的任务:首先,它能只允许所有者使用其货币;其次,在新区块被挖掘之前,它能防止其他人篡改其提交的交易(在入口点)。
私钥用于对数据进行签名,公钥可用于验证其完整性。
例如:Bob想给Sally发送2个NoobCoin,然后他们的钱包软件生成了这个交易并将其提交给矿工,以便将其包含在下一个块中。一名矿工试图将2枚货币的接收人改为Josh。不过,幸运的是,Bob已经用他的私钥签署了交易数据,允许任何人使用Bob的公钥去验证交易数据是否被更改(因为没有其他任何人的公钥能够验证交易)。
可以(从前面的代码块中)看到我们的签名就是一堆字节,所以现在创建一个方法来生成签名。我们首先需要的是StringUtil类中的几个helper方法:
//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()); }
不要过分担心这些方法具体的逻辑。你只需要知道的是:applyECDSASig方法接收发送方的私钥和字符串输入,对其进行签名并返回字节数组。verifyECDSASig接受签名、公钥和字符串数据,如果签名是有效的,则返回true,否则false。getStringFromKey从任意key返回编码的字符串。
现在让我们在Transaction类中使用这些签名方法,分别创建generateSignature()和verifiySignature()方法:
public void generateSignature(PrivateKey privateKey) { String data = StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient) + Float.toString(value) ; signature = StringUtil.applyECDSASig(privateKey,data); } public boolean verifySignature() { String data = StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient) + Float.toString(value) ; return StringUtil.verifyECDSASig(sender, data, signature); }
在现实中,你可能希望签署更多的信息,比如使用的输出(outputs)/输入(inputs)和/或时间戳(time-stamp)(现在我们只签署了最基本的)。
在将新的交易添加到块中时,矿工将对签名进行验证。
当我们检查区块链的合法性的时候,其实也可以检查签名。
3.测试钱包(Wallets)和签名(Signatures)
现在我们差不多完成了一半了,先来测试下已经完成的是不是可以正常工作。在NoobChain类中,让我们添加一些新变量并替换main方法的内容如下:
import java.security.Security; import java.util.ArrayList; 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.verifySignature()); } }
可以发现我们使用了boncey castle来作为安全实现的提供者。
还创建了两个钱包,钱包A和钱包B,然后打印了钱包A的私钥和公钥。还新建一笔交易。然后使用钱包A的公钥对这笔交易进行了签名。
输出:
嗯,签名验证是true,符合期望。
现在是时候小开心一下了。现在我们只需要创建和校验输出(outputs)和输入(inputs)然后把交易存储到区块链中。
4. 输入(Inputs)与输出(Outputs)1:加密货币是如何拥有的…
如果你想拥有1个比特币,你必须收到1个比特币。总账不会真的给你添加一个比特币,从发送者那里减去一个比特币,发送者提到他/她以前收到一个比特币,然后创建一个交易输出,显示1比特币被发送到你的地址。(交易输入是对以前交易输出的引用。)
你的钱包余额是所有发送给你的未使用的交易输出的总和。
ps:这里略微有点绕,总之你就记住进账和出账这回事情。
从现在开始,我们将遵循比特币惯例并调用未使用的交易输出: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的值。transactionOutputId将用于查找相关的TransactionOutput,从而允许矿工检查你的所有权。
下面是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); } }
交易输出将显示从交易发送到每一方的最终金额。当在新的交易中作为输入引用时,它们将作为你要发送的货币的证明,能够证明你有钱可发送。
5. 输入(Inputs)与输出(Outputs)2:处理交易……
链中的块可能接收到许多交易,而区块链可能非常非常长,处理新交易可能需要数亿年的时间,因为我们必须查找并检查它的输入。要解决这个问题,我们就需要存在一个额外的集合(collection)来保存所有未使用的可被作为输入(inputs)的交易。在下面的ImportChain类中,添加一个所有UTXO的集合:
public class ImportChain { 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) {
现在我们把之前的那些实现放在一起来处理一笔交易吧。先在Transaction类中的添加一个方法processTransaction:
public boolean processTransaction() { if(verifySignature() == false) { System.out.println("#Transaction Signature failed to verify"); return false; } //Gathers transaction inputs (Making sure they are unspent): for(TransactionInput i : inputs) { i.UTXO = ImportChain.UTXOs.get(i.transactionOutputId); } //Checks if transaction is valid: if(getInputsValue() < ImportChain.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) { ImportChain.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 ImportChain.UTXOs.remove(i.UTXO.id); } return true; }
还添加了getInputsValue方法。使用此方法,我们执行一些检查以确保交易是有效的,然后收集输入并生成输出。(要了解更多信息,请参阅代码中的注释行)。
重要的是,在最后,我们从UTXO的列表中删除input,这意味着交易输出只能作为一个输入使用一次…而且必须使用完整的输入值,因为发送方要将“更改”返回给自己。
红色箭头是输出。请注意,绿色输入是对以前输出的引用。
最后,让我们将钱包类更新为:
可以汇总得到的余额(通过循环遍历UTXO列表并检查事务输出是否为Mine())
并可以生成交易。
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>(); 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 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); } } public float getBalance() { float total = 0; for (Map.Entry<String, TransactionOutput> item: ImportChain.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; } public Transaction sendFunds(PublicKey _recipient,float value ) { if(getBalance() < value) { System.out.println("#Not Enough funds to send transaction. Transaction Discarded."); return null; } 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。
然而,在一个块中可能有1000个交易,太多的交易不能包括在散列计算中……
没事,别担心,我们可以使用交易的merkle根,就是下面的那个getMerkleRoot()方法。
现在在StringUtils中添加一个helper方法getMerkleRoot():
public static String getMerkleRoot(ArrayList<Transaction> transactions) { int count = transactions.size(); List<String> previousTreeLayer = new ArrayList<String>(); for(Transaction transaction : transactions) { previousTreeLayer.add(transaction.transactionId); } List<String> treeLayer = previousTreeLayer; while(count > 1) { treeLayer = new ArrayList<String>(); for(int i=1; i < previousTreeLayer.size(); i+=2) { 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构造函数,因为不再需要传入字符串数据(还记得上集中我们的Block构造函数传入了一个data的字符串,这里我们往块里添加的是交易,也就是transaction),并且在计算哈希方法中包含了merkle根。
并且新增了addTransaction方法来添加一笔交易,并且只有在交易被成功添加时才返回true。
ok,我们的区块链上交易所需的每个零部件都实现了。是时候运转一下了。
7. 大结局
现在我们开始测试吧。发送货币进出钱包,并更新我们的区块链有效性检查。
但首先我们需要一个方法来引入新的币。有许多方法可以创建新的币,比如,在比特币区块链上:矿工可以将交易持有在自己手里,作为对每个块被开采的奖励。
这里,我们将只发行(release)我们希望拥有的所有货币,在第一个块(起源块)。就像比特币一样,我们将对起源块进行硬编码。
现在把ImportChain类更新,包含如下内容:
import java.security.Security; import java.util.ArrayList; import java.util.HashMap; //import java.util.Base64; //import com.google.gson.GsonBuilder; public class ImportChain { 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.verifySignature()) { 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); } }
运行结果:
代码链接:https://github.com/importsource/blockchain-samples-transaction/tree/master
本文分享自微信公众号 - ImportSource(importsource),作者:贺卓凡
原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。
原始发表时间:2018-03-05
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句