写在前面
先在前面说一下,我没有完全实现整个过程,因为有一个地方确实实现不了,等以后有条件再使用别的方法尝试一下。前一段时间看了一篇14年的论文,里面主要说的是比特币中随机数相同会造成私钥泄漏的问题,作者分析了当下存在很多因私钥泄漏,比特币频频被盗的现象,然后找到了问题所在,并且使用Blockchainr工具将其实现了,找出了比特币网络中存在的使用相同随机数的用户私钥。
目的
处于自身学习的角度,我要自己完成作者所提到的步骤,最终找到相同的随机数并且恢复私钥。
整体思路
先编写解析区块文件的程序,将后缀为.dat的区块文件解析出来,然后将数据存储到数据库,之后再借助14年那篇PPT的思想,利用作者提到的dabloom过滤器去找到相同的签名r,这时我们不能仅仅导出签名r,还要导出相关的信息,这样我们才可以知道是否来源于同一用户,才能去计算用户私钥。
具体过程
目前我可以实现解析区块并导出到数据库中,把.dat文件以二进制形式读取,编写区块结构并给出区块中各个字段的长度,以及通过已知字段的值,去读取某些未知长度字段的数据,如:解锁脚本可以通过脚本长度来读取,还有我们可以通过下图这样的思想去解析区块:
从block入手,里面包含blockheader、tx因为一个区块有多条交易,我们定义tx为一个集合Txs,Txs中的每条交易又包含input、output当然它们也会存在多条,所以依然定义为集合,这样我们就可以通过遍历的方式将一个区块的所有数据导出。下面给出部分代码:
1 | class Block: |
2 | def __init__(self,blockchain): |
3 | self.blockheight = 1 |
4 | self.continueParsing = True |
5 | self.magicNum = 0 |
6 | self.blocksize = 0 |
7 | self.blockheader = '' |
8 | self.txCount = 0 |
9 | self.Txs = [] |
这样的话从block入手,就可以逐层解码文件,最终得到完整的区块信息。
ScriptSig
因为我们要找到签名$r$,所以必须要解析签名脚本,我主要是通过解码$16$进制的字符串得到的,做之前我们要清楚解锁脚本的结构,这样才能正确处理,下面给出一张解锁脚本的结构图:
知道这些的话我们就可以通过脚本长度,签名长度等来进一步分割字符串,并且给出判定条件当前置交易索引不为$0xffffffff$时解析scriptsig,因为相等时交易时coinbase交易,是系统给的交易而不是经过人签名得来的,所以不具备$ECDSA$签名脚本,详情可以看我的另外一篇博客:https://blog.csdn.net/qq_35324057/article/details/104072715
下面给出实现这部分的代码:
1 | if 0xffffffff != self.txpreindex: |
2 | scriptsig = ScriptSig(self.inscriptsig) |
3 | self.sigr = scriptsig.sigR |
4 | self.sigs = scriptsig.sigS |
5 | self.inpubkey = scriptsig.pubkey |
6 | else: |
7 | self.sigr = '' |
8 | self.sigs = '' |
9 | self.inpubkey = '' |
1 | class ScriptSig: |
2 | def __init__(self,blockchain): |
3 | # if blockchain[8:10] == '20': |
4 | # # self.sigR = blockchain[10:74] |
5 | # # if blockchain[76:78] == '20': |
6 | # # self.sigS = blockchain[78:142] |
7 | # # else: |
8 | # # self.sigS = blockchain[78:144] |
9 | # # else : |
10 | # # self.sigR = blockchain[10:76] |
11 | # # if blockchain[78:80] == '20': |
12 | # # self.sigS = blockchain[78:142] |
13 | # # else: |
14 | # # self.sigS = blockchain[78:144] |
15 | self.scriptLength = int(blockchain[0:2],16)*2 |
16 | self.script = blockchain[2:2 + self.scriptLength] |
17 | self.rLength = int(blockchain[8:10],16)*2 |
18 | self.sigR = blockchain[10:10+self.rLength] |
19 | self.sLength = int(blockchain[10+self.rLength+2:10+self.rLength+2+2],16)*2 |
20 | self.sigS = blockchain[10+self.rLength+2+2:10+self.rLength+2+2+self.sLength] |
21 | |
22 | if SIGHASH_ALL != int(blockchain[self.scriptLength:self.scriptLength + 2], 16): |
23 | self.pubkey = "Script op_code is not SIGHASH_ALL" |
24 | |
25 | else: |
26 | self.pubkey = blockchain[2 + self.scriptLength + 2:2 + self.scriptLength + 2 + 66] |
这样就得到了计算私钥需要使用的两个值:$sigR,sigS$,但是我们还需要得到交易的哈希值$Z$,这一点真的太难了,为什么这么说呢,因为交易的哈希值并不是简单的对区块中的某个值做哈希运算,而是需要找到该交易的$prehash$所指向的交易,为什么要这样做呢,不知道的小伙伴请看我的上面提到的那篇博客,我们要是用前驱交易的交易输出部分,或者称为加密脚本,并且还需要一些删减增加字符串,然后最终得到我们的交易字符串,对其进行两次$sha256$运算得到$Z$,这一点是无法实现的。有时间的话我会单独写一篇关于计算交易哈希值的文章。
Redis
对于redis是什么我就不多说了,就是一种key-value的数据库。
安装
- pycharm安装redis包:
1 | pip install redis |
- 然后再下载安装redis数据库
- 安装RedisDesktopManager ———-redis数据库的可视化工具
插入区块数据
这也是我实现的最大的难点,主要就是如果key相同的话,因为每个区块都有相同的字段,所以key的值就会被覆盖,如果一个.dat文件中有多个区块的话,到最后插入的数据也只是最后一个区块的数据,而不能将所有的区块数据插入,实现这一点浪费了我极大的时间和经历,希望分享出来能帮到大家!
思想
主要的思想就是用不同的key来标识不同区块不同tx不同input中的数据。
如:我们用$previousHash+$字段名,来标识不同区块的数据,用$previousHash+Txseq+$字段名,来标识一个区块中不同的tx中的数据,依次类推,可以得到插入所有数据的方法,下面给出一些代码:
1 | #insert into redis DB |
2 | |
3 | #blockheader |
4 | # re.set(self.previoushash + 'Version',self.version) |
5 | # re.set(self.previoushash + 'PreviousHash', self.previoushash) |
6 | # re.set(self.previoushash + 'MerkleRoot', self.Hash) |
7 | # re.set(self.previoushash + 'Timestamp', self.time) |
8 | # re.set(self.previoushash + 'Difficulty', self.difficulty) |
9 | # re.set(self.previoushash + 'Nonce', self.nonce) |
10 | # |
11 | # # block |
12 | # re.set(self.previoushash + 'MagicNum', self.magicnum) |
13 | # re.set(self.previoushash + 'Blocksize', self.blocksize) |
14 | # re.set(self.previoushash + 'Txcount', self.txcount) |
15 | # |
16 | # # Tx |
17 | # re.set(self.previoushash + self.txseq + 'TxVersion', self.txversion) |
18 | # re.set(self.previoushash + self.txseq + 'InCount', self.incount) |
19 | # re.set(self.previoushash + self.txseq + 'Txseq', self.txseq) |
20 | # re.set(self.previoushash + self.txseq + 'OutCount', self.outcount) |
21 | # re.set(self.previoushash + self.txseq + 'LockTime', self.locktime) |
22 | # |
23 | # # Input |
24 | # re.set(self.previoushash + self.txseq + self.inputseq + 'TxPrevHash', self.txprevhash) |
25 | # re.set(self.previoushash + self.txseq + self.inputseq + 'TxPreIndex', self.txpreindex) |
26 | # re.set(self.previoushash + self.txseq + self.inputseq + 'InScriptLen', self.inscriptlen) |
27 | # re.set(self.previoushash + self.txseq + self.inputseq + 'InScriptSig', self.inscriptsig) |
28 | # re.set(self.previoushash + self.txseq + self.inputseq + 'InSequence', self.sequence) |
取数据
如果往里面存数据理解的话,取数据也很好理解了,主要就是利用了区块中本来就存在几个数据,$txcount$、$inputcount$、$outputcount$,利用这几个数据的话,就可以遍历它们得到数据,很简单就可以实现,如:$get(previousHash+i+’txversion’)$,其中$i$就是从0遍历到$txcount$,前面还有个东西忘了说,你查询数据用到的数据一定要存储起来,比如$previousHash$,就需要按序号存储起来,并且字段名方便提取,这样的话,提取数据的时候,用起来才会很方便。
1 | for i in range(0,19972): |
2 | hash = re.get('Hash' + str(i)) |
3 | txcount = re.get(hash + 'Txcount') |
4 | # print(txcount) |
5 | for j in range(0,int(txcount)): |
6 | # print ("Locktime:%s" % re.get(hash + str(j) + 'InCount')) |
7 | incount = re.get(hash + str(j) + 'InCount') |
总结
还有一些地方没有说,就不写了,总之这个过程还是很有收获的,就是在做的过程中很难过,每天需要有点进度,我却卡在数据库上面很长时间,希望大家每天也能有所收获,无论希望多渺茫,都要继续加油!