使用Java编写自己的区块链

使用Java编写自己的区块链

  • 准备工作
  • 开发环境
  • 开始开发
    • Transaction类
    • Block类
    • BlockChain类
    • 实现交易功能
    • 实现创建新块功能
    • 工作量证明
    • Blockchain作为API接口
    • 绑定节点ID
    • 创建Controller类
    • 运行区块链
    • 一致性(共识)
    • 注册节点
    • 实现共识算法
    • 创建NodesController类

使用Java编写自己的区块链_第1张图片
关于区块链技术,网络上有很多入门、科普的文章,如果大家对于区块链感兴趣,应该已经通过网络了解区块链的基本概念了,这里就不再赘述基本概念了。

相信阅读本文章的朋友们应该都和我一样对于区块链技术感到新奇,都想知道区块链在代码上怎么实现的,所以本文是实战为主,理论为辅的,毕竟大家应该都看过不少的理论文章了,但是对于区块链具体实现还不是很清楚,本文就是用Java语言来实现一个简易的区块链。

准备工作

使用Java语言编写区块链程序,需要掌握基本的JavaSE以及JavaWeb开发,能够使用Java开发简单的项目,并且对于HTTP协议有一定的了解。

相信大家都听说过区块链的记录构成是不可变、有序的链结构,记录可以是交易、文件或任何你想要的数据,重要的是它们是通过哈希值(Hash)连接起来的。

如果你还不知道什么是哈希,可以查看 这篇文章 。

开发环境

  • JDK 1.8
  • Tomcat 9.0
  • Maven 3.6
  • IntelliJ IDEA 2018及以上版本
  • Springboot 2.3.7.RELEASE
  • alibaba fastjson 1.2.47
  • Postman

pom.xml文件配置内容:

<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-testartifactId>
        <scope>testscope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintagegroupId>
                <artifactId>junit-vintage-engineartifactId>
            exclusion>
        exclusions>
    dependency>
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>fastjsonartifactId>
        <version>1.2.47version>
    dependency>
dependencies>

开始开发

Transaction类

首先创建一个Transaction类,主要有三个参数分别是:sender(发送者)、recipient(接收者)、amount(金额)。

以下是Transaction类的代码:

package com.feonix.blockchain.pojo;

import java.io.Serializable;

/**
 * 交易类
 */
public class Transaction implements Serializable {
    /**
     * 发送者
     */
    private String sender;
    /**
     * 接收者
     */
    private String recipient;
    /**
     * 交易金额
     */
    private long amount;

    public Transaction() {
    }

    public Transaction(String sender, String recipient, long amount) {
        this.sender = sender;
        this.recipient = recipient;
        this.amount = amount;
    }

    public String getSender() {
        return sender;
    }

    public void setSender(String sender) {
        this.sender = sender;
    }

    public String getRecipient() {
        return recipient;
    }

    public void setRecipient(String recipient) {
        this.recipient = recipient;
    }

    public long getAmount() {
        return amount;
    }

    public void setAmount(long amount) {
        this.amount = amount;
    }

    @Override
    public String toString() {
        return "Transaction{" +
                "sender='" + sender + '\'' +
                ", recipient='" + recipient + '\'' +
                ", amount=" + amount +
                '}';
    }
}

Transaction类是用来表示交易的实体类,交易中所涉及的要素都在类中体现出来。

Block类

再创建一个Block区块类,每个区块包含属性:index(索引)、timestamp(时间戳)、transactions(交易列表)、proof(工作量证明)、previous_hash(前一个区块的哈希值)。

以下是一个区块的结构:

block = {
    "index": 2,
    "previous_hash": "d86a71b5af281c5b32cf50323114975ae0394ca2754b0a590390e65e5bd6cc68",
    "proof": 35293,
    "timestamp": 1608699216469,
    "transactions": [
        {
            "amount": 5,
            "recipient": "327598e7426e4c6593e167444a13efvc",
            "sender": "672798e7426e4c6593e167444a13fcad"
        }
    ]
}

以下是Block类的具体实现:

package com.feonix.blockchain.pojo;

import java.io.Serializable;
import java.util.List;

/**
 * 区块类
 */
public class Block implements Serializable {
    /**
     * 索引
     */
    private int index;
    /**
     * 时间戳
     */
    private long timestamp;
    /**
     * 交易列表
     */
    private List<Transaction> transactions;
    /**
     * 工作量证明
     */
    private long proof;
    /**
     * 前一个区块的哈希值
     */
    private String previous_hash;

    public Block() {
    }

    public Block(int index, long timestamp, List<Transaction> transactions, long proof, String previous_hash) {
        this.index = index;
        this.timestamp = timestamp;
        this.transactions = transactions;
        this.proof = proof;
        this.previous_hash = previous_hash;
    }

    public int getIndex() {
        return index;
    }

    public void setIndex(int index) {
        this.index = index;
    }

    public long getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(long timestamp) {
        this.timestamp = timestamp;
    }

    public List<Transaction> getTransactions() {
        return transactions;
    }

    public void setTransactions(List<Transaction> transactions) {
        this.transactions = transactions;
    }

    public long getProof() {
        return proof;
    }

    public void setProof(long proof) {
        this.proof = proof;
    }

    public String getPrevious_hash() {
        return previous_hash;
    }

    public void setPrevious_hash(String previous_hash) {
        this.previous_hash = previous_hash;
    }

    @Override
    public String toString() {
        return "Block{" +
                "index=" + index +
                ", timestamp=" + timestamp +
                ", transactions=" + transactions +
                ", proof=" + proof +
                ", previous_hash='" + previous_hash + '\'' +
                '}';
    }
}

BlockChain类

接下来创建BlockChain类,在构造器中创建了两个主要的集合,一个用于储存区块链,一个用于储存交易列表,这是本文中所用到的核心类,关于区块链的操作都封装在这个类中。

下面是BlockChain类的基础代码框架:

package com.feonix.blockchain.dao;

import com.feonix.blockchain.pojo.Block;
import com.feonix.blockchain.pojo.Transaction;

public class BlockChain {
    /**
     * 存储区块链
     */
    private List<Block> chain;
    /**
     * 当前交易信息列表
     */
    private List<Transaction> currentTransactions;

    private BlockChain() {
        // 初始化区块链
        this.chain = new ArrayList<Block>();
        // 初始化当前的交易信息列表
        this.currentTransactions = new ArrayList<Transaction>();
    }

    public List<Block> getChain() {
        return chain;
    }

    public void setChain(List<Block> chain) {
        this.chain = chain;
    }

    public List<Transaction> getCurrentTransactions() {
        return currentTransactions;
    }

    public void setCurrentTransactions(List<Transaction> currentTransactions) {
        this.currentTransactions = currentTransactions;
    }

    public Block getLastBlock() {
        return null;
    }

    public Block newBlock(long proof, String previous_hash) {
        return null;
    }

    public static String hash(Block block) {
        return null;
    }
}

BlockChain类用来管理区块链,它能存储交易,加入新块等,到这里,区块链的概念就清楚了,每个新的区块都包含上一个区块的Hash,这是关键的一点,它保障了区块链不可变性。如果攻击者破坏了前面的某个区块,那么后面所有区块的Hash都会变得不正确。不理解的话,慢慢消化,可以参考 区块链记账原理。

接下来让我们进一步完善这个区块链程序,由于需要计算区块的hash,我们需要先编写一个计算hash值的工具类:

package com.feonix.blockchain.util;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * 加密
 */
public class Encrypt {
    /**
     * 传入字符串,返回 SHA-256 加密字符串
     *
     * @param strText
     * @return
     */
    public static String getSHA256(final String strText) {
        return SHA(strText, "SHA-256");
    }

    /**
     * 传入字符串,返回 SHA-512 加密字符串
     *
     * @param strText
     * @return
     */
    public static String getSHA512(final String strText) {
        return SHA(strText, "SHA-512");
    }

    /**
     * 传入字符串,返回 MD5 加密字符串
     *
     * @param strText
     * @return
     */
    public static String getMD5(final String strText) {
        return SHA(strText, "SHA-512");
    }

    /**
     * 字符串 SHA 加密
     *
     * @param strText
     * @param strType
     * @return
     */
    private static String SHA(final String strText, final String strType) {
        // 返回值
        String strResult = null;

        // 是否是有效字符串
        if (strText != null && strText.length() > 0) {
            try {
                // SHA 加密开始
                // 创建加密对象,传入加密类型
                MessageDigest messageDigest = MessageDigest.getInstance(strType);
                // 传入要加密的字符串
                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;
    }
}

实现交易功能

接下来我们需要实现一个交易/记账功能,所以来添加一个newTransactions方法,并完善getLastBlock方法:

	/**
     * 获取到区块链中最后一个区块
     *
     * @return
     */
    public Block getLastBlock() {
        return getChain().get(getChain().size() - 1);
    }
    
	/**
     * 生成新交易信息,信息将加入到下一个待挖的区块中
     *
     * @param sender    发送方的地址
     * @param recipient 接收方的地址
     * @param amount    交易数量
     * @return 返回该交易事务的块的索引
     */
    public int newTransactions(String sender, String recipient, long amount) {

        Transaction transaction = new Transaction();
        transaction.setSender(sender);
        transaction.setRecipient(recipient);
        transaction.setAmount(amount);

        getCurrentTransactions().add(transaction);

        return getLastBlock().getIndex() + 1;
    }

newTransactions方法向列表中添加一个交易记录,并返回该记录将被添加到的区块 (下一个待挖掘的区块)的索引,等下在用户提交交易时会有用。

实现创建新块功能

当Blockchain实例化后,我们需要构造一个创世区块(没有前区块的第一个区块),并且给它加上一个工作量证明。
每个区块都需要经过工作量证明,俗称挖矿,稍后会继续讲解。

为了构造创世块,我们还需要完善剩下的几个方法,并且把该类设计为单例:

package com.feonix.blockchain.dao;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.feonix.blockchain.pojo.Block;
import com.feonix.blockchain.pojo.Transaction;
import com.feonix.blockchain.util.Encrypt;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class BlockChain {
    /**
     * 存储区块链
     */
    private List<Block> chain;
    /**
     * 当前交易信息列表
     */
    private List<Transaction> currentTransactions;

    private static BlockChain blockChain = null;

    private BlockChain() {
        // 初始化区块链
        this.chain = new ArrayList<Block>();
        // 初始化当前的交易信息列表
        this.currentTransactions = new ArrayList<Transaction>();

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

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

    public List<Block> getChain() {
        return chain;
    }

    public void setChain(List<Block> chain) {
        this.chain = chain;
    }

    public List<Transaction> getCurrentTransactions() {
        return currentTransactions;
    }

    public void setCurrentTransactions(List<Transaction> currentTransactions) {
        this.currentTransactions = currentTransactions;
    }

    /**
     * 获取到区块链中最后一个区块
     *
     * @return
     */
    public Block getLastBlock() {
        return getChain().get(getChain().size() - 1);
    }

    /**
     * 在区块链上新建一个区块
     *
     * @param proof         新区块的工作量证明
     * @param previous_hash 上一个区块的hash值
     * @return 返回新建的区块
     */
    public Block newBlock(long proof, String previous_hash) {
        Block block = new Block();
        block.setIndex(getChain().size() + 1);
        block.setTimestamp(System.currentTimeMillis());
        block.setTransactions(getCurrentTransactions());
        block.setProof(proof);
        block.setPrevious_hash(previous_hash != null ? previous_hash : hash(getLastBlock()));

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

        getChain().add(block);

        return block;
    }

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

        Transaction transaction = new Transaction();
        transaction.setSender(sender);
        transaction.setRecipient(recipient);
        transaction.setAmount(amount);

        getCurrentTransactions().add(transaction);

        return getLastBlock().getIndex() + 1;
    }
    
    /**
     * 生成区块的 SHA-256格式的 hash值
     *
     * @param block 区块
     * @return 返回该区块的hash
     */
    public static String hash(Block block) {
        return Encrypt.getSHA256(JSON.toJSONString(block));
    }
}

从上面的代码和注释可以对区块链有了直观的了解,接下来编写一些简单的测试代码来验证一下这些代码是否正常工作:

package com.feonix.blockchain;

import com.alibaba.fastjson.JSON;
import com.feonix.blockchain.dao.BlockChain;
import com.feonix.blockchain.pojo.Block;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.HashMap;
import java.util.Map;

@SpringBootTest
class BlockChainApplicationTests {
    @Test
    public void blockChainTest() {
        BlockChain blockChain = BlockChain.getInstance();

        Block block = blockChain.newBlock(300, null);
        System.out.println(JSON.toJSONString(block));

        // 一个区块中可以包含一笔交易记录
        blockChain.newTransactions("123", "222", 33);
        Block block1 = blockChain.newBlock(500, null);
        System.out.println(JSON.toJSONString(block1));

        // 一个区块中可以包含多笔交易记录
        blockChain.newTransactions("321", "555", 133);
        blockChain.newTransactions("000", "111", 10);
        blockChain.newTransactions("789", "369", 65);
        Block block2 = blockChain.newBlock(600, null);
        System.out.println(JSON.toJSONString(block2));

        // 查看整个区块链
        Map<String, Object> chain = new HashMap<String, Object>();
        chain.put("chain", blockChain.getChain());
        chain.put("length", blockChain.getChain().size());
        System.out.println(JSON.toJSONString(chain));
    }
}

运行结果:

// 挖出来的新区块
{
    "index": 2,
    "previous_hash": "c1d0e4bcdee5d2364031aab5d1b7aa71b6ee7843dc0b0a1adbb18e9c3ab80ecf",
    "proof": 300,
    "timestamp": 1608706922412,
    "transactions": []
}

// 包含一笔交易的区块
{
    "index": 3,
    "previous_hash": "a496d448bd27b9f11aea0a50c036bb6c95f40f2218d30d93a1c5c4729daae618",
    "proof": 500,
    "timestamp": 1608706922522,
    "transactions": [
        {
            "amount": 33,
            "recipient": "222",
            "sender": "123"
        }
    ]
}

// 包含多笔交易的区块
{
    "index": 4,
    "previous_hash": "0ac6ca410468f1cce92c895a5e39c5b125116877ecee8941a518d904433ff4ce",
    "proof": 600,
    "timestamp": 1608706922525,
    "transactions": [
        {
            "amount": 133,
            "recipient": "555",
            "sender": "321"
        },
        {
            "amount": 10,
            "recipient": "111",
            "sender": "000"
        },
        {
            "amount": 65,
            "recipient": "369",
            "sender": "789"
        }
    ]
}

// 整个区块链,索引为1的是创世区块
{
    "chain": [
        {
            "index": 1,
            "previous_hash": "0",
            "proof": 100,
            "timestamp": 1608706922412,
            "transactions": []
        },
        {
            "index": 2,
            "previous_hash": "c1d0e4bcdee5d2364031aab5d1b7aa71b6ee7843dc0b0a1adbb18e9c3ab80ecf",
            "proof": 300,
            "timestamp": 1608706922412,
            "transactions": []
        },
        {
            "index": 3,
            "previous_hash": "a496d448bd27b9f11aea0a50c036bb6c95f40f2218d30d93a1c5c4729daae618",
            "proof": 500,
            "timestamp": 1608706922522,
            "transactions": [
                {
                    "amount": 33,
                    "recipient": "222",
                    "sender": "123"
                }
            ]
        },
        {
            "index": 4,
            "previous_hash": "0ac6ca410468f1cce92c895a5e39c5b125116877ecee8941a518d904433ff4ce",
            "proof": 600,
            "timestamp": 1608706922525,
            "transactions": [
                {
                    "amount": 133,
                    "recipient": "555",
                    "sender": "321"
                },
                {
                    "amount": 10,
                    "recipient": "111",
                    "sender": "000"
                },
                {
                    "amount": 65,
                    "recipient": "369",
                    "sender": "789"
                }
            ]
        }
    ],
    "length": 4
}

通过以上的测试,可以看出区块链的直观数据,一个初步的区块链代码已经完成了,但是还有很多事情没有做,接下来我们看看区块是怎么挖出来的。

工作量证明

首先理解一下工作量证明,新的区块依赖工作量证明算法(PoW)来构造。PoW的目标是找出一个符合特定条件的数字,这个数字很难计算出来,但是很好验证。这就是工作量证明的核心思想。

为了方便理解,举个例子:

假设一个整数 x 加上另一个整数 y 的积的 Hash 值必须以 0 开头,即 hash(x + y) = 0b918943…e3f9。设变量 x = 10,求 y 的值?

Java代码实现如下:

package com.feonix.blockchain;

import com.feonix.blockchain.util.Encrypt;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ProofTest {

    @Test
    public void testProof() {

        int x = 10;
        int y = 0;

        while (!Encrypt.getSHA256((x + y) + "").startsWith("0")) {
            y++;
        }

        System.out.println("y=" + y);
        System.out.println("hash=" + Encrypt.getSHA256((x + y) + ""));
    }
}

运行结果:

y=29
hash=0b918943df0962bc7a1824c0555a389347b4febdc7cf9d1254406d80ce44e3f9

在比特币中,使用称为Hashcash的工作量证明算法,它和上面的问题很类似。矿工们为了争夺创建区块的权利而争相计算结果。通常,计算难度与目标字符串需要满足的特定字符的数量成正比,矿工算出结果后,会获得比特币奖励。

当然,在网络上非常容易验证这个结果。

接下来让我们来实现一个相似的PoW算法,规则是寻找一个数p,使其与前一个区块的proof拼接起来的字符串的Hash值以4个“0”开头:

	/**
     * 简单的工作量证明:
     * - 查找一个 p' 使得 hash(pp') 以4个0开头
     * - p 是上一个块的证明, p' 是当前的证明
     *
     * @param last_proof 上一个块的证明
     * @return
     */
    public long proofOfWork(long last_proof) {
        long proof = 0;
        while (!validProof(last_proof, proof)) {
            proof += 1;
        }
        return proof;
    }

    /**
     * 验证证明: 是否hash(last_proof, proof)以4个0开头?
     *
     * @param last_proof 上一个块的证明
     * @param proof      当前的证明
     * @return 以4个0开头返回true,否则返回false
     */
    public boolean validProof(long last_proof, long proof) {
        String guess = last_proof + "" + proof;
        String guess_hash = Encrypt.getSHA256(guess);
        return guess_hash.startsWith("0000");
    }

使用4个“0”开头用来演示比较合适,修改开头0的个数可以改变算法复杂度,增加一个0都会大大增加计算出结果的时间。

现在Blockchain类基本已经完成了,接下来使用Springboot提供的web服务能力接收HTTP请求来进行交互。

Blockchain作为API接口

我们将使用Springboot提供的web编程能力编写接收HTTP请求的网络服务,可以很方便的将请求数据映射到相应的方法上进行处理,现在我们来让BlockChain运行基于Java Web的网络服务上。

我们先来创建三个服务接口:

  • /wallet/trans 创建一个交易并添加到区块
  • /wallet/mine 告诉服务器去挖掘新的区块
  • /wallet/chain 查看整个区块链

绑定节点ID

我们的“Tomcat服务器”将扮演区块链网络中的一个节点,而每个节点都需要有一个唯一的标识符,也就是id。在这里我们使用UUID来作为节点ID,我们需要在服务器启动时,将UUID设置到ServletContext属性中,这样我们的服务器就拥有了唯一标识,这一步我们可以配置监听类来完成,我们来编写一个监听类:

package com.feonix.blockchain.config;

import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;

import javax.servlet.ServletContext;
import java.util.UUID;
/**
 * 这里利用Springboot的事件监听机制,当Springboot初始化完成时,就会扫描这些监听类,
 * 进行相应的数据初始化
 */
@Component
public class InitIDListener implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        // 将 ApplicationContext 转化为 WebApplicationContext
        WebApplicationContext webApplicationContext =
                (WebApplicationContext) contextRefreshedEvent.getApplicationContext();
        // 从 webApplicationContext 中获取  servletContext
        ServletContext servletContext = webApplicationContext.getServletContext();

        String uuid = UUID.randomUUID().toString().replace("-", "");
        // servletContext设置值,把UUID绑定到servletContext
        servletContext.setAttribute("uuid", uuid);
    }
}

创建Controller类

接下来创建一个WalletController类,用来对外提供HTTP请求服务接口:

package com.feonix.blockchain.controller;

import com.alibaba.fastjson.JSON;
import com.feonix.blockchain.dao.BlockChain;
import com.feonix.blockchain.pojo.Block;
import com.feonix.blockchain.pojo.Transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.ServletContext;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/wallet")
public class WalletController {
    @Autowired  // 这里直接注入servletContext
    private ServletContext servletContext;

    // 发起新交易
    @RequestMapping("/trans")
    public String trans(Transaction trans) {
        return null;
    }

    // 挖矿
    @RequestMapping("/mine")
    public String mine() {
        return null;
    }

    // 查看整个区块链数据
    @RequestMapping("/chain")
    public String fullChain() {
        return null;
    }
}

我们先来完善最简单的fullChain的代码,这个接口用于向客户端输出整个区块链的数据(JSON格式):

	// 查看整个区块链数据
    @RequestMapping("/chain")
    public String fullChain() {
        BlockChain blockChain = BlockChain.getInstance();
        Map<String, Object> resp = new HashMap<String, Object>();
        resp.put("chain", blockChain.getChain());
        resp.put("length", blockChain.getChain().size());

        return JSON.toJSONString(resp);
    }

然后是发起交易功能,每一个区块都可以记录交易数据,具体实现代码如下:

	// 发起新交易
    @RequestMapping("/trans")
    public String trans(Transaction trans) {
        Map<String, Object> resp = new HashMap<String, Object>();

        // 判断获取到的交易参数是否完整
        if (trans == null || trans.getSender() == null || trans.getRecipient() == null || trans.getAmount() <= 0) {
            resp.put("message", "Error: Missing values");
            return JSON.toJSONString(resp);
        }

        // 发送交易
        BlockChain blockChain = BlockChain.getInstance();
        int index = blockChain.newTransactions(trans.getSender(), trans.getRecipient(), trans.getAmount());

        // 向客户端返回处理结果
        resp.put("message", "Transaction will be added to Block " + index);

        return JSON.toJSONString(resp);
    }

接下来是挖矿,这正是神奇所在。它很简单,只做了以下三件事:

  • 计算工作量证明
  • 通过新增一个交易授予矿工(自己)一个币
  • 构造新的区块并将其添加到链中

代码实现如下:

	// 挖矿
    @RequestMapping("/mine")
    public String mine() {
        BlockChain blockChain = BlockChain.getInstance();
        Block lastBlock = blockChain.getLastBlock();
        long lastProof = Long.parseLong(lastBlock.getProof() + "");
        long proof = blockChain.proofOfWork(lastProof);

        // 给工作量证明的节点提供奖励,发送者为 "0" 表明是新挖出的币
        String uuid = (String) servletContext.getAttribute("uuid");
        blockChain.newTransactions("0", uuid, 1);

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

        return JSON.toJSONString(resp);
    }

注意挖矿的交易接收者是我们自己的服务器节点,我们做的大部分工作都只是围绕Blockchain类的方法进行交互。到此,我们的区块链就算完成了,我们来实际运行下。

运行区块链

由于我们这里也没有写前端的web页面,只写了后端的API,所以只能使用 Postman 之类的软件去和API进行交互。首先启动Tomcat服务器,然后通过post请求http://localhost:8080/wallet/trans 来添加新的交易信息:
使用Java编写自己的区块链_第2张图片

但是这时候还没有新的区块可以写入这个交易信息,所以我们还需要请求 http://localhost:8080/wallet/mine 来进行挖矿,挖出一个新的区块来存储这笔交易:
使用Java编写自己的区块链_第3张图片

经过多次挖矿和交易之后,来看一下完整的区块链,通过请求http://localhost:8080/wallet/chain 来查看所有的区块信息:

{
    "chain": [
        {
            "index": 1,
            "previous_hash": "0",
            "proof": 100,
            "timestamp": 1608699205833,
            "transactions": []
        },
        {
            "index": 2,
            "previous_hash": "d86a71b5af281c5b32cf50323114975ae0394ca2754b0a590390e65e5bd6cc68",
            "proof": 35293,
            "timestamp": 1608699216469,
            "transactions": [
                {
                    "amount": 1,
                    "recipient": "4fd33a6e70b84cdc9130348ce1d60fb8",
                    "sender": "0"
                }
            ]
        },
        {
            "index": 3,
            "previous_hash": "9233f72c4122fe55211e9668b75376dadc8115a3faea3cd5404abff967085436",
            "proof": 35089,
            "timestamp": 1608699223812,
            "transactions": [
                {
                    "amount": 6,
                    "recipient": "327598e7426e4c6593e167444a13efvc",
                    "sender": "672798e7426e4c6593e167444a13fcad"
                },
                {
                    "amount": 6,
                    "recipient": "327598e7426e4c6593e167444a13efvc",
                    "sender": "672798e7426e4c6593e167444a13fcad"
                },
                {
                    "amount": 1,
                    "recipient": "4fd33a6e70b84cdc9130348ce1d60fb8",
                    "sender": "0"
                }
            ]
        },
        {
            "index": 4,
            "previous_hash": "5324f91fa12fdcdaa7dd8fe91492c2ed2d6c7573d3d9c163b373c9127b2b3432",
            "proof": 119678,
            "timestamp": 1608699226009,
            "transactions": [
                {
                    "amount": 1,
                    "recipient": "4fd33a6e70b84cdc9130348ce1d60fb8",
                    "sender": "0"
                }
            ]
        },
        {
            "index": 5,
            "previous_hash": "190d443153a7a846ccc9ce8abfdd373b7807e192c1fe19b779dfe25fd735a906",
            "proof": 146502,
            "timestamp": 1608712131192,
            "transactions": [
                {
                    "amount": 6,
                    "recipient": "327598e7426e4c6593e167444a13efvc",
                    "sender": "672798e7426e4c6593e167444a13fcad"
                },
                {
                    "amount": 1,
                    "recipient": "4fd33a6e70b84cdc9130348ce1d60fb8",
                    "sender": "0"
                }
            ]
        }
    ],
    "length": 5
}

一致性(共识)

我们已经有了一个基本的区块链可以接受交易和挖矿。但是区块链系统应该是分布式的。既然是分布式的,那么我们究竟拿什么保证所有节点有同样的链呢?这就是一致性问题,我们要想在网络上有多个节点,就必须实现一个一致性的算法。

注册节点

在实现一致性算法之前,我们需要找到一种方式让一个节点知道它相邻的节点。每个节点都需要保存一份包含网络中其它节点的记录。因此让我们新增2个接口:

  • /nodes/register 接收URL形式的新节点列表
  • /nodes/resolve执行一致性算法,解决任何冲突,确保节点拥有正确的链

我们需要修改下BlockChain的构造函数并提供一个注册节点方法:

package com.feonix.blockchain.dao;
...
import java.net.URL;
...
	private Set<String> nodes;
    private BlockChain() {
        ...
        // 用于存储网络中其他节点的集合
        nodes = new HashSet<String>();
        ...
    }
    
    public Set<String> getNodes() {
        return nodes;
    }
	/**
     * 注册网络节点
     *
     * @param address 节点地址
     * @throws MalformedURLException
     */
    public void registerNode(String address) throws MalformedURLException {
        URL url = new URL(address);
        String node = String.format("%s:%s", url.getHost(), url.getPort() == -1 ? url.getDefaultPort() : url.getPort());
        nodes.add(node);
    }
    ...

我们用 HashSet 集合来储存节点,这是一种避免出现重复添加节点的简单方法。

实现共识算法

前面提到,冲突是指不同的节点拥有不同的链,为了解决这个问题,规定最长的、有效的链才是最终的链,换句话说,网络中有效最长链才是实际的链。

我们使用以下算法,来达到网络中的共识:

...
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
...
	/**
     * 检查是否是有效链,遍历每个区块验证hash和proof,来确定一个给定的区块链是否有效
     *
     * @param chain
     * @return
     */
    public boolean validChain(List<Block> chain) {
        Block lastBlock = chain.get(0);
        int currentIndex = 1;
        while (currentIndex < chain.size()) {
            Block block = chain.get(currentIndex);

            // 检查block的hash是否正确
            if (block.getPrevious_hash() == null || !block.getPrevious_hash().equals(hash(lastBlock))) {
                return false;
            }

            lastBlock = block;
            currentIndex++;
        }
        return true;
    }

    /**
     * 共识算法解决冲突,使用网络中最长的链.
     * 遍历所有的邻居节点,并用上一个方法检查链的有效性,
     * 如果发现有效更长链,就替换掉自己的链
     *
     * @return 如果链被取代返回true, 否则返回false
     * @throws IOException
     */
    public boolean resolveConflicts() throws IOException {
        List<Block> newChain = null;
        // 寻找最长的区块链
        long maxLength = this.chain.size();

        // 获取并验证网络中的所有节点的区块链
        for (String node : nodes) {
            URL url = new URL("http://" + node + "/wallet/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();

                // System.out.println("responseData ------> " + responseData.toString());

                JSONObject jsonData = JSON.parseObject(responseData.toString());
                long length = jsonData.getLong("length");
                List<Block> chain = jsonData.getJSONArray("chain").toJavaList(Block.class);

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

        }
        // 如果发现一个新的有效链比我们的长,就替换当前的链
        if (newChain != null) {
            this.chain = newChain;
            return true;
        }
        return false;
    }
    ...

第一个方法 validChain() 用来检查是否是有效链,遍历每个块验证hash和proof.

第2个方法 resolveConflicts() 用来解决冲突,遍历所有的邻居节点,并用上一个方法检查链的有效性, 如果发现有效更长链,就替换掉自己的链

创建NodesController类

NodesController是用来进行节点注册和解决冲突的服务控制器,包含register和resolve两个网络服务接口方法

package com.feonix.blockchain.controller;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.feonix.blockchain.dao.BlockChain;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.net.MalformedURLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/nodes")
public class NodesController {

    // 注册节点
    @RequestMapping(value = "/register", method = RequestMethod.POST, produces = "application/json")
    public String register(@RequestBody String jsonParam) throws MalformedURLException {
        Map<String, Object> result = new HashMap<String, Object>();

        JSONObject jsonObject = JSON.parseObject(jsonParam);
        List<String> addresses = jsonObject.getJSONArray("addresses").toJavaList(String.class);

        // 对获取到的节点地址集合判断是否为空
        if (addresses == null) {
            result.put("message", "Error: Please supply a valid list of nodes");
            return JSON.toJSONString(result);
        }

        // 注册节点
        BlockChain blockChain = BlockChain.getInstance();
        for (String address : addresses) {
            blockChain.registerNode(address);
        }

        // 向客户端返回处理结果
        result.put("message", "New nodes have been added");
        result.put("registered_nodes", blockChain.getNodes().toArray());

        return JSON.toJSONString(result);
    }

    // 解决冲突
    @RequestMapping("/resolve")
    public String resolve() throws IOException {
        BlockChain blockChain = BlockChain.getInstance();
        boolean resolved = blockChain.resolveConflicts();

        Map<String, Object> result = new HashMap<String, Object>();
        if (resolved) {
            result.put("message", "Our chain was replaced");
            result.put("new_chain", blockChain.getChain());
        } else {
            result.put("message", "Our chain is authoritative");
            result.put("chain", blockChain.getChain());
        }

        return JSON.toJSONString(result);
    }
}

我们可以在不同的机器运行节点,或在一台机机开启不同的网络端口来模拟多节点的网络,这里在同一台机器开启不同的端口演示,配置两个不同端口的服务器即可,我这里启动了两个节点:http://localhost:8080 和 http://localhost:8081。

两个节点相互注册:
使用Java编写自己的区块链_第4张图片
使用Java编写自己的区块链_第5张图片
然后在8080节点上挖两个块,确保8080节点上的链最长:
使用Java编写自己的区块链_第6张图片
接着在8081节点上访问接口/nodes/resolve ,这时8081节点的链会通过共识算法被8080节点的链取代:

{
    "new_chain": [
        {
            "index": 1,
            "previous_hash": "0",
            "proof": 100,
            "timestamp": 1608713986647,
            "transactions": []
        },
        {
            "index": 2,
            "previous_hash": "0977d2825796c2f313abad35f664c3e60623bbf84ec8a9ee21e6b6bef30c5b9f",
            "proof": 35293,
            "timestamp": 1608714234444,
            "transactions": [
                {
                    "amount": 1,
                    "recipient": "ece1a32593ea497c80ef9853f415b97e",
                    "sender": "0"
                }
            ]
        },
        {
            "index": 3,
            "previous_hash": "13f96d1d571f02df0e7a4c922ae3f3cf5fdc2ecac56f5fc252cbeda05daf3e02",
            "proof": 35089,
            "timestamp": 1608714236413,
            "transactions": [
                {
                    "amount": 1,
                    "recipient": "ece1a32593ea497c80ef9853f415b97e",
                    "sender": "0"
                }
            ]
        }
    ],
    "message": "Our chain was replaced"
}

到此为止我们就完成了一个区块链的开发,虽然这只是一个最基本的区块链,而且开发过程中也有很多细节方面没有考虑到。但是我们不妨以这个简单的区块链为基础,发挥自己的能力去重构、拓展、完善这个区块链程序,直至成为自己的一个小项目。


本文代码地址如下:

https://gitee.com/demo./BlockChain.git

你可能感兴趣的:(Java,区块链,java)