最近我开始学习区块链,一边分析比特币代码,一边按照自己的理解实现了一遍(https://github.com/hindsights/xbtc),虽然功能不太完善,好在是基本上把比特币的架构和重要技术细节理清楚了。现在回想起来,整个过程还算顺利,但也走了一些弯路,所以想在这里做一下总结,提取一些分析代码的方法和准则。
准备工作
文档优先
代码包含太多的细节,所以分析代码很费时,必须充分利用已有的书籍、文档、图表、文章等各种类型的资料,包括官方和第三方的。阅读文档贯穿整个分析过程,根据当前分析的焦点选择相应的文档。
比特币相关的书有《精通比特币》、《区块链技术指南》。比特币的官方文档有开发者指南和参考,以及各种wiki文章。网上还有很多比特币和区块链相关的博客,像csdn、博客园、等网站上都可以找到大量这方面的内容,从各个角度和层面去分析比特币。
部署运行
通过部署运行,获得对程序的直观体验。
比特币的程序有多种形式,有图形化界面的钱包,有命令行形式的后台守护进程和前台命令行控制工具。通过这些,可以更形象具体的理解比特币的功能和形态。
了解系统的其它外部特性
了解系统依赖于哪些第三方库、服务和工具等,了解系统的对外接口和其它对外呈现的状态和特性。
比特币主要用到leveldb和加密算法的库,包括各种哈希算法和椭圆曲线算法库(secp256k1)。对外提供的接口可以从bitcoin-cli中看到,有查看区块链信息的,有查看网络状况的,有挖矿的,有对原始交易信息做处理的,等等。
原则和方法
逐层深入
代码包含太多的细节,阅读代码时很容易被这些细节影响和耽误,不能快速掌握系统的结构和逻辑。所以要控制好节奏,在对一个层次的结构和逻辑有了认识之后,再进入更深的层次。
比特币的代码规模不算大,层次也不算太多,但细节上非常复杂,因为去中心化的数据要通过很多校验和处理,包括系统升级后对历史数据的兼容等,在前期要尽量避免深入这些细节。
比特币的代码可以大致分成3层:应用层、网络层、数据层。应用层面向用户,实现钱包和交易生成等上层功能。网络层负责搭建和维护P2P网络,以便进行交易数据同步。数据层则负责存储和处理区块链数据,可以进一步分为数据处理层(包括校验等处理)、内存缓存层和磁盘存储层。
多侧面、多角度的分析
类似RUP的4+1视图模型,可以从不同的角度去分析一个系统。例如静态方面可以看代码里有哪些功能模块和类,有哪些核心的数据类型和数据结构;动态方面可以看代码里有哪些线程,分别负责什么任务,用的什么网络接口和IO模型;场景方面可以看主要的用户功能是通过什么流程实现的。
我们还可以从数据的角度去分析,因为很多应用是为以数据为核心或者由数据来驱动的。这时如果理解了数据的用途、来源、格式、存储和其它各种处理操作,也就理解了整个系统的结构和逻辑。
比特币的P2P网络部分用的是select模型,直接使用的BSD socket,没有像http/rpc部分那样使用libevent库。线程方面有处理网络I/O的,也有磁盘读写、脚本校验、任务调度的。比特币的核心数据类型有Transaction/Block/Coin相关的一些类,核心的数据结构有区块链数据和UTXO表,在CChainState/CChain类和一些全局变量里。主要的用户场景有生成交易和挖矿。
动静结合
阅读代码是个静态的过程,但代码的意义在于动态的运行,静态的代码只是系统的一部分,有了数据的输入,整个系统才算是完整。所以把静态的阅读和动态的运行调试结合起来,可以更好更快的了解系统的逻辑和流程。
在这方面我有些失误,因为bitcoin core用的是makefile,我不想在命令行下调试,又懒得自己去创建xcode项目,就一直以静态阅读代码为主,偶尔通过查看日志来检查代码运行情况。等xbtc开发到了中期,很多运行细节光靠阅读代码没办法搞清楚,无奈之下,才花时间写了个bitcoin core的cmake项目文件,生成xcode的工程,开始在xcode里做调试。有了调试之后,既方便观察系统的运行流程,也可以更准确的观察代码运行过程中的数据和状态变化。
调试也会有一些副作用。调试时容易陷入代码运行的繁琐细节中,从而迷失方向和浪费时间。所以需要控制好节奏,保持对大局的清楚认识,把握住重点,根据需要逐层深入。
选择合适的突破口
在刚开始分析时,我们常常会面对一大堆陌生的代码,不知从何下手。这时候就需要找到一个合适的突破口,从一点突击,逐步掌握整个系统。选择的方式有下面几种:
- 比较容易的模块
- 比较熟悉的模块
- 比较核心的模块
这就像打仗的时候,有时候要从敌人的防线中寻找弱点进行攻击,等突破防线后再扩大战果。有时候可以绕开对方防线,直接攻击敌人首脑,来个斩首行动。还有时候要打攻坚战,集中力量击溃敌人的主力,使其整个体系迅速崩溃瓦解。
我在这方面也走了弯路。我是习惯性的从比特币的main函数入手开始分析。先看了网上一些分析启动流程的文章,对程序初始化过程方面有了了解,但对整个系统还是没什么认识,而且也不知道下一步从哪里着手比较好。后来我就看网络相关的模块,找到监听tcp端口的位置,然后开始分析初始peer地址的获取过程,进而找到建立peer连接和处理peer通信的地方。这时候我才醒悟过来,这跟我熟悉的P2P系统的架构没什么差别。为什么一开始我没有想到从这个角度入手?真是浪费了不少时间。总结起来,我犯了两个错误,一是文档方面看的不够,特别是官方的一些架构文档。二是没有先想清楚,就急着去分析代码,而且是按照以前的老习惯去分析代码,没有根据对系统架构的理解和自身的情况选择合适的方法和入口。
- 函数调用流程
- 数据流向
软件系统是由代码和数据交互而成。从代码这条线来看,可以分析函数调用的流程,理清程序的逻辑。从数据线来看,可以分析数据从哪里来、到哪里去、在哪里存储、怎么存储、做哪些处理等等。前者适合代码单线顺序化执行的系统,后者适合数据密集的、数据驱动的系统。
- 函数调用分派点
典型的有http请求调度和p2p协议报文处理调度,从这些地方可以快速把握系统的网络入口和分支,了解函数调用和数据流的走向。
- 特殊标识点
如果代码特别乱,可以从一些已知的标记入手,找到相关的代码,理解相对应的逻辑和流程。
- 早期版本
早期版本虽然功能不够完善,但代码更简单,更容易看懂,可以作为主要分析目标或者作为对比参考的资料。还可以寻找同类项目的轻量级实现。
我到后期才想到这一点,在网上找到了bitcoin的早期版本代码。跟最新版比起来,老版确实简单很多。而且网上一些分析比特币代码的文章也是基于老版本的。另外我还下载了libbitcoin和btcd这两个bitcoin开源实现的代码,不过它们实现的都比较完善,代码量较大,只作为对比参考使用。
坚持全局视野
阅读一个软件的代码跟看一本书、学一门知识的目的是一样的,都是要把这些原始材料消化后在大脑中形成知识体系。这个知识体系就像导航软件的地图一样,可以在我们身陷细节之时给我们指引方向。所以我们阅读代码的时候,一边生成这个知识体系,一边要把这个知识体系当成导航系统,时刻进行参考,避免迷失。
保持好的心态,避开陷阱和负面因素的影响
我感受最深的是“急于求成”心态的负面影响。不只是在阅读和分析代码时,在开发、调试、解决各种问题时经常也会遇到这种状况。例如经过长时间的努力,感觉目标马上就要实现了,胜利就在眼前了,这时候很容易“急于求成”,变得盲目而偏执,忘记大局,也忘记思考正确的方法和策略,最后迷失方向绕了弯路甚至是走错了路。所以要保持平静客观的心态,把握好节奏,经常参考“导航系统”,保持大局观,经常去反思方法和策略。
结语
上面总结的这些方法,具体怎么选择,还需要根据具体情况而定。我们需要通过学习和总结积累更多的方法和操作模型,对遇到的系统,选择最匹配的一个或几个模型来加以运用,这样就能更快速更准确的理解和掌握新的系统。