這篇文章將去介紹如何使用區塊鏈進行交易。
【本文禁止任何形式的全文粘貼式轉載,本文來自 zacky31 的隨筆】
目標:
在上一篇文章中,我們已經創建了一個可信任的區塊鏈。但是目前所創建的鏈中包含的有用信息還是比較少的。今天,我將會用交易信息來替換之前的 data 內容,我將會創建一個簡單的加密貨幣,叫作 “noobcoin”。
前提:
- 已經了解了區塊鏈基本知識
- 用到 GSON 和 bounceycastle
開始吧
在加密貨幣中,貨幣的所有權將會以交易的方式被傳遞,參與交易的人將會有一個地址,用來發送和接收交易金額。
上圖,反映了一次交易過程。
首先,我們創建一個 Wallet 類,用來存放公鑰和私鑰。
import java.security.PrivateKey; import java.security.PublicKey; public class Wallet {
public PrivateKey privateKey; public PublicKey publicKey; }
那么,創建好的公鑰和私鑰是干嘛用的呢?
其實,公鑰對於貨幣來說就是我們所需要的地址。在每次交易過程中可以共享給交易方的。我們的私鑰是用來加密每次的交易,這樣保證了擁有私鑰的人才能夠進行交易。私鑰只能被自己說知道!同時,公鑰伴隨着交易時傳遞給交易方,可以用來去驗證簽名,判斷數據是否被篡改。
接下來,就需要生成公鑰和私鑰鍵值對。我將會使用橢圓曲線加密來生成。那就來修改一下 Wallet 類吧。
import java.security.*; import java.security.spec.ECGenParameterSpec; public class Wallet { public PrivateKey privateKey; public PublicKey publicKey; public Wallet() { generateKeyPair(); } private void generateKeyPair() { try { Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); KeyPairGenerator keyGen = KeyPairGenerator.getInstance("ECDSA", "BC"); SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); ECGenParameterSpec ecSpec = new ECGenParameterSpec("prime192v1"); keyGen.initialize(ecSpec, random); KeyPair keyPair = keyGen.generateKeyPair(); privateKey = keyPair.getPrivate(); publicKey = keyPair.getPublic(); } catch (Exception e) { throw new RuntimeException(e); } } public static void main(String[] args) { Wallet wallet = new Wallet(); System.out.println(wallet.publicKey); System.out.println(wallet.privateKey); } }
到目前為止,我們已經創建好了錢包,去考慮如何設計交易吧。
每一次交易過程中,都需要攜帶一下數據:
- 付款方的公鑰
- 收款方的公鑰
- 交易金額
- 輸入,之前的交易參考,證明付款方有資金進行交易
- 輸出,顯示這次交易中接收的相關地址
- 加密簽名,用來證明交易過程中數據未被篡改
那趕緊去創建一個 Transaction 類吧。
import java.security.PublicKey; import java.util.ArrayList; public class Transcation { public String transcationId; public PublicKey sender; public PublicKey reciepient; public float value; public byte[] signature; public ArrayList<TranscationInput> inputs = new ArrayList<TranscationInput>(); public ArrayList<TranscationOutput> outputs = new ArrayList<TranscationOutput>(); private static int sequence = 0; public Transcation(PublicKey from, PublicKey to, float value, ArrayList<TranscationInput> inputs) { this.sender = from; this.reciepient = to; this.value = value; this.inputs = inputs; } private String calulateHash() { sequence++;
return StringUtil.applySha256( StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient) + Float.toString(value) + sequence ); } }
目前為止,程序會報錯,一會,我將會創建 TransactionInput 類 和 TransactionOutput 類。同時,交易類中需要包含生成和驗證簽名的方法以及驗證交易的方法。不過,這些都得稍等一下。
考慮一下簽名的目的以及其工作方式?
簽名在區塊鏈中起到兩個重要的作用:首先,簽名保證了付款方的消費;其次,阻止了其他人篡改已發生的交易。這就是利用私鑰加密數據,公鑰去驗證其可靠性。
那接下來就開始去做這些事情吧。在此之前,在 StringUtils 類中,添加兩個方法。applyECDSASig() 用來生成簽名,verifyECDSASig() 用來去驗證簽名。
public static byte[] applyECDSASig(PrivateKey privateKey, String input) { Signature dsa; byte[] output = new byte[0]; try { dsa = Signature.getInstance("ECDSA", "BS"); 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; } 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() 和 verifiySignatur() 方法。
public void generateSignature(PrivateKey privateKey) { String data = StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient); signature = StringUtil.applyECDSASig(privateKey, data); } public boolean verfiySignature() { String data = StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient); return StringUtil.verifyECDSASig(sender, data, signature); }
已經完成了一大半了,先去 Chain 類中去測試一下吧。
import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; import java.util.ArrayList; public class Chain { public static ArrayList<Block> blockChain = new ArrayList<>(); public static int difficulty = 5; public static Wallet walletA; public static Wallet walletB; public static void main(String[] args) { Security.addProvider(new BouncyCastleProvider()); walletA = new Wallet(); walletB = new Wallet(); System.out.println("Private and Public keys:"); System.out.println(StringUtil.getStringFromKey(walletA.privateKey)); System.out.println(StringUtil.getStringFromKey(walletA.publicKey)); Transcation transcation = new Transcation(walletA.publicKey, walletB.publicKey, 5, null); transcation.generateSignature(walletA.privateKey); System.out.println("Is signature verified"); System.out.println(transcation.verfiySignature()); } }
我們可以看到,成功生成了私鑰和公鑰,並能夠去驗證真實性。
考慮這樣一個場景,當你擁有一個比特幣,你必須前面收到過一個比特幣,比特幣的賬本不會在你的賬戶中增加一個比特幣也不會從消費者那里減去一個比特幣,消費者只能指向他或她之前接受過一個比特幣,所以一個交易輸出被創建用來顯示一個比特幣發送給你的地址(交易的輸入指向前一個交易的輸出)。
從這一個點出發,我們會依照比特幣中的說明,把所有未使用的交易輸出稱為 UTXO。
那么去創建一個 TransactionInput 類。
public class TransactionInput { public String transactionOutputId; public TransactionOutput UTXO; public TransactionInput(String transactionOutputId) { this.transactionOutputId = transactionOutputId; } }
以及 TransactionOutputs 類。 交易的輸出會顯示從這次交易中給每一方最終發送的金額,從而在新的交易中被引用未輸入,作為證明你可以發送的金額數量。
import java.security.PublicKey; public class TransactionOutput { public String id; public PublicKey reciepient; public float value; public String parentTransactionId; 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); } public boolean isMine(PublicKey publicKey) { return (publicKey == reciepient); } }
在鏈條中的區塊會收到很多交易信息,所以區塊鏈會非常非常的長,這就會花費很長時間來處理一個新的交易因為我們必須尋找和檢查它的輸入,為了繞過這個我們保存了一個額外的集合稱之為為使用的交易作為可用的輸入,所以在主函數中增加一個集合稱為 UTXO。
那現在修改一下 Transaction 類。添加處理交易的方法。
public boolean processTransaction() { if(verifySignature() == false) { System.out.println("#Transaction Signature failed to verify"); return false; } for(TransactionInput i : inputs) { i.UTXO = Chain.UTXOs.get(i.transactionOutputId); } if(getInputsValue() < Chain.minimumTransaction) { System.out.println("Transaction Inputs too small: " + getInputsValue()); System.out.println("Please enter the amount greater than " + Chain.minimumTransaction); return false; } float leftOver = getInputsValue() - value; transactionId = calulateHash(); outputs.add(new TransactionOutput( this.reciepient, value,transactionId)); outputs.add(new TransactionOutput( this.sender, leftOver,transactionId)); for(TransactionOutput o : outputs) { Chain.UTXOs.put(o.id , o); } for(TransactionInput i : inputs) { if(i.UTXO == null) continue;
Chain.UTXOs.remove(i.UTXO.id); } return true; } public float getInputsValue() { float total = 0; for(TransactionInput i : inputs) { if(i.UTXO == null) continue;
total += i.UTXO.value; } return total; }
這個方法中我們執行了一些檢查來確保交易是有效的,我們收集了輸入來產生輸出,最重要的是,到了喂結束的時候,我們拋棄了輸入在我們的 UTXO 列表,這就意味着一個可以使用的交易輸出必須之前一定是輸入,所以輸入的值必須被完全使用,所以付款人必須改變它們自身的金額狀態。這和我們實際的情況非常類似,當你給出5元然后花費了1元,就會找給你4元。
最后,更新一下 Wallet ,添加獲取余額等方法。你可以隨意添加一些其他功能都您的錢包中去,比如保存您的交易的歷史記錄等等。
public float getBalance() { float total = 0; for (Map.Entry<String, TransactionOutput> item: Chain.UTXOs.entrySet()){ TransactionOutput UTXO = item.getValue(); if(UTXO.isMine(publicKey)) {
UTXOs.put(UTXO.id,UTXO);
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; }
現在,已經有一個交易系統了,需要將其放到區塊鏈中。我們把區塊中的一些沒有用的信息替換成交易的列表,但是在一個單一的區塊中可能存放了1000個交易,這就會導致大量的 hash 計算,不用擔心在這里我們使用了交易的merkle 樹,稍后你會看到。讓我們增加一個幫助方法來創建 merkleroot 在 StringUtils 類中
public static String getMerkleRoot(ArrayList<Transaction> transactions) { int count = transactions.size(); ArrayList<String> previousTreeLayer = new ArrayList<>(); for (Transaction transaction : transactions) { previousTreeLayer.add(transaction.transactionId); } ArrayList<String> treeLayer = previousTreeLayer; while (count > 1) { treeLayer = new ArrayList<>(); 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; } public static String getDificultyString(int difficulty) { return new String(new char[difficulty]).replace('\0', '0'); }
Block 類代碼如下:
import java.util.ArrayList; import java.util.Date; public class Block { public String hash; public String previousHash; public String merkleRoot; public String data; public ArrayList<Transaction> transactions = new ArrayList<>(); public long timeStamp; public int nonce; public Block(String previousHash) { this.previousHash = previousHash; this.timeStamp = new Date().getTime(); this.hash = calculateHash(); } public Block(String data, String previousHash) { this.data = data; this.previousHash = previousHash; this.timeStamp = new Date().getTime(); this.hash = calculateHash(); } public String calculateHash() { String calculatedhash = StringUtil.applySha256( previousHash + Long.toString(timeStamp) + Integer.toString(nonce) + merkleRoot ); return calculatedhash; } public void mineBlock(int difficulty) { merkleRoot = StringUtil.getMerkleRoot(transactions); String target = StringUtil.getDificultyString(difficulty); while (!hash.substring(0, difficulty).equals(target)) { nonce++; hash = calculateHash(); } System.out.println("Block Mined!!! : " + hash); } public boolean addTransaction(Transaction transaction) { 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; } }
完整的測試一下。
之前我們已經測試過從錢包中發送貨幣,然后修改區塊鏈進行有效性檢查。但是首先讓我們創建一些新的貨幣吧,有很多方法來創建新的貨幣,以比特幣中的區塊鏈舉個例子:挖礦者挖礦成功就會得到一個獎勵。但在這里我們只希望在創世紀區塊中釋放貨幣。就像比特幣中一下,所以我們修改我們的主函數以達到下面的目的。
- 創世紀區塊發布100個貨幣給 walletA
- 修改區塊鏈進行有效性驗證該賬戶是否進行了交易
- 進行測試看是否一切都在運行中
Chain 類。
import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; import java.util.ArrayList; import java.util.HashMap; public class Chain { public static ArrayList<Block> blockchain = new ArrayList<>(); public static HashMap<String, TransactionOutput> UTXOs = new HashMap<>(); 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) { Security.addProvider(new BouncyCastleProvider()); walletA = new Wallet(); walletB = new Wallet(); Wallet coinbase = new Wallet(); genesisTransaction = new Transaction(coinbase.publicKey, walletA.publicKey, 100f, null); genesisTransaction.generateSignature(coinbase.privateKey); genesisTransaction.transactionId = "0"; genesisTransaction.outputs.add(new TransactionOutput(genesisTransaction.reciepient, genesisTransaction.value, genesisTransaction.transactionId)); UTXOs.put(genesisTransaction.outputs.get(0).id, genesisTransaction.outputs.get(0)); 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 void addBlock(Block newBlock) { newBlock.mineBlock(difficulty); blockchain.add(newBlock); } public static Boolean isChainValid() { Block currentBlock; Block previousBlock; String hashTarget = new String(new char[difficulty]).replace('\0', '0'); HashMap<String, TransactionOutput> tempUTXOs = new HashMap<>(); tempUTXOs.put(genesisTransaction.outputs.get(0).id, genesisTransaction.outputs.get(0)); for (int i = 1; i < blockchain.size(); i++) { currentBlock = blockchain.get(i); previousBlock = blockchain.get(i - 1); if (!currentBlock.hash.equals(currentBlock.calculateHash())) { System.out.println("#Current Hashes not equal"); return false; } if (!previousBlock.hash.equals(currentBlock.previousHash)) { System.out.println("#Previous Hashes not equal"); return false; } if (!currentBlock.hash.substring(0, difficulty).equals(hashTarget)) { System.out.println("#This block hasn't been mined"); return false; } 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; } }
運行結果。
現在錢包可以在區塊鏈中安全的發送金額,當錢包擁有金額時才可以發送給別人。這也就意味着你擁有了你自己的加密貨幣。
總結一下,我們在區塊鏈中實現了:
- 允許所有用戶創建錢包
- 利用 ECDSA 為錢包創建公鑰和私鑰
- 通過數字簽名算法來證明所有權這樣可以安全的轉移資金
- 最后允許所有用戶可以在區塊鏈中增加交易