EOS源码解读心得
主要知识点
1.EOS出块过程
生产、同步区块的核心代码逻辑:
区块的创造过程
- 检查区块生产者的合法性
- 如果当前主干已经不是最长链,需要切换到最长的分叉链上切换过程如下
- 弹出一定数量的区块,直到碰到分叉处
- 将分叉中的新区块写入主干数据库
- 切换后的分叉成为主干
- 如果上述过程失败,则恢复状态
- 如果不需要切换分支,则往主干数据库中写入区块
- 返回结果,以表明这次写入是否造成了分支切换
交易的写入
- 遍历交易中的所有操作,对于每个操作:
- 获取操作的相关信息,并且执行操作
- 记录这次操作执行
- 更新因这次操作而消耗的资源统计数据
- 记录交易的内容
- 返回结果
一笔交易的生命周期
2.EOS区块的签名与验签
区块头摘要
区块头摘要就是对整个区块数据做Hash运算得到的结果,由于Hash的结果长度固定,而且单向不可逆,完全符合摘要的要求。
区块id实际是由两部分组成:区块内容的Hash值 拼接 区块序号
digest_type block_header::digest()const
{
return digest_type::hash(*this);
}
生成签名摘要
digest_type block_header_state::sig_digest()const {
auto header_bmroot = digest_type::hash( std::make_pair( header.digest(), blockroot_merkle.get_root() ) );
return digest_type::hash( std::make_pair(header_bmroot, pending_schedule_hash) );
}
签名
void sign_block( const std::function<signature_type( const digest_type& )>& signer_callback ) {
auto p = pending->_pending_block_state;
p->sign( signer_callback );
static_cast<signed_block_header&>(*p->block) = p->header;
}
void block_header_state::sign( const std::function<signature_type(const digest_type&)>& signer ) {
auto d = sig_digest();
header.producer_signature = signer( d ); //signer调用签名函数
EOS_ASSERT( block_signing_key == fc::crypto::public_key( header.producer_signature, d ), wrong_signing_key, "block is signed with unexpected key" );
}
签名算法 ECDSA-secp256k1 和 ECDSA-r1
cleos create key命令生成基于secp256k1算法的公私钥
平时使用的EOS公钥都是基于secp256k1算法的
base58Check在Base58编码的基础上增加了校验码,比特币和EOS都使用了Base58Check编码
//private_key.cpp
signature private_key::sign( const sha256& digest, bool require_canonical ) const
{
return signature(_storage.visit(sign_visitor(digest, require_canonical)));
}
signature::storage_type operator()(const KeyType& key) const
{
return signature::storage_type(key.sign(_digest, _require_canonical));
}
//signature.cpp
static signature::storage_type parse_base58(const std::string& base58str)
//elliptic.hpp
signature_type sign( const sha256& digest, bool require_canonical = true ) const
{
return signature_type(private_key::regenerate(_data).sign_compact(digest, require_canonical));
}
从签名中提取公钥
public_key_type block_header_state::signee()const {
return fc::crypto::public_key( header.producer_signature, sig_digest(), true );
}
签名算法 ECDSA-secp256k1 和 ECDSA-r1
//public_key.cpp
public_key::public_key( const signature& c, const sha256& digest, bool check_canonical )
:_storage(c._storage.visit(recovery_visitor(digest, check_canonical)))
{
}
//elliptic.hpp
public_key_type recover(const sha256& digest, bool check_canonical) const {
return public_key_type(public_key(_data, digest, check_canonical).serialize());
}
//elliptic_secp256k1.cpp
public_key::public_key( const compact_signature& c, const fc::sha256& digest, bool check_canonical )
验签
result.verify_signee( result.signee() );
void block_header_state::verify_signee( const public_key_type& signee )const {
EOS_ASSERT( block_signing_key == signee, wrong_signing_key, "block not signed by expected key",
("block_signing_key", block_signing_key)( "signee", signee ) );
}
3.EOS交易的签名与验签
交易的数据结构
交易ID: 通过对交易内容本身经过Hash运算得出
transaction_id_type transaction::id() const {
digest_type::encoder enc;
fc::raw::pack( enc, *this );
return enc.result();
}
+-----------+---------------+------------------+---------------------+------------------+-----------+----------+----------+----+------------+
| expiration| ref_block_num | ref_block_prefix | max_net_usage_words | max_cpu_usage_ms | delay_sec | action 1 | action 2 |... | signatures |
+-----------+---------------+------------------+---------------------+------------------+-----------+----------+----------+----+------------+
+---------------------------------------------head ------------------------------------------------+-----------------body------------------+
结构中没有id字段,它是运算出来的
一个交易在纳入区块之前必须含有签名,用以验证交易的合法性,除此之外,交易体也包含了签名的摘要,同样用于交易的验证机制。
生成签名
生成签名摘要
digest_type transaction::sig_digest( const chain_id_type& chain_id, const vector<bytes>& cfd )const {
digest_type::encoder enc;
fc::raw::pack( enc, chain_id );
fc::raw::pack( enc, *this );
if( cfd.size() ) {
fc::raw::pack( enc, digest_type::hash(cfd) );
} else {
fc::raw::pack( enc, digest_type() );
}
return enc.result();
}
签名
const signature_type& signed_transaction::sign(const private_key_type& key, const chain_id_type& chain_id) {
signatures.push_back(key.sign(sig_digest(chain_id, context_free_data)));
return signatures.back();
}
signature private_key::sign( const sha256& digest, bool require_canonical ) const
{
return signature(_storage.visit(sign_visitor(digest, require_canonical)));
}
从签名中提取公钥
fc::microseconds transaction::get_signature_keys( const vector<signature_type>& signatures,
const chain_id_type& chain_id, fc::time_point deadline, const vector<bytes>& cfd,
flat_set<public_key_type>& recovered_pub_keys, bool allow_duplicate_keys)const{
...
const digest_type digest = sig_digest(chain_id, cfd);
...
* 遍历交易的所有签名
* 通过签名和签名摘要(digest)抽取公钥
recov = public_key_type( sig, digest )
* 把公钥和签名组成的配对放入缓存,以便下次快捷访问
}
检测交易所需要的权限
authorization.check_authorization(
trn.actions,
recovered_keys,
{},
trx_context.delay,
[&trx_context](){ trx_context.checktime(); },
false
);
4.同步区块
P2P 网络介绍
如何发现节点
- 节点会记住它最近成功连接的网络节点,当重新启动后它可以迅速与先前的对等节点网络重新建立连接。
- 节点会在失去已有连接时尝试发现新节点。
- 当建立一个或多个连接后,节点将一条包含自身 IP 地址消息发送给其相邻节点。相邻节点再将此消息依次转发给它们各自的相邻节点,从而保证节点信息被多个节点所接收、保证连接更稳定。
- 新接入的节点可以向它的相邻节点发送获取地址 getaddr 消息,要求它们返回其已知对等节点的 IP 地址列表。节点可以找到需连接到的对等节点。
- 在节点启动时,可以给节点指定一个正活跃节点 IP, 如果没有,客户端也维持一个列表,列出了那些长期稳定运行的节点。这样的节点也被称为种子节点(其实和 BT 下载的种子文件道理是一样的),就可以通过种子节点来快速发现网络中的其他节点。
节点通信
节点通常采用 TCP 协议、使用指定端口与相邻节点建立连接,建立连接时也会有认证 “握手” 的通信过程(handshake_message)。握手包内容包括:网络版本、chain_id、node_id、p2p_address、节点名称等配置信息,以及链的状态(当前节点的不可逆区块数、不可逆区块id、最新区块id、最新区块数)。
当节点收到相邻节点的handshake_message,接着就开始对比区块信息。如果其自身的本地区块链比其他节点的区块链更长,并告诉其他节点需要补充区块(notice_message),其他节点发送 sync_request_message 消息来请求区块,其他节点收到区块数据后并验证后更新到本地区块链中。如果自身的本地区块链比其他节点的短,则向其他节点发送 sync_request_message 去请求区块。
从另一个节点同步区块
同步时的状态比较
区块同步的更详细的说明: