Part2 :使用 Java 创建你的第一个区块链[译]

2019-07-24  本文已影响0人  风雪围城

 本篇是该系列的第二篇,你可以在这里找到第一篇。原文链接在此
 在第本篇,我们将

 上面的这些过程,其实就产生了我们自己的加密货币。
 在上一篇文章中,我们有了一个可验证的、基本的区块链。但是,我们的链中仅仅存储了一些无用的信息。今天,我们将把这些无用的信息替换为交易数据。这允许我们可以创建一个简单的加密货币,我们称之为 “NoobCoin”。
 本文中,还将使用 Bouncy CastleGSON 库。

1. 准备钱包

 在加密货币中,货币的所有权在区块链上以交易的形式流转,交易者拥有一个可以地址可以转入转出。因此,对于一个钱包而言,至少需要能够存储这些地址。更多的,钱包也可以作为一个软件用以在区块链上产生新的交易。

transaction.png

 现在,让我们俩创建一个持有我们公钥和私钥的 Wallet 类:

package noobchain;

import java.security.PrivateKey;
import java.security.PublicKey;

public class Wallet {
    public PrivateKey privateKey;
    public PublicKey publicKey ;
}

公钥和私钥有什么用?
 对于我们的加密货币 noobcoin 来说,公钥扮演者我们的钱包地址的角色,我们可以随意分享自己的公钥。私钥,是用来 签署(sign) 交易的,没有人能花费我们的 noobcoin,除非,他拥有我们的私钥。所以,用户的私钥一定要被保管好。公钥部分通常会和交易一起发送的,用以验证交易前面是否合法,交易内容是否被篡改。

key.png

 公钥和私钥通过 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. 交易和签名

 每一个交易,都要携带一定的数据:

 现在,我们来创建一个 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 仅有一次机会作为输入...所有输入值,都将在本次交易中被使用,如果没有使用完,剩余的部分会返回到自身中。

image
 最后,让我们来更新下钱包:

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 类:

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 上下载到这个项目。

参考

哈希树

上一篇下一篇

猜你喜欢

热点阅读