derekray avatar

Tinychain源码阅读笔记1

derekray

Published: 18 Dec 2018 › Updated: 18 Dec 2018Tinychain源码阅读笔记1

Tinychain源码阅读笔记1

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

Leave Tinychain源码阅读笔记1 to:

Written by

Read more #cn posts


Best Posts From derekray

We have not curated any of derekray's posts yet. But you can encourage our curation team to review posts by visiting them regularly and by referring other readers. Because we give priority to frequently read content.

More Posts From derekray