1-区块链基础概述
区块链(英语:blockchain或block chain)是借由密码学串接并保护内容的串连文字记录(又称区块)。每一个区块包含了前一个区块的加密散列、相应时间戳记以及交易资料(通常用默克尔树(Merkle tree)算法计算的散列值表示),这样的设计使得区块内容具有难以篡改的特性。用区块链技术所串接的分布式账本能让两方有效记录交易,且可永久查验此交易。目前区块链技术最大的应用是数字货币,例如比特币的发明。
比特币(英语:Bitcoin,缩写:BTC 或 XBT)是一种基于去中心化,采用点对点网络与共识主动性,开放源代码,以区块链作为底层技术的加密货币,比特币由中本聪(网名)(Satoshi Nakamoto)于2008年10月31日发表论文,2009年1月3日,创世区块诞生。在某些国家、央行、政府机关则将比特币视为虚拟商品,而不认为是货币。
区块链结构
在加密货币应用中,区块链结构的作用就是用作账本,每一个区块都是一页账册,它们相互之间通过哈希值进行连接形成一条完整有序的链表,每个区块的头部哈希是它们的唯一标识。
从上面的区块链结构可以看得出,一个区块由主要的三部分组成,分别是区块头部以及区块主体还有单独的确认数。区块头部存储了整个区块的基本信息:区块高度本质是区块的索引值,标识了该区块在整个区块中的索引位置,难度指标记录了当前区块链网络中所挖取的区块的难度值,PoW结果即符合难度指标下求解出的nonce值,时间戳记录了当前区块生成的时间,区块头部哈希唯一标识该区块,其由区块头部数据经过哈希算法得出,前置区块preHash记录了该区块前一位区块的哈希值。区块主体Body只负责存储开始挖矿操作前,交易池中所存储的全部交易数据。最后第三部分是确认数,即当某一结点发现区块时将其广播出去后,得到其他节点确认的数量,比特币中,以6个为界,6个以上确认数的区块一般被认为是不可能出现错误的区块,以太坊中,则以12个确认数为界。
在区块链结构中,展现的最重要的内容就是通过哈希算法,确保数据的不可篡改性,上面的区块链结构对区块链内容进行部分省略,真实结构中区块头部存储了额外的属性“Merkle Hash”,MerkleHash通过对所有交易数据进行哈希计算得来,这与我们一般进行哈希计算不同,首先对每笔交易进行哈希计算,随后将结果按照顺序两两相加,在对相加的结果进行哈希计算,不断重复,直到算得唯一的哈希值为止,以该值为Merkle Hash,该值可以保证区块中交易的不可篡改,只要改动任意一笔交易都会导致merklehash的值被改变从而无法通过校验
(碰到奇数情况,最后一个哈希值无法配对的情况下,则该哈希值进行自加运算自己与自己相加后计算哈希值)
除了上面提到的Merkle Hash,记录在区块头部的头部Hash则是用来唯一表示该区块,其由区块头部的全部数据生成
如果一个恶意的攻击者修改了一个区块中的某个交易,那么Merkle Hash验证就不会通过。所以,他只能重新计算Merkle Hash,然后把区块头的Merkle Hash也修改了。这时,我们就会发现,这个区块本身的Block Hash就变了,所以,下一个区块指向它的链接就断掉了。由于比特币区块的哈希必须满足一个难度值,因此,攻击者必须先重新计算这个区块的Block Hash,然后,再把后续所有区块全部重新计算并且伪造出来,才能够修改整个区块链。在后面的挖矿中,我们会看到,修改一个区块的成本就已经非常非常高了,要修改后续所有区块,这个攻击者必须掌握全网51%以上的算力才行,所以,修改区块链的难度是非常非常大的,并且,由于正常的区块链在不断增长,同样一个区块,修改它的难度会随着时间的推移而不断增加。
比特币使用的加密算法分为两种:SHA-256和RipeMD160,其利用这两种哈希算法的方式分别是
- 对数据进行两次SHA-256计算,这种算法在比特币协议中通常被称为hash256或者dhash
- 先计算SHA-256,再计算RipeMD160,这种算法在比特币协议中通常被称为hash160
定义区块链结构
区块结构设计
代码语言:javascript复制@Data
public class Block implements Serializable {
/**
* 区块 Header
*/
private BlockHeader header;
/**
* 区块 Body
*/
private BlockBody body;
/**
* 确认数
*/
private int confirmNum = 0;
}
区块头部设计
代码语言:javascript复制/**
* 区块头
*/
@Data
public class BlockHeader implements Serializable {
/**
* 区块高度
*/
private Integer index;
/**
* 难度指标
*/
private BigInteger difficulty;
/**
* PoW 问题的答案
*/
private Long nonce;
/**
* 时间戳
*/
private Long timestamp;
/**
* 区块头 Hash
*/
private String hash;
/**
* 上一个区块的 hash 地址
*/
private String previousHash;
public BlockHeader(Integer index, String previousHash) {
this.index = index;
this.timestamp = System.currentTimeMillis();
this.previousHash = previousHash;
}
public BlockHeader() {
this.timestamp = System.currentTimeMillis();
}
@Override
public String toString() {
return "BlockHeader{"
"index=" index
", difficulty=" difficulty
", nonce=" nonce
", timestamp=" timestamp
", hash='" hash '''
", previousHash='" previousHash '''
'}';
}
/**
* 计算当前区块头的 hash 值
* @return
*/
public String hash() {
return Hash.sha3("BlockHeader{"
"index=" index
", difficulty=" difficulty
", nonce=" nonce
", timestamp=" timestamp
", previousHash='" previousHash '''
'}');
}
}
区块主体设计
代码语言:javascript复制/**
* 区块数据
*/
public class BlockBody implements Serializable {
/**
* 区块所包含的交易记录
*/
private List<Transaction> transactions;
public BlockBody(List<Transaction> transactions) {
this.transactions = transactions;
}
public BlockBody() {
this.transactions = new ArrayList<>();
}
public List<Transaction> getTransactions() {
return transactions;
}
/**
* 添加一笔交易打包到区块
* @param transaction
*/
public void addTransaction(Transaction transaction) {
transactions.add(transaction);
}
@Override
public String toString() {
return "BlockBody{"
"transactions=" transactions
'}';
}
}
区块链结构设计
代码语言:javascript复制/**
* 区块链主类
*/
@Component
public class BlockChain {
//日志打印工具
private static Logger logger = LoggerFactory.getLogger(BlockChain.class);
@Autowired
private DBAccess dbAccess; //数据库操作
@Autowired
private AppClient appClient; //客户端启动
@Autowired
private Miner miner; //矿工类,用于进行挖矿
@Autowired
private TransactionPool transactionPool; //交易池
@Autowired
private TransactionExecutor transactionExecutor; //交易执行器
@Autowired
private AppConfig appConfig; //系统配置
// 是否正在同步区块
private boolean syncing = true;
/**
* 挖取一个区块
* @return
*/
public Block mining() throws Exception {
Optional<Block> lastBlock = getLastBlock(); //获取当前区块链上最后一个区块
/**
* 创建新区块,这里需要注意这个新建区块的过程不是简单的新建一个对象,而是在该方法
* 中完成了区块对象的创建,以及挖矿计算(工作量证明),并且写入了该区块的基本信息。
* 同时写入了第一笔交易,即CoinBase交易(矿工奖励)
*/
Block block = miner.newBlock(lastBlock);
// 获取当前未打包的交易记录
for (Iterator t = transactionPool.getTransactions().iterator(); t.hasNext();) {
//向区块中添加交易记录
block.getBody().addTransaction((Transaction) t.next());
t.remove(); // 已打包的交易移出交易池
}
// 存储区块信息到数据库
dbAccess.putLastBlockIndex(block.getHeader().getIndex());
dbAccess.putBlock(block);
logger.info("Find a New Block, {}", block);
// 判断系统是否设置了自动发现其他节点,如果设置了,需要在挖矿成功后,将新区块广播到所有节点
if (appConfig.isNodeDiscover()) {
// 触发挖矿事件,并等待其他节点确认区块
ApplicationContextProvider.publishEvent(new NewBlockEvent(block));
} else {
// 未开启节点自动发现,不需要发布信息
transactionExecutor.run(block);
}
return block;
}
/**
* 发送交易
* @param credentials 交易发起者的凭证
* @param to 交易接收者
* @param amount
* @param data 交易附言
* @return
* @throws Exception
*/
public Transaction sendTransaction(Credentials credentials, String to, BigDecimal amount, String data) throws
Exception {
//校验付款和收款地址
Preconditions.checkArgument(to.startsWith("0x"), "收款地址格式不正确");
Preconditions.checkArgument(!credentials.getAddress().equals(to), "收款地址不能和发送地址相同");
//构建交易对象
Transaction transaction = new Transaction(credentials.getAddress(), to, amount);
transaction.setPublicKey(Keys.publicKeyEncode(credentials.getEcKeyPair().getPublicKey().getEncoded()));
transaction.setStatus(TransactionStatusEnum.APPENDING);
transaction.setData(data);
transaction.setTxHash(transaction.hash());
//签名
String sign = Sign.sign(credentials.getEcKeyPair().getPrivateKey(), transaction.toSignString());
transaction.setSign(sign);
//先验证私钥是否正确
if (!Sign.verify(credentials.getEcKeyPair().getPublicKey(), sign, transaction.toSignString())) {
throw new RuntimeException("私钥签名验证失败,非法的私钥");
}
// 加入交易池,等待打包
transactionPool.addTransaction(transaction);
if (appConfig.isNodeDiscover()) {
//触发交易事件,向全网广播交易,并等待确认
ApplicationContextProvider.publishEvent(new NewTransactionEvent(transaction));
}
return transaction;
}
/**
* 获取最后一个区块
* @return
*/
public Optional<Block> getLastBlock() {
return dbAccess.getLastBlock();
}
/**
* 添加一个节点
* @param ip
* @param port
* @return
*/
public void addNode(String ip, int port) throws Exception {
appClient.addNode(ip, port);
Node node = new Node(ip, port);
dbAccess.addNode(node);
}
}
加密货币的两种货币模型(UTXO和Account模型)
在加密货币领域存在两种货币存储模型,一种是以太坊所使用的也是最常见,最为普遍应用的账户模型(Account模型),另一种则是中本聪提出的目前比特币所使用的UTXO模型(未使用的交易输出)
Account模型即账户模型,简单来讲,就是系统会始终维护一张账户表,账户表记录了有关于使用者的所有账户信息(核心在于始终维护账户余额)。在每次计算账户余额时都需要遍历区块链,获取有关该用户的所有交易信息,然后对交易信息核算,得到账户余额,这种模型在为支付而产生的比特币身上并不适用。
UTXO模型则直接从概念上去除了货币与钱包模型,只保留了支付输入和支付输出两个概念,即每笔交易都是由输入和输出组成的,例如:A所有的未使用的交易输出为10,此时A向B转账2,本质上就是A的10单位未支付输出转化为两部分(UTXO模型中不存在使用一部分未支付输出的概念,转换的前提一定是用尽这个用户的所有未支付输出),一部分是转换给B的2单位未支付输出,另一部分就是转给A(自己)的8单位未支付输出,这个概念更类似于我们现实生活中的纸币概念,每张纸币的源头一定是国家进行发行,每张纸币不能再细分,只会以找零的形式回到我们手中一部分。
UTXO模型交易的一些要求:
- 除了挖矿( coinbase 交易) 之外,所有的资金来源都必须来自前面某一个或者几个交易的 UTXO,你可以把它理解成一个特殊的链表。
- UTXO 是交易的最小单元,不嫩再分割.
- 任何一笔交易的交易输入总量必须等于交易输出总量,等式两边必须配平.
- 比特币种没有余额概念,只有分散到区块链里的 UTXO.
例如,我们以一个稍微复杂的交易图来展示两种货币模型在计算余额过程中的区别
现在,在Account模型的前提下,我们要计算甲的账户余额就需要遍历整个交易网络,筛选出有关于甲的所有交易,然后进行计算
而通过UTXO模型,我们只需要计算最后剩余的UTXO列表,找出有关于甲的UTXO相加,最终的结果即为甲当前余额
可以看到UTXO模型充分考虑到了比特币的交易属性,以及区块链的结构特性,能够更加快速的获得账户余额
Account模型建立
代码语言:javascript复制/**
* 钱包账户
*/
@Data
public class Account implements Serializable {
/**
* 钱包地址
*/
protected String address;
/**
* 钱包私钥
*/
protected String priKey;
/**
* 账户余额
*/
protected BigDecimal balance;
}
代码语言:javascript复制/**
* 账户控制工具类, 锁定,解锁等操作
*/
@Component
public class Personal {
@Autowired
private DBAccess dbAccess;
/**
* 创建一个默认账户
* @return
*/
public Account newAccount() throws Exception
{
ECKeyPair keyPair = Keys.createEcKeyPair();
return newAccount(keyPair);
}
/**
* 使用指定的秘钥创建一个默认账户
* @param keyPair
* @return
*/
public Account newAccount(ECKeyPair keyPair)
{
Account account = new Account(keyPair.getAddress(), keyPair.exportPrivateKey(), BigDecimal.ZERO);
dbAccess.putAccount(account); // 存储账户
return account;
}
}
加密货币的挖矿流程
比特币进行挖矿的本质,就是解数学难题的过程(利用随机数和固定数据进行哈希计算得到在指定范围下的哈希值),我们俗称的矿工,就是不断利用设备进行计算挖矿的人员。
挖矿的主要流程可以概括如下:
- 账户与账户之间交易后,交易发起者将交易向整个网络广播
- 所有接收到广播的节点,对交易信息进行验证后,将交易加入本节点的交易池中
- 矿工节点在创建新区块对象后,会首先在交易列表中增加一笔交易,这笔交易就是系统奖励给矿工的奖励交易,也被称为CoinBase,往往是区块链的第一笔交易
- 在矿工节点开始挖矿后,读取交易池中的所有交易(后续加入的交易,只在交易池中保留,不干扰挖矿过程中的交易列表)
- 根据读取到的交易列表进行哈希计算,直到最终得到唯一的Merkle Hash,将MerkleHash写入到区块头部
- 对区块头部数据进行哈希计算得到头部哈希
- 开始进行POW工作量证明
- 找到符合条件的哈希值,成功建立新区块
- 将新区块信息广播到整个网络,其他节点在收到广播后对区块进行验证,验证通过后将其加入本节点区块链结构,并返回确认信息
- 其他挖矿节点在收到新区块的并验证后,停止当前挖矿进程,开始根据新的难度挖取下一个区块
POW共识机制
上文提到,加密货币在挖矿的过程中需要确定共识机制,即所有节点都需要确定一套大家共同遵照的准则,按照该准则确定新区块的发现。比特币则采用了基于POW(Proof Of Work:工作量证明)的共识机制
工作量证明(Proof-of-Work,PoW)是一种对应服务与资源滥用、或是阻断服务攻击的经济对策。一般是要求用户进行一些耗时适当的复杂运算,并且答案能被服务方快速验算,以此耗用的时间、设备与能源做为担保成本,以确保服务与资源是被真正的需求所使用。
上面这句话给出了两个要点,一个是从用户角度,这些复杂运算是耗时的,可以认证用户是真正有需求的高质量用户,第二点是服务方能够快速演算,保证能够短时间验算结果并提供后续服务。这些特性最大程度上保证了比特币挖矿的环境,复杂的耗时运算保证了挖矿节点都是高算力且稳定的节点,具有维持区块链结构的能力,服务方能够快速验证,保证了其他节点在收到新区块消息后能够快速验算并更新区块链结构
简单的工作量证明实例:
教师在布置作业,希望通过作业检验同学们的工作量,为每位同学布置了一道高阶方程习题,由于高阶方程并没有有效的数学解法,同学们只能通过穷举法一个个尝试答案,为了保证同学们之间不可以相互抄袭,老师规定方程最高阶的系数是同学自己的学号,因此,每位同学的方程最终解都不一定相同,同时,老师可以用较短的时间快速检验同学们的解是否正确
最快解出方程并经过其他同学和老师检验的同学便可以证明自己的工作量,得到部分奖励。
以上实例解释了工作量证明如何在系统中生效,比特币的挖矿过程与之类似,但并不是计算高阶方程,而是进行哈希运算,挖矿过程中,节点会通过更改区块头部nonce不断生成不同的头部哈希,然后将头部哈希与当前难度范围进行比较,难度范围就是哈希值前0的个数,随着比特币网络中算力的增加,难度范围会不断加大。例如,当前难度为4,就指需要生成头部哈希的前四位都是0的nonce值
代码语言:javascript复制hash256(block data, nonce=0) = 291656f37cdcf493c4bb7b926e46fee5c14f9b76aff28f9d00f5cca0e54f376f
hash256(block data, nonce=1) = f7b2c15c4de7f482edee9e8db7287a6c5def1c99354108ef33947f34d891ea8d
hash256(block data, nonce=2) = b6eebc5faa4c44d9f5232631f39ddf4211443d819208da110229b644d2a99e12
hash256(block data, nonce=3) = 00aeaaf01166a93a2217fe01021395b066dd3a81daffcd16626c308c644c5246
hash256(block data, nonce=4) = 26d33671119c9180594a91a2f1f0eb08bdd0b595e3724050acb68703dc99f9b5
hash256(block data, nonce=5) = 4e8a3dcab619a7ce5c68e8f4abdc49f98de1a71e58f0ce9a0d95e024cce7c81a
hash256(block data, nonce=6) = 185f634d50b17eba93b260a911ba6dbe9427b72f74f8248774930c0d8588c193
hash256(block data, nonce=7) = 09b19f3d32e3e5771bddc5f0e1ee3c1bac1ba4a85e7b2cc30833a120e41272ed
...
hash256(block data, nonce=709132) = 0000bc1afba7277ef31c8ecd1f3fef071cf993485fe5eab08e4f7647f47be95c
节点在发现新区块后可以安全广播,得益于两点,首先在进行新区块的哈希计算前,区块头部增加了MerkleHash,其通过当前交易池内的所有交易确定,所以如果有恶意节点篡改交易数据会导致MerkleHash发生变化,进而导致头部哈希发生变化无法通过验证,想要修改交易数据必须重新计算MerkleHash以及之后的所有区块数据(因为后面节点的头部哈希依赖于该节点的哈希值即前置哈希preHash),同时,挖矿节点也不需要担心自己的工作量结果被盗窃,因为交易列表中的第一笔交易记录了奖励交易CoinBase,这笔交易是指向挖矿节点的,如果被修改也会导致前面所说的结果发生。
比特币的奖励和难度值都是动态变化的,比特币总量约2100万枚,初始状态下每发现一个新区块奖励50枚比特币,每挖去剩余总量的一半都会导致新发现区块链的奖励减半,例如当发现1050万枚比特币后,新发现奖励变为25枚,当之后又发现525万枚后,奖励减半为12.5枚比特币。比特币网络的难度值也是不断变化的,它的难度值保证大约每10分钟产生一个区块,而难度值在每2015个区块调整一次:如果区块平均生成时间小于10分钟,说明全网算力增加,难度值也会增加,如果区块平均生成时间大于10分钟,说明全网算力减少,难度值也会减少。因此,难度值随着全网算力的增减会动态调整。
POW算法实现
代码语言:javascript复制/**
* 工作量证明实现
*/
public class ProofOfWork {
/**
* 难度目标位, target=24 时大约 30 秒出一个区块
*/
public static final int TARGET_BITS = 18;
/**
* 区块
*/
private Block block;
/**
* 难度目标值
*/
private BigInteger target;
/**
* <p>创建新的工作量证明,设定难度目标值</p>
* <p>对1进行移位运算,将1向左移动 (256 - TARGET_BITS) 位,得到我们的难度目标值</p>
* @param block
* @return
*/
public static ProofOfWork newProofOfWork(Block block) {
BigInteger targetValue = BigInteger.valueOf(1).shiftLeft((256 - TARGET_BITS));
return new ProofOfWork(block, targetValue);
}
private ProofOfWork(Block block, BigInteger target) {
this.block = block;
this.target = target;
}
/**
* 运行工作量证明,开始挖矿,找到小于难度目标值的Hash
* @return
*/
public PowResult run() {
long nonce = 0;
String shaHex = "";
while (nonce < Long.MAX_VALUE) {
byte[] data = this.prepareData(nonce);
shaHex = Hash.sha3String(data);
if (new BigInteger(shaHex, 16).compareTo(this.target) == -1) {
break;
} else {
nonce ;
}
}
return new PowResult(nonce, shaHex, this.target);
}
/**
* 验证区块是否有效
* @return
*/
public boolean validate() {
byte[] data = this.prepareData(this.getBlock().getHeader().getNonce());
// 与目标难度值进行比较
return new BigInteger(Hash.sha3String(data), 16).compareTo(this.target) == -1;
}
/**
* 准备数据
* 注意:在准备区块数据时,一定要从原始数据类型转化为byte[],不能直接从字符串进行转换
* @param nonce
* @return
*/
private byte[] prepareData(long nonce) {
byte[] prevBlockHashBytes = {};
if (StringUtils.isNotBlank(this.getBlock().getHeader().getPreviousHash())) {
//这里要去掉 hash 值的 0x 前缀, 否则会抛出异常
String prevHash = Numeric.cleanHexPrefix(this.getBlock().getHeader().getPreviousHash());
prevBlockHashBytes = new BigInteger(prevHash, 16).toByteArray();
}
return ByteUtils.merge(
prevBlockHashBytes,
ByteUtils.toBytes(this.getBlock().getHeader().getTimestamp()),
ByteUtils.toBytes(TARGET_BITS),
ByteUtils.toBytes(nonce)
);
}
public Block getBlock() {
return block;
}
public static BigInteger getTarget() {
return BigInteger.valueOf(1).shiftLeft((256 - TARGET_BITS));
}
}
代码语言:javascript复制/**
* PoW 挖矿算法实现
*/
@Component
public class PowMiner implements Miner {
@Autowired
private DBAccess dbAccess;
@Override
public Block newBlock(Optional<Block> block) {
//获取挖矿账户
Account account;
Optional<Account> minerAccount = dbAccess.getMinerAccount();
if (!minerAccount.isPresent()) {
throw new RuntimeException("没有找到挖矿账户,请先创建挖矿账户.");
}
Block newBlock;
//block.isPresent()判断类对象是否存在,避免空指针异常
if (block.isPresent()) {
Block prev = block.get(); //传入的区块是末尾区块,以该区块为前置区块创建新区快
//创建区块的header和body
BlockHeader header = new BlockHeader(prev.getHeader().getIndex() 1, prev.getHeader().getHash()); //传入index和前置hash
BlockBody body = new BlockBody();
newBlock = new Block(header, body); //创建新区块
} else {
//创建创世区块
newBlock = createGenesisBlock();
}
//创建挖矿奖励交易
Transaction transaction = new Transaction();
account = minerAccount.get();
transaction.setTo(account.getAddress());
transaction.setData("Miner Reward.");
transaction.setTxHash(transaction.hash());
transaction.setAmount(Miner.MINING_REWARD);
//如果不是创世区块,则使用工作量证明挖矿
if (block.isPresent()) {
ProofOfWork proofOfWork = ProofOfWork.newProofOfWork(newBlock);
PowResult result = proofOfWork.run();
newBlock.getHeader().setDifficulty(result.getTarget());
newBlock.getHeader().setNonce(result.getNonce());
newBlock.getHeader().setHash(result.getHash());
}
newBlock.getBody().addTransaction(transaction);
//更新最后一个区块索引
dbAccess.putLastBlockIndex(newBlock.getHeader().getIndex());
return newBlock;
}
/**
* 创建创世区块
* @return
*/
private Block createGenesisBlock() {
BlockHeader header = new BlockHeader(1, null);
header.setNonce(PowMiner.GENESIS_BLOCK_NONCE);
header.setDifficulty(ProofOfWork.getTarget());
header.setHash(header.hash());
BlockBody body = new BlockBody();
return new Block(header, body);
}
@Override
public boolean validateBlock(Block block) {
ProofOfWork proofOfWork = ProofOfWork.newProofOfWork(block);
return proofOfWork.validate();
}
}
PowResult对象
代码语言:javascript复制/**
* PoW 计算结果
*/
@Data
public class PowResult {
/**
* 计数器
*/
private Long nonce;
/**
* 新区块的哈希值
*/
private String hash;
/**
* 目标难度值
*/
private BigInteger target;
@Override
public String toString() {
return "PowResult{"
"nonce=" nonce
", hash='" hash '''
", target=" target
'}';
}
}
挖矿接口
代码语言:javascript复制/**
* 挖矿接口
*/
public interface Miner {
/**
* 挖矿奖励
*/
BigDecimal MINING_REWARD = BigDecimal.valueOf(50);
/**
* 创世区块难度值
*/
Long GENESIS_BLOCK_NONCE = 100000L;
/**
* 挖出一个新的区块
* @param block
* @return
* @throws Exception
*/
Block newBlock(Optional<Block> block) throws Exception;
/**
* 检验一个区块
* @param block
* @return
*/
boolean validateBlock(Block block);
}
加密货币的交易流程
加密货币交易的流程中,最核心的两个概念就是数据加密以及数据签名,二者概念相近,但用法正好相反。数据签名和数据加密的过程都是使用公开的密钥系统,但实现的过程正好相反:
- 数据加密使用的是接受方的密钥对,任何知道接受方公钥的都可以向接受方发送消息,但是只有拥有私钥的才能解密出来;
- 数据签名使用的是发送方的密钥对,任何接受方都可以用公钥解密,验证数据的正确性。
首先为了保证交易数据中接收方的私密性,我们使用数据加密算法对交易的数据进行加密,使用接收方的公钥加密后,向全网络广播该交易,全网络中所有节点上的区块链经过验证后都会记录这笔交易,但只有交易的接收方能够通过私钥解密这笔交易,获得交易的信息。
其次为了保证发送方以及交易数据的安全性,发送方在发送前需要将交易信息取摘要,然后对交易信息的摘要利用自己的私钥进行签名,这样所有网络上的节点在接收该笔交易记录后会首先用发送方的公钥对摘要进行验证,如果通过验证,则保证了交易信息和发送方没有被篡改,可以将这笔交易存入交易池中等待打包到数据链上
数字加密
比特币中的非对称加密使用的是ECDSA椭圆曲线加密算法,比特币中所用的椭圆曲线的参数由secp256k1定义,定义在Standard Efficient Cryptography(SEC)中,该算法在比特币出现之前几乎没有被人使用过,但是随着比特币的应用,现在越来越受到关注。secp256k1和传统的椭圆曲线算法相比具有如下特性:
- secp256k1是用非随机的方式生成,而传统的椭圆曲线则是用随机方式生成
- 因为secp256k1用非随机方式生成,因此效率很高,如果实现中优化的好,效率会比常用的椭圆曲线高30%
比特币钱包地址的生成过程见图,其中就是用到了椭圆曲线算法进行加密,首先私钥利用椭圆曲线生成公钥,随后利用公钥进行哈希运算得到比特币的地址,理论上两个计算过程都是单项不可逆的
数字签名
非对称加密的一个主要应用场景就是数字签名,签名算法是使用私钥签名,公钥验证的方法,对一个消息的真伪进行确认。数字签名在比特币中的作用就是证明某人是比特币的合法所有人,简单说就是对消息的发送者验明正身。
签名的目的是为了证明,该消息确实是由持有私钥privateKet的人发出的,任何其他人都可以对签名进行验证。验证方法是,由私钥持有人公开对应的公钥publicKey,其他人用公钥publicKey对消息message和签名signature进行验证
代码语言:javascript复制Boolean verify(message, signature, publicKey);
数字签名算法在电子商务、在线支付这些领域有非常重要的作用,因为它能通过密码学理论证明:
- 签名不可伪造,因为私钥只有签名人自己知道,所以其他人无法伪造签名;
- 消息不可篡改,如果原始消息被人篡改了,对签名进行验证将失败;
- 签名不可抵赖,如果对签名进行验证通过了,签名人不能抵赖自己曾经发过这一条消息。
对消息进行签名,实际上是对消息的哈希进行签名,这样可以使任意长度的消息在签名前先转换为固定长度的哈希数据。对哈希进行签名相当于保证了原始消息的不可伪造性。
假设A的钱包有一笔未花费输出UTXO,包含5个BTC,现在A给B转账1BTC,那么比特币网络中的其他节点怎样证明A就是这笔UTXO的所有人,只有他才能动用这笔钱呢?假设A给B转账的交易为T:
- A的钱包对T进行hash生成数字摘要;
- A的钱包用私钥对(1)中生成的交易的摘要进行签名;
- 交易T被广播到比特币网络中;
- 网络中的节点收到此交易,对交易进行各种验证,其中就包括验证A的签名。
交易对象的创建
代码语言:javascript复制/**
* 交易对象
*/
@Data
public class Transaction {
/**
* 付款人地址
*/
private String from;
/**
* 付款人签名
*/
private String sign;
/**
* 收款人地址
*/
private String to;
/**
* 收款人公钥
*/
private String publicKey;
/**
* 交易金额
*/
private BigDecimal amount;
/**
* 交易时间戳
*/
private Long timestamp;
/**
* 交易 Hash 值
*/
private String txHash;
/**
* 交易状态
*/
private TransactionStatusEnum status = TransactionStatusEnum.APPENDING;
/**
* 交易错误信息
*/
private String errorMessage;
/**
* 附加数据
*/
private String data;
/**
* 当前交易所属区块高度
*/
private int blockNumber;
public Transaction(String from, String to, BigDecimal amount) {
this.from = from;
this.to = to;
this.amount = amount;
this.timestamp = System.currentTimeMillis();
}
public Transaction() {
this.timestamp = System.currentTimeMillis();
}
/**
* 计算交易信息的Hash值
* @return
*/
public String hash() {
return Hash.sha3(this.toSignString());
}
/**
* 参与签名的字符串
* @return
*/
public String toSignString() {
return "Transaction{"
"from='" from '''
", to='" to '''
", publicKey=" publicKey
", amount=" amount
", timestamp=" timestamp
", data='" data '''
'}';
}
@Override
public String toString() {
return "Transaction{"
"from='" from '''
", sign='" sign '''
", to='" to '''
", publicKey='" publicKey '''
", amount=" amount
", timestamp=" timestamp
", txHash='" txHash '''
", status=" status
", errorMessage='" errorMessage '''
", data='" data '''
", blockNumber=" blockNumber
'}';
}
}
交易执行方法
代码语言:javascript复制/**
* 交易执行器
*/
@Component
public class TransactionExecutor {
@Autowired
private DBAccess dbAccess;
@Autowired
private TransactionPool transactionPool;
/**
* 执行区块中的交易
* @param block
*/
public void run(Block block) throws Exception {
for (Transaction transaction : block.getBody().getTransactions())
{
Optional<Account> recipient = dbAccess.getAccount(transaction.getTo());
//如果收款地址账户不存在,则创建一个新账户
if (!recipient.isPresent()) {
recipient = Optional.of(new Account(transaction.getTo(), BigDecimal.ZERO));
}
//挖矿奖励
if (null == transaction.getFrom()) {
recipient.get().setBalance(recipient.get().getBalance().add(transaction.getAmount()));
dbAccess.putAccount(recipient.get());
continue;
}
//账户转账
Optional<Account> sender = dbAccess.getAccount(transaction.getFrom());
//验证签名
boolean verify = Sign.verify(
Keys.publicKeyDecode(transaction.getPublicKey()),
transaction.getSign(),
transaction.toSignString());
if (!verify) {
transaction.setStatus(TransactionStatusEnum.FAIL);
transaction.setErrorMessage("交易签名错误");
continue;
}
//验证账户余额
if (sender.get().getBalance().compareTo(transaction.getAmount()) == -1) {
transaction.setStatus(TransactionStatusEnum.FAIL);
transaction.setErrorMessage("账户余额不足");
continue;
}
// 更新交易区块高度
transaction.setBlockNumber(block.getHeader().getIndex());
// 缓存交易哈希对应的区块高度, 方便根据 hash 查询交易状态
dbAccess.put(transaction.getTxHash(), block.getHeader().getIndex());
// 将待打包交易池中包含此交易的记录删除,防止交易重复打包( fix bug for #IWSPJ)
for (Iterator i = transactionPool.getTransactions().iterator(); i.hasNext();) {
Transaction tx = (Transaction) i.next();
if (tx.getTxHash().equals(transaction.getTxHash())) {
i.remove();
}
}
//执行转账操作,更新账户余额
sender.get().setBalance(sender.get().getBalance().subtract(transaction.getAmount()));
recipient.get().setBalance(recipient.get().getBalance().add(transaction.getAmount()));
dbAccess.putAccount(sender.get());
dbAccess.putAccount(recipient.get());
}// end for
// 更新区块信息
dbAccess.putBlock(block);
}
}
交易池
代码语言:javascript复制/**
* 交易池
*/
@Component
public class TransactionPool {
private List<Transaction> transactions = new ArrayList<>();
/**
* 添加交易
* @param transaction
*/
public void addTransaction(Transaction transaction) {
boolean exists = false;
//检验交易是否存在
for (Transaction tx : this.transactions) {
if (Objects.equal(tx.getTxHash(), transaction.getTxHash())) {
exists = true;
}
}
if (!exists) {
this.transactions.add(transaction);
}
}
public List<Transaction> getTransactions() {
return transactions;
}
/**
* 将交易移除交易池
*/
public void removeTransaction(String txHash)
{
for (Iterator i = transactions.iterator(); i.hasNext();) {
Transaction tx = (Transaction) i.next();
if (Objects.equal(tx.getTxHash(), txHash)) {
i.remove();
}
}
}
}
发送交易方法
代码语言:javascript复制/**
* 发送交易
* @param credentials 交易发起者的凭证
* @param to 交易接收者
* @param amount
* @param data 交易附言
* @return
* @throws Exception
*/
public Transaction sendTransaction(Credentials credentials, String to, BigDecimal amount, String data) throws
Exception {
//校验付款和收款地址
Preconditions.checkArgument(to.startsWith("0x"), "收款地址格式不正确");
Preconditions.checkArgument(!credentials.getAddress().equals(to), "收款地址不能和发送地址相同");
//构建交易对象
Transaction transaction = new Transaction(credentials.getAddress(), to, amount);
transaction.setPublicKey(Keys.publicKeyEncode(credentials.getEcKeyPair().getPublicKey().getEncoded()));
transaction.setStatus(TransactionStatusEnum.APPENDING);
transaction.setData(data);
transaction.setTxHash(transaction.hash());
//签名
String sign = Sign.sign(credentials.getEcKeyPair().getPrivateKey(), transaction.toSignString());
transaction.setSign(sign);
//先验证私钥是否正确
if (!Sign.verify(credentials.getEcKeyPair().getPublicKey(), sign, transaction.toSignString())) {
throw new RuntimeException("私钥签名验证失败,非法的私钥");
}
// 加入交易池,等待打包
transactionPool.addTransaction(transaction);
if (appConfig.isNodeDiscover()) {
//触发交易事件,向全网广播交易,并等待确认
ApplicationContextProvider.publishEvent(new NewTransactionEvent(transaction));
}
return transaction;
}
本文所参考的文章地址或项目开源地址:
- 廖雪峰区块链教程
- 比特币源码分析
- 中本聪比特币白皮书
- ppblock/jblock项目