Tinychain源码阅读笔记1-装载区块数据
运行测试tinychain之后,开始分析它的代码。
tinychain.py
入口main
if __name__ == '__main__':
signing_key, verifying_key, my_address = init_wallet()
main()
先看init_wallet函数
@lru_cache()
def init_wallet(path=None):
path = path or WALLET_PATH
if os.path.exists(path):
with open(path, 'rb') as f:
signing_key = ecdsa.SigningKey.from_string(
f.read(), curve=ecdsa.SECP256k1)
else:
logger.info(f"generating new wallet: '{path}'")
signing_key = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1)
with open(path, 'wb') as f:
f.write(signing_key.to_string())
verifying_key = signing_key.get_verifying_key()
my_address = pubkey_to_address(verifying_key.to_string())
logger.info(f"your address is {my_address}")
return signing_key, verifying_key, my_address
init_wallet返回signing_key, verifying_key, my_address,即私钥、公钥、地址。
用装饰器@lru_cache()装饰init_wallet函数的目的是缓存init_wallet函数返还的结果,加快读取私钥、公钥及地址的速度。在默认情况下,init_wallet函数会在tinychain目录底下生成wallet.dat这个文件来存储私钥,以后再启动则会从这个文件读取私钥。当然你也可以设置TC_WALLET_PATH这个环境变量指定存储私钥的文件。
WALLET_PATH = os.environ.get('TC_WALLET_PATH', 'wallet.dat')
比特币采用secp256k1标准所定义的一种特殊的椭圆曲线进行非对称加密,生成比特币的密钥和地址生成涉及了许多关于密码学的细节,大家可以参考《精通比特币》第四章
def main():
load_from_disk()
workers = []
server = ThreadedTCPServer(('0.0.0.0', PORT), TCPHandler)
def start_worker(fnc):
workers.append(threading.Thread(target=fnc, daemon=True))
workers[-1].start()
logger.info(f'[p2p] listening on {PORT}')
start_worker(server.serve_forever)
if peer_hostnames:
logger.info(
f'start initial block download from {len(peer_hostnames)} peers')
send_to_peer(GetBlocksMsg(active_chain[-1].id))
ibd_done.wait(60.) # Wait a maximum of 60 seconds for IBD to complete.
start_worker(mine_forever)
[w.join() for w in workers]
接着看main函数,首先调用了load_from_disk这个函数,即启动时要从本机中加载已经存在的区块数据。
@with_lock(chain_lock)
def load_from_disk():
if not os.path.isfile(CHAIN_PATH):
return
try:
with open(CHAIN_PATH, "rb") as f:
msg_len = int(binascii.hexlify(f.read(4) or b'\x00'), 16)
new_blocks = deserialize(f.read(msg_len))
logger.info(f"loading chain from disk with {len(new_blocks)} blocks")
for block in new_blocks:
connect_block(block)
except Exception:
logger.exception('load chain failed, starting from genesis')
注意@with_lock(chain_lock)这个装饰器,这里用到了递归锁。
#Synchronize access to the active chain and side branches.
chain_lock = threading.RLock()
def with_lock(lock):
def dec(func):
@wraps(func)
def wrapper(*args, **kwargs):
with lock:
return func(*args, **kwargs)
return wrapper
return dec
load_from_disk首先从CHAIN_PATH指定的路径文件读取区块数据,该文件的前4的字节代表的是区块数据的长度,随后将区块数据解析成对象,先看看Block类是如何定义的。
class Block(NamedTuple):
# A version integer.
version: int
# A hash of the previous block's header.
prev_block_hash: str
# A hash of the Merkle tree containing all txns.
merkle_hash: str
# A UNIX timestamp of when this block was created.
timestamp: int
# The difficulty target; i.e. the hash of this block header must be under
# (2 ** 256 >> bits) to consider work proved.
bits: int
# The value that's incremented in an attempt to get the block header to
# hash to a value below `bits`.
nonce: int
txns: Iterable[Transaction]
def header(self, nonce=None) -> str:
"""
This is hashed in an attempt to discover a nonce under the difficulty
target.
"""
return (
f'{self.version}{self.prev_block_hash}{self.merkle_hash}'
f'{self.timestamp}{self.bits}{nonce or self.nonce}')
@property
def id(self) -> str: return sha256d(self.header())
首先介绍下NamedTuple,NamedTuple能让继承其的对象像tuple一样,有可索引的属性,并且iterable。关于区块的详细信息,大家参考源码中作者的注释以及《精通比特币》第九章关于区块的介绍,在这不再赘述。
再看看deserialize函数
def deserialize(serialized: str) -> object:
"""NamedTuple-flavored serialization from JSON."""
gs = globals()
def contents_to_objs(o):
if isinstance(o, list):
return [contents_to_objs(i) for i in o]
elif not isinstance(o, Mapping):
return o
_type = gs[o.pop('_type', None)]
bytes_keys = {
k for k, v in get_type_hints(_type).items() if v == bytes}
for k, v in o.items():
o[k] = contents_to_objs(v)
if k in bytes_keys:
o[k] = binascii.unhexlify(o[k]) if o[k] else o[k]
return _type(**o)
return contents_to_objs(json.loads(serialized))
首先将区块数据的字符串形式转化为dict组成的list,将其传递给contents_to_objs,contents_to_objs是个递归函数,这样做目前只是因为最外层的Block被json.loads成dict格式了,Block内部的txn元素内的成员还没有转化为对应类型的值。我们看一段chain.dat存储的数据:
{
"_type": "Block",
"bits": 24,
"merkle_hash": "7116e6e9a67539e1c70d9a06acbf87f894ff77f95d61bd0e4e2f4f9070cd1ea3",
"nonce": 23667976,
"prev_block_hash": "000000043d6851eb0c97631dfbed8ab5128e94228443d91ba9ec5a9748baa447",
"timestamp": 1544702316,
"txns": [
{
"_type": "Transaction",
"locktime": null,
"txins": [
{
"_type": "TxIn",
"sequence": 0,
"to_spend": null,
"unlock_pk": null,
"unlock_sig": "3139"
}
],
"txouts": [
{
"_type": "TxOut",
"to_address": "153Gxg4HNyknpn88Ga9V7a2aArgZxbD6Yw",
"value": 9999999900
}
]
},
{
"_type": "Transaction",
"locktime": null,
"txins": [
{
"_type": "TxIn",
"sequence": 0,
"to_spend": {
"_type": "OutPoint",
"txid": "5798ecabb8e07f012b9af35690867bd722ccc85bf1053f03dd647c956d1a9555",
"txout_idx": 0
},
"unlock_pk": "2aafdccd664e82e0d38fa32594b7dc91ffd52c4ded34184f52ad3e9726b3ae24ac41a0f235c8ebc09f02f6e055621c8d61ec535a6617b9942ea6baacd9eab6eb",
"unlock_sig": "eda2504458f472e3855c51edfb87ef96f8932ca0c67316872fce81c967369db25e9a7c805687177eb3710ea3c4a55ff70a22d9222f6aa11fd78cf4a74eca0426"
}
],
"txouts": [
{
"_type": "TxOut",
"to_address": "17yL98ybgpFPSSfq9z6X5G17osh1DMY3sW",
"value": 100
}
]
}
],
"version": 0
}
# The (signature, pubkey) pair which unlocks the TxOut for spending.
unlock_sig: bytes
unlock_pk: bytes
unlock_sig和unlock_pk是解锁交易输出的签名与临时公钥,bytes类型,这里我们需要把它从16进制字符串转化成相应的bytes。contents_to_objs函数执行的具体流程是先判断传递的对象是否是list,是的话对list遍历递归调用contents_to_objs,用globals()获取全局定义的对象的所有属性,继续递归遍历这些属性,最后将**o这个dict参数传递给所需要实例化的类,就能读取到我们想要的Block了,这段代码递归运用的非常巧妙,大家看一下Block与Transaction类是如何定义的,就能明白了。