美文网首页
使用Java编写自己的区块链

使用Java编写自己的区块链

作者: _FeoniX_ | 来源:发表于2020-12-23 19:39 被阅读0次
    区块链

    关于区块链技术,网络上有很多入门、科普的文章,如果大家对于区块链感兴趣,应该已经通过网络了解区块链的基本概念了,这里就不再赘述基本概念了。

    相信阅读本文章的朋友们应该都和我一样对于区块链技术感到新奇,都想知道区块链在代码上怎么实现的,所以本文是实战为主,理论为辅的,毕竟大家应该都看过不少的理论文章了,但是对于区块链具体实现还不是很清楚,本文就是用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.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </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 来添加新的交易信息:

    发送交易

    但是这时候还没有新的区块可以写入这个交易信息,所以我们还需要请求 http://localhost:8080/wallet/mine 来进行挖矿,挖出一个新的区块来存储这笔交易:

    挖矿

    经过多次挖矿和交易之后,来看一下完整的区块链,通过请求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:8080http://localhost:8081

    两个节点相互注册:

    注册节点 注册节点

    然后在8080节点上挖两个块,确保8080节点上的链最长:

    挖矿2次

    接着在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编写自己的区块链

          本文链接:https://www.haomeiwen.com/subject/efpunktx.html