搭建你的第一個區塊鏈網絡(四)

前一篇文章: 搭建你的第一個區塊鏈網絡(三)

UTXO

組成部分

UTXO是比特幣中一個重要的概念,這一節我們來實現一個簡單的UTXO。我們把UTXO的組成部分分為以下三點:

  • UTXOId: 標識該UTXO
  • TxInput: 交易輸入,即coin的輸入地址以及金額
  • TxOutput: 交易輸出,即coin的輸出地址以及金額

其中TxInputTxOutput分別具有以下幾個屬性:

TxInput: 交易輸入

  • preTxId: 指向的前一個UTXO的id
  • value: 輸入的金額
  • unLockScript: 解鎖腳本

其中交易輸入需要引用之前的UTXO的輸出。這樣很容易知道當前的交易輸入的金額是由之前的哪一筆交易中的交易輸出的金額傳遞的。
保證每一筆未消費的金額都可以找到它的源地址。
解鎖腳本的作用是用於解鎖當前交易輸入所引用的交易輸出的。因為每一筆金額都有所屬,被鎖定在某一個地址上面。只有該金額的所有者才具有權限消費進行消費。而解鎖腳本一般都是一個數字簽名。

TxOutput 交易輸出

  • value :輸出的金額
  • lockScript: 鎖定腳本

每當一筆coin被轉移,都會被鎖定在一個地址上面,因此鎖定腳本一般都是一個地址。

對於每一筆UTXO,輸入的金額一定是等於輸出的金額的。另外UTXO有一個特點,就是不能夠只花費其中一部分。而是需要全部消費,而多餘的再返還給原地址。
比如用戶a具有10個coin被鎖定在一個UTXO中,如果a需要轉賬給b5個coin,那麼需要將10個coin全部花費掉,其中5個coin輸出到b的地址,剩餘的5個coin輸出到a的地址。
因此一筆UTXO可以有多個交易輸出,同時也可以有多個輸入。

大致概念介紹差不多了,我們來實現它:

#TxInput.java
//因為我們採用的序列化保存區塊,而該數據需要寫入區塊,因此需要實現Serializable接口
public class TxInput implements Serializable{
    private static final long serialVersionUID = 1L;
    // 所引用的前一個交易ID
    public String preTxId;
    // 該輸入中包含的coin
    public int values;
    // 解鎖腳本 通常為數字簽名
    public String unLockScript;
    public TxInput(String txId, TxOutput top, Wallet wallet) throws Exception {
        //對引用的Txoutput中的地址進行簽名,用於解鎖引用的TxOutPut.
        this.unLockScript = wallet.sign(top.getLockScript());
        //記錄引用的上一個交易ID
        this.preTxId = txId;
        //coin值等於引用的Txoutput的coin值
        this.values = top.value;
    }
}

接下來是交易輸出:

#TxOutput.java

@Getter
public class TxOutput implements Serializable{
    //同理需要實現Serializable接口
    private static final long serialVersionUID = 1L;
    // 交易輸出的coin值。
    public int value;
    //鎖定腳本 通常為地址
    public String lockScript;
    public TxOutput(int value,String address){
        this.value = value;
        this.lockScript = address;
    }
}

最後是UTXO的實現: 我們使用Transaction進行表示。

#Transaction.java

@Getter
@Setter
public class Transaction implements Serializable{
    //為了後期調試方便,引入了log4j的包,導入方法和之前一樣
    private transient static final Logger LOGGER = Logger.getLogger(Transaction.class);
    private static final long serialVersionUID = 1L;
    //COINBASE之後再進行解釋
    private transient static final int COINBASE = 50;
    //UTXOId
    public String txId;
    // 交易輸入的集合
    public ArrayList<TxInput> tips;
    // 交易輸出的集合 String:address
    public HashMap<String, TxOutput> tops;

    private Transaction() {
        #這裡只創建了保存交易輸出的集合,因為涉及到Coinbase,暫時先不創建ArrayList
        this.tops = new HashMap<>(4);
    }
    @Override
    public String toString(){
        return JSONObject.toJSONString(this);
    }
}

log4j日誌包:

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

這裡為了方便起見分別使用ArrayListHashMap存儲交易輸入與輸出.

創建UTXO

接下來是創建UTXO的核心方法,比較複雜,我們先來分析一下:
首先傳入的參數需要有: 發送coin的源地址,發送coin的目的地址,coin的值。
返回值為一個Transaction實例。

接下來分析如何創建UTXO:

  1. 首先需要遍歷整個區塊鏈,查找到所有未消費的被鎖定在源地址的交易輸出。
  2. 將查找到的所有包含符合條件的交易輸出的UTXO記錄在集合中。
  3. 遍歷該集合,將每一筆UTXO中未消費的輸出相加,直到滿足轉賬金額或者是統計完全部UTXO。
  4. 將統計的每一筆UTXO中交易輸出創建為新的交易輸入用於消費。
  5. 判斷是否coin值剛好等於需要轉賬的coin。如果相等則創建一個交易輸出將coin轉賬到目的地址。
  6. 如果有多餘的則再創建一個交易輸出返回多餘的coin到源地址。

OK,分析完了可以開發了:

#Transaction.java
    public static Transaction newUTXO(String fromAddress, String toAddress, int value)
            throws NoSuchAlgorithmException, Exception {
        //第一步遍歷區塊鏈統計UTXO
        Transaction[] txs = Blockchain.getInstance().findAllUnspendableUTXO(fromAddress);
        if (txs.length == 0) {
            LOGGER.info("當前地址"+fromAddress+"沒有未消費的UTXO!!!");
            throw new Exception("當前地址"+fromAddress+"沒有未消費的UTXO,交易失敗!!!");
        }
        TxOutput top;
        // 記錄需要使用的TxOutput
        HashMap<String, TxOutput> tops = new HashMap<String, TxOutput>();
        int maxValue = 0;
        // 遍歷交易集合
        for (int i = 0; i < txs.length; i++) {
            // 查找包括地址為fromAddress的TxOutput
            if (txs[i].tops.containsKey(fromAddress)) {
                top = txs[i].tops.get(fromAddress);
                // 添加進Map
                tops.put(txs[i].txId, top);
                // 記錄該TxOutput中的value
                maxValue += top.value;
                // 如果大於需要使用的則退出
                if (maxValue >= value) {
                    break;
                }
            }
        }
        // 是否有足夠的coin
        if (maxValue >= value) {
            // 創建tx
            Transaction t = new Transaction();
            t.tips = new ArrayList<TxInput>(tops.size());
            // 遍歷所有需要用到的Txoutput
            tops.forEach((s, to) -> {
                // 變為TxInput
                try {
                    t.tips.add(new TxInput(s, to, Wallet.getInstance()));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            //如果值不均等
            if(maxValue>value){
                //創建TxOutput返還多餘的coin
                top = new TxOutput(maxValue-value, Wallet.getInstance().getAddress());
                t.tops.put(top.getLockScript(), top);
            }
            //目的地址
            top = new TxOutput(value, toAddress);
            t.tops.put(top.getLockScript(), top);
            LOGGER.info("創建UTXO: "+t.toString());
            return t;
        }
        LOGGER.info("當前地址餘額不足!!,餘額為"+maxValue);
        throw new Exception("當前地址餘額不足!!,餘額為"+maxValue);
    }

統計未消費的UTXO

然後是另外一個核心方法,統計區塊鏈中符合條件的全部未消費的UTXO:
我們使用比較簡單易理解的方式,先統計地址匹配的所有的交易輸出。
然後統計所有的滿足條件的交易輸入。交易輸入需要滿足兩個條件:

  1. 地址是自己的地址
  2. 交易輸入中引用的UTXOid可以追溯到。

我們將符合條件的TxInput中引用的UTXOId在所有未消費的UTXO中匹配,
如果匹配到說明該UTXO已經被花費掉了,我們移除掉花費掉的UTXO,剩下的就是滿足條件的未消費的UTXO了。

#Blockchain.java
public Transaction[] findAllUnspendableUTXO(String address)
            throws FileNotFoundException, ClassNotFoundException, IOException {
        LOGGER.info("查找所有未消費的UTXO...............");
        HashMap<String, Transaction> txs = new HashMap<>();
        Block block = this.block;
        Transaction tx;
        // 從當前區塊向前遍歷查找UTXO txOutput
        do{
            //從區塊中獲取交易信息
            tx = block.getTransaction();
            // 如果存在交易信息,且TxOutput地址包含address
            if (tx != null && tx.getTops().containsKey(address)) {
                txs.put(tx.getTxId(), tx);
            }
            //指向前一個區塊
            block = block.getPrevBlock();
            //一直遍歷到創世區塊
        }while(block!=null && block.hasPrevBlock()) ;
        // 再遍歷一次查找已消費的UTXO
        block = this.block;
        do {
            tx = block.getTransaction();
            if (tx != null) {
                // 如果交易中的TxInput包含的交易ID存在於txs,移除
                tx.getTips().forEach(tip -> {
                    try {
                        //需要滿足兩個條件,一是Txinput中引用的UTXOId存在,說明該UTXO已經被使用了
                        //二是需要保證地址相同,確認該TxInput是coin所有者消費的
                        if (Wallet.getInstance().verify(address,tip.unLockScript) 
                                && txs.containsKey(tip.preTxId))
                            //滿足兩個條件則移除該UTXO
                            txs.remove(tip.preTxId);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
            }
            block = block.getPrevBlock();
        }while (block!=null && block.hasPrevBlock());
        //創建UTXO數組返回
        Transaction[] t = new Transaction[txs.size()];
        return txs.values().toArray(t);
    }

看這裡的代碼:

 if (Wallet.getInstance().verify(address,tip.unLockScript) && txs.containsKey(tip.preTxId))
    ...

首先驗證TxInput的解鎖腳本是否對我們錢包的地址進行簽名得到的,即驗證這一筆輸入是否是自己消費的。
如果是自己消費的然後比對UTXO的Id,如果相同則說明這筆UTXO已經被消費掉了。那麼需要移除它。
在錢包中添加一個新的方法,用於驗證解鎖腳本是否可以解鎖交易輸出。我們簡單採用哈希值匹配的方式模擬驗證。

#Wallet.java
    public boolean verify(String data,String sign) throws DecoderException, Exception {
        LOGGER.info("驗證簽名: "+data);
        String[] str = data.split("%%%");
        // 原文     encry(hash(原文))
        if(str.length!=2){
            return false;
        }
        String hash2 = Hex.encodeHexString(this.decrypt(str[1]));
        String hash3 = Util.getSHA256(data);
        if(hash3.equals(hash2)){
            LOGGER.info("簽名驗證成功!!");
            return true;
        }
        LOGGER.info("簽名驗證失敗!!");
        return false;
    }

更新區塊信息

加入了UTXO的概念,那我們需要更新區塊以及區塊鏈的屬性信息了。

#Block.java
@Getter
@Setter
public class Block implements Serializable{
    ...
    //當前區塊中的交易
    public Transaction transaction;
    ...
    //添加一個新的構造方法
    public Block(int blkNum,Transaction transaction,String prevBlockHash){
        this.blkNum = blkNum;
        this.transaction = transaction;
        this.prevBlockHash = prevBlockHash;
        this.timeStamp = Util.getTimeStamp();
    }
    ...
}

然後是區塊鏈,也要更新一個方法:

#Blockchain.java
public final class Blockchain {
    ...
    public Block addBlock(Transaction tx) throws IOException {
        int num = this.block.getBlkNum();
        Block block = new Block(num + 1, tx, this.block.curBlockHash);
        // 每次將區塊添加進區塊鏈之前需要計算難度值
        block.setNonce(Pow.calc(block));
        // 計算區塊哈希值
        String hash = Util.getSHA256(block.getBlkNum() + block.getData() + block.getPrevBlockHash()
                + block.getPrevBlockHash() + block.getNonce());
        block.setCurBlockHash(hash);
        // 序列化
        Storage.Serialize(block);
        this.block = block;
        LOGGER.info("當前區塊信息為:"+block.toString());
        return this.block;
    }
    ...
}

之前區塊中將字符串保存為區塊信息,我們更新為一筆交易。需要創建一筆交易才可以創建區塊。

Coinbase

關於UTXO,我們之前講到每一筆輸出都會對應着一個輸入,那麼第一筆被輸出的coin是哪裡來的呢,
在比特幣中,每產出一個區塊將會獎勵一定數量的BItcoin,稱為Coinbase。同理,我們這裡也實現它。
因此第一筆被輸出的coin來自於coinbase。我們將coinbase固定為50,正如之前設定的屬性:

#Transaction.java
    private transient static final int COINBASE = 50;

所以我們還需要一個生成coinbase的交易的構造方法:

#Blockchain.java
    public static Transaction newCoinBase() throws NoSuchAlgorithmException, Exception {
        Transaction t = new Transaction();
        t.tips = new ArrayList<>();
        t.tops.put(Wallet.getInstance().getAddress(), new TxOutput(COINBASE, Wallet.getInstance().getAddress()));
        LOGGER.info("創建Coinbase....."+t.toString());
        return t;
    }

可以看到,交易輸出的地址我們設置為錢包的地址。
比較簡單,接下來修改一下創世區塊的生成方法,將coinbase的交易添加進去。

#Blockchain.java
    private Block CrtGenesisBlock() throws NoSuchAlgorithmException, Exception {
        // Block block = new Block(1,"Genesis Block","00000000000000000");
        Block block = new Block(1, Transaction.newCoinBase(), "00000000000000000");
        ...
    }

測試

一切都完成了,測試一下:

#Test.java
public class Test {
    public static void main(String[] args) throws NoSuchAlgorithmException, Exception {
        Blockchain.getInstance().addBlock(
            Transaction.newUTXO(
                Wallet.getInstance().getAddress(), "address", 30));
        Blockchain.getInstance().addBlock(
            Transaction.newUTXO(
                "address", "address1", 20));
    }
}

分析一下測試用例:

Blockchain.getInstance()
#############################
#Blockchain.java CrtGenesisBlock()
Block block = new Block(1, Transaction.newCoinBase(), "00000000000000000");
#newCoinBase()
t.tops.put(Wallet.getInstance().getAddress(), new TxOutput(COINBASE, Wallet.getInstance().getAddress()));

首先獲取區塊鏈實例,因此創建了創始區塊,看上面的代碼我們可以知道創建了一筆coinbase交易,50個coin被鎖定在我們錢包的地址。

Blockchain.getInstance().addBlock(
            Transaction.newUTXO(
                Wallet.getInstance().getAddress(), "address", 30));

然後是第二個區塊,我們創建了一個UTXO,從錢包的地址轉移30個coin到地址address

        Blockchain.getInstance().addBlock(
            Transaction.newUTXO(
                "address", "address1", 20));

最後是第三個區塊,從地址address轉移20個coin到地址address1.
測試一下:

...
[INFO ] 2020-05-18 14:10:19,501 method:org.xd.chain.transaction.Transaction.newCoinBase(Transaction.java:42)
創建Coinbase.....{"tips":[],"tops":{"R1363548f8946529dd73922b26547b6110f9159a30cc9b0474435c1c0db37842cd3c4100082182227e356c811830d6b5b56d862880b85f20355cfbc387641653a":{"lockScript":"R1363548f8946529dd73922b26547b6110f9159a30cc9b0474435c1c0db37842cd3c4100082182227e356c811830d6b5b56d862880b85f20355cfbc387641653a","value":50}}}
...
[INFO ] 2020-05-18 14:10:19,757 method:org.xd.chain.core.Blockchain.findAllUnspendableUTXO(Blockchain.java:117)
查找所有未消費的UTXO...............
[INFO ] 2020-05-18 14:10:19,804 method:org.xd.chain.wallet.Wallet.sign(Wallet.java:86)
使用私鑰對數據簽名: R1363548f8946529dd73922b26547b6110f9159a30cc9b0474435c1c0db37842cd3c4100082182227e356c811830d6b5b56d862880b85f20355cfbc387641653a
[INFO ] 2020-05-18 14:10:20,382 method:org.xd.chain.transaction.Transaction.newUTXO(Transaction.java:100)
創建UTXO: {"tips":[{"unLockScript":"523133363335343866383934363532396464373339323262323635343762363131306639313539613330636339623034373434333563316330646233373834326364336334313030303832313832323237653335366338313138333064366235623536643836323838306238356632303335356366626333383736343136353361%%%4251d1ae7091f422bef3a95b29867ebeaeeedb0e20239a4edf96967d1a1f15a8f061873acc01aa86c726e1ad128aefeaaf5c447aed27e5729bdd24f0026d6c23","values":50}],"tops":{"R1363548f8946529dd73922b26547b6110f9159a30cc9b0474435c1c0db37842cd3c4100082182227e356c811830d6b5b56d862880b85f20355cfbc387641653a":{"lockScript":"R1363548f8946529dd73922b26547b6110f9159a30cc9b0474435c1c0db37842cd3c4100082182227e356c811830d6b5b56d862880b85f20355cfbc387641653a","value":20},"address":{"lockScript":"address","value":30}}}
...
[INFO ] 2020-05-18 14:10:20,468 method:org.xd.chain.core.Blockchain.addBlock(Blockchain.java:86)
當前區塊信息為:{"blkNum":2,"curBlockHash":"00005f0690489e8763bd3db0ce7112592cdb118507945c65d07bfe27e0ad3031","nonce":4350,"prevBlockHash":"000011de81afdac44e08e81b9be434cbcb625808a9d0f8008275ab9a6ffb809f","timeStamp":"2020-05-18 14:10:20","transaction":{"tips":[{"unLockScript":"523133363335343866383934363532396464373339323262323635343762363131306639313539613330636339623034373434333563316330646233373834326364336334313030303832313832323237653335366338313138333064366235623536643836323838306238356632303335356366626333383736343136353361%%%4251d1ae7091f422bef3a95b29867ebeaeeedb0e20239a4edf96967d1a1f15a8f061873acc01aa86c726e1ad128aefeaaf5c447aed27e5729bdd24f0026d6c23","values":50}],"tops":{"R1363548f8946529dd73922b26547b6110f9159a30cc9b0474435c1c0db37842cd3c4100082182227e356c811830d6b5b56d862880b85f20355cfbc387641653a":{"lockScript":"R1363548f8946529dd73922b26547b6110f9159a30cc9b0474435c1c0db37842cd3c4100082182227e356c811830d6b5b56d862880b85f20355cfbc387641653a","value":20},"address":{"lockScript":"address","value":30}}}}
[INFO ] 2020-05-18 14:10:20,575 method:org.xd.chain.core.Blockchain.findAllUnspendableUTXO(Blockchain.java:117)
查找所有未消費的UTXO...............
...
[INFO ] 2020-05-18 14:10:20,725 method:org.xd.chain.wallet.Wallet.verify(Wallet.java:124)
驗證簽名: address
...
[INFO ] 2020-05-18 14:10:20,731 method:org.xd.chain.wallet.Wallet.sign(Wallet.java:86)
使用私鑰對數據簽名: address
[INFO ] 2020-05-18 14:10:20,734 method:org.xd.chain.transaction.Transaction.newUTXO(Transaction.java:100)
創建UTXO: {"tips":[{"unLockScript":"61646472657373%%%8ad16095a9f1947e323eb5ef3601a0cc2ad552ad3f7331406123577a1cc0c68dc614f3262505f079f7c3acfc1d681fdb432f7ba0f4ac3d69cb46dead5446b2cd","values":30}],"tops":{"R1363548f8946529dd73922b26547b6110f9159a30cc9b0474435c1c0db37842cd3c4100082182227e356c811830d6b5b56d862880b85f20355cfbc387641653a":{"lockScript":"R1363548f8946529dd73922b26547b6110f9159a30cc9b0474435c1c0db37842cd3c4100082182227e356c811830d6b5b56d862880b85f20355cfbc387641653a","value":10},"address1":{"lockScript":"address1","value":20}}}
...
[INFO ] 2020-05-18 14:10:20,990 method:org.xd.chain.core.Blockchain.addBlock(Blockchain.java:86)
當前區塊信息為:{"blkNum":3,"curBlockHash":"00006e8918bba9831374f578caa3d80fa936997598072145bc0654cbca2d084e","nonce":74607,"prevBlockHash":"00005f0690489e8763bd3db0ce7112592cdb118507945c65d07bfe27e0ad3031","timeStamp":"2020-05-18 14:10:20","transaction":{"tips":[{"unLockScript":"61646472657373%%%8ad16095a9f1947e323eb5ef3601a0cc2ad552ad3f7331406123577a1cc0c68dc614f3262505f079f7c3acfc1d681fdb432f7ba0f4ac3d69cb46dead5446b2cd","values":30}],"tops":{"R1363548f8946529dd73922b26547b6110f9159a30cc9b0474435c1c0db37842cd3c4100082182227e356c811830d6b5b56d862880b85f20355cfbc387641653a":{"lockScript":"R1363548f8946529dd73922b26547b6110f9159a30cc9b0474435c1c0db37842cd3c4100082182227e356c811830d6b5b56d862880b85f20355cfbc387641653a","value":10},"address1":{"lockScript":"address1","value":20}}}}

省略掉其他日誌信息,看起來測試是沒有問題的,共生成了3個區塊。創世區塊中有一筆Coinbase交易。區塊2中成功轉移30coin到地址address,返還20coin到原地址。區塊3中成功從地址address轉移20coin到地址address1,返還10coin到地址address

還有部分未完善部分,比如coinbase只在創世區塊中生成了。每個區塊中只含有一筆交易等等,後期慢慢完善。

Github倉庫地址在這裡,隨時保持更新中…..

Github地址:Jchain