1. 程式人生 > >用spring boot 2從零開始建立區塊鏈

用spring boot 2從零開始建立區塊鏈

一、區塊鏈物件模型的基礎屬性(BlockChain)

    @ApiModelProperty(value = "當前交易列表", dataType = "List<Transaction>")
    @JSONField(serialize = false)
    @JsonIgnore
    private List<Transaction> currentTransactions;

    @ApiModelProperty(value = "所有交易列表", dataType = "List<Transaction>")
    private List<Transaction> transactions;

    @ApiModelProperty(value = "區塊列表", dataType = "List<BlockChain>")
    @JSONField(serialize = false)
    @JsonIgnore
    private List<BlockChain> chain;

    @ApiModelProperty(value = "叢集的節點列表", dataType = "Set<String>")
    @JSONField(serialize = false)
    @JsonIgnore
    private Set<String> nodes;

    @ApiModelProperty(value = "上一個區塊的雜湊值", dataType = "String", example = "f461ac428043f328309da7cac33803206cea9912f0d4e8d8cf2786d21e5ff403")
    private String previousHash = "";

    @ApiModelProperty(value = "工作量證明", dataType = "Integer", example = "100")
    private Integer proof = 0;

    @ApiModelProperty(value = "當前區塊的索引序號", dataType = "Long", example = "2")
    private Long index = 0L;

    @ApiModelProperty(value = "當前區塊的時間戳", dataType = "Long", example = "1526458171000")
    private Long timestamp = 0L;

    @ApiModelProperty(value = "當前區塊的雜湊值", dataType = "String", example = "g451ac428043f328309da7cac33803206cea9912f0d4e8d8cf2786d21e5ff401")
    private String hash;

注:上面有些註解來自swagger,主要為了方便生成線上文件以及直接除錯rest介面。相對之前最基本的區塊鏈hello world(python3實現)一文,每個區塊中的data,在這裡細分為transactions、currentTransactions。另外區塊“鏈”本質上可以理解為連結串列,所以得有一個List<?> chain;此外這裡引入了所謂“工作量證明”,用於驗證每個區域的hash值不是隨便來的,而是要達到一定規則的運算量才能獲取,可以理解為控制挖礦速度的難度係數。 

二、BlockChain的常規操作

2.1 生成新塊newBlock

    public BlockChain newBlock(Integer proof, String previousHash) {
        BlockChain block = new BlockChain();
        block.index = chain.size() + 1L;
        block.timestamp = System.currentTimeMillis();
        block.transactions.addAll(currentTransactions);
        block.proof = proof;
        block.previousHash = previousHash;
        currentTransactions.clear();
        chain.add(block);
        return block;
    }

2.2 生成第1個"創世"塊

連結串列總歸要有一個Head節點,區塊鏈也不例外

   public void newSeedBlock() {
        newBlock(100, "1");
    }

約定previousHash=1的,即為所謂的"創世"塊  

2.3 生成hash值

    public String getHash() {
        String json = jsonUtil.toJson(this.getCurrentTransactions()) +
                jsonUtil.toJson(this.getTransactions()) +
                jsonUtil.toJson(this.getChain()) +
                this.getPreviousHash() + this.getProof() + this.getIndex() + this.getTimestamp();
        hash = SHAUtils.getSHA256Str(json);
        return hash;
    }

這裡把區塊的主要屬性:交易資料、連結串列中所有元素、工作量證明、區塊索引號、時間戳 拼在一起,然後計算sha256。總之,這些主要屬性中的任何一個屬性發生變化,整個hash值就變了。

2.4 工作量證明

相信對區塊鏈有了解的同學,都知道“挖礦”。為了控制挖礦的難度,得有一個規則來約束下,所以就有了這個工作量證明,這裡我們模擬一個簡單的策略:

    public Boolean validProof(Integer lastProof, Integer proof) {
        System.out.println("validProof==>lastProof:" + lastProof + ",proof:" + proof);
        String guessHash = SHAUtils.getSHA256Str(String.format("{%d}{%d}", lastProof, proof));
        return guessHash.startsWith("00");
    }

把上一塊的proof值與本區塊的proof在一起,算sha256值,如果正好前2位是00,表示證明通過。(注:0的個數越多,挖礦難度越大,有興趣的同學可以自己調整試下)

2.5 區塊鏈驗證資料是否正確

為了防止區塊鏈的節點中混入非法髒資料(或被篡改),需要一個檢測資料完整性的方法

    public boolean validChain(List<BlockChain> chain) {
        if (CollectionUtils.isEmpty(chain)) {
            return false;
        }

        BlockChain previousBlock = chain.get(0);
        int currentIndex = 1;
        while (currentIndex < chain.size()) {
            BlockChain block = chain.get(currentIndex);
            if (!block.getPreviousHash().equals(previousBlock.getHash())) {
                return false;
            }

            if (!validProof(previousBlock.getProof(), block.getProof())) {
                return false;
            }
            previousBlock = block;
            currentIndex += 1;
        }
        return true;
    }

規則很簡單:

a)每個區塊的previousHash值,必須等於前一個塊的hash值

b)  驗證每個塊上的proof值是否有效

2.6 叢集中的分叉校驗

區塊鏈是一個去中心化的分散式體系,每個節點都能挖礦,挖出來的“新區塊”都能加入鏈中,如果出現節點之間的區塊鏈資料不一致,需要一個策略來做仲裁,可以定一個簡單的規則:鏈最長的節點認定為有效的,其它節點都以此為準。

為了模擬這種情況,在BlockChain類的屬性中,特地留了一個nodes節點列表,用於登記叢集中的其它節點資訊。

 public void registerNode(String address) {
        nodes.add(address);
    }

上面的方法,將把其它節點的例項(類似http://localhost:8081/),登記到節點列表中。知道了叢集中所有其它節點,就可以一一檢查誰的鏈條最長,程式碼如下:

    public boolean resolveConflicts() {
        int maxLength = getChain().size();
        List<BlockChain> newChain = new ArrayList<>();
        for (String node : getNodes()) {
            RestTemplate template = new RestTemplate();
            Map map = template.getForObject(node + "chain", Map.class);
            int length = MapUtils.getInteger(map, "length");
            String json = jsonUtil.toJson(MapUtils.getObject(map, "chain"));
            List<BlockChain> chain = jsonUtil.fromJson(json, new TypeReference<List<BlockChain>>() {
            });
            if (length > maxLength && validChain(chain)) {
                maxLength = length;
                newChain = chain;
            }
        }
        if (!CollectionUtils.isEmpty(newChain)) {
            this.chain = newChain;
            return true;
        }
        return false;
    }

大意是遍歷整個節點,逐一請求其它節點的rest介面,獲取其完整的連結串列,然後跟自己對比,如果比自己長的,就把自己給換掉。這樣輪一圈後,自身的連結串列,就被替換為整個叢集中最長的那個。

三、除錯執行

為了方便除錯,本文引入了swagger(不熟悉的同學可以參考spring cloud 學習(10) - 利用springfox整合swagger一文),然後加一堆rest api,跑起來,就可以直接測了:

點選檢視原圖

3.1 呼叫/chain檢視下初始值:

{
  "chain": [
    {
      "transactions": [],
      "previousHash": "1",
      "proof": 100,
      "index": 1,
      "timestamp": 1527427873298,
      "hash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479"
    }
  ],
  "length": 1
}

可以看到就只有一個“創世”塊,其previousHash為特定值1

3.2 呼叫/mine挖一塊礦

{
  "previousHash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479",
  "index": 2,
  "proof": 172,
  "message": "New Block Forged",
  "transactions": [
    {
      "sender": "0",
      "recepient": "50130c5283e640779b4e5e7a5afd2e6b",
      "amount": 1
    }
  ]
}

挖到礦(即:產生一個新的區塊block),系統自動獎勵本節點1個幣(從transaction可以看出這一點),同時這筆獎勵的交易被寫入新塊中。這時再來看下/chain

{
    "chain": [
        {
            "transactions": [],
            "previousHash": "1",
            "proof": 100,
            "index": 1,
            "timestamp": 1527427873298,
            "hash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479"
        },
        {
            "transactions": [
                {
                    "sender": "0",
                    "recepient": "50130c5283e640779b4e5e7a5afd2e6b",
                    "amount": 1
                }
            ],
            "previousHash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479",
            "proof": 172,
            "index": 2,
            "timestamp": 1527427956435,
            "hash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93"
        }
    ],
    "length": 2
}

可以看到,有二個區塊加入"連結串列"中了,可以繼續再挖一塊,最終/chain可能長成這樣:

{
    "chain": [
        {
            "transactions": [],
            "previousHash": "1",
            "proof": 100,
            "index": 1,
            "timestamp": 1527427873298,
            "hash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479"
        },
        {
            "transactions": [
                {
                    "sender": "0",
                    "recepient": "50130c5283e640779b4e5e7a5afd2e6b",
                    "amount": 1
                }
            ],
            "previousHash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479",
            "proof": 172,
            "index": 2,
            "timestamp": 1527427956435,
            "hash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93"
        },
        {
            "transactions": [
                {
                    "sender": "0",
                    "recepient": "50130c5283e640779b4e5e7a5afd2e6b",
                    "amount": 1
                }
            ],
            "previousHash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93",
            "proof": 153,
            "index": 3,
            "timestamp": 1527428128077,
            "hash": "d2b50c6ae768fd48591d07a054de78d058c11f889cec15588d270f72b6e420f1"
        }
    ],
    "length": 3
}

3.3 呼叫/transactions/new 發起一筆新交易

引數如下:

{
  "amount": 1.0,
  "recepient": "block-on-other-node",
  "sender": "50130c5283e640779b4e5e7a5afd2e6b"
}

注:sender一般取為當前礦機的標識,即本節點的nodeId,接收方一般指其它節點(這裡我們隨便輸入點內容,當作演示),然後交易的金額為“1”個幣,成功後,將返回

{
  "message": "Transaction will be added to Block 4"
}  

但這時,如果呼叫/chain檢視整個鏈的資料,會發現沒有變化,因為這筆交易資料,只是放在本區塊的currentTransactions列表中(注:該屬性並未json序列化輸出,忘記的同學,可以拉到本文最開頭,複習下幾個重要的屬性)。只有下一個可用區塊產生時,這筆交易才會寫入新的區塊中,so,我們再繼續挖一塊新礦,呼叫/mine,然後再檢視/chain

{
    "chain": [
        {
            "transactions": [],
            "previousHash": "1",
            "proof": 100,
            "index": 1,
            "timestamp": 1527427873298,
            "hash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479"
        },
        {
            "transactions": [
                {
                    "sender": "0",
                    "recepient": "50130c5283e640779b4e5e7a5afd2e6b",
                    "amount": 1
                }
            ],
            "previousHash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479",
            "proof": 172,
            "index": 2,
            "timestamp": 1527427956435,
            "hash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93"
        },
        {
            "transactions": [
                {
                    "sender": "0",
                    "recepient": "50130c5283e640779b4e5e7a5afd2e6b",
                    "amount": 1
                }
            ],
            "previousHash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93",
            "proof": 153,
            "index": 3,
            "timestamp": 1527428128077,
            "hash": "d2b50c6ae768fd48591d07a054de78d058c11f889cec15588d270f72b6e420f1"
        },
        {
            "transactions": [
                {
                    "sender": "50130c5283e640779b4e5e7a5afd2e6b",
                    "recepient": "block-on-other-node",
                    "amount": 1
                },
                {
                    "sender": "0",
                    "recepient": "50130c5283e640779b4e5e7a5afd2e6b",
                    "amount": 1
                }
            ],
            "previousHash": "d2b50c6ae768fd48591d07a054de78d058c11f889cec15588d270f72b6e420f1",
            "proof": 86,
            "index": 4,
            "timestamp": 1527428477991,
            "hash": "4b3c261d2f878cebbdc1ade1a09809bb647abbd990353e529f630220a53d60ed"
        }
    ],
    "length": 4
}

剛才的交易,已經被寫入最後一個剛挖出的Block中。

3.4 模擬多節點資料不一致,使用/resolve仲裁解決

a) 再啟動一個新埠的執行例項

方法一:參考下圖,idea中設定執行時的環境變數,填上server.port=8081,就可以在另一個埠上啟動

點選檢視原圖

方法二:在build.gradle里加一個task

task 8081 << {
	bootRun.systemProperty 'server.port', '8081'
}

然後就可以命令列下,直接gradle 8081 bootRun 

方法三:java -jar xxx.jar --name="Spring" --server.port=8081 直接在執行jar的時候指定埠

b) 呼叫/register 將新節點例項(即:8081埠的節點),註冊到8080的節點上

引數如下:

{
  "nodes": [
    "http://localhost:8081/"
  ]
}  

反過來,把8080老節點也註冊到新節點上(即:相當於兩兩相互註冊)。註冊成功後,這時呼叫8081新節點上的/chain ,因為這是個新節點,裡面只有一個創世塊,顯然跟8080老節點上的資料不一致

c) 新8081節點上呼叫/resolve 

輸出如下:

{
  "newChain": [
    {
      "transactions": [],
      "previousHash": "1",
      "proof": 100,
      "index": 1,
      "timestamp": 1527427873298,
      "hash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479"
    },
    {
      "transactions": [
        {
          "sender": "0",
          "recepient": "50130c5283e640779b4e5e7a5afd2e6b",
          "amount": 1
        }
      ],
      "previousHash": "9fbb08a5f332baf42012b8541122eccb60a603834fa98e9b5789898022b23479",
      "proof": 172,
      "index": 2,
      "timestamp": 1527427956435,
      "hash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93"
    },
    {
      "transactions": [
        {
          "sender": "0",
          "recepient": "50130c5283e640779b4e5e7a5afd2e6b",
          "amount": 1
        }
      ],
      "previousHash": "df079acca767eaca611383f7b1bb2b37daa3b01502f3219cb79ecddd010f7d93",
      "proof": 153,
      "index": 3,
      "timestamp": 1527428128077,
      "hash": "d2b50c6ae768fd48591d07a054de78d058c11f889cec15588d270f72b6e420f1"
    },
    {
      "transactions": [
        {
          "sender": "50130c5283e640779b4e5e7a5afd2e6b",
          "recepient": "block-on-other-node",
          "amount": 1
        },
        {
          "sender": "0",
          "recepient": "50130c5283e640779b4e5e7a5afd2e6b",
          "amount": 1
        }
      ],
      "previousHash": "d2b50c6ae768fd48591d07a054de78d058c11f889cec15588d270f72b6e420f1",
      "proof": 86,
      "index": 4,
      "timestamp": 1527428477991,
      "hash": "4b3c261d2f878cebbdc1ade1a09809bb647abbd990353e529f630220a53d60ed"
    }
  ],
  "message": "Our chain was replaced"
}

最後一行的message: Our chain was replaced 表示,本節點的區塊鏈已經被叢集其它節點中最長的那個替換掉了。