eos源码赏析(二十一):EOS智能合约之区块签名的天龙八“步”

2021-11-23 10:37:08 浏览数 (1)

在上篇文章中我们提到了,由用户操作会产生各种事务,事务的链上执行是由push_transaction来完成的,我们简单的划分了下,具体可参考eos源码赏析(二十):EOS智能合约之push_transaction的天龙八“步” 。我们知道,在区块生产或者打包事务信息的时候免不了对数据进行签名,同时对于非本节点的区块信息也要进行验签。当然,针对每一个事务也都有签名及验签的过程,我们今天以区块的签名过程为例,来谈谈eosio中的签名是如何实现的。由于本人在该领域接触较少,行文中难免出现纰漏和差错,还望各位读者能及时批评指正。

本文主要分为以下内容:

  • SHA256简介eos区块签名的天龙八“步”

1、SHA256简介

我们在eos的源码阅读过程中,不管有没有在意,或多或少的都会遇到SHA256,或者在合约的开发过程中遇到checksum256。我们来看:

SHA全拼:Secure Hash Algorithm,又称为安全散列算法,是一种能计算出一个数字消息所对应到的,长度固定的字符串(又称消息摘要)的算法。且若输入的消息不同,它们对应到不同字符串的概率很高;而SHA是FIPS所认证的五种安全散列算法。这些算法之所以称作“安全”是基于以下两点(根据官方标准的描述):由消息摘要反推原输入消息,从计算理论上来说是很困难的。想要找到两组不同的消息对应到相同的消息摘要,从计算理论上来说也是很困难的。任何对输入消息的变动,都有很高的概率导致其产生的消息摘要迥异。

对于任意长度的消息,SHA256都会产生一个256bit长的哈希值,称作消息摘要。这个摘要相当于是个长度为32个字节的数组,通常用一个长度为64的十六进制字符串来表示,这就是我们看到在eosio中,一些数据经过hash之后变成了64位的字符串的原因。

在eosio中基于安全裤openssl实现了SHA的部分功能,关于如何实现以及SHA实现的原理不作为本文的主要内容,包括中秋节期间被媒体大做文章的黎曼猜想,感兴趣的朋友也可以在群内一起讨论,我们接下来看区块生产之后是如何进行签名的。

2、eos区块签名的天龙八“步”

在上篇文章中,我们将push_transaction简单的分为八步,有利于我们进行代码的阅读,在本文中同样将区块签名的过程分为八步,通过每一步日志打印的结果来查看eos中区块签名进行了哪些动作:

  • 第一步:producer_plugin区块生产之后启动签名
代码语言:javascript复制
void producer_plugin_impl::produce_block() {
   //获取chain及block_header相关内容
   auto signature_provider_itr = _signature_providers.find( pbs->block_signing_key );
   ....
   //等等操作,这里根据当前节点的sign_key进行签名
   ....
   //启动签名
   chain.sign_block( [&]( const digest_type& d ) {
      auto debug_logger = maybe_make_debug_time_logger();
      return signature_provider_itr->second(d);
   } );
}
  • 第二步:controller中开始进行区块签名,这里我们加了日志方便接下来的对比
代码语言:javascript复制
   void sign_block( const std::function<signature_type( const digest_type& )>& signer_callback  ) {
       std::string strState = "";
      auto p = pending->_pending_block_state;
       strState = fc::json::to_string(*p);
      dlog("contorller sign_block begin:${state}", ("state", strPending));
      p->sign( signer_callback );
       strState = fc::json::to_string(*p);
       dlog("contorller sign_block end:${state}", ("state", strPending));

      static_cast<signed_block_header&>(*p->block) = p->header;
   } /// sign_block
  • 第三步:调用block_header_state中sign
代码语言:javascript复制
 void block_header_state::sign( const std::function<signature_type(const digest_type&)>& signer ) {
     auto d = sig_digest();
     dlog(block_header_state::sign":${state}", ("state", d));
     header.producer_signature = signer( d );
     EOS_ASSERT( block_signing_key == fc::crypto::public_key( header.producer_signature, d ), wrong_signing_key, "block is signed with unexpected key" );
  }
  • 第四步:调用block_header_state中的sign_digest获取摘要信息

这里我们也加了相应的日志方便对比:

代码语言:javascript复制
  digest_type   block_header_state::sig_digest()const {
      std::string strHeaderDig = "";
      strHeaderDig = fc::json::to_string(header.digest());
      dlog("block_header_state::sig_digest begin,header digest:${state}", ("state", strHeaderDig));
      std::string strBmRoot = "";
      strBmRoot = fc::json::to_string(blockroot_merkle.get_root());
      dlog("block_header_state::sig_digest begin,bm root:${state}", ("state", strBmRoot));
     auto header_bmroot = digest_type::hash( std::make_pair( header.digest(), blockroot_merkle.get_root() ) );
      dlog("header_bmroot:${state}", ("state", header_bmroot));
      dlog("pending_schedule_hash:${state}", ("state", pending_schedule_hash));
     return digest_type::hash( std::make_pair(header_bmroot, pending_schedule_hash) );
  }

在这一步中,我们看到首先对区块的头信息header进行了hash获取了其摘要信息,而后将摘要信息和默克尔树的最后一个元素pair之后再次进行hash,最后将本次hash的结果和本节点轮流出块的hash(每个生产节点是固定的)pair之后再次进行hash,也就是进行了三次hash的过程。关于默克尔树在区块链或者说在eos中的应用,我们在后续的文章中也会做一些简单的介绍,然后我们来看取区块头本身的hash是如何实现的。

  • 第五步:获取区块头信息的摘要信息及默克尔树的最后一个元素
代码语言:javascript复制
   digest_type block_header::digest()const
   {
      return digest_type::hash(*this);
   }
   //按递增顺序获取当前节点的默克尔树
   DigestType get_root() const {
   if (_node_count > 0) {
      return _active_nodes.back();
    } else {
        return DigestType();
     }
  }

可以看到,获取区块头信心的摘要信息也是经过一次hash散列完成。

  • 第六步:基于openssl的sha256 hash的实现
代码语言:javascript复制
    static sha256 hash( const T& t ) 
    { 
      sha256::encoder e; 
      fc::raw::pack(e,t);//将需要散列的信息t打包至加密信息e里面
      return e.result(); //返回打包的结果
    }

   //sha256的结果
    sha256 sha256::encoder::result() {
      sha256 h;
      SHA256_Final((uint8_t*)h.data(), &my->ctx );
      return h;
    }

在hash的实现过程中,我们可以看到使用了fc库中的pack将需要散列的信息打包到加密变量e里面,而在查看pack的过程中可以发现其依据变量类型对pack进行了多次重载,最终使用openssl库中的SHA256_Final将hash结果返回。

  • 第七步:将签名结果放到区块头信息中
代码语言:javascript复制
header.producer_signature = signer( d );
  • 第八步:签名前几签名后信息对比

我们在签名的各个步骤中分别加了日志,由于区块信息较长,这里我们贴出区块头信息在签名前和签名后的对比。

代码语言:javascript复制
//区块头信息签名之前
    "header": {
        "timestamp": "2018-09-26T10:58:49.000",
        "producer": "eosio",
        "confirmed": 0,
        "previous": "0001336d4c819c9656e3d8f9619afb65b4d94eb368cb7cbf1b8a0b3175dcfdff",
        "transaction_mroot": "0000000000000000000000000000000000000000000000000000000000000000",
        "action_mroot": "c2fd5cfedbf61c357b14a05dcdb3ab186aabb394c385f2f2a4daf79fb35cf454",
        "schedule_version": 0,
        "header_extensions": [],
        "producer_signature": "SIG_K1_111111111111111111111111111111111111111111111111111111111111111116uk5ne"
    },
//区块头信息签名之后
    "header": {
        "timestamp": "2018-09-26T10:58:49.000",
        "producer": "eosio",
        "confirmed": 0,
        "previous": "0001336d4c819c9656e3d8f9619afb65b4d94eb368cb7cbf1b8a0b3175dcfdff",
        "transaction_mroot": "0000000000000000000000000000000000000000000000000000000000000000",
        "action_mroot": "c2fd5cfedbf61c357b14a05dcdb3ab186aabb394c385f2f2a4daf79fb35cf454",
        "schedule_version": 0,
        "header_extensions": [],
        "producer_signature": "SIG_K1_KdhkFF5W2YVtNdwmCVYmdw3WMoKCcCgestut6wHsWtRuekjaHv7BZkWU4UXJqboozf6JonXru9hVfQVcptWCN23s6YpFjX"
    }

通过对比可以发现,在基本信息保持一致的情况下,经过上面的八步操作,producer_signatrue发生了变化,至此也完成了区块签名的整个流程。

本文从区块生产过程出发,一步步介绍区块在生产过程中是如何实现SHA256签名的。eos中关于hash的内容很多,从钱包到账户,从action到transaction皆是如此,感兴趣的同学可以自己摸索下。

在后续的文章中,我们会针对验签及默克尔树进行讨论。

0 人点赞