考察(その3)です。CTransactionSignatureSerializerの構造をみていきます。

本日は、このCTransactionSignatureSerializerの構造を解読していきましょう。

template <class T>
class CTransactionSignatureSerializer
{
private:
    const T& txTo;             //!< reference to the spending transaction (the one being serialized)
    const CScript& scriptCode; //!< output script being consumed
    const unsigned int nIn;    //!< input index of txTo being signed
    const bool fAnyoneCanPay;  //!< whether the hashtype has the SIGHASH_ANYONECANPAY flag set
    const bool fHashSingle;    //!< whether the hashtype is SIGHASH_SINGLE
    const bool fHashNone;      //!< whether the hashtype is SIGHASH_NONE

public:
    CTransactionSignatureSerializer(const T& txToIn, const CScript& scriptCodeIn, unsigned int nInIn, int nHashTypeIn) :
        txTo(txToIn), scriptCode(scriptCodeIn), nIn(nInIn),
        fAnyoneCanPay(!!(nHashTypeIn & SIGHASH_ANYONECANPAY)),
        fHashSingle((nHashTypeIn & 0x1f) == SIGHASH_SINGLE),
        fHashNone((nHashTypeIn & 0x1f) == SIGHASH_NONE) {}

    /** Serialize the passed scriptCode, skipping OP_CODESEPARATORs */
    template<typename S>
    void SerializeScriptCode(S &s) const {
        CScript::const_iterator it = scriptCode.begin();
        CScript::const_iterator itBegin = it;
        opcodetype opcode;
        unsigned int nCodeSeparators = 0;
        while (scriptCode.GetOp(it, opcode)) {
            if (opcode == OP_CODESEPARATOR)
                nCodeSeparators++;
        }
        ::WriteCompactSize(s, scriptCode.size() - nCodeSeparators);
        it = itBegin;
        while (scriptCode.GetOp(it, opcode)) {
            if (opcode == OP_CODESEPARATOR) {
                s.write((char*)&itBegin[0], it-itBegin-1);
                itBegin = it;
            }
        }
        if (itBegin != scriptCode.end())
            s.write((char*)&itBegin[0], it-itBegin);
    }

    /** Serialize an input of txTo */
    template<typename S>
    void SerializeInput(S &s, unsigned int nInput) const {
        // In case of SIGHASH_ANYONECANPAY, only the input being signed is serialized
        if (fAnyoneCanPay)
            nInput = nIn;
        // Serialize the prevout
        ::Serialize(s, txTo.vin[nInput].prevout);
        // Serialize the script
        if (nInput != nIn)
            // Blank out other inputs' signatures
            ::Serialize(s, CScript());
        else
            SerializeScriptCode(s);
        // Serialize the nSequence
        if (nInput != nIn && (fHashSingle || fHashNone))
            // let the others update at will
            ::Serialize(s, (int)0);
        else
            ::Serialize(s, txTo.vin[nInput].nSequence);
    }

    /** Serialize an output of txTo */
    template<typename S>
    void SerializeOutput(S &s, unsigned int nOutput) const {
        if (fHashSingle && nOutput != nIn)
            // Do not lock-in the txout payee at other indices as txin
            ::Serialize(s, CTxOut());
        else
            ::Serialize(s, txTo.vout[nOutput]);
    }

    /** Serialize txTo */
    template<typename S>
    void Serialize(S &s) const {
        // Serialize nVersion
        ::Serialize(s, txTo.nVersion);
        // Serialize vin
        unsigned int nInputs = fAnyoneCanPay ? 1 : txTo.vin.size();
        ::WriteCompactSize(s, nInputs);
        for (unsigned int nInput = 0; nInput < nInputs; nInput++)
             SerializeInput(s, nInput);
        // Serialize vout
        unsigned int nOutputs = fHashNone ? 0 : (fHashSingle ? nIn+1 : txTo.vout.size());
        ::WriteCompactSize(s, nOutputs);
        for (unsigned int nOutput = 0; nOutput < nOutputs; nOutput++)
             SerializeOutput(s, nOutput);
        // Serialize nLockTime
        ::Serialize(s, txTo.nLockTime);
    }
};

ビットコインを解読する際は、Serializeというメソッドが重要です。ここから解読していくのです。

template<typename S>
void Serialize(S &s) const

この部分ですね。Sはストリームのメソッドが実装されたクラスが渡されます。ビットコインにおけるストリームのメソッドが実装されたクラスで有名なものは「CDataStream」と「CHashWriter」です。このようなストリームを持つクラスのインスタンスに、このCTransactionSignatureSerializerのインスタンスをストリーム演算子(<<と>>ですね)で渡すと、このSerializeとUnserializeが呼ばれる仕組みになっています。

このCTransactionSignatureSerializerはECDSAの「秘密鍵」に渡すハッシュを生成しますので、ハッシュを生成するストリームである「CHashWriter」に、このインスタンスを渡して、CHashWriterからハッシュを得て、そのハッシュをECDSAの「メッセージハッシュ」として署名する。そんな流れになっています。

つまり、ECDSAの署名の内容はCTransactionSignatureSerializerの一存で決まる仕組み(1または3から始まるアドレス)です。なぜなら、ECDSAの署名(なお、他の公開鍵暗号方式も同じ仕組み)は「秘密鍵」と「メッセージハッシュ」で決まるからです。秘密鍵も重要ですが、それと同じ位、この「メッセージハッシュ」も大切な要素です。この内容で公開鍵による検証の有効無効が定まるからです。

そこでSerializeの内容をみていきます。トランザクションの内容をくまなく辿って、各メンバまたは各Serializeを呼び出していますね。このトランザクションを構成するクラス(CTxInやCTxOut)などにもSerializeが実装されており、連鎖的に呼び出せるようになっています。

これで、CTransactionの各メンバ、CTxIn、CTxOutの内容次第でCTransactionSignatureSerializerが出力するシリアライズの内容が変化することがわかりました。ここで、CHashWriterとは何か。これは、そのシリアライズされたバイナリをハッシュターゲットにします。よって、CTransactionSignatureSerializerが出力するシリアライズの内容でトランザクションを作成するための署名用メッセージハッシュが定まる仕組みになっているのがわかりました。

それで、このCTransactionSignatureSerializerとCTransactionを何度も読み返すと、非常に低い発生確率ですが起こりえる確率で一つ抜け穴があります。

ああ、本考察はできる限り簡素に書こうと考えていたのが、本日は気合を入れました(^^;。休憩です。