pycoin学习笔记(6) 对交易签名的源码简析(PayToAddressScript)

(个人学习用,可能理解上存在谬误)

上篇中,签名的步骤如下,

solver = build_hash160_lookup([exponent])
signed_new_tx = unsigned_new_tx.sign(solver)

对于pycoin是如何完成交易签名的,很有必要从其源码中进行研究

    def sign(self, hash160_lookup, hash_type=None, **kwargs):
        """
        Sign a standard transaction.
        hash160_lookup:
            A dictionary (or another object with .get) where keys are hash160 and
            values are tuples (secret exponent, public_pair, is_compressed) or None
            (in which case the script will obviously not be signed).
        """
        if hash_type is None:
            hash_type = self.SIGHASH_ALL #1
        self.check_unspents() #2
        for idx, tx_in in enumerate(self.txs_in):
            if self.is_signature_ok(idx) or tx_in.is_coinbase():
                continue
            try:
                if self.unspents[idx]:
                    self.sign_tx_in(
                        hash160_lookup, idx, self.unspents[idx].script, hash_type=hash_type, **kwargs)#3
            except SolvingError:
                pass

        return self

大致流程:

1)先检查是否交易输入都有其对应的锁定脚本(#2处)

2)逐个对未签名过的交易输入进行签名操作(#3处)

#1处,将hash_type默认设为SIGHASH_ALL,这也是目前最主流的方式,具体细节可自行搜索


继续进入sign_tx_in方法:

    def sign_tx_in(self, hash160_lookup, tx_in_idx, tx_out_script, hash_type=None, **kwargs):
        if hash_type is None:
            hash_type = self.SIGHASH_ALL
        r = self.solve(hash160_lookup, tx_in_idx, tx_out_script,
                       hash_type=hash_type, **kwargs) #1
        if isinstance(r, bytes):
            self.txs_in[tx_in_idx].script = r #2
        else:
            self.txs_in[tx_in_idx].script = r[0] #3
            self.set_witness(tx_in_idx, r[1])

在#1处调用solve进行签名,返回解锁脚本。然后在#2处将解锁脚本设置入相应的交易输入TxIn的script字段中,完成了对该交易输入的签名。

#3处涉及到segwit相关的东西,不深入研究了

继续跟进solve

    def solve(self, hash160_lookup, tx_in_idx, tx_out_script, hash_type=None, **kwargs):
        """
        Sign a standard transaction.
        hash160_lookup:
            An object with a get method that accepts a hash160 and returns the
            corresponding (secret exponent, public_pair, is_compressed) tuple or
            None if it's unknown (in which case the script will obviously not be signed).
            A standard dictionary will do nicely here.
        tx_in_idx:
            the index of the tx_in we are currently signing
        tx_out:
            the tx_out referenced by the given tx_in
        """
        if hash_type is None:
            hash_type = self.SIGHASH_ALL
        tx_in = self.txs_in[tx_in_idx]

        is_p2h = (len(tx_out_script) == 23 and byte2int(tx_out_script) == opcodes.OP_HASH160 and
                  indexbytes(tx_out_script, -1) == opcodes.OP_EQUAL) #1
        if is_p2h:
            hash160 = ScriptPayToScript.from_script(tx_out_script).hash160
            p2sh_lookup = kwargs.get("p2sh_lookup")
            if p2sh_lookup is None:
                raise SolvingError("p2sh_lookup not set")
            if hash160 not in p2sh_lookup:
                raise SolvingError("hash160=%s not found in p2sh_lookup" %
                                   b2h(hash160))

            script_to_hash = p2sh_lookup[hash160]
        else:
            script_to_hash = tx_out_script #2

        # Leave out the signature from the hash, since a signature can't sign itself.
        # The checksig op will also drop the signatures from its hash.
        def signature_for_hash_type_f(hash_type, script):#3
            return self.signature_hash(script, tx_in_idx, hash_type)

        def witness_signature_for_hash_type(hash_type, script):
            return self.signature_for_hash_type_segwit(script, tx_in_idx, hash_type)
        witness_signature_for_hash_type.skip_delete = True

        signature_for_hash_type_f.witness = witness_signature_for_hash_type

        if tx_in.verify(
                tx_out_script, signature_for_hash_type_f, lock_time=self.lock_time,
                tx_version=self.version):
            return

        the_script = script_obj_from_script(tx_out_script)
        solution = the_script.solve(
            hash160_lookup=hash160_lookup, signature_type=hash_type,
            existing_script=self.txs_in[tx_in_idx].script, existing_witness=tx_in.witness,
            script_to_hash=script_to_hash, signature_for_hash_type_f=signature_for_hash_type_f, **kwargs)#4
        return solution

只关注主干流程。#1处判断是否为p2sh脚本,此处使用的是p2pkh因此为false,进入#2;否则其实是找寻赎回脚本redeem script,这方面留待以后研究。

#3处定义了闭包,返回的是当前交易的哈希值(即待签名的值)。

#4处开始正式生成解锁脚本

因此接下来需要对两个重要流程进行跟踪,

首先是#3处的signature_hash方法:

    def signature_hash(self, tx_out_script, unsigned_txs_out_idx, hash_type):
        """
        Return the canonical hash for a transaction. We need to
        remove references to the signature, since it's a signature
        of the hash before the signature is applied.

        tx_out_script: the script the coins for unsigned_txs_out_idx are coming from
        unsigned_txs_out_idx: where to put the tx_out_script
        hash_type: one of SIGHASH_NONE, SIGHASH_SINGLE, SIGHASH_ALL,
        optionally bitwise or'ed with SIGHASH_ANYONECANPAY
        """

        # In case concatenating two scripts ends up with two codeseparators,
        # or an extra one at the end, this prevents all those possible incompatibilities.
        tx_out_script = tools.delete_subscript(tx_out_script, int2byte(opcodes.OP_CODESEPARATOR))

        # blank out other inputs' signatures
        txs_in = [self._tx_in_for_idx(i, tx_in, tx_out_script, unsigned_txs_out_idx)
                  for i, tx_in in enumerate(self.txs_in)] #1
        txs_out = self.txs_out

        # Blank out some of the outputs
        if (hash_type & 0x1f) == self.SIGHASH_NONE:
            # Wildcard payee
            txs_out = []

            # Let the others update at will
            for i in range(len(txs_in)):
                if i != unsigned_txs_out_idx:
                    txs_in[i].sequence = 0

        elif (hash_type & 0x1f) == self.SIGHASH_SINGLE:
            # This preserves the ability to validate existing legacy
            # transactions which followed a buggy path in Satoshi's
            # original code; note that higher level functions for signing
            # new transactions (e.g., is_signature_ok and sign_tx_in)
            # check to make sure we never get here (or at least they
            # should)
            if unsigned_txs_out_idx >= len(txs_out):
                # This should probably be moved to a constant, but the
                # likelihood of ever getting here is already really small
                # and getting smaller
                return (1 << 248)

            # Only lock in the txout payee at same index as txin; delete
            # any outputs after this one and set all outputs before this
            # one to "null" (where "null" means an empty script and a
            # value of -1)
            txs_out = [self.TxOut(0xffffffffffffffff, b'')] * unsigned_txs_out_idx
            txs_out.append(self.txs_out[unsigned_txs_out_idx])

            # Let the others update at will
            for i in range(len(self.txs_in)):
                if i != unsigned_txs_out_idx:
                    txs_in[i].sequence = 0

        # Blank out other inputs completely, not recommended for open transactions
        if hash_type & self.SIGHASH_ANYONECANPAY:
            txs_in = [txs_in[unsigned_txs_out_idx]]

        tmp_tx = self.__class__(self.version, txs_in, txs_out, self.lock_time) #2
        return from_bytes_32(tmp_tx.hash(hash_type=hash_type))#3
只关注SIGHASH_ALL的情况,#1处是最为关键的步骤,它对当前交易的所有交易输入进行以下操作:
    def _tx_in_for_idx(self, idx, tx_in, tx_out_script, unsigned_txs_out_idx):
        if idx == unsigned_txs_out_idx:
            return self.TxIn(tx_in.previous_hash, tx_in.previous_index, tx_out_script, tx_in.sequence)
        return self.TxIn(tx_in.previous_hash, tx_in.previous_index, b'', tx_in.sequence)

即,对当前要签名的交易输入,将锁定脚本放到该TxIn结构的script字段中(此处最终是放入解锁脚本的地方);对不是当前要签名的TxIn,将其script设为空。

这样处理后,生成一个新的tx(#2处),并将其序列化,然后哈希之,这个值就是当前想要进行签名的交易输入所对应的待签名值(#3处)。

回到solve方法,#4处是最终实际签名(生成解锁脚本)的地方,是ScriptPayToAddress对象(因为此处是p2pkh)的solve方法,跟进之:

    def solve(self, **kwargs):
        """
        The kwargs required depend upon the script type.
        hash160_lookup:
            dict-like structure that returns a secret exponent for a hash160
        signature_for_hash_type_f:
            function returning sign value for a given signature type
        signature_type:
            usually SIGHASH_ALL (1)
        """
        # we need a hash160 => secret_exponent lookup
        db = kwargs.get("hash160_lookup")
        if db is None:
            raise SolvingError("missing hash160_lookup parameter")
        result = db.get(self.hash160)
        if result is None:
            raise SolvingError("can't find secret exponent for %s" % self.address())
        # we got it
        signature_for_hash_type_f = kwargs.get("signature_for_hash_type_f")
        signature_type = kwargs.get("signature_type")
        script_to_hash = kwargs.get("script_to_hash")

        secret_exponent, public_pair, compressed = result

        binary_signature = self._create_script_signature(
            secret_exponent, signature_for_hash_type_f, signature_type, script_to_hash)#1
        binary_public_pair_sec = encoding.public_pair_to_sec(public_pair, compressed=compressed)#2

        solution = tools.bin_script([binary_signature, binary_public_pair_sec])#3
        return solution

#1处产生了签名值,_create_script_signature方法为:

    def _create_script_signature(
            self, secret_exponent, signature_for_hash_type_f, signature_type, script):
        sign_value = signature_for_hash_type_f(signature_type, script)
        order = ecdsa.generator_secp256k1.order()
        r, s = ecdsa.sign(ecdsa.generator_secp256k1, secret_exponent, sign_value)
        if s + s > order:
            s = order - s

大致上是用椭圆曲线,对待签名值签名然后返回

#2处获得公钥,#3处打包成解锁脚本


总结下对交易进行签名的大致流程:即对每一项交易输入TxIn,生成其对应的待签名值,签名生成解锁脚本并设置入该交易输入TxIn的script字段。

特此记录,方便日后复查。


你可能感兴趣的:(杂项)