本日は、このCTransactionSignatureSerializerの構造を解読していきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
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を何度も読み返すと、非常に低い発生確率ですが起こりえる確率で一つ抜け穴があります。
ああ、本考察はできる限り簡素に書こうと考えていたのが、本日は気合を入れました(^^;。休憩です。
また流出事故が起きてしまいました。ところが、それは本当にセキュリティの問題でしょうか?
実は確率の問題も絡んでおり、そのような場合からの流出(ハッキング)を確実に防ぐ効果的な方法があります。なぜその方法で防げるのか。理論的な側面を詳しく説明し、考察(その7)の最後に解決策をお教えします。