最近有点背...

用Java搭建一条区块链

前言

为了更好的理解区块链的底层实现原理,决定自己动手模拟实现一条区块链。

思路分析

通过之前的学习,从文本知识的角度,我们知道,创世区块、记账原理、挖矿原理、工作量证明、共识机制等等区块链的相关知识。

创建一条区块链,首先默认构造创世区块。在此基础上,我们可以发布交易,并进行挖矿,计算出工作量证明,将交易记录到区块中,每成功的挖一次矿,块高就+1。当然在此过程中,可能会出现“造假”的问题。也就是说,每一个新注册的节点,都可以有自己的链。这些链长短不一,为了保证账本的一致性,需要通过一种一致性共识算法来找到最长的链,作为样本,同步数据,保证每个节点上的账本信息都是一致的。

数据结构

  • 区块链
    这里写图片描述
    如图所示,索引为1的区块即为创始区块。可想而知,可以用List<区块>来表示区块链。其中,区块链的高度即为链上区块的块数,上图区块高度为4。
  • 区块
    这里写图片描述
    单个区块的数据结构有索引、交易列表、时间戳、工作量证明、上一个区块的hash组成。
  • 交易列表
    这里写图片描述
    整个区块链就是一个超级大的分布式账本,当发生交易时,矿工们通过计算工作量证明的方法来进行挖矿(本文中挖到矿将得到1个币的奖励),将发生的交易记录到账本之中。

    Web API

    我们将通过Postman来模拟请求。请求API如下:
    /nodes/register 注册网络节点 /nodes/resolve 一致性共识算法 /transactions/new 新建交易 /mine 挖矿 /chain 输出整条链的数据

    项目目录结构

    Gradle Web 项目
    这里写图片描述
    1
    2
    3
    4
    5
    6
    dependencies {
    compile('javax:javaee-api:7.0')
    compile('org.json:json:20160810')

    testCompile('junit:junit:4.12')
    }

实现代码

注释写的很详细,如果遇到不懂的地方,欢迎大家一同讨论。

  • BlockChain类 ,所有的核心代码都在其中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    // 存储区块链
    private List<Map<String, Object>> chain;
    // 该实例变量用于当前的交易信息列表
    private List<Map<String, Object>> currentTransactions;
    // 网络中所有节点的集合
    private Set<String> nodes;


    private static BlockChain blockChain = null;

    private BlockChain() {
    // 初始化区块链以及当前的交易信息列表
    chain = new ArrayList<Map<String, Object>>();
    currentTransactions = new ArrayList<Map<String, Object>>();
    // 初始化存储网络中其他节点的集合
    nodes = new HashSet<String>();

    // 创建创世区块
    newBlock(100, "0");
    }

    /**
    * 在区块链上新建一个区块
    * @param proof 新区块的工作量证明
    * @param previous_hash 上一个区块的hash值
    * @return 返回新建的区块
    */
    public Map<String, Object> newBlock(long proof, String previous_hash) {

    Map<String, Object> block = new HashMap<String, Object>();
    block.put("index", getChain().size() + 1);
    block.put("timestamp", System.currentTimeMillis());
    block.put("transactions", getCurrentTransactions());
    block.put("proof", proof);
    // 如果没有传递上一个区块的hash就计算出区块链中最后一个区块的hash
    block.put("previous_hash", previous_hash != null ? previous_hash : hash(getChain().get(getChain().size() - 1)));

    // 重置当前的交易信息列表
    setCurrentTransactions(new ArrayList<Map<String, Object>>());

    getChain().add(block);

    return block;
    }

    // 创建单例对象
    public static BlockChain getInstance() {
    if (blockChain == null) {
    synchronized (BlockChain.class) {
    if (blockChain == null) {
    blockChain = new BlockChain();
    }
    }
    }
    return blockChain;
    }

    /**
    * @return 得到区块链中的最后一个区块
    */
    public Map<String, Object> lastBlock() {
    return getChain().get(getChain().size() - 1);
    }

    /**
    * 生成新交易信息,信息将加入到下一个待挖的区块中
    * @param sender 发送方的地址
    * @param recipient 接收方的地址
    * @param amount 交易数量
    * @return 返回该交易事务的块的索引
    */
    public int newTransactions(String sender, String recipient, long amount) {

    Map<String, Object> transaction = new HashMap<String, Object>();
    transaction.put("sender", sender);
    transaction.put("recipient", recipient);
    transaction.put("amount", amount);

    getCurrentTransactions().add(transaction);

    return (Integer) lastBlock().get("index") + 1;
    }

    /**
    * 生成区块的 SHA-256格式的 hash值
    * @param block 区块
    * @return 返回该区块的hash
    */
    public static Object hash(Map<String, Object> block) {
    return new Encrypt().Hash(new JSONObject(block).toString());
    }

    /**
    * 注册节点
    * @param address 节点地址
    * @throws MalformedURLException
    */
    public void registerNode(String address) throws MalformedURLException {
    URL url = new URL(address);
    String node = url.getHost() + ":" + (url.getPort() == -1 ? url.getDefaultPort() : url.getPort());
    nodes.add(node);
    }

    /**
    * 验证是否为有效链,遍历每个区块验证hash和proof,来确定一个给定的区块链是否有效
    * @param chain
    * @return
    */
    public boolean vaildChain(List<Map<String,Object>> chain) {
    Map<String,Object> lastBlock = chain.get(0);
    int currentBlockIndex = 1;
    while (currentBlockIndex < lastBlock.size()) {
    Map<String,Object> currentBlock = chain.get(currentBlockIndex);
    //检查区块的hash是否正确
    if (!currentBlock.get("previous_hash").equals(hash(lastBlock))) {
    return false;
    }
    lastBlock = currentBlock;
    currentBlockIndex ++;
    }
    return true;
    }

    /**
    * 使用网络中最长的链. 遍历所有的邻居节点,并用上一个方法检查链的有效性,
    * 如果发现有效更长链,就替换掉自己的链
    * @return 如果链被取代返回true, 否则返回false
    * @throws IOException
    */
    public boolean resolveConflicts() throws IOException {
    //获得当前网络上所有的邻居节点
    Set<String> neighbours = this.nodes;

    List<Map<String, Object>> newChain = null;

    // 寻找最长的区块链0
    long maxLength = this.chain.size();

    // 获取并验证网络中的所有节点的区块链
    for (String node : neighbours) {

    URL url = new URL("http://" + node + "/chain");
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    connection.connect();

    if (connection.getResponseCode() == 200) {
    BufferedReader bufferedReader = new BufferedReader(
    new InputStreamReader(connection.getInputStream(), "utf-8"));
    StringBuffer responseData = new StringBuffer();
    String response = null;
    while ((response = bufferedReader.readLine()) != null) {
    responseData.append(response);
    }
    bufferedReader.close();

    JSONObject jsonData = new JSONObject(responseData.toString());
    long length = jsonData.getLong("blockLength");
    List<Map<String, Object>> chain = (List) jsonData.getJSONArray("chain").toList();

    // 检查长度是否长,链是否有效
    if (length > maxLength && vaildChain(chain)) {
    maxLength = length;
    newChain = chain;
    }
    }

    }
    // 如果发现一个新的有效链比我们的长,就替换当前的链
    if (newChain != null) {
    this.chain = newChain;
    return true;
    }
    return false;
    }
  • Proof 类 ,计算工作量证明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    /**
    * 计算当前区块的工作量证明
    * @param last_proof 上一个区块的工作量证明
    * @return
    */
    public long ProofOfWork(long last_proof){
    long proof = 0;
    while (!(vaildProof(last_proof,proof))) {
    proof ++;
    }
    return proof;
    }

    /**
    * 验证证明,是否拼接后的Hash值以4个0开头
    * @param last_proof 上一个区块工作量证明
    * @param proof 当前区块的工作量证明
    * @return
    */
    public boolean vaildProof(long last_proof, long proof) {
    String guess = last_proof + "" + proof;
    String guess_hash = new Encrypt().Hash(guess);
    boolean flag = guess_hash.startsWith("0000");
    return flag;
    }
  • Encrypt 类 ,Hash计算工具类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public class Encrypt {
    /**
    * 传入字符串,返回 SHA-256 加密字符串
    * @param strText
    * @return
    */
    public String Hash(final String strText) {
    // 返回值
    String strResult = null;
    // 是否是有效字符串
    if (strText != null && strText.length() > 0) {
    try {
    // 创建加密对象,传入要加密类型
    MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
    // 传入要加密的字符串
    messageDigest.update(strText.getBytes());
    // 执行哈希计算,得到 byte 数组
    byte byteBuffer[] = messageDigest.digest();
    // 將 byte 数组转换 string 类型
    StringBuffer strHexString = new StringBuffer();
    // 遍历 byte 数组
    for (int i = 0; i < byteBuffer.length; i++) {
    // 转换成16进制并存储在字符串中
    String hex = Integer.toHexString(0xff & byteBuffer[i]);
    if (hex.length() == 1) {
    strHexString.append('0');
    }
    strHexString.append(hex);
    }
    // 得到返回結果
    strResult = strHexString.toString();
    } catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
    }
    }
    return strResult;
    }
    }
  • FullChain 类,输出整条链的信息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * @Author: cfx
    * @Description: 该Servlet用于输出整个区块链的数据(Json)
    * @Date: Created in 2018/5/9 17:24
    */
    @WebServlet("/chain")
    public class FullChain extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    BlockChain blockChain = BlockChain.getInstance();
    Map<String,Object> response = new HashMap<String, Object>();
    response.put("chain",blockChain.getChain());
    response.put("blockLength",blockChain.getChain().size());

    JSONObject jsonObject = new JSONObject(response);
    resp.setContentType("application/json");
    PrintWriter printWriter = resp.getWriter();
    printWriter.println(jsonObject);
    printWriter.close();
    }
    }
  • InitialID 类 ,初始化时执行,随机的uuid作为矿工的账户地址。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
    * @Author: cfx
    * @Description: 初始化时,使用UUID来作为节点ID
    * @Date: Created in 2018/5/9 17:17
    */
    @WebListener
    public class InitialID implements ServletContextListener {

    public void contextInitialized(ServletContextEvent sce) {
    ServletContext servletContext = sce.getServletContext();
    String uuid = UUID.randomUUID().toString().replace("-", "");
    servletContext.setAttribute("uuid", uuid);
    System.out.println("uuid is : "+servletContext.getAttribute("uuid"));
    }

    public void contextDestroyed(ServletContextEvent sce) {
    }
    }
  • Register 类 ,节点注册类,记录网络上所有的节点,用户共识算法,保证所有的节点上的账本都是一致的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    /**
    * @Author: cfx
    * @Description: 注册网络节点
    * @Date: Created in 2018/5/10 11:26
    */
    @WebServlet("/nodes/register")
    public class Register extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    req.setCharacterEncoding("utf-8");
    // 读取客户端传递过来的数据并转换成JSON格式
    BufferedReader reader = req.getReader();
    String input = null;
    StringBuffer requestBody = new StringBuffer();
    while ((input = reader.readLine()) != null) {
    requestBody.append(input);
    }
    JSONObject jsonValue = new JSONObject(requestBody.toString());
    BlockChain blockChain = BlockChain.getInstance();
    blockChain.registerNode(jsonValue.getString("nodes"));

    PrintWriter printWriter = resp.getWriter();
    printWriter.println(new JSONObject().append("message","The Nodes is : " + blockChain.getNodes()));
    printWriter.close();

    }
    }
  • NewTransaction 类,新建交易类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    /**
    * @Author: cfx
    * @Description: 该Servlet用于接收并处理新的交易信息
    * @Date: Created in 2018/5/9 17:22
    */
    @WebServlet("/transactions/new")
    public class NewTransaction extends HttpServlet {

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

    req.setCharacterEncoding("utf-8");
    // 读取客户端传递过来的数据并转换成JSON格式
    BufferedReader reader = req.getReader();
    String input = null;
    StringBuffer requestBody = new StringBuffer();
    while ((input = reader.readLine()) != null) {
    requestBody.append(input);
    }
    JSONObject jsonValues = new JSONObject(requestBody.toString());

    // 检查所需要的字段是否位于POST的data中
    String[] required = { "sender", "recipient", "amount" };
    for (String string : required) {
    if (!jsonValues.has(string)) {
    // 如果没有需要的字段就返回错误信息
    resp.sendError(400, "Missing values");
    }
    }

    // 新建交易信息
    BlockChain blockChain = BlockChain.getInstance();
    int index = blockChain.newTransactions(jsonValues.getString("sender"), jsonValues.getString("recipient"),
    jsonValues.getLong("amount"));

    // 返回json格式的数据给客户端
    resp.setContentType("application/json");
    PrintWriter printWriter = resp.getWriter();
    printWriter.println(new JSONObject().append("message", "Transaction will be added to Block " + index));
    printWriter.close();
    }
    }
  • Mine , 挖矿类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    /**
    * @Author: cfx
    * @Description: 该Servlet用于运行工作算法的证明来获得下一个证明,也就是所谓的挖矿
    * @Date: Created in 2018/5/9 17:21
    */
    @WebServlet("/mine")
    public class Mine extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    BlockChain blockChain = BlockChain.getInstance();

    //计算出工作量证明
    Map<String,Object> lastBlock = blockChain.lastBlock();
    Long last_proof = Long.parseLong(lastBlock.get("proof") + "");
    Long proof = new Proof().ProofOfWork(last_proof);

    //奖励计算出工作量证明的矿工1个币的奖励,发送者为"0"表明这是新挖出的矿。
    String uuid = (String) this.getServletContext().getAttribute("uuid");
    blockChain.newTransactions("0",uuid,1);

    //构建新的区块
    Map<String,Object> newBlock = blockChain.newBlock(proof,null);
    Map<String, Object> response = new HashMap<String, Object>();
    response.put("message", "New Block Forged");
    response.put("index", newBlock.get("index"));
    response.put("transactions", newBlock.get("transactions"));
    response.put("proof", newBlock.get("proof"));
    response.put("previous_hash", newBlock.get("previous_hash"));

    // 返回新区块的数据给客户端
    resp.setContentType("application/json");
    PrintWriter printWriter = resp.getWriter();
    printWriter.println(new JSONObject(response));
    printWriter.close();
    }
    }
  • Consensus 类 ,通过判断不同节点上链的长度,来找出最长链,这就是一致性共识算法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /**
    * @Author: cfx
    * @Description: 一致性共识算法,解决共识冲突,保证所有的节点都在同一条链上(最长链)
    * @Date: Created in 2018/5/10 11:38
    */
    @WebServlet("/nodes/resolve")
    public class Consensus extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    BlockChain blockChain = BlockChain.getInstance();
    boolean flag = blockChain.resolveConflicts();
    System.out.println("是否解决一致性共识冲突:" + flag);
    }
    }

运行结果

以下是本人之前的测试记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
首次请求/chain:
初始化Blockchain
{
"chain": [
{
"index": 1,
"proof": 100,
"transactions": [],
"timestamp": 1526284543591,
"previous_hash": "0"
}
],
"chainLenth": 1
}

请求/nodes/register,进行网络节点的注册。
request:
{
"nodes": "http://lcoalhost:8080"
}
response:
{"message":["All Nodes are:[lcoalhost:8080]"]}

请求/mine,进行挖矿。
{
"index": 2,
"proof": 35293,
"message": "New Block Forged",
"transactions": [
{
"amount": 1,
"sender": "0",
"recipient": "e91467fe51bd43b8ad7892b3bc09bd4e"
}
],
"previous_hash": "c4b2bb2f6e042680aed249309791cac96da6c1f65b811c306088723ae3c73f66"
}
请求/chain,查看链上所有区块的数据
{
"chain": [
{
"index": 1,
"proof": 100,
"transactions": [],
"timestamp": 1526284543591,
"previous_hash": "0"
},
{
"index": 2,
"proof": 35293,
"transactions": [
{
"amount": 1,
"sender": "0",
"recipient": "e91467fe51bd43b8ad7892b3bc09bd4e"
}
],
"timestamp": 1526284661678,
"previous_hash": "c4b2bb2f6e042680aed249309791cac96da6c1f65b811c306088723ae3c73f66"
}
],
"chainLenth": 2
}

请求/transactions/new,新建交易。
request:
{
"sender": "d4ee26eee15148ee92c6cd394edd974e",
"recipient": "someone-other-address",
"amount": 6
}
response:
{
"message": [
"Transaction will be added to Block 3"
]
}
请求/mine,计算出工作量证明。将上面的交易记录到账本之中。
{
"index": 3,
"proof": 35089,
"message": "New Block Forged",
"transactions": [
{
"amount": 6,
"sender": "d4ee26eee15148ee92c6cd394edd974e",
"recipient": "someone-other-address"
},
{
"amount": 1,
"sender": "0",
"recipient": "e91467fe51bd43b8ad7892b3bc09bd4e"
}
],
"previous_hash": "a12748a35d57a4a371cefc4a8c294236d69c762d28b889abb2ae34a31d2b7597"
}

请求/chain,查看链上所有区块的数据
{
"chain": [
{
"index": 1,
"proof": 100,
"transactions": [],
"timestamp": 1526284543591,
"previous_hash": "0"
},
{
"index": 2,
"proof": 35293,
"transactions": [
{
"amount": 1,
"sender": "0",
"recipient": "e91467fe51bd43b8ad7892b3bc09bd4e"
}
],
"timestamp": 1526284661678,
"previous_hash": "c4b2bb2f6e042680aed249309791cac96da6c1f65b811c306088723ae3c73f66"
},
{
"index": 3,
"proof": 35089,
"transactions": [
{
"amount": 6,
"sender": "d4ee26eee15148ee92c6cd394edd974e",
"recipient": "someone-other-address"
},
{
"amount": 1,
"sender": "0",
"recipient": "e91467fe51bd43b8ad7892b3bc09bd4e"
}
],
"timestamp": 1526284774452,
"previous_hash": "a12748a35d57a4a371cefc4a8c294236d69c762d28b889abb2ae34a31d2b7597"
}
],
"chainLenth": 3
}

存在的问题

有一个问题没有解决,就是我们启动多实例来模拟不同的网络节点时,并不能解决节点加入同一个Set的问题,也就是说根本无法通过节点本身来获得其他网络节点,进而判断最长链。所以/nodes/resolve请求暂时时无用的。期间也有想方法解决,比如通过所谓的“第三方”–数据库,当一个节点注册时,保存到数据库中;当第二个节点加入时,也加入到数据库中…当需要请求解决一致性算法时,去数据库中读取节点信息遍历即可。但是,自己没有去实现。这是我的想法,毕竟是两个不相干的实例。如果有朋友有其他的解决方案,请一定要告诉我!谢谢。

总结

通过简单的Demo实现区块链,当然其中简化了大量的实现细节,所以说其实并没有多少实际参考价值。但是意义在于,能帮助我们更容易的理解区块链,为之后的学习打下夯实的基础。

参考文章

https://learnblockchain.cn/2017/11/04/bitcoin-pow/
http://blog.51cto.com/zero01/2086195等。