溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java如何構(gòu)建區(qū)塊鏈實現(xiàn)交易

發(fā)布時間:2022-01-06 16:45:56 來源:億速云 閱讀:144 作者:iii 欄目:互聯(lián)網(wǎng)科技

這篇文章主要介紹“Java如何構(gòu)建區(qū)塊鏈實現(xiàn)交易”,在日常操作中,相信很多人在Java如何構(gòu)建區(qū)塊鏈實現(xiàn)交易問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Java如何構(gòu)建區(qū)塊鏈實現(xiàn)交易”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

UTXO池

與區(qū)塊相關的數(shù)據(jù)存儲在 blocks 這個數(shù)據(jù)桶中,而交易數(shù)據(jù)則存儲在 chainstate 這個數(shù)據(jù)桶中,讓我們來回憶一下,chainstate 數(shù)據(jù)桶的數(shù)據(jù)結(jié)構(gòu):

  • 'c' + 32-byte transaction hash -> unspent transaction output record for that transaction

    某筆交易的UTXO記錄

  • 'B' -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs

    數(shù)據(jù)庫所表示的UTXO的區(qū)塊Hash

從那篇文章開始,我們已經(jīng)實現(xiàn)了比特幣的交易機制,但是我們還沒有用到 chainstate 數(shù)據(jù)桶去存儲我們的交易輸出。所以,這將是我們現(xiàn)在要去做的事情。

chainstate 不會去存儲交易數(shù)據(jù)。相反,它存儲的是 UTXO 集,也就是未被花費的交易輸出集合。除此之外,它還存儲了"數(shù)據(jù)庫所表示的UTXO的區(qū)塊Hash",我們這里先暫且忽略這一點,因為我們還沒有用到區(qū)塊高度(這一點我們會在后面的文章進行實現(xiàn))。

那么,我們?yōu)槭裁葱枰?UTXO 池呢?

一起來看一下我們前面實現(xiàn)的 findUnspentTransactions 方法:

   /**
     * 查找錢包地址對應的所有未花費的交易
     *
     * @param pubKeyHash 錢包公鑰Hash
     * @return
     */
    private Transaction[] findUnspentTransactions(byte[] pubKeyHash) throws Exception {
        Map<String, int[]> allSpentTXOs = this.getAllSpentTXOs(pubKeyHash);
        Transaction[] unspentTxs = {};

        // 再次遍歷所有區(qū)塊中的交易輸出
        for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) {
            Block block = blockchainIterator.next();
            for (Transaction transaction : block.getTransactions()) {

                String txId = Hex.encodeHexString(transaction.getTxId());

                int[] spentOutIndexArray = allSpentTXOs.get(txId);

                for (int outIndex = 0; outIndex < transaction.getOutputs().length; outIndex++) {
                    if (spentOutIndexArray != null && ArrayUtils.contains(spentOutIndexArray, outIndex)) {
                        continue;
                    }

                    // 保存不存在 allSpentTXOs 中的交易
                    if (transaction.getOutputs()[outIndex].isLockedWithKey(pubKeyHash)) {
                        unspentTxs = ArrayUtils.add(unspentTxs, transaction);
                    }
                }
            }
        }
        return unspentTxs;
    }

該方法是用來查找錢包地址對應的包含未花費交易輸出的交易信息。由于交易信息是存儲在區(qū)塊當中,所以我們現(xiàn)有的做法是遍歷區(qū)塊鏈中的每個區(qū)塊,然后遍歷每個區(qū)塊中的交易信息,再然后遍歷每個交易中的交易輸出,并檢查交易輸出是否被相應的錢包地址所鎖定,效率非常低下。截止2018年3月29號,比特幣中有 515698 個區(qū)塊,并且這些數(shù)據(jù)占據(jù)了140+Gb 的磁盤空間。這也就意味著一個人必須運行全節(jié)點(下載所有的區(qū)塊數(shù)據(jù))才能驗證交易信息。此外,驗證交易信息需要遍歷所有的區(qū)塊。

針對這個問題的解決辦法是需要有一個存儲了所有UTXOs(未花費交易輸出)的索引,這就是 UTXOs 池所要做的事情:UTXOs池其實是一個緩存空間,它所緩存的數(shù)據(jù)需要從構(gòu)建區(qū)塊鏈中所有的交易數(shù)據(jù)中獲得(通過遍歷所有的區(qū)塊鏈,不過這個構(gòu)建操作只需要執(zhí)行一次即可),并且它后續(xù)還會用于錢包余額的計算以及新的交易數(shù)據(jù)的驗證。截止到2017年9月,UTXOs池大約為 2.7Gb。

好了,讓我們來想一下,為了實現(xiàn) UTXOs 池我們需要做哪些事情。當前,有下列方法被用于查找交易信息:

  1. Blockchain.getAllSpentTXOs —— 查詢所有已被花費的交易輸出。它需要遍歷區(qū)塊鏈中所有區(qū)塊中交易信息。

  2. Blockchain.findUnspentTransactions —— 查詢包含未被花費的交易輸出的交易信息。它也需要遍歷區(qū)塊鏈中所有區(qū)塊中交易信息。

  3. Blockchain.findSpendableOutputs —— 該方法用于新的交易創(chuàng)建之時。它需要找到足夠多的交易輸出,以滿足所需支付的金額。需要調(diào)用 Blockchain.findUnspentTransactions 方法。

  4. Blockchain.findUTXO —— 查詢錢包地址所對應的所有未花費交易輸出,然后用于計算錢包余額。需要調(diào)用

    Blockchain.findUnspentTransactions 方法。

  5. Blockchain.findTransaction —— 通過交易ID查詢交易信息。它需要遍歷所有的區(qū)塊直到找到交易信息為止。

如你所見,上面這些方法都需要去遍歷數(shù)據(jù)庫中的所有區(qū)塊。由于UTXOs池只存儲未被花費的交易輸出,而不會存儲所有的交易信息,因此我們不會對有 Blockchain.findTransaction 進行優(yōu)化。

那么,我們需要下列這些方法:

  1. Blockchain.findUTXO —— 通過遍歷所有的區(qū)塊來找到所有未被花費的交易輸出.

  2. UTXOSet.reindex —— 調(diào)用上面 findUTXO 方法,然后將查詢結(jié)果存儲在數(shù)據(jù)庫中。也即需要進行緩存的地方。

  3. UTXOSet.findSpendableOutputs —— 與 Blockchain.findSpendableOutputs 類似,區(qū)別在于會使用 UTXO 池。

  4. UTXOSet.findUTXO —— 與Blockchain.findUTXO 類似,區(qū)別在于會使用 UTXO 池。

  5. Blockchain.findTransaction —— 邏輯保持不變。

這樣,兩個使用最頻繁的方法將從現(xiàn)在開始使用緩存!讓我們開始編碼吧!

定義 UTXOSet

@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class UTXOSet {
    private Blockchain blockchain;
}

重建 UTXO 池索引:

public class UTXOSet {
 
   ...
 
  /**
    * 重建 UTXO 池索引
    */
    @Synchronized   
    public void reIndex() {
        log.info("Start to reIndex UTXO set !");
        RocksDBUtils.getInstance().cleanChainStateBucket();
        Map<String, TXOutput[]> allUTXOs = blockchain.findAllUTXOs();
        for (Map.Entry<String, TXOutput[]> entry : allUTXOs.entrySet()) {
            RocksDBUtils.getInstance().putUTXOs(entry.getKey(), entry.getValue());
        }
        log.info("ReIndex UTXO set finished ! ");
    }
    
    ...
}

此方法用于初始化 UTXOSet。首先,需要清空 chainstate 數(shù)據(jù)桶,然后查詢所有未被花費的交易輸出,并將它們保存到 chainstate 數(shù)據(jù)桶中。

實現(xiàn) findSpendableOutputs 方法,供 Transation.newUTXOTransaction 調(diào)用

public class UTXOSet {
 
   ... 
 
   /**
     * 尋找能夠花費的交易
     *
     * @param pubKeyHash 錢包公鑰Hash
     * @param amount     花費金額
     */
    public SpendableOutputResult findSpendableOutputs(byte[] pubKeyHash, int amount) {
        Map<String, int[]> unspentOuts = Maps.newHashMap();
        int accumulated = 0;
        Map<String, byte[]> chainstateBucket = RocksDBUtils.getInstance().getChainstateBucket();
        for (Map.Entry<String, byte[]> entry : chainstateBucket.entrySet()) {
            String txId = entry.getKey();
            TXOutput[] txOutputs = (TXOutput[]) SerializeUtils.deserialize(entry.getValue());

            for (int outId = 0; outId < txOutputs.length; outId++) {
                TXOutput txOutput = txOutputs[outId];
                if (txOutput.isLockedWithKey(pubKeyHash) && accumulated < amount) {
                    accumulated += txOutput.getValue();

                    int[] outIds = unspentOuts.get(txId);
                    if (outIds == null) {
                        outIds = new int[]{outId};
                    } else {
                        outIds = ArrayUtils.add(outIds, outId);
                    }
                    unspentOuts.put(txId, outIds);
                    if (accumulated >= amount) {
                        break;
                    }
                }
            }
        }
        return new SpendableOutputResult(accumulated, unspentOuts);
    }
    
    ...
    
}

實現(xiàn) findUTXOs 接口,供 CLI.getBalance 調(diào)用:

public class UTXOSet {
 
   ... 
 
   /**
     * 查找錢包地址對應的所有UTXO
     *
     * @param pubKeyHash 錢包公鑰Hash
     * @return
     */
    public TXOutput[] findUTXOs(byte[] pubKeyHash) {
        TXOutput[] utxos = {};
        Map<String, byte[]> chainstateBucket = RocksDBUtils.getInstance().getChainstateBucket();
        if (chainstateBucket.isEmpty()) {
            return utxos;
        }
        for (byte[] value : chainstateBucket.values()) {
            TXOutput[] txOutputs = (TXOutput[]) SerializeUtils.deserialize(value);
            for (TXOutput txOutput : txOutputs) {
                if (txOutput.isLockedWithKey(pubKeyHash)) {
                    utxos = ArrayUtils.add(utxos, txOutput);
                }
            }
        }
        return utxos;
    }
    
    ...
    
}

以上這些方法都是先前 Blockchain 中相應方法的微調(diào)版,先前的方法將不再使用。

有了UTXO池之后,意味著我們的交易數(shù)據(jù)分開存儲到了兩個不同的數(shù)據(jù)桶中:交易數(shù)據(jù)存儲到了 block 數(shù)據(jù)桶中,而UTXO存儲到了 chainstate 數(shù)據(jù)桶中。這就需要一種同步機制來保證每當一個新的區(qū)塊產(chǎn)生時,UTXO池能夠及時同步最新區(qū)塊中的交易數(shù)據(jù),畢竟我們不想頻地進行 reIndex 。因此,我們需要如下方法:

更新UTXO池:

public class UTXOSet {
 
   ... 

   /**
     * 更新UTXO池
     * <p>
     * 當一個新的區(qū)塊產(chǎn)生時,需要去做兩件事情:
     * 1)從UTXO池中移除花費掉了的交易輸出;
     * 2)保存新的未花費交易輸出;
     *
     * @param tipBlock 最新的區(qū)塊
     */
    @Synchronized
    public void update(Block tipBlock) {
        if (tipBlock == null) {
            log.error("Fail to update UTXO set ! tipBlock is null !");
            throw new RuntimeException("Fail to update UTXO set ! ");
        }
        for (Transaction transaction : tipBlock.getTransactions()) {

            // 根據(jù)交易輸入排查出剩余未被使用的交易輸出
            if (!transaction.isCoinbase()) {
                for (TXInput txInput : transaction.getInputs()) {
                    // 余下未被使用的交易輸出
                    TXOutput[] remainderUTXOs = {};
                    String txId = Hex.encodeHexString(txInput.getTxId());
                    TXOutput[] txOutputs = RocksDBUtils.getInstance().getUTXOs(txId);

                    if (txOutputs == null) {
                        continue;
                    }

                    for (int outIndex = 0; outIndex < txOutputs.length; outIndex++) {
                        if (outIndex != txInput.getTxOutputIndex()) {
                            remainderUTXOs = ArrayUtils.add(remainderUTXOs, txOutputs[outIndex]);
                        }
                    }

                    // 沒有剩余則刪除,否則更新
                    if (remainderUTXOs.length == 0) {
                        RocksDBUtils.getInstance().deleteUTXOs(txId);
                    } else {
                        RocksDBUtils.getInstance().putUTXOs(txId, remainderUTXOs);
                    }
                }
            }

            // 新的交易輸出保存到DB中
            TXOutput[] txOutputs = transaction.getOutputs();
            String txId = Hex.encodeHexString(transaction.getTxId());
            RocksDBUtils.getInstance().putUTXOs(txId, txOutputs);
        }

    }
    
    ...
    
}

讓我們將 UTXOSet 用到它們所需之處去:

public class CLI {

   ...

   /**
     * 創(chuàng)建區(qū)塊鏈
     *
     * @param address
     */
    private void createBlockchain(String address) {
        Blockchain blockchain = Blockchain.createBlockchain(address);
        UTXOSet utxoSet = new UTXOSet(blockchain);
        utxoSet.reIndex();
        log.info("Done ! ");
    }
    
    ...
    
}

當創(chuàng)建一個新的區(qū)塊鏈是,我們需要重建 UTXO 池索引。截止目前,這是唯一一處用到 reIndex 的地方,盡管看起有些多余,因為在區(qū)塊鏈創(chuàng)建之初僅僅只有一個區(qū)塊和一筆交易。

修改 CLI.send 接口:

public class CLI {
	
	...

   /**
     * 轉(zhuǎn)賬
     *
     * @param from
     * @param to
     * @param amount
     */
    private void send(String from, String to, int amount) throws Exception {
        
        ...
        
        Blockchain blockchain = Blockchain.createBlockchain(from);
        Transaction transaction = Transaction.newUTXOTransaction(from, to, amount, blockchain);
        Block newBlock = blockchain.mineBlock(new Transaction[]{transaction});
        new UTXOSet(blockchain).update(newBlock);
		
        ...
    }
    
    ...
    
}

當一個新的區(qū)塊產(chǎn)生后,需要去更新 UTXO 池數(shù)據(jù)。

讓我們來檢查一下它們的運行情況:

$ java -jar blockchain-java-jar-with-dependencies.jar  createwallet
wallet address : 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf

$ java -jar blockchain-java-jar-with-dependencies.jar  createwallet
wallet address : 1HX7bWwCjvxkjq65GUgAVRFfTZy6yKWkoG

$ java -jar blockchain-java-jar-with-dependencies.jar  createwallet
wallet address : 1L1RoFgyjCrNPCPHmSEBtNiV3h3wiF9mZV

$ java -jar blockchain-java-jar-with-dependencies.jar  createblockchain -address 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf

Elapsed Time: 164.961 seconds 
correct hash Hex: 00225493862611bc517cb6b3610e99d26d98a6b52484c9fa745df6ceff93f445 

Done ! 

$ java -jar blockchain-java-jar-with-dependencies.jar  getbalance -address 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf
Balance of '1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf': 10

$ java -jar blockchain-java-jar-with-dependencies.jar  send -from 1HX7bWwCjvxkjq65GUgAVRFfTZy6yKWkoG -to  1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf -amount 5
java.lang.Exception: ERROR: Not enough funds

$ java -jar blockchain-java-jar-with-dependencies.jar  send -from 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf -to 1HX7bWwCjvxkjq65GUgAVRFfTZy6yKWkoG -amount 2
Elapsed Time: 54.92 seconds 
correct hash Hex: 0001ab21f71ff2d6d532bf3b3388db790c2b03e28d7bd27bd669c5f6380a4e5b 

Success!

$ java -jar blockchain-java-jar-with-dependencies.jar  send -from 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf -to 1L1RoFgyjCrNPCPHmSEBtNiV3h3wiF9mZV -amount 2
Elapsed Time: 54.92 seconds 
correct hash Hex: 0009b925cc94e3db8bab2958b1fc2d1764aa15531e20756d92c3a93065c920f0 

Success!

$ java -jar blockchain-java-jar-with-dependencies.jar  getbalance -address 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf
Balance of '1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf': 6

$ java -jar blockchain-java-jar-with-dependencies.jar  getbalance -address 1HX7bWwCjvxkjq65GUgAVRFfTZy6yKWkoG
Balance of '1HX7bWwCjvxkjq65GUgAVRFfTZy6yKWkoG': 2

$ java -jar blockchain-java-jar-with-dependencies.jar  getbalance -address 1L1RoFgyjCrNPCPHmSEBtNiV3h3wiF9mZV
Balance of '1L1RoFgyjCrNPCPHmSEBtNiV3h3wiF9mZV': 2

獎勵機制

前面的章節(jié)中我們省略了礦工挖礦的獎勵機制。時機已經(jīng)成熟,該實現(xiàn)它了。

礦工獎勵其實是一個 coinbase 交易(創(chuàng)幣交易)。當一個礦工節(jié)點開始去生產(chǎn)一個新的區(qū)塊時,他會從隊列中取出一些交易數(shù)據(jù),并且為它們預制一個 coinbase 交易。這筆 coinbase 交易中僅有的交易輸出包含了礦工的公鑰hash。

只需要更新 send 命令接口,我們就可以輕松實現(xiàn)礦工的獎勵機制:

public class CLI {
	
	...

   /**
     * 轉(zhuǎn)賬
     *
     * @param from
     * @param to
     * @param amount
     */
    private void send(String from, String to, int amount) throws Exception {
        
        ...
        
        Blockchain blockchain = Blockchain.createBlockchain(from);
        // 新交易
        Transaction transaction = Transaction.newUTXOTransaction(from, to, amount, blockchain);
        // 獎勵
        Transaction rewardTx = Transaction.newCoinbaseTX(from, "");
        Block newBlock = blockchain.mineBlock(new Transaction[]{transaction, rewardTx});
        new UTXOSet(blockchain).update(newBlock);
		
        ...
    }
    
    ...
 	   
}

還需要修改交易驗證方法,coinbase 交易直接驗證通過:

public class Blockchain {
	
  /**
     * 交易簽名驗證
     *
     * @param tx
     */
    private boolean verifyTransactions(Transaction tx) {
        if (tx.isCoinbase()) {
            return true;
        }
    
        ...
    }
    
    ...
    
}

在我們的實現(xiàn)邏輯中,代幣的發(fā)送也是區(qū)塊的生產(chǎn)者,因此,獎勵也歸他所有。

讓我們來驗證一下獎勵機制:

$ java -jar blockchain-java-jar-with-dependencies.jar  createwallet 
wallet address : 1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD

$ java -jar blockchain-java-jar-with-dependencies.jar  createwallet 
wallet address : 17crpQoWy7TEkY9UPjZ3Qt9Fc2rWPUt8KX

$ java -jar blockchain-java-jar-with-dependencies.jar  createwallet 
wallet address : 12L868QZW1ySYzf2oT5ha9py9M5JrSRhvT

$ java -jar blockchain-java-jar-with-dependencies.jar  createblockchain -address 1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD

Elapsed Time: 17.973 seconds
correct hash Hex: 0000defe83a851a5db3803d5013bbc20c6234f176b2c52ae36fdb53d28b33d93 

Done ! 

$ java -jar blockchain-java-jar-with-dependencies.jar  send -from 1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD -to 17crpQoWy7TEkY9UPjZ3Qt9Fc2rWPUt8KX -amount 6
Elapsed Time: 30.887 seconds
correct hash Hex: 00005fd36a2609b43fd940577f93b8622e88e854f5ccfd70e113f763b6df69f7 

Success!


$ java -jar blockchain-java-jar-with-dependencies.jar  send -from 1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD -to 12L868QZW1ySYzf2oT5ha9py9M5JrSRhvT -amount 3
Elapsed Time: 45.267 seconds
correct hash Hex: 00009fd7c59b830b60ec21ade7672921d2fb0962a1b06a42c245450e47582a13 

Success!

$ java -jar blockchain-java-jar-with-dependencies.jar  getbalance -address 1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD
Balance of '1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD': 21

$ java -jar blockchain-java-jar-with-dependencies.jar  getbalance -address 17crpQoWy7TEkY9UPjZ3Qt9Fc2rWPUt8KX
Balance of '17crpQoWy7TEkY9UPjZ3Qt9Fc2rWPUt8KX': 6

$ java -jar blockchain-java-jar-with-dependencies.jar  getbalance -address 12L868QZW1ySYzf2oT5ha9py9M5JrSRhvT
Balance of '12L868QZW1ySYzf2oT5ha9py9M5JrSRhvT': 3

1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD 這個地址一共收到了三份獎勵:

  • 第一次是開采創(chuàng)世區(qū)塊;

  • 第二次是開采區(qū)塊:00005fd36a2609b43fd940577f93b8622e88e854f5ccfd70e113f763b6df69f7

  • 第三次是開采區(qū)塊:00009fd7c59b830b60ec21ade7672921d2fb0962a1b06a42c245450e47582a13

Merkle Tree

Merkle Tree(默克爾樹) 是這篇文章中我們需要重點討論的一個機制。

正如我前面提到的那樣,整個比特幣的數(shù)據(jù)庫占到了大約140G的磁盤空間。由于比特幣的分布式特性,網(wǎng)絡中的每一個節(jié)點必須是獨立且自給自足的。每個比特幣節(jié)點都是路由、區(qū)塊鏈數(shù)據(jù)庫、挖礦、錢包服務的功能集合。每個節(jié)點都參與全網(wǎng)絡的路由功能,同時也可能包含其他功能。每個節(jié)點都參與驗證并傳播交易及區(qū)塊信息,發(fā)現(xiàn)并維持與對等節(jié)點的連接。一個全節(jié)點(full node)包括以下四個功能:

Java如何構(gòu)建區(qū)塊鏈實現(xiàn)交易

隨著越來越多的人開始使用比特幣,這條規(guī)則開始變得越來越難以遵循:讓每一個人都去運行一個完整的節(jié)點不太現(xiàn)實。在中本聰發(fā)布的 比特幣白皮書 中,針對這個問題提出了一個解決方案:Simplified Payment Verification (SPV)(簡易支付驗證)。SPV是比特幣的輕量級節(jié)點,它不需要下載所有的區(qū)塊鏈數(shù)據(jù),也不需要驗證區(qū)塊和交易數(shù)據(jù)。相反,當SPV想要驗證一筆交易的有效性時,它會從它所連接的全節(jié)點上檢索所需要的一些數(shù)據(jù)。這種機制保證了在只有一個全節(jié)點的情況,可以運行多個SPV輕錢包節(jié)點。

更多有關SPV的介紹,請查看:《精通比特幣(第二版)》第八章

為了使SPV成為可能,就需要有一種方法在沒有全量下載區(qū)塊數(shù)據(jù)的情況下,來檢查一個區(qū)塊是否包含了某筆交易。這就是 Merkle Tree 發(fā)揮作用的地方了。

比特幣中所使用的Merkle Tree是為了獲得交易的Hash值,隨后這個已經(jīng)被Pow(工作量證明)系統(tǒng)認可了的Hash值會被保存到區(qū)塊頭中。到目前為止,我們只是簡單地計算了一個區(qū)塊中每筆交易的Hash值,然后在準備Pow數(shù)據(jù)時,再對這些交易進行 SHA-256 計算。雖然這是一個用于獲取區(qū)塊交易唯一表示的一個不錯的方式,但是這種方式不具備 Merkle Tree 的優(yōu)點。

更多有關Merkle Tree的介紹,請查看:《精通比特幣(第二版)》第九章

來看一下Merkle Tree的結(jié)構(gòu):

Java如何構(gòu)建區(qū)塊鏈實現(xiàn)交易

每一個區(qū)塊都會構(gòu)建一個Merkle Tree,它從最底部的葉子節(jié)點開始往上構(gòu)建,每一個交易的Hash就是一個葉子節(jié)點(比特幣中用的雙SHA256算法)。葉子節(jié)點的數(shù)量必須是偶數(shù)個,但是并不是每一個區(qū)塊都能包含偶數(shù)筆交易數(shù)據(jù)。如果存在奇數(shù)筆交易數(shù)據(jù),那么最后一筆交易數(shù)據(jù)將會被復制一份(這僅僅發(fā)生在Merkle Tree中,而不是區(qū)塊中)。

從下往上移動,葉子節(jié)點成對分組,它們的Hash值被連接到一起,并且在此基礎上再次計算出新的Hash值。新的Hash 形成新的樹節(jié)點。這個過程不斷地被重復,直到最后僅剩一個被稱為根節(jié)點的樹節(jié)點。這個根節(jié)點的Hash就是區(qū)塊中交易數(shù)據(jù)們的唯一代表,它會被保存到區(qū)塊頭中,并被用于參與POW系統(tǒng)的計算。

Merkle樹的好處是節(jié)點可以在不下載整個塊的情況下驗證某筆交易的合法性。 為此,只需要交易Hash,Merkle樹根Hash和Merkle路徑。

Merkle Tree代碼實現(xiàn)如下:

package one.wangwei.blockchain.transaction;

import com.google.common.collect.Lists;
import lombok.Data;
import one.wangwei.blockchain.util.ByteUtils;
import org.apache.commons.codec.digest.DigestUtils;

import java.util.List;

/**
 * 默克爾樹
 *
 * @author wangwei
 * @date 2018/04/15
 */
@Data
public class MerkleTree {

    /**
     * 根節(jié)點
     */
    private Node root;
    /**
     * 葉子節(jié)點Hash
     */
    private byte[][] leafHashes;

    public MerkleTree(byte[][] leafHashes) {
        constructTree(leafHashes);
    }

    /**
     * 從底部葉子節(jié)點開始往上構(gòu)建整個Merkle Tree
     *
     * @param leafHashes
     */
    private void constructTree(byte[][] leafHashes) {
        if (leafHashes == null || leafHashes.length < 1) {
            throw new RuntimeException("ERROR:Fail to construct merkle tree ! leafHashes data invalid ! ");
        }
        this.leafHashes = leafHashes;
        List<Node> parents = bottomLevel(leafHashes);
        while (parents.size() > 1) {
            parents = internalLevel(parents);
        }
        root = parents.get(0);
    }

    /**
     * 構(gòu)建一個層級節(jié)點
     *
     * @param children
     * @return
     */
    private List<Node> internalLevel(List<Node> children) {
        List<Node> parents = Lists.newArrayListWithCapacity(children.size() / 2);
        for (int i = 0; i < children.size() - 1; i += 2) {
            Node child1 = children.get(i);
            Node child2 = children.get(i + 1);

            Node parent = constructInternalNode(child1, child2);
            parents.add(parent);
        }

        // 內(nèi)部節(jié)點奇數(shù)個,只對left節(jié)點進行計算
        if (children.size() % 2 != 0) {
            Node child = children.get(children.size() - 1);
            Node parent = constructInternalNode(child, null);
            parents.add(parent);
        }

        return parents;
    }

    /**
     * 底部節(jié)點構(gòu)建
     *
     * @param hashes
     * @return
     */
    private List<Node> bottomLevel(byte[][] hashes) {
        List<Node> parents = Lists.newArrayListWithCapacity(hashes.length / 2);

        for (int i = 0; i < hashes.length - 1; i += 2) {
            Node leaf1 = constructLeafNode(hashes[i]);
            Node leaf2 = constructLeafNode(hashes[i + 1]);

            Node parent = constructInternalNode(leaf1, leaf2);
            parents.add(parent);
        }

        if (hashes.length % 2 != 0) {
            Node leaf = constructLeafNode(hashes[hashes.length - 1]);
            // 奇數(shù)個節(jié)點的情況,復制最后一個節(jié)點
            Node parent = constructInternalNode(leaf, leaf);
            parents.add(parent);
        }

        return parents;
    }

    /**
     * 構(gòu)建葉子節(jié)點
     *
     * @param hash
     * @return
     */
    private static Node constructLeafNode(byte[] hash) {
        Node leaf = new Node();
        leaf.hash = hash;
        return leaf;
    }

    /**
     * 構(gòu)建內(nèi)部節(jié)點
     *
     * @param leftChild
     * @param rightChild
     * @return
     */
    private Node constructInternalNode(Node leftChild, Node rightChild) {
        Node parent = new Node();
        if (rightChild == null) {
            parent.hash = leftChild.hash;
        } else {
            parent.hash = internalHash(leftChild.hash, rightChild.hash);
        }
        parent.left = leftChild;
        parent.right = rightChild;
        return parent;
    }

    /**
     * 計算內(nèi)部節(jié)點Hash
     *
     * @param leftChildHash
     * @param rightChildHash
     * @return
     */
    private byte[] internalHash(byte[] leftChildHash, byte[] rightChildHash) {
        byte[] mergedBytes = ByteUtils.merge(leftChildHash, rightChildHash);
        return DigestUtils.sha256(mergedBytes);
    }

    /**
     * Merkle Tree節(jié)點
     */
    @Data
    public static class Node {
        private byte[] hash;
        private Node left;
        private Node right;
    }

}

然后修改 Block.hashTransaction 接口:

public class Block {
    
   ... 

   /**
     * 對區(qū)塊中的交易信息進行Hash計算
     *
     * @return
     */
    public byte[] hashTransaction() {
        byte[][] txIdArrays = new byte[this.getTransactions().length][];
        for (int i = 0; i < this.getTransactions().length; i++) {
            txIdArrays[i] = this.getTransactions()[i].hash();
        }
        return new MerkleTree(txIdArrays).getRoot().getHash();
    }
    
    ...
	
}

MerkleTree的根節(jié)點的Hash值,就是區(qū)塊中交易信息的唯一代表。

到此,關于“Java如何構(gòu)建區(qū)塊鏈實現(xiàn)交易”的學習就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續(xù)學習更多相關知識,請繼續(xù)關注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI