博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
用spring boot 2从零开始创建区块链
阅读量:7117 次
发布时间:2019-06-28

本文共 12666 字,大约阅读时间需要 42 分钟。

区块链这么火的技术,大java怎能落后,所以有了本文,主要代码参考自  , 中文翻译: 。

一、区块链对象模型的基础属性(BlockChain)

区块链的基本数据模型参考: 。主要属性如下:

@ApiModelProperty(value = "当前交易列表", dataType = "List
") @JSONField(serialize = false) @JsonIgnore private List
currentTransactions; @ApiModelProperty(value = "所有交易列表", dataType = "List
") private List
transactions; @ApiModelProperty(value = "区块列表", dataType = "List
") @JSONField(serialize = false) @JsonIgnore private List
chain; @ApiModelProperty(value = "集群的节点列表", dataType = "Set
") @JSONField(serialize = false) @JsonIgnore private Set
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接口。相对之前一文,每个区块中的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
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
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
chain = jsonUtil.fromJson(json, new TypeReference
>() { }); if (length > maxLength && validChain(chain)) { maxLength = length; newChain = chain; } } if (!CollectionUtils.isEmpty(newChain)) { this.chain = newChain; return true; } return false; }

大意是遍历整个节点,逐一请求其它节点的rest接口,获取其完整的链表,然后跟自己对比,如果比自己长的,就把自己给换掉。这样轮一圈后,自身的链表,就被替换为整个集群中最长的那个。

 

三、调试运行

为了方便调试,本文引入了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 表示,本节点的区块链已经被集群其它节点中最长的那个替换掉了。

 

最后,文中演示的所有代码,已经托管在github上,地址: 欢迎大家Fork.

转载地址:http://nofel.baihongyu.com/

你可能感兴趣的文章
【视频教程】微信小程序开发【一个实例】
查看>>
一看就懂的Mybatis框架入门笔记
查看>>
Rails 5.2.3 RC1 发布,Ruby Web 应用开发框架
查看>>
Android RecyclerView从入门到玩坏
查看>>
Riot v4.0.0-alpha.10 发布,JavaScript 的 MVP 框架
查看>>
Linux中管理用户和组命令
查看>>
将本地docker容器迁移到服务端
查看>>
甲骨文匯編1
查看>>
从0打卡leetcode之day 6--最长回文串
查看>>
使用python创建跨平台的fork()炸弹
查看>>
CentOS 6.5下rsync服务器安装配置
查看>>
Trapping Rain Water@LeetCode
查看>>
SSM-SpringMVC-03:SpringMVC执行流程一张有意思的图
查看>>
愚人节老板发话了,免费送书 + 免费入驻Java知识星球!!
查看>>
题解 P2350 【[HAOI2012]外星人】
查看>>
[20180423]关于闪回表与主外键约束.txt
查看>>
新经资讯项目业务逻辑梳理
查看>>
JDK1.8源码(五)——java.util.ArrayList 类
查看>>
Android - SharedPreferences
查看>>
Deepin 操作系统联合创始人宣布离职
查看>>