7, Observing UnserializeTransaction and SerializeTransaction.

Today, let’s observe the serialization of CTransaction (UnserializeTransaction and SerializeTransaction) which is related to the CTransactionSignatureSerializer and CTransaction discussed in Part 3.

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
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;
}

Bitcoin tends to consolidate multiple functions into a single function. This tendency is also seen in UnserializeTransaction and SerializeTransaction, where multiple functions are incorporated. To understand these codes, one must keep in mind the code of SignatureHash that was implemented in the old core.

Therefore, when we organize UnserializeTransaction and SerializeTransaction focusing only on the functions that CTransactionSignatureSerializer acts upon, it becomes as follows.

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;
}

It feels quite simplified, doesn't it? Even with just the functions that CTransactionSignatureSerializer acts upon, Bitcoin operates without issues. Of course, even multisig works fine.

Here, the usage of stream operators (<< and >>) should be noted. The >> operator serves to extract data from the stream s. Specifically, the UnserializeTransaction function sequentially extracts four elements from s as specified in the code. Conversely, the << operator is used to send data to the stream s. Thus, the SerializeTransaction function sequentially writes the four elements into s.

Next, among these four elements, the transaction information is held by vin and vout. These are defined as variable-length arrays (std::vector). vin represents the inputs (the part that summarizes owned balances), and vout represents the outputs (the part that summarizes the recipients and surplus). The reason you can specify multiple recipients when sending coins is that multiple recipients can be set in vout. Specifically, vin contains the input information of the transaction, and vout contains the output information of the transaction, each being used according to their respective purposes.

With this in mind, let's take a look at CTxIn (vin) and CTxOut (vout).