原用Java编写第一个区块链

Posted zacky31

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了原用Java编写第一个区块链相关的知识,希望对你有一定的参考价值。

  这篇文章将去介绍如何使用区块链进行交易。

 【本文禁止任何形式的全文粘贴式转载,本文来自 zacky31 的随笔】

目标:

  在上一篇文章中,我们已经创建了一个可信任的区块链。但是目前所创建的链中包含的有用信息还是比较少的。今天,我将会用交易信息来替换之前的 data 内容,我将会创建一个简单的加密货币,叫作 “noobcoin”。

前提:

  • 已经了解了区块链基本知识
  • 用到 GSONbounceycastle 

开始吧

  在加密货币中,货币的所有权将会以交易的方式被传递,参与交易的人将会有一个地址,用来发送和接收交易金额。

  上图,反映了一次交易过程。

  首先,我们创建一个 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 为钱包创建公钥和私钥
  • 通过数字签名算法来证明所有权这样可以安全的转移资金
  • 最后允许所有用户可以在区块链中增加交易

以上是关于原用Java编写第一个区块链的主要内容,如果未能解决你的问题,请参考以下文章

用Java编写第一个区块链

CTF的区块链入门资料

手把手教你区块链java开发智能合约nft-第五篇(铸造第一个NFT)

第111篇 在区块链浏览器上发布合约源码

我居然用Java构建了一个属于我自己的区块链

区块链开发编写调试第一个以太坊智能合约