本日の考察(その5)は、UnserializeTransactionとSerializeTransactionを観察です。

本日は、その3で考察したCTransactionSignatureSerializerとCTransactionが関連する、CTransactionのシリアライズ(UnserializeTransactionとSerializeTransaction)を観察しましょう。

template<typename Stream, typename TxType>
inline void UnserializeTransaction(TxType& tx, Stream& s) {
    const bool fAllowWitness = !(s.GetVersion() & SERIALIZE_TRANSACTION_NO_WITNESS);
    const bool fAllowMWEB = !(s.GetVersion() & SERIALIZE_NO_MWEB);

    s >> tx.nVersion;
    unsigned char flags = 0;
    tx.vin.clear();
    tx.vout.clear();
    /* Try to read the vin. In case the dummy is there, this will be read as an empty vector. */
    s >> tx.vin;
    if (tx.vin.size() == 0 && fAllowWitness) {
        /* We read a dummy or an empty vin. */
        s >> flags;
        if (flags != 0) {
            s >> tx.vin;
            s >> tx.vout;
        }
    } else {
        /* We read a non-empty vin. Assume a normal vout follows. */
        s >> tx.vout;
    }
    if ((flags & 1) && fAllowWitness) {
        /* The witness flag is present, and we support witnesses. */
        flags ^= 1;
        for (size_t i = 0; i < tx.vin.size(); i++) {
            s >> tx.vin[i].scriptWitness.stack;
        }
        if (!tx.HasWitness()) {
            /* It's illegal to encode witnesses when all witness stacks are empty. */
            throw std::ios_base::failure("Superfluous witness record");
        }
    }
    if ((flags & 8) && fAllowMWEB) {
        /* The MWEB flag is present, and we support MWEB. */
        flags ^= 8;

        s >> tx.mweb_tx;
        if (tx.mweb_tx.IsNull()) {
            if (tx.vout.empty()) {
                /* It's illegal to include a HogEx with no outputs. */
                throw std::ios_base::failure("Missing HogEx output");
            }

            /* If the MWEB flag is set, but there are no MWEB txs, assume HogEx txn. */
            tx.m_hogEx = true;
        }
    }
    if (flags) {
        /* Unknown flag in the serialization */
        throw std::ios_base::failure("Unknown transaction optional data");
    }
    s >> tx.nLockTime;
}

template<typename Stream, typename TxType>
inline void SerializeTransaction(const TxType& tx, Stream& s) {
    const bool fAllowWitness = !(s.GetVersion() & SERIALIZE_TRANSACTION_NO_WITNESS);
    const bool fAllowMWEB = !(s.GetVersion() & SERIALIZE_NO_MWEB);

    s << tx.nVersion;
    unsigned char flags = 0;
    // Consistency check
    if (fAllowWitness) {
        /* Check whether witnesses need to be serialized. */
        if (tx.HasWitness()) {
            flags |= 1;
        }
    }
    if (fAllowMWEB) {
        if (tx.m_hogEx || !tx.mweb_tx.IsNull()) {
            flags |= 8;
        }
    }

    if (flags) {
        /* Use extended format in case witnesses are to be serialized. */
        std::vector vinDummy;
        s << vinDummy;
        s << flags;
    }
    s << tx.vin;
    s << tx.vout;
    if (flags & 1) {
        for (size_t i = 0; i < tx.vin.size(); i++) {
            s << tx.vin[i].scriptWitness.stack;
        }
    }
    if (flags & 8) {
        s << tx.mweb_tx;
    }
    s << tx.nLockTime;
}

ビットコインは、複数の機能を単一の関数にまとめる傾向があります。この傾向は、UnserializeTransactionやSerializeTransactionにおいても見られ、これらの関数には複数の機能が組み込まれています。そのため、これらのコードを理解するには、以前の古いコアに実装されていたSignatureHashのコードが頭にないと読めません。

そこで、CTransactionSignatureSerializerが作用する機能だけに絞ってUnserializeTransactionやSerializeTransactionを整理すると以下のようになります。

template<typename Stream, typename TxType>
inline void UnserializeTransaction(TxType& tx, Stream& s) {
    s >> tx.nVersion;
    s >> tx.vin;
    s >> tx.vout;
    s >> tx.nLockTime;
}

template<typename Stream, typename TxType>
inline void SerializeTransaction(const TxType& tx, Stream& s) {
    s << tx.nVersion;
    s << tx.vin;
    s << tx.vout;
    s << tx.nLockTime;
}

あれっ、という感じですよね。ずいぶんとすっきりしました。CTransactionSignatureSerializerが作用する機能だけなら、これだけでもビットコインは問題なく動作します。もちろん、マルチシグでさえ動作します。

ここで注目すべきは、ストリーム演算子(<<および>>)の使用方法です。>>演算子はデータをストリームsから取り出す役割を果たします。具体的には、UnserializeTransaction関数は、コード中にある4つの要素を順番にsから取り出します。反対に、<<演算子はデータをストリームsに送り出す役割を持ちます。つまり、SerializeTransaction関数は、コード中の4つの要素を順番にsへ書き込む処理を行います。

次に、これら4つの要素のうち、取引情報を保持するのはvinとvoutです。これらは可変長配列(std::vector)として定義されています。vinは入力(所有する残高をまとめる部分)であり、voutは出力(受け取り先および余剰分をまとめる部分)を表します。コインを送る際に複数の送付先を指定できる理由は、このvoutに複数の送付先を設定できるからです。具体的には、vinにはトランザクションの入力情報が、voutにはトランザクションの出力情報が格納され、各々がそれぞれの目的に応じて使用されます。

これらを念頭に置き、CTxIn(vin)とCTxOut(vout)をみていきましょう。