Custom Python Coinbase Serializer
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.
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: