Tinychain源码阅读笔记1

in #cn6 years ago (edited)

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类是如何定义的,就能明白了。