1. 基础数据类型
class String; // 基础字符串数据结构
class Blob; // 基础二进制数据,用来表示对象序列化之后的线性二进制数据
class CriticalSection; // 临界区,多线程互斥对象
class BigInt; // 区块链中很多地方的数值采用大整数来表示,例如余额,挖矿难度等。例如用一个32字节的无符号大整数,表示0到2^256-1的整数。
2. 公钥和私钥
非对称加密函数,公私钥对可以在不联网的情况下,任意生成,并且全球唯一。
通常为32到64字节的无结构二进制数据。
typedef BYTE PublicKey[32]; //公钥,公开,在区块链系统中用来表明特定身份,供他人验证其对特定账户的控制权。
typedef BYTE PrivateKey[64];//私钥,不公开,通过数字签名来证明其对账户的控制。
3. 账户地址
地址是账户的标识符,是一个32字节的无结构二进制数据,由公钥的哈希值 SHA256(PublicKey) 得到。
每个公钥,对应一个唯一的地址,对应一个唯一的账户。
typedef BYTE HashValue[32]; //SHA256的哈希值
typedef HashValue; //账户地址
4. 数字签名
typedef BYTE Signature[64]; //数字签名数据
二、必要的基本函数
1. 生成数字签名
void Sign(Blob data, PrivateKey sk, Signature sigdata); //数字签名
2. 验证数字签名是否正确
对于给定数据和签名,验证是不是对应的签名者签署的。
bool VerifySignature(Blob data, PublicKey pk, Signature sigdata); //检查数字签名是否正确
3. 哈希函数
HashValue SHA256(Blob data); // SHA256 哈希函数
三、智能合约
像一个 C++的类,定义了一些状态,以及修改这些状态的函数。
一个区块链系统中,可以有多个智能合约同时存在,但是每个仅会有一个实例。
这里我们就数字货币给出一个极度简化的智能合约的例子
class MyCoin
{
// internal state,账本,数组,key为用户的账户地址,value为该用户的余额
hash_map<Address, BigInt> _Ledger;
// internal function,查询指定用户的余额
BigInt _GetBalance(Address addr)
{
// 如果存在该账户地址
if(_Ledger.has(addr))
return _Ledger[addr];
else
return 0;
}
// 转账函数
void Transfer(Address signer, Address from, Address to, BigInt amount)
{
// signer为签名者
if(signer != from)
return;
if(amount > 0 && _GetBalance(from) >= amount)
{
_Ledger[from] -= amount;
amount += _GetBalance(to);
_Ledger[to] = amount;
}
}
// 挖矿奖励函数
void CoinBase(int height, Address miner)
{
BigInt reward = 5000000000; // 这里简化为,每次奖励50个币
if(reward > 0)
{
reward += _GetBalance(miner);
_Ledger[miner] = reward;
}
}
};
四、交易 (Transaction)
一个交易表示对特定相关账户一次状态修改请求。
交易中不携带任何逻辑代码,仅仅是指定这个交易将调用智能合约里面的哪个公开函数及其调用参数。
当然在我们这个极度简化的系统中,只有一种交易,即前面的转账(Transfer)。
交易的发起方必须为扣款方(from),并且整个交易携带对交易内容的数字签名,以确信该交易由扣款方发起。
基于我们这里的例子,一个交易至少含有以下结构:
struct Transaction
{
String InvokeFunctionName; // 在我们这里 始终为 "Transfer"
Blob InvokeArguments; // 序列化之后的调用参数
PublicKey Signer; // 发起者的公钥,注意这里不是地址
Signature SignData; // 由发起者的私钥对交易的签名
};
五、区块(Block)
1. 结构
一个区块包含一批未被确认的交易,以及共识机制验证数据和块头元数据。
一个最简化的工作量证明(Proof-of-Work)下的区块定义可以是这样:
struct Block
{
int Timestamp; // 出块时间
HashValue PrevBlock; // 上一个块的哈希值,指向上一个区块的"指针"
Address Miner; // 矿工地址
int TxnCount; // 这个块中包含的交易个数
Transaction Txns[TxnCount]; // 完整的交易列表
BigInt PowTarget; // Difficulty Target,工作量证明的目标 (共识验证数据)
int PowNonce; // 工作量证明的Nonce值 (共识验证数据),通过变动该计数器来达成工作量证明要求的结果
};
2. 一个区块被确认有三个条件:
a. PowTarget必须小于当前挖矿难度的要求
这个区块的共识验证要满足其特定共识算法的要求。在工作量证明算法中,PowTarget必须小于当前挖矿难度的要求,同时 ((BigInt&)SHA256(Block)) < Block::PowTarget。
b. 每个交易都是合规的
这个块所包含的交易必须没有被之前的区块包含过,并且每个交易必须能够保证其数字签名能够被其Signer的公钥正确验证。至于交易所执行的逻辑是否正确,是否出错则无关紧要。
c. 只会确认一个相同的PrevBlock的块
在所有分叉块中,即具有相同PrevBlock的块,只有优先的块会被确认。这一点不同的共识算法有不同的情况。
六、P2P通信原语
区块链的网络层仅用到了P2P网络技术中简单的部分,用基于TCP长连接的Gossip协议实现一个数据块的全网广播(Flooding)。
我们这里将其抽象下面的通讯原语:
interface BroadcastNetwork
{
template<typename T>
void Broadcast(const T& object); // 将对象序列化并广播出去
function OnRecvBlock; // 接收到一个区块的回调函数
function OnRecvTransaction; // 接收到一个交易的回调函数
};
七、内存池(Mempool)原语
内存池在区块链系统中用来记录尚未被确认的交易,很容易用比如哈希表来实现。
interface Mempool
{
bool Has(Transaction txn);
void Insert(Transaction new_txn);
void Remove(Transaction txns[count]);
int Collect(Transaction txns[max_count]); // 用于挖矿时合成新的区块,从mempool中挑出一系列交易来填充Txns数组,最多挑TxnMaxCount个,并返回实际填充的个数
};
八、区块归档数据库原语
区块链系统中的区块以及交易,在被确认之后,将从内存中移除,并写入归档数据库中。
这个部分很容易用一个Key-value storage系统来实现,当然用SQL数据可也是可以的,就是效率低一些。
interface ArchiveDatabase
{
void Archive(Transactiontxns[count]);
void Archive(Block blk);
void Has(Transaction txn);
void Has(Block blk);
}
九、算力难度调整
1. 挖矿难度
比特币挖矿难度调整方式非常简单,难度目标调整即不断将256位的难度值减小,如277315号区块的难度值十六进制表示为:0x0000000000000003A30C00000000000000000000000000000000000000000000
这个数字在二进制表示下前60位均是0,如果要增加难度只需要减小这个值,随着难度值的减小,起始0的个数增多,可寻找的哈希值范围减小,挖矿难度就越大。
2. 难度调整
难度的调整是在每个完整节点中独立自动发生的。
假设每2016个区块,所有节点都会按统一的公式自动调整难度。
假设没10分钟会出一个块。
如果区块产生的速率比10分钟快则增加难度,比10分钟慢则降低难度。
公式可以总结为:
新难度值 = 旧难度值 ×(过去2016个区块花费时长/20160分钟)
这个算法的一个最简化定义如下
static const int TARGET_ADJUST_INTERVAL = 256; // 每隔256个块调整一次算力难度
static const int BLOCK_CREATION_INTERVAL = 600*1000; // 每十分钟出一个块
// cur_target 是目前的难度值
// nth_block_interval 是(目前块的时间戳-上次调整难度的时间戳)
BigInt PowTargetAdjustment(BigInt cur_target, int nth_block_interval)
{
return cur_target * (nth_block_interval / (BLOCK_CREATION_INTERVAL * TARGET_ADJUST_INTERVAL));
}
PowTargetAdjustment是用来根据近期出块速度来调整出块算力难度要求,从而使得出块的平均间隔的期望可以大体稳定在一个预先设定的值(BLOCK_CREATION_INTERVAL)。
这是一个和工作量证明共识算法有关的算法,并不是所有区块链系统都有。
十、共识机制
这里采用的是工作量证明(Proof-of-Work,PoW),是一种对应服务与资源滥用、或是阻断服务***的经济对策。
1. 工作量证明的特点是什么?
即是难于计算,却易于验证。
2. 产生工作量的方法是什么?
不断哈希不同的值, 直到哈希值符合一定的条件。
3. 工作量证明如何验证?
接收方对证明进行哈希, 看是否符合上述条件,可以快速验证。
十一、全节点工作代码
到这里一个不出块的区块链节点,即全节点就可以工作了。
全节点是区块链网络中的大多数节点,是区块链底层P2P网络得以稳定鲁棒运行的保障,同时也实现了区块数据和交易数据的高度冗余的全网存储。
虽然不出块,全节点不同于互联网架构的客户端。
一个全节点不需要信赖其他节点,更不存在一个服务器。
全节点能够独立自主地验证区块链完整的历史演进过程,进而重构其上的状态 (例如一个账户的余额),而不是去向一个需要信赖的服务器查询。
下面给出一个不考虑分叉情况下最简单的基于工作量证明的区块链系统的伪代码:
工作量证明如何验证:接收方对证明进行哈希, 看是否符合条件,可以快速验证。
static const int TARGET_ADJUST_INTERVAL = 256; // 每隔256个块调整一次算力难度
static const int BLOCK_CREATION_INTERVAL = 600*1000; // 每十分钟出一个块
static const int TRANSCATION_PERBLOCK_MAX = 1024; // 每块最多包含1024个交易
BroadcastNetwork* g_pNet = BroadcastNetwork::Create(...); // P2P全网广播
Mempool* g_pMempool = Mempool::Create(...); // 内存池
ArchiveDatabase* g_pArchiDB = ArchiveDatabase::Create(...); // 区块归档数据库
MyCoin g_MyLedger; // 账簿
// 当前区块链的头
Block g_BlockHead = Block::GenesisBlock(6); // 初始化为创始区块
HashValue g_BlockHeadHash = SHA256(g_BlockHead); // 当前区块的hash
int g_BlockNextHeight = 1; // 下一个区块的高度
CriticalSection g_BlockHeadCS; // 临界区,多线程互斥对象
// 下一个块的共识相关信息 (工作量证明)
PowTarget g_NextPowTarget = Block::InitialPowTarget(); // 初始挖矿难度
int g_LastTargetAdjustedTime; // 上一次调整难度的时间
// 收到来自网络广播的 交易
g_pNet-> OnRecvTransaction = [](Transaction txn) {
// 内存池中是否已经存在该交易了,或者,数据库中已经归档了该交易,即已经有区块把这个交易打包了
if(g_pMempool->Has(txn) || g_pArchiDB->Has(txn))
return; // 忽略已经存在的交易
// 验证交易的签名是否合法
if(!VerifySignature(
txn.InvokeFunctionName + txn.InvokeArguments +txn.Signer,
txn.Signer,
txn.Signature
)
){
return; // 验证签名是否正确
}
g_pNet->Broadcast(txn); // 基本验证合法之后,接力这个交易的广播
g_pMempool->Insert(txn); // 插入到内存池中
};
// 收到来自网络广播的 区块
g_pNet-> OnRecvBlock = [](Block blk) {
// 忽略乱序到达的块,忽略分叉块
// 收到的块的前指针 如果不等于 当前节点的链的最后一个区块的hash
if(blk.PrevBlock != g_BlockHeadHash)
return;
// 忽略不满足当前算力要求的块
// 收到的块的工作量证明的目标 不能大于 当前节点的链的挖矿难度
if(blk.PowTarget > g_NextPowTarget)
return;
// 忽略过于大的块
// 收到的块的交易总数 不能大于 当前节点的链规定的总数
if(blk.TxnCount > TRANSCATION_PERBLOCK_MAX)
return;
// 计算收到的块的hash值
HashValue h = SHA256(blk);
// 忽略未达到当前标称算力要求的块
// 收到的块的hash值 要大于等于 其工作量证明的目标
if( ((BigInt&)h) >= blk.PowTarget )
return;
// 校验全部块中的交易
for(Int32 i=0; i<blk.TxnsCount; i++)
{
auto& txn = blk.Txns[i];
if( g_pArchiDB->Has(txn) || // 包含之前已经被确认过的交易
!VerifySignature(
txn.InvokeFunctionName + txn.InvokeArguments +txn.Signer,
txn.Signer,
txn.Signature
) // 包含验签失败的交易
){
return;
}
}
// 至此这个区块被确认
g_pNet->Broadcast(txn); // 确认之后,尽快接力这个区块的广播
/** 开始更新账本金额 start **/
g_MyLedger.CoinBase(g_BlockNextHeight, Miner); // 执行出块奖励
// 执行每一条交易,然后归档
for(auto& txn : blk.Txns)
{
// 调用交易中指定的函数,更新账本
g_MyLedger[txn.InvokeFunctionName](txn.Signer, txn.InvokeArguments…);
g_pArchiDB->Archive(txn); // 归档到数据库
g_pMempool->Remove(txn); // 从内存池中删除,如果存在的话
}
g_pArchiDB->Archive(g_BlockHead); // 归档上一个区块
/** 开始更新账本金额 end **/
// 更新区块链头,这部分代码需要和挖矿过程中构造新的块的步骤互斥
// 上锁
g_BlockHeadCS.Lock();
{
if(g_BlockNextHeight%TARGET_ADJUST_INTERVAL == 1)
{
// 进行算力调整,周期为 TARGET_ADJUST_INTERVAL 个区块
if(g_BlockNextHeight > 1)
{
// 调整难度
g_NextPowTarget = PowTargetAdjustment(
g_NextPowTarget,
blk.Timestamp - g_LastTargetAdjustedTime
);
}
// 更新最新调整挖矿难度的时间
g_LastTargetAdjustedTime = blk.Timestamp;
}
// 更新区块链头在最新的这个块
g_BlockHeadHash = h;
g_BlockHead = blk;
g_BlockNextHeight ++;
}
// 解锁
g_BlockHeadCS.Unlock();
};
十二、矿工节点工作代码
区块链网络计算接力过程是由出块节点完成了,也就是所谓的矿工节点。
这些少数节点,和大量的全节点混在一起,大部分节点收到最新的区块是来自于其他全节点的接力广播,而不是直接来自于一个出块节点。
当然,作为接受方,也无从判断发送方是中继的全节点,还是刚刚出块的矿工节点。
这也有效地保护了真正出块节点的安全性,避免暴露矿工节点的物理IP地址。
一个出块节点,首先是一个全节点,除了上面定义的这些行为之外,还需要一个额外的过程,运行在一个或者多个线程上。
我们定义最简化的出块过程如下:
产生工作量的方法是:不断哈希不同的值, 直到哈希值符合一定的条件。
void Mining()
{
// 一直循环
while(g_KeepMining)
{
// 构造新的块,这个部分需要和区块链头更新代码互斥
// 上锁
g_BlockHeadCS.Lock();
{
int next_height = g_BlockNextHeight; // 获取最新的、下一个区块的高度
Block new_block; // 实例化一个空区块
new_block.Timestamp = os::GetCurrentTime(); // 区块生成的时间戳
new_block.PrevBlock = g_BlockHeadHash; // 指向最新的块(即上一个区块)
new_block.Miner = g_MyAddress; // 矿工地址
new_block.TxnCount = g_pMempool->Collect(new_block.Txns[TRANSCATION_PERBLOCK_MAX]); // 从内存中获取最多TRANSCATION_PERBLOCK_MAX个交易,赋值到new_block.Txns,然后将交易个数返回给TxnCount
new_block.PowTarget = g_NextPowTarget; // 目标难度
new_block.PowNonce = os::Random<Int64>(); // 随机初始值
}
// 解锁
g_BlockHeadCS.Unlock();
// 开始挖矿
// 下一个区块的高度是否为当前链所需的下一个块的高度
while(next_height == g_BlockNextHeight)
{
// 当前块的hash值是否小于所需的最小目标
// 这里其实就是把它变成2进制, 二进制前面有多少位是没有0的, 因为有1的话, 这个十进制的值肯定是很大的, 就不会符合条件。
if( ((BigInt64&)SHA256(new_block)) < new_block.PowTarget )
{
// 挖矿成功
g_pNet->Broadcast(new_block); // 立即广播出去
g_pNet->OnRecvBlock(new_block); // 更新本节点的区块链头
break; // 开始去挖下一个块
}
// 如果大于这个目标值, 说明前面的位数没有满足前多少位为0的条件 哈希不成功, 那么就改变随机数值,组成新的区块头,继续哈希。
// 如果当前Nonce不符合,则尝试下一个Nonce
new_block.PowNonce++;
}
// 大多数情况下,其他节点出了新的块,并更新了区块高度
// 则挖矿被打断,去挖更新之后的下一个块
}
}
内容来自
https://zhuanlan.zhihu.com/p/101525927
https://www.jianshu.com/p/9466260c72dc