TeachMeBitcoin

Custom Python Coinbase Serializer

From TeachMeBitcoin, the free encyclopedia ⏱️ 4 min read

Building a Coinbase Transaction Serializer in Pure Python

To understand how a mining pool or individual node serializes the block creator's reward payment from scratch, we can implement a custom Coinbase Transaction Serializer in Python.

By writing this serializer using only Python's standard libraries, we can model how the null input outpoint is structured, how the BIP 34 block height is serialized into the coinbase script, how arbitrary pool tags are appended, and how outputs are structured to pay out block rewards.


💻 1. The Coinbase Serializer Source Code

Save the following standard-library Python script as python_coinbase_serializer.py.

import hashlib
import struct

class BitcoinCoinbaseSerializer:
    def double_sha256(self, data: bytes) -> bytes:
        """Returns the double-SHA256 hash of a byte array."""
        return hashlib.sha256(hashlib.sha256(data).digest()).digest()

    def serialize_varint(self, value: int) -> bytes:
        """Serializes an integer into standard CompactSize (VarInt) byte format."""
        if value < 0xfd:
            return struct.pack("B", value)
        elif value <= 0xffff:
            return b"\xfd" + struct.pack("<H", value)
        elif value <= 0xffffffff:
            return b"\xfe" + struct.pack("<I", value)
        else:
            return b"\xff" + struct.pack("<Q", value)

    def serialize_bip34_height(self, height: int) -> bytes:
        """Serializes the block height as a BIP 34 length-prefixed little-endian array."""
        # Convert integer to little-endian bytes
        height_bytes = bytearray()
        temp = height
        while temp > 0:
            height_bytes.append(temp & 0xff)
            temp >>= 8

        # Ensure at least 1 byte representation
        if not height_bytes:
            height_bytes.append(0)

        length = len(height_bytes)
        return struct.pack("B", length) + bytes(height_bytes)

    def serialize_coinbase_tx(self, 
                              block_height: int, 
                              block_subsidy: int, 
                              fees_collected: int, 
                              miner_signature_text: str, 
                              payout_script_pub_key: bytes) -> bytes:
        """
        Serializes a standard, valid coinbase transaction from first principles.
        """
        # 1. Transaction Version (4 Bytes, LE)
        version = struct.pack("<I", 1)

        # 2. Input Count (always 1 for Coinbase)
        input_count = self.serialize_varint(1)

        # --- INPUT STRUCT ---
        # 3. Prev Outpoint Hash (32 Bytes of Zeroes)
        prev_outpoint_hash = b"\x00" * 32

        # 4. Prev Outpoint Index (4 Bytes of 0xFF)
        prev_outpoint_index = struct.pack("<I", 0xffffffff)

        # 5. Build Coinbase Script (signature script field)
        bip34_height_bytes = self.serialize_bip34_height(block_height)
        miner_ascii_bytes = miner_signature_text.encode("ascii", errors="replace")

        # Coinbase Script combines BIP 34 height prefix + arbitrary text
        coinbase_script = bip34_height_bytes + miner_ascii_bytes

        # Coinbase Script must be bounded between 2 and 100 bytes
        if not (2 <= len(coinbase_script) <= 100):
            raise ValueError(f"Coinbase script length ({len(coinbase_script)}) violates bounds [2, 100].")

        # 6. Coinbase Script Length (VarInt)
        coinbase_script_len = self.serialize_varint(len(coinbase_script))

        # 7. Sequence (4 Bytes of 0xFF)
        sequence = struct.pack("<I", 0xffffffff)

        input_serialized = prev_outpoint_hash + prev_outpoint_index + coinbase_script_len + coinbase_script + sequence

        # --- OUTPUT STRUCT ---
        # For simplicity, we create one payout output paying the total block reward
        output_count = self.serialize_varint(1)

        # 8. Payout Payout Value (8 Bytes, uint64, LE)
        total_payout_sats = block_subsidy + fees_collected
        payout_value = struct.pack("<Q", total_payout_sats)

        # 9. scriptPubKey Length (VarInt)
        payout_script_len = self.serialize_varint(len(payout_script_pub_key))

        output_serialized = payout_value + payout_script_len + payout_script_pub_key

        # 10. Locktime (4 Bytes of Zeroes, LE)
        locktime = struct.pack("<I", 0)

        # Combine all parts
        raw_tx_bytes = version + input_count + input_serialized + output_count + output_serialized + locktime
        return raw_tx_bytes

if __name__ == "__main__":
    serializer = BitcoinCoinbaseSerializer()

    # Define simulation parameters for Block 840,000 (Halving Block)
    height = 840000
    subsidy_sats = 312500000 # 3.125 BTC in Satoshis
    fees_sats = 24500000     # 0.245 BTC total fees collected in block
    miner_tag = "/Mined by TeachMeBitcoin/ExtraNonce:4821/"

    # Let's pay to a standard P2WPKH (SegWit native address) scriptPubKey
    # Form: OP_0 (0x00) + Push 20 bytes (0x14) + 20-byte hash
    mock_recipient_hash = bytes.fromhex("3f9a721dfbb38640be2ea2f7e02e1c322b64d46")
    payout_script = b"\x00\x14" + mock_recipient_hash

    print("=== Bitcoin Coinbase Serializer ===")
    print(f"[*] Block Height:       {height}")
    print(f"[*] Block Subsidy:      {subsidy_sats} Sats ({subsidy_sats / 1e8:.3f} BTC)")
    print(f"[*] Fees Collected:     {fees_sats} Sats ({fees_sats / 1e8:.3f} BTC)")
    print(f"[*] Custom Miner Tag:   '{miner_tag}'")
    print(f"[*] Recipient Script:   {payout_script.hex()}\n")

    try:
        # Serialize Coinbase Transaction
        serialized_tx = serializer.serialize_coinbase_tx(
            block_height=height,
            block_subsidy=subsidy_sats,
            fees_collected=fees_sats,
            miner_signature_text=miner_tag,
            payout_script_pub_key=payout_script
        )

        print("[✔] Raw Coinbase Transaction (Hex):")
        print(serialized_tx.hex())
        print(f"\n[*] Total Transaction Size: {len(serialized_tx)} Bytes")

        # Calculate TXID (double-SHA256 of raw bytes, displayed in little-endian representation)
        txid = serializer.double_sha256(serialized_tx)[::-1].hex()
        print(f"[✔] Calculated Transaction ID (TXID):\n    {txid}")

    except Exception as e:
        print(f"[!] Serialization failure: {e}")

As shown, the serializer properly packs versioning, constructs the null input with all zeroes, handles the custom BIP 34 length calculations, embeds custom miner text inside the signature array, pays the exact sum of block rewards, and calculates the transaction identifier.

☕ Help support TeachMeBitcoin

TeachMeBitcoin is an ad-free, open-source educational repository curated by a passionate team of Bitcoin researchers and educators for public benefit. If you found our articles helpful, please consider supporting our hosting and ongoing content updates with a clean donation:

Ethereum: 0x578417C51783663D8A6A811B3544E1f779D39A85
Bitcoin: bc1q77k9e95rn669kpzyjr8ke9w95zhk7pa5s63qzz
Solana: 4ycT2ayqeMucixj3wS8Ay8Tq9NRDYRPKYbj3UGESyQ4J
Address copied to clipboard!