5, Consideration (Part 3). Let’s take a look at the structure of CTransactionSignatureSerializer.
Today, we will decode the structure of this CTransactionSignatureSerializer.
There has been another leakage incident. However, there is an effective method that can surely prevent leakage (hacking). But why does this method work? We will explain the theoretical aspects thoroughly and reveal the solution at the end of section 9.
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<class T> 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<class T> 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<class T> 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<class T> 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); } };
When decoding Bitcoin, the Serialize method is crucial. This is where we start decoding.
template<typename S>
void Serialize(S &s) const
In this part, an instance of a class with stream methods implemented is passed as S
. The well-known classes in Bitcoin that implement stream methods are "CDataStream" and "CHashWriter." When an instance of this CTransactionSignatureSerializer is passed to an instance of a class with such streams using stream operators (<<
and >>
), the Serialize and Unserialize methods are called.
This CTransactionSignatureSerializer generates a hash to be passed to the ECDSA "private key." By passing this instance to the stream that generates the hash, "CHashWriter," obtaining the hash from CHashWriter, and signing the hash as the ECDSA "message hash," this process is completed.
In other words, the content of the ECDSA signature is determined solely by CTransactionSignatureSerializer (addresses starting with 1 or 3). This is because ECDSA signatures (and other public-key cryptography methods as well) are determined by the "private key" and the "message hash." While the private key is important, the "message hash" is equally crucial. The validity of verification by the public key is determined by this content.
Next, let's look at the content of Serialize. It thoroughly traces the transaction content and calls Serialize for each member. Classes that constitute this transaction (such as CTxIn and CTxOut) also have Serialize implemented, allowing for chain calls.
This reveals that the serialized content output by CTransactionSignatureSerializer varies depending on the content of each member of CTransaction, CTxIn, and CTxOut. Now, what is CHashWriter? It targets the serialized binary for hashing. Thus, we understand that the signature message hash for creating a transaction is determined by the serialized content output by CTransactionSignatureSerializer.
After repeatedly reviewing this CTransactionSignatureSerializer and CTransaction, we found a very low-probability but possible loophole.