diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100755 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml new file mode 100755 index 0000000..d8d6852 --- /dev/null +++ b/.idea/checkstyle-idea.xml @@ -0,0 +1,15 @@ + + + + 10.3.4 + JavaOnly + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100755 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100755 index 0000000..79a352f --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100755 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100755 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/blockchain.sh b/blockchain.sh new file mode 100755 index 0000000..69b3e33 --- /dev/null +++ b/blockchain.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e +# Check if the jar has been built. +if [ ! -e target/blockchain-jar-with-dependencies.jar ]; then + echo "Compiling blockchain project to a JAR" + mvn package -DskipTests +fi +java -jar target/blockchain-jar-with-dependencies.jar "$@" \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100755 index 0000000..bccbd72 --- /dev/null +++ b/pom.xml @@ -0,0 +1,124 @@ + + + + 4.0.0 + + org.example + blockchain + 1.0-SNAPSHOT + jar + + blockchain Maven Webapp + + http://www.example.com + + + UTF-8 + 1.8 + 1.8 + + + + + junit + junit + 4.11 + test + + + org.projectlombok + lombok + 1.16.20 + + + org.apache.commons + commons-lang3 + 3.7 + + + commons-codec + commons-codec + 1.11 + + + commons-cli + commons-cli + 1.4 + + + com.esotericsoftware + kryo + 4.0.1 + + + org.rocksdb + rocksdbjni + 5.9.2 + + + + + blockchain + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.1.0 + + + + true + lib/ + com.example.blockchain.Main + + + + jar-with-dependencies + + + + + make-assembly + + package + + + single + + + + + + + diff --git a/src/main/java/com/example/blockchain/Main.java b/src/main/java/com/example/blockchain/Main.java new file mode 100755 index 0000000..a107905 --- /dev/null +++ b/src/main/java/com/example/blockchain/Main.java @@ -0,0 +1,10 @@ +package com.example.blockchain; + +import com.example.blockchain.cli.CLI; + +public class Main { + public static void main(String[] args) { + CLI cli = new CLI(args); + cli.run(); + } +} diff --git a/src/main/java/com/example/blockchain/block/Block.java b/src/main/java/com/example/blockchain/block/Block.java new file mode 100755 index 0000000..99e0211 --- /dev/null +++ b/src/main/java/com/example/blockchain/block/Block.java @@ -0,0 +1,77 @@ +package com.example.blockchain.block; + +import com.example.blockchain.pow.PowResult; +import com.example.blockchain.pow.ProofOfWork; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.codec.binary.Hex; + +import java.time.Instant; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class Block { + + /** + * 区块hash值 + */ + private String hash; + + /** + * 前一个区块的hash值 + */ + private String prevBlockHash; + + /** + * 区块数据,未来会替换为交易 + */ + private String data; + + /** + * 时间戳,单位秒 + */ + private long timeStamp; + + /** + * 区块的高度 + */ + private long height; + + /** + * 工作量证明计数器 + */ + private long nonce; + + + + + /** + * 创建新的区块 + * + * @param previousHash + * @param data + * @return + */ + public static Block newBlock(String previousHash, String data,long height) { + Block block = new Block("", previousHash, data, Instant.now().getEpochSecond(),height,0); + ProofOfWork pow = ProofOfWork.newProofOfWork(block); + PowResult powResult = pow.run(); + block.setHash(powResult.getHash()); + block.setNonce(powResult.getNonce()); + return block; + } + + private static final String ZERO_HASH = Hex.encodeHexString(new byte[32]); + + /** + * 创建创世区块 + * @return + */ + public static Block newGenesisBlock() { + return Block.newBlock(ZERO_HASH, "Genesis Block",0); + } + + +} diff --git a/src/main/java/com/example/blockchain/block/Blockchain.java b/src/main/java/com/example/blockchain/block/Blockchain.java new file mode 100755 index 0000000..93e420e --- /dev/null +++ b/src/main/java/com/example/blockchain/block/Blockchain.java @@ -0,0 +1,129 @@ +package com.example.blockchain.block; + +import com.example.blockchain.store.RocksDBUtils; +import com.example.blockchain.util.ByteUtils; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; + +@Data +@AllArgsConstructor +public class Blockchain { + + + /** + * 最后一个区块的hash + */ + private String lastBlockHash; + + + /** + * 创建区块链 + * @return + */ + public static Blockchain newBlockchain() { + + String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash(); + if (StringUtils.isBlank(lastBlockHash)){ + //对应的bucket不存在,说明是第一次获取区块链实例 + Block genesisBlock = Block.newGenesisBlock(); + lastBlockHash = genesisBlock.getHash(); + RocksDBUtils.getInstance().putBlock(genesisBlock); + RocksDBUtils.getInstance().putLastBlockHash(lastBlockHash); + + } + return new Blockchain(lastBlockHash); + } + + /** + * 根据block,添加区块 + * @param block + */ + public void addBlock(Block block) { + + RocksDBUtils.getInstance().putLastBlockHash(block.getHash()); + RocksDBUtils.getInstance().putBlock(block); + this.lastBlockHash = block.getHash(); + + } + + /** + * 根据data添加区块 + * @param data + */ + public void addBlock(String data) throws Exception{ + + String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash(); + Block lastBlock = RocksDBUtils.getInstance().getBlock(lastBlockHash); + if (StringUtils.isBlank(lastBlockHash)){ + throw new Exception("还没有数据库,无法直接添加区块。。"); + } + this.addBlock(Block.newBlock(lastBlockHash,data,lastBlock.getHeight()+1)); + + } + + + /** + * 区块链迭代器:内部类 + */ + public class BlockchainIterator{ + + /** + * 当前区块的hash + */ + private String currentBlockHash; + + /** + * 构造函数 + * @param currentBlockHash + */ + public BlockchainIterator(String currentBlockHash) { + this.currentBlockHash = currentBlockHash; + } + + /** + * 判断是否有下一个区块 + * @return + */ + public boolean hashNext() { + if (ByteUtils.ZERO_HASH.equals(currentBlockHash)) { + return false; + } + Block lastBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash); + if (lastBlock == null) { + return false; + } + // 如果是创世区块 + if (ByteUtils.ZERO_HASH.equals(lastBlock.getPrevBlockHash())) { + return true; + } + return RocksDBUtils.getInstance().getBlock(lastBlock.getPrevBlockHash()) != null; + } + + + /** + * 迭代获取区块 + * @return + */ + public Block next() { + Block currentBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash); + if (currentBlock != null) { + this.currentBlockHash = currentBlock.getPrevBlockHash(); + return currentBlock; + } + return null; + } + } + + /** + * 添加方法,用于获取迭代器实例 + * @return + */ + public BlockchainIterator getBlockchainIterator() { + return new BlockchainIterator(lastBlockHash); + } + + + + +} diff --git a/src/main/java/com/example/blockchain/cli/CLI.java b/src/main/java/com/example/blockchain/cli/CLI.java new file mode 100755 index 0000000..bc56626 --- /dev/null +++ b/src/main/java/com/example/blockchain/cli/CLI.java @@ -0,0 +1,129 @@ +package com.example.blockchain.cli; + +import com.example.blockchain.block.Block; +import com.example.blockchain.block.Blockchain; +import com.example.blockchain.pow.ProofOfWork; +import com.example.blockchain.store.RocksDBUtils; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; + +import java.text.SimpleDateFormat; +import java.util.Date; + +public class CLI { + private String[] args; + private Options options = new Options(); + + + + public CLI(String[] args) { + this.args = args; + + Option helpCmd = Option.builder("h").desc("show help").build(); + options.addOption(helpCmd); + + Option data = Option.builder("data").hasArg(true).desc("add block").build(); + + options.addOption(data); + + } + + /** + * 打印帮助信息 + */ + private void help() { + System.out.println("Usage:"); + System.out.println(" createblockchain -address ADDRESS - Create a blockchain and send genesis block reward to ADDRESS"); + System.out.println(" addblock -data DATA - Get balance of ADDRESS"); + System.out.println(" printchain - Print all the blocks of the blockchain"); + System.exit(0); + } + + /** + * 验证入参 + * + * @param args + */ + private void validateArgs(String[] args) { + if (args == null || args.length < 1) { + help(); + } + } + + /** + * 命令行解析入口 + */ + public void run() { + this.validateArgs(args); + try { + CommandLineParser parser = new DefaultParser(); + CommandLine cmd = parser.parse(options, args); + + switch (args[0]) { + case "createblockchain": + createBlockchainWithGenesisBlock(); + break; + case "addblock": + String data = cmd.getOptionValue("data"); + addBlock(data); + break; + case "printchain": + this.printChain(); + break; + case "h": + this.help(); + break; + default: + this.help(); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + RocksDBUtils.getInstance().closeDB(); + } + } + + /** + * 创建创世块 + */ + private void createBlockchainWithGenesisBlock(){ + Blockchain.newBlockchain(); + } + + /** + * 添加区块 + * + * @param data + */ + private void addBlock(String data) throws Exception { + Blockchain blockchain = Blockchain.newBlockchain(); + blockchain.addBlock(data); + } + + /** + * 打印出区块链中的所有区块 + */ + private void printChain() { + Blockchain blockchain = Blockchain.newBlockchain(); + Blockchain.BlockchainIterator iterator = blockchain.getBlockchainIterator(); + while (iterator.hashNext()) { + Block block = iterator.next(); + System.out.println("第" + block.getHeight() + "个区块信息:"); + + if (block != null){ + boolean validate = ProofOfWork.newProofOfWork(block).validate(); + System.out.println("validate = " + validate); + System.out.println("\tprevBlockHash: " + block.getPrevBlockHash()); + System.out.println("\tData: " + block.getData()); + System.out.println("\tHash: " + block.getHash()); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String date = sdf.format(new Date(block.getTimeStamp() * 1000L)); + System.out.println("\ttimeStamp:" + date); + System.out.println(); + } + } + } +} diff --git a/src/main/java/com/example/blockchain/pow/PowResult.java b/src/main/java/com/example/blockchain/pow/PowResult.java new file mode 100755 index 0000000..c480974 --- /dev/null +++ b/src/main/java/com/example/blockchain/pow/PowResult.java @@ -0,0 +1,19 @@ +package com.example.blockchain.pow; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class PowResult { + + /** + * 计数器 + */ + private long nonce; + /** + * hash值 + */ + private String hash; + +} diff --git a/src/main/java/com/example/blockchain/pow/ProofOfWork.java b/src/main/java/com/example/blockchain/pow/ProofOfWork.java new file mode 100755 index 0000000..6d19839 --- /dev/null +++ b/src/main/java/com/example/blockchain/pow/ProofOfWork.java @@ -0,0 +1,117 @@ +package com.example.blockchain.pow; + +import com.example.blockchain.block.Block; +import com.example.blockchain.util.ByteUtils; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.StringUtils; + +import java.math.BigInteger; + +@Data +@AllArgsConstructor +public class ProofOfWork { + + /** + * 难度目标位 + * 0000 0000 0000 0000 1001 0001 0000 .... 0001 + * 256位Hash里面前面至少有16个零 + */ + public static final int TARGET_BITS = 16; + + /** + * 要验证的区块 + */ + private Block block; + + /** + * 难度目标值 + */ + private BigInteger target; + + + /** + * 创建新的工作量证明对象 + * + * 对1进行移位运算,将1向左移动 (256 - TARGET_BITS) 位,得到我们的难度目标值 + * @param block + * @return + */ + public static ProofOfWork newProofOfWork(Block block) { + /* + 1.创建一个BigInteger的数值1. + 0000000.....00001 + 2.左移256-bits位 + + 以8 bit为例 + 0000 0001 + 0010 0000 + + 8-6 + */ + + BigInteger targetValue = BigInteger.ONE.shiftLeft((256 - TARGET_BITS)); + return new ProofOfWork(block, targetValue); + } + + /** + * 运行工作量证明,开始挖矿,找到小于难度目标值的Hash + * @return + */ + public PowResult run() { + long nonce = 0; + String shaHex = ""; + System.out.printf("开始进行挖矿:%s \n", this.getBlock().getData()); + long startTime = System.currentTimeMillis(); + while (nonce < Long.MAX_VALUE) { + byte[] data = this.prepareData(nonce); + shaHex = DigestUtils.sha256Hex(data); + System.out.printf("\r%d: %s",nonce,shaHex); + if (new BigInteger(shaHex, 16).compareTo(this.target) == -1) { + System.out.println(); + System.out.printf("耗时 Time: %s seconds \n", (float) (System.currentTimeMillis() - startTime) / 1000); + System.out.printf("当前区块Hash: %s \n\n", shaHex); + break; + } else { + nonce++; + } + } + return new PowResult(nonce, shaHex); + } + + /** + * 根据block的数据,以及nonce,生成一个byte数组 + * + * 注意:在准备区块数据时,一定要从原始数据类型转化为byte[],不能直接从字符串进行转换 + * @param nonce + * @return + */ + private byte[] prepareData(long nonce) { + byte[] prevBlockHashBytes = {}; + if (StringUtils.isNoneBlank(this.getBlock().getPrevBlockHash())) { + prevBlockHashBytes = new BigInteger(this.getBlock().getPrevBlockHash(), 16).toByteArray(); + } + + return ByteUtils.merge( + prevBlockHashBytes, + this.getBlock().getData().getBytes(), + ByteUtils.toBytes(this.getBlock().getTimeStamp()), + ByteUtils.toBytes(TARGET_BITS), + ByteUtils.toBytes(nonce) + ); + + } + + /** + * 验证区块是否有效 + * + * @return + */ + public boolean validate() { + byte[] data = this.prepareData(this.getBlock().getNonce()); + return new BigInteger(DigestUtils.sha256Hex(data), 16).compareTo(this.target) == -1; + } + +} diff --git a/src/main/java/com/example/blockchain/store/RocksDBUtils.java b/src/main/java/com/example/blockchain/store/RocksDBUtils.java new file mode 100755 index 0000000..c013c5f --- /dev/null +++ b/src/main/java/com/example/blockchain/store/RocksDBUtils.java @@ -0,0 +1,153 @@ +package com.example.blockchain.store; + +import com.example.blockchain.block.Block; +import com.example.blockchain.util.SerializeUtils; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; + +import java.util.HashMap; +import java.util.Map; + +public class RocksDBUtils { + /** + * 区块链数据文件 + */ + private static final String DB_FILE = "blockchain.db"; + + /** + * 区块桶前缀 + */ + private static final String BLOCKS_BUCKET_KEY = "blocks"; + + /** + * 最新一个区块的hash + */ + private static final String LAST_BLOCK_KEY = "l"; + + + + private volatile static RocksDBUtils instance; + + + /** + * 获取RocksDBUtils的单例 + * @return + */ + public static RocksDBUtils getInstance() { + if (instance == null) { + synchronized (RocksDBUtils.class) { + if (instance == null) { + instance = new RocksDBUtils(); + } + } + } + return instance; + } + + + private RocksDBUtils() { + openDB(); + initBlockBucket(); + } + + + private RocksDB db; + + /** + * block buckets + */ + private Map blocksBucket; + + + /** + * 打开数据库 + */ + private void openDB() { + try { + db = RocksDB.open(DB_FILE); + } catch (RocksDBException e) { + throw new RuntimeException("打开数据库失败。。 ! ", e); + } + } + /** + * 初始化 blocks 数据桶 + */ + private void initBlockBucket() { + try { + // + byte[] blockBucketKey = SerializeUtils.serialize(BLOCKS_BUCKET_KEY); + byte[] blockBucketBytes = db.get(blockBucketKey); + if (blockBucketBytes != null) { + blocksBucket = (Map) SerializeUtils.deserialize(blockBucketBytes); + } else { + blocksBucket = new HashMap<>(); + db.put(blockBucketKey, SerializeUtils.serialize(blocksBucket)); + } + } catch (RocksDBException e) { + throw new RuntimeException("初始化block的bucket失败。。! ", e); + } + } + + /** + * 保存区块 + * + * @param block + */ + public void putBlock(Block block) { + try { + blocksBucket.put(block.getHash(), SerializeUtils.serialize(block)); + db.put(SerializeUtils.serialize(BLOCKS_BUCKET_KEY), SerializeUtils.serialize(blocksBucket)); + } catch (RocksDBException e) { + throw new RuntimeException("存储区块失败。。 ", e); + } + } + + /** + * 查询区块 + * + * @param blockHash + * @return + */ + public Block getBlock(String blockHash) { + return (Block) SerializeUtils.deserialize(blocksBucket.get(blockHash)); + } + + /** + * 保存最新一个区块的Hash值 + * + * @param tipBlockHash + */ + public void putLastBlockHash(String tipBlockHash) { + try { + blocksBucket.put(LAST_BLOCK_KEY, SerializeUtils.serialize(tipBlockHash)); + db.put(SerializeUtils.serialize(BLOCKS_BUCKET_KEY), SerializeUtils.serialize(blocksBucket)); + } catch (RocksDBException e) { + throw new RuntimeException("数据库存储最新区块hash失败。。 ", e); + } + } + + /** + * 查询最新一个区块的Hash值 + * + * @return + */ + public String getLastBlockHash() { + byte[] lastBlockHashBytes = blocksBucket.get(LAST_BLOCK_KEY); + if (lastBlockHashBytes != null) { + return (String) SerializeUtils.deserialize(lastBlockHashBytes); + } + return ""; + } + + /** + * 关闭数据库 + */ + public void closeDB() { + try { + db.close(); + } catch (Exception e) { + throw new RuntimeException("关闭数据库失败。。 ", e); + } + } + +} diff --git a/src/main/java/com/example/blockchain/util/ByteUtils.java b/src/main/java/com/example/blockchain/util/ByteUtils.java new file mode 100755 index 0000000..0a1468d --- /dev/null +++ b/src/main/java/com/example/blockchain/util/ByteUtils.java @@ -0,0 +1,36 @@ +package com.example.blockchain.util; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.lang3.ArrayUtils; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.stream.Stream; + +public class ByteUtils { + + public static final String ZERO_HASH = Hex.encodeHexString(new byte[32]); + + + /** + * 将多个字节数组合并成一个字节数组 + * @param bytes + * @return + */ + public static byte[] merge(byte[]... bytes) { + Stream stream = Stream.of(); + for (byte[] b : bytes) { + stream = Stream.concat(stream, Arrays.stream(ArrayUtils.toObject(b))); + } + return ArrayUtils.toPrimitive(stream.toArray(Byte[]::new)); + } + + /** + * long 转化为 byte[] + * @param val + * @return + */ + public static byte[] toBytes(long val) { + return ByteBuffer.allocate(Long.BYTES).putLong(val).array(); + } +} diff --git a/src/main/java/com/example/blockchain/util/SerializeUtils.java b/src/main/java/com/example/blockchain/util/SerializeUtils.java new file mode 100755 index 0000000..33c680c --- /dev/null +++ b/src/main/java/com/example/blockchain/util/SerializeUtils.java @@ -0,0 +1,33 @@ +package com.example.blockchain.util; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +public class SerializeUtils { + + /** + * 序列化 + * @param object 需要序列化的对象 + * @return + */ + public static byte[] serialize(Object object) { + Output output = new Output(4096, -1); + new Kryo().writeClassAndObject(output, object); + byte[] bytes = output.toBytes(); + output.close(); + return bytes; + } + + /** + * 反序列化 + * @param bytes 对象对应的字节数组 + * @return + */ + public static Object deserialize(byte[] bytes) { + Input input = new Input(bytes); + Object obj = new Kryo().readClassAndObject(input); + input.close(); + return obj; + } +} diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100755 index 0000000..9f88c1f --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,7 @@ + + + + Archetype Created Web Application + diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp new file mode 100755 index 0000000..c38169b --- /dev/null +++ b/src/main/webapp/index.jsp @@ -0,0 +1,5 @@ + + +

Hello World!

+ +