January 03, 2018

用 Java 实现一个简易区块链

首先定义 Block Class:

import javax.xml.bind.DatatypeConverter;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * Block Class
 * @Author: Lisa Ding
 */
class Block {
    private int index = 0;
    private Timestamp timestamp = null;
    private String data = "";
    private String previousHash = "";
    private BigInteger nonce = new BigInteger("0");
    private int difficulty = 0;

    /**
     * the Block constructor.
     * @param index This is the position within the chain. Genesis is at 0.
     * @param timestamp This is the time this block was added.
     * @param data This is the transaction to be included on the blockchain.
     * @param difficulty This is the number of leftmost nibbles that need to be 0.
     */
    public Block(int index, Timestamp timestamp, String data, int difficulty) {
        this.index = index;
        this.timestamp = timestamp;
        this.data = data;
        this.difficulty = difficulty;
    }

    /**
     * This method computes a hash of the concatenation of the index, timestamp, data, previousHash, nonce, and difficulty.
     * @return a String holding Hexadecimal characters
     */
    public String calculateHash() {

        Date date = new Date();
        date.setTime(this.timestamp.getTime());

        String concat = Integer.toString(this.index) // index
                + new SimpleDateFormat("yyyymmdd").format(date)// timestamp
                + this.data // data
                + this.previousHash // previousHash
                + new String(this.nonce.toByteArray()) // nonce
                + this.difficulty; // difficulty

        MessageDigest md;
        String hex = "";

        try {
            md = MessageDigest.getInstance("SHA-256");
            md.update(concat.getBytes());
            byte[] digest = md.digest();
            hex = DatatypeConverter.printHexBinary(digest);

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        return hex;
    }

    /**
     * The proof of work methods finds a good hash. It increments the nonce until it produces a good hash. This method
     * calls calculateHash() to compute a hash of the concatenation of the index, timestamp, data, previousHash, nonce,
     * and difficulty. If the hash has the appropriate number of leading hex zeroes, it is done and returns that proper
     * hash. If the hash does not have the appropriate number of leading hex zeroes, it increments the nonce by 1 and tries
     * again. It continues this process, burning electricity and CPU cycles, until it gets lucky and finds a good hash.
     * @param difficulty determines how much work is required to produce a proper hash. This is the number of leftmost
     *                   nibbles that need to be 0.
     * @return a String with a hash that has the appropriate number of leading hex zeroes.
     */
    public String proofOfWork(int difficulty) {

        // Initially set nonce to zero.
        this.nonce = new BigInteger("0");
        String hash = "";
        int proofOfWork = 0;

        do {

            this.nonce = this.nonce.add(new BigInteger("1"));
            hash = calculateHash();
            proofOfWork = getProofOfWork(hash);

        } while (proofOfWork!= difficulty);

        return hash;
    }

    /**
     * Helper method to find the number of leftmost 0's (representing proof of work).
     * @param hash the hash string
     * @return the number of leftmost 0's (representing proof of work)
     */
    public static int getProofOfWork(String hash) {
        int proofOfWork = 0;
        for (int i = 0; i < hash.length(); i++) {
            char c = hash.toCharArray()[i];

            if (c == '0') {
                proofOfWork++;
            } else {
                break;
            }
        }
        return proofOfWork;
    }

    /**
     * Simple getter method
     * @return difficulty
     */
    public int getDifficulty() {
        return difficulty;
    }

    /**
     * Simple setter method
     * @param difficulty determines how much work is required to produce a proper hash
     */
    public void setDifficulty(int difficulty) {
        this.difficulty = difficulty;
    }

    /**
     * Override Java's toString method
     * @return A JSON representation of all of this block's data is returned.
     */
    @Override
    public String toString() {

        return "[" + this.index + ","
                + this.timestamp + ","
                + this.data + ","
                + this.previousHash + ","
                + this.nonce + ","
                + this.difficulty + "]\n";
    }

    /**
     * Simple getter method
     * @return previous hash
     */
    public String getPreviousHash() {
        return previousHash;
    }

    /**
     * Simple setter method
     * @param previousHash a hashpointer to this block's parent
     */
    public void setPreviousHash(String previousHash) {
        this.previousHash = previousHash;
    }

    /**
     * Simple getter method
     * @return index of block
     */
    public int getIndex() {
        return index;
    }

    /**
     * Simple setter method
     * @param index the index of this block in the chain
     */
    public void setIndex(int index) {
        this.index = index;
    }

    /**
     * Simple getter method
     * @return timestamp of this block
     */
    public Timestamp getTimestamp() {
        return timestamp;
    }

    /**
     * Simple setter method
     * @param timestamp of when this block was created
     */
    public void setTimestamp(Timestamp timestamp) {
        this.timestamp = timestamp;
    }

    /**
     * Simple getter method
     * @return this block's transaction
     */
    public String getData() {
        return data;
    }

    /**
     * Simple setter method
     * @param data represents the transaction held by this block
     */
    public void setData(String data) {
        this.data = data;
    }

    /**
     * Simple getter method
     * @return this block's transaction
     */
    public BigInteger getNonce() {
        return nonce;
    }

    /**
     * Simple setter method
     * @param nonce represents a BigInteger value determined by a proof of work routine.
     */
    public void setNonce(BigInteger nonce) {
        this.nonce = nonce;
    }
}

然后实现 BlockChain Class:

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Scanner;

/**
 * Block Chain Class
 * @Author: Lisa Ding
 */
public class BlockChain {

    /**
     * an ArrayList to hold Blocks
     */
    private static ArrayList<Block> chain;

    /**
     * a chain hash to hold a SHA256 hash of the most recently added Block.
     */
    private static String chainHash;

    /**
     * BlockChain default constructor.
     */
    public BlockChain() {

        // Initialize the block chain.
        chain = new ArrayList<>();

        // Create a genesis block with an empty string as the previous hash and a difficulty of 2.
        Block block = new Block(0, new Timestamp(System.currentTimeMillis()), "{Genesis}", 2);

        // Add the genesis block to block chain.
        chain.add(block);

        // Compute current chain hash
        chainHash = block.proofOfWork(2);
    }

    /**
     * Get system time
     * @return the current system time
     */
    public Timestamp getTime() {
        return new Timestamp(System.currentTimeMillis());
    }

    /**
     * Retrieve the latest block in the block chain
     * @return a reference to the most recently added Block.
     */
    public Block getLatestBlock() {
        return chain.get(chain.size() - 1);
    }

    /**
     * Add a block to the block chain
     * @prerequisite: the block is already constructed
     * Time complexity: With the prerequisite, it takes O(1) time to add block in block chain
     * @param newBlock is added to the BlockChain as the most recent block
     */
    public void addBlock(Block newBlock) {
        chain.add(newBlock);
    }

    /**
     * This method uses the toString method defined on each individual block.
     * @return a json String representation of the entire chain is returned.
     */
    @Override
    public String toString() {

        String entireChain = "{";
        for (Block chains : chain) {
            entireChain = entireChain + chains.toString();
        }

        return entireChain + "}";
    }

    /**
     * If the chain only contains one block, the genesis block at position 0, this routine computes the hash of the block
     * and checks that the hash has the requisite number of leftmost 0's (proof of work) as specified in the difficulty
     * field. It also checks that the chain hash is equal to this computed hash. If either check fails, return false.
     * Otherwise, return true. If the chain has more blocks than one, begin checking from block one. Continue checking
     * until you have validated the entire chain. The first check will involve a computation of a hash in Block 0 and a
     * comparison with the hash pointer in Block 1. If they match and if the proof of work is correct, go and visit the
     * next block in the chain. At the end, check that the chain hash is also correct.
     *
     * To analyze time complexity:
     * First, it will take O(N) time to loop from the front to end of the block in the block chain.
     * In the loop, there's a static get ProofOfWork method to obtain the proof of work in the block.
     * For difficulty = 4, on average the total time complexity should be O(4 * N)
     * For difficulty = 5, on average the total time complecity should be O(5 * N)
     *
     * @return true if and only if the chain is valid
     */
    public boolean isChainValid() {

        // Condition 1: If the chain only contains one block:
        if (chain.size() == 1) {

            // Check 1: if the proof of work is correct (it computes the hash of the block and checks that the hash has
            // the requisite number of leftmost 0's (proof of work) as specified in the difficulty field.)
            Block block = chain.get(0);

            int difficulty = block.getDifficulty();
            int proofOfWork = Block.getProofOfWork(block.calculateHash());

            if (proofOfWork != difficulty) {
                System.out.println("the hash of the block does not has the requisite number of leftmost 0's (proof of work)" +
                        " as specified in the difficulty field." +
                        "number of leftmost 0's (proof of work): " + proofOfWork + "\n" +
                        "difficulty: " + difficulty);
                return false;
            }

            // Check 2: if the chain hash is equal to this computed hash.
            if (! chainHash.equals(block.calculateHash())) {
                System.out.println("the chain hash is not equal to the computed hash.\n" +
                        "the chain hash: " + Integer.toString(block.hashCode()) + "\n" +
                        "the computed hash: " + block.calculateHash());
                return false;
            }

        // Condition 2: If the chain has more blocks than one:
        } else {

            // begin checking from block one
            for (int i = 1; i < chain.size(); i++) {

                Block prevBlock = chain.get(i - 1);
                Block curBlock = chain.get(i);

                // Check 1:  if a computation of a hash in previous block is equal to the hash pointer in current block.
                if (!prevBlock.calculateHash().equals(curBlock.getPreviousHash())) {
                    System.out.println("The hash in previous block is not equal to the hash pointer in current block " +
                            "at chain block " + i + ".\n" +
                            "hash in previous block: " + prevBlock.calculateHash() + "\n" +
                            "hash pointer in current block: " + curBlock.getPreviousHash());
                    return false;
                }

                // Check 2: if the proof of work is correct
                Block block = chain.get(i);

                int difficulty = block.getDifficulty();
                int proofOfWork = Block.getProofOfWork(block.calculateHash());

                if (proofOfWork < difficulty) {
                    System.out.println("the hash of the block does not has the requisite number of leftmost 0's (proof of work)" +
                            " as specified in the difficulty field." +
                            "number of leftmost 0's (proof of work): " + proofOfWork + "\n" +
                            "difficulty: " + difficulty);
                    return false;
                }

            }
        }

        // Return true it the chain passes all checks.
        return true;
    }

最后为 BlockChain Class 添加 main method:

    /**
     * Test driver for Blockchain Class.
     * @param args is unused
     */
    public static void main(String[] args) {

        // Create a BlockChain object
        // The blockchain is initialized with a Genesis block when calling the constrictor.
        BlockChain blockChain = new BlockChain();

        System.out.println("Block Chain Menu\n" +
                "1. Add a transaction to the blockchain.\n" +
                "2. Verify the blockchain.\n" +
                "3. View the blockchain.\n" +
                "4. Exit");

        String input = "";

        do {

            System.out.println("\nPlease enter an option:");

            Scanner scan = new Scanner(System.in);
            input = scan.nextLine();

            // Menu Option 1: Add a transaction to the blockchain.
            switch (input) {

                case "1":

                    System.out.println("Enter difficulty");
                    int difficulty = Integer.parseInt(scan.nextLine());
                    System.out.println("Enter transaction");
                    String transaction = scan.nextLine();

                    // Create a new block according to difficulty and transaction
                    Block newBlock = new Block(chain.size(), blockChain.getTime(), transaction, difficulty);

                    // Set its previous hash
                    newBlock.setPreviousHash(chainHash);

                    // compute nonce value by calling proofOfWork, and set current hash to chainHash;
                    chainHash = newBlock.proofOfWork(difficulty);

                    // Add this block to block chain. This operation takes O(1) time
                    blockChain.addBlock(newBlock);

                    System.out.println("The block has been added to the block chain.");

                    /*
                      Analysis with blocks of difficulty 4 and blocks with difficulty 5:
                      I use a stop watch to record how much time it takes to add block with difficulty 4 and 5.
                      For difficulty 4, it takes around 4 - 5 seconds to add a block
                      For difficulty 5, it takes 6 - 7 seconds to add a block.
                      The more the difficulty, the more time it takes to add the block.
                     */

                    break;

                // Menu Option 2: Verify the block chain.
                case "2":

                    System.out.println("Verifying");
                    System.out.println("Chain verification: " + (blockChain.isChainValid() ? "true" : "false"));

                    break;

                // Menu Option 3: View the block chain
                case "3":

                    System.out.println("chain:" + blockChain);
                    System.out.println("chainHash == \n" + chainHash);

                    break;
            }
        } while (!input.equals("4"));
    }
}
comments powered by Disqus