# Copyright (c) The btclib developers
# Distributed under the MIT software license, see the accompanying
# LICENSE file or https://opensource.org/license/mit for the full text.
"""The networks this node can join, and the genesis block of each.
`Chain` and its four leaves -- `Main`, `TestNet`, `SigNet`, `RegTest` --
carry a network's magic, its seed addresses, its script-flag activation
heights and its own genesis block, built once by `create_genesis` below
from the constants each leaf supplies. `config.py`'s `_resolve_chain` is
what turns a chain's name, read from the command line or a functional
test, into one of these.
"""
from dataclasses import dataclass
from datetime import UTC, datetime
from btclib.block import Block, BlockHeader, merkle_root_and_mutated_from_transactions
from btclib.p2p.magic import magic_from_network
from btclib.script import script
from btclib.tx.out_point import OutPoint
from btclib.tx.tx import Tx
from btclib.tx.tx_in import TxIn
from btclib.tx.tx_out import TxOut
__all__ = ["Chain", "Main", "RegTest", "SigNet", "TestNet"]
def create_genesis(
time: int, nonce: int, difficulty: int, version: int, reward: int
) -> Block:
"""Build a network's genesis block from its own header fields and reward.
The same coinbase text and public key on every network -- Bitcoin's
own genesis message and Satoshi's pubkey -- since what makes one
network's genesis differ from another's is the header alone: its
time, nonce, starting difficulty and version, plus how much the one
coinbase output pays.
"""
script_sig = script.serialize(
[
"FFFF001D",
b"\x04",
b"The Times 03/Jan/2009 Chancellor on brink of second bailout for banks",
]
)
script_pub_key = script.serialize(
[
"04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f",
"OP_CHECKSIG",
]
)
tx_in = TxIn(
prev_out=OutPoint(),
script_sig=script_sig,
sequence=0xFFFFFFFF,
)
tx_out = TxOut(
value=reward,
script_pub_key=script_pub_key,
)
tx = Tx(
version=1,
lock_time=0,
vin=[tx_in],
vout=[tx_out],
)
header = BlockHeader(
version=version,
previous_block_hash="00" * 32,
merkle_root="00" * 32,
time=datetime.fromtimestamp(time, UTC),
bits=difficulty.to_bytes(4, "big"),
nonce=nonce,
check_validity=False,
)
# btclib's own, so that the root this builds and the root
# Block.assert_valid compares against are one implementation
header.merkle_root = merkle_root_and_mutated_from_transactions([tx])[0]
header.assert_valid()
# the block and not the header alone: the coinbase above is the only
# copy of the genesis block anywhere in this node -- no peer serves
# it and no `getdata` asks for it -- and the block filter of height
# zero is built from its output script like every other block's
return Block(header, [tx], check_validity=False)
[docs]
@dataclass
class Chain:
"""A network this node can join: its magic, its seeds and its genesis.
`Main`, `TestNet`, `SigNet` and `RegTest` below are its four leaves,
each hardcoding one network's own constants in its `__init__` rather
than taking them as arguments, since there is exactly one of each
and nothing else ever builds one.
"""
name: str
port: int
# Core's own rpc port for this chain, `CreateBaseChainParams`
# (`src/chainparamsbase.cpp`, at bitcoin/bitcoin@05e49b342f): one
# below `port` on every leaf below, not `port + 1` -- the client on
# the other end is `bitcoin-cli` or another program written against
# Core, so CLAUDE.md's own `Following Bitcoin Core` is what decides
# this rather than any pattern internal to this file.
# `config.py`'s `Config.__init__` used to compute `port + 1`
# instead (btclib-org/btclib-node#605), which happens to be Core's
# own *Tor* incoming-connection port for each of these four chains
# (the comment directly above `CreateBaseChainParams` names them:
# 8334/18334/38334/18445), a port this node does not listen on at
# all.
rpc_port: int
addresses: list[str]
genesis_block: Block
# (height, name) pairs, each read by interpreter.get_flags as the
# activation height of a script flag; every leaf below sets this in
# its own __init__, which is why it carries no default here.
flags: list[tuple[int, str]]
# Core's fPowAllowMinDifficultyBlocks: a block more than two target
# spacings after its parent may be mined at the network's easiest
# target, so that a chain nobody is hashing still moves.
pow_allow_min_difficulty_blocks: bool
# Core's fPowNoRetargeting: the target never moves off the one the
# genesis carries.
pow_no_retargeting: bool
# Core's nSubsidyHalvingInterval: how many blocks the coinbase reward
# stays at one level before halving again. subsidy() below is what
# reads it.
subsidy_halving_interval: int
# Core's BIP34Height: the first height a coinbase must commit to its
# own height (BIP34, bad-cb-height). Read by main.update_chain,
# through BlockContext.bip34_height.
bip34_height: int
# Core's `IsBIP30Repeat` (`src/validation.cpp:6218-6222`,
# at bitcoin/bitcoin@204256c73f): the (height, block hash) of every
# block this chain's own history already carries a BIP30 violation
# in, exempted from `chainstate.utxo_index.UtxoIndex.add_block`'s own
# BIP30 check rather than failing it. Empty on every leaf but `Main`,
# since the two blocks that violate it are two specific mainnet
# blocks mined in 2010, not a property of the rule.
bip30_exceptions: list[tuple[int, bytes]]
# Core's `nMinimumChainWork` (`src/kernel/chainparams.cpp`,
# at bitcoin/bitcoin@ca7162cde5), read as `int(hex_string, 16)`: the
# chain work below which `main.update_ibd_status` will not leave
# `IsInitialBlockDownload`, whatever the tip's own age. Each leaf's
# own `__init__` below cites the line its value comes from.
minimum_chain_work: int
# Core's `nPruneAfterHeight` (`src/kernel/chainparams.cpp`, at
# bitcoin/bitcoin@ca7162cde5): `rpc.callbacks.prune_blockchain`'s own
# "Blockchain is too short for pruning." floor, below which Core
# refuses `pruneblockchain` outright rather than merely clamping it.
# Each leaf's own `__init__` below cites the line its value comes
# from.
prune_after_height: int
@property
def genesis(self) -> BlockHeader:
"""Return the genesis header, which is what most callers want."""
return self.genesis_block.header
[docs]
def subsidy(self, height: int) -> int:
"""Return the block reward at `height`: fifty bitcoin, halved by height.
Core's `GetBlockSubsidy` (`src/validation.cpp:1844`,
at bitcoin/bitcoin@204256c73f): fifty bitcoin, right-shifted once
per `subsidy_halving_interval` blocks, forced to zero once that
shift is undefined for a native int.
"""
halvings = height // self.subsidy_halving_interval
if halvings >= 64: # noqa: PLR2004
return 0
return (50 * 10**8) >> halvings
@property
def magic(self) -> bytes:
"""The network's four-byte magic, the octets a message starts with."""
# bytes, and the four octets that go on the wire: a hex string
# here is what made the resynchronisation in
# p2p.connection.Connection.parse_messages hunt for ASCII inside
# a binary buffer.
#
# For signet this is the default signet's, which is what
# magic_from_network answers; a custom signet derives its own
# from its challenge, via magic_from_signet_challenge.
return magic_from_network(self.name)
@property
def pow_limit_bits(self) -> bytes:
"""The network's easiest target, as its genesis block's own bits."""
# A genesis block is mined at its network's easiest target, so
# the limit is already stated once, in create_genesis' argument.
# btclib's validation defaults to mainnet's, which would reject
# every regtest and signet block, so it has to be passed in.
return self.genesis.bits
[docs]
@dataclass
class Main(Chain):
"""Mainnet: the chain real bitcoin moves on."""
# the class docstring above already says which network this is; the
# fields below are literal constants, not a decision this __init__
# makes that a docstring would need to explain
def __init__(self) -> None: # noqa: D107
self.name = "mainnet"
self.port = 8333
self.rpc_port = 8332
self.addresses = [
"seed.bitcoin.sipa.be",
"dnsseed.bluematt.me",
"dnsseed.bitcoin.dashjr.org",
"seed.bitcoinstats.com",
"seed.bitcoin.jonasschnelli.ch",
"seed.btc.petertodd.org",
"seed.bitcoin.sprovoost.nl",
"dnsseed.emzy.de",
"seed.bitcoin.wiz.biz",
]
self.genesis_block = create_genesis(
1231006505, 2083236893, 0x1D00FFFF, 1, 50 * 10**8
)
self.flags = [
(170061, "P2SH"),
(363725, "DERSIG"),
(388381, "CHECKLOCKTIMEVERIFY"),
(419328, "CHECKSEQUENCEVERIFY"),
(481824, "WITNESS"),
(481824, "NULLDUMMY"),
(709632, "TAPROOT"),
]
self.pow_allow_min_difficulty_blocks = False
self.pow_no_retargeting = False
self.subsidy_halving_interval = 210000
self.bip34_height = 227931
self.bip30_exceptions = [
(
91842,
bytes.fromhex(
"00000000000a4d0a398161ffc163c503763b1f4360639393e0e4c8e300e0caec"
),
),
(
91880,
bytes.fromhex(
"00000000000743f190a18c5577a3c2d2a1f610ae9601ac046a38084ccb7cd721"
),
),
]
# src/kernel/chainparams.cpp:141, at bitcoin/bitcoin@ca7162cde5
self.minimum_chain_work = int(
"0000000000000000000000000000000000000001128750f82f4c366153a3a030", 16
)
# src/kernel/chainparams.cpp:154, at bitcoin/bitcoin@ca7162cde5
self.prune_after_height = 100000
[docs]
@dataclass
class TestNet(Chain):
"""Testnet3: the long-running public test chain."""
# the class docstring above already says which network this is; the
# fields below are literal constants, not a decision this __init__
# makes that a docstring would need to explain
def __init__(self) -> None: # noqa: D107
self.name = "testnet"
self.port = 18333
self.rpc_port = 18332
self.addresses = [
"testnet-seed.bitcoin.jonasschnelli.ch",
"seed.tbtc.petertodd.org",
"seed.testnet.bitcoin.sprovoost.nl",
"testnet-seed.bluematt.me",
]
self.genesis_block = create_genesis(
1296688602, 414098458, 0x1D00FFFF, 1, 50 * 10**8
)
self.flags = [
(395, "P2SH"),
(330776, "DERSIG"),
(581885, "CHECKLOCKTIMEVERIFY"),
(770112, "CHECKSEQUENCEVERIFY"),
(834624, "WITNESS"),
(834624, "NULLDUMMY"),
(1628640000, "TAPROOT"), # wrong, this is the date
]
self.pow_allow_min_difficulty_blocks = True
self.pow_no_retargeting = False
self.subsidy_halving_interval = 210000
self.bip34_height = 21111
self.bip30_exceptions = []
# src/kernel/chainparams.cpp:265, at bitcoin/bitcoin@ca7162cde5
self.minimum_chain_work = int(
"0000000000000000000000000000000000000000000017dde1c649f3708d14b6", 16
)
# src/kernel/chainparams.cpp:273, at bitcoin/bitcoin@ca7162cde5
self.prune_after_height = 1000
[docs]
@dataclass
class SigNet(Chain):
"""The default public signet, not a custom one built from its own challenge.
Every flag activates at height 0, since signet is a fresh chain each
time it is reset rather than one carrying mainnet's own history.
"""
# the class docstring above already says which network this is; the
# fields below are literal constants, not a decision this __init__
# makes that a docstring would need to explain
def __init__(self) -> None: # noqa: D107
self.name = "signet"
self.port = 38333
self.rpc_port = 38332
self.addresses = ["178.128.221.177"]
self.genesis_block = create_genesis(
1598918400, 52613770, 0x1E0377AE, 1, 50 * 10**8
)
self.flags = [
(0, "P2SH"),
(0, "DERSIG"),
(0, "CHECKLOCKTIMEVERIFY"),
(0, "CHECKSEQUENCEVERIFY"),
(0, "WITNESS"),
(0, "NULLDUMMY"),
(0, "TAPROOT"),
]
self.pow_allow_min_difficulty_blocks = False
self.pow_no_retargeting = False
self.subsidy_halving_interval = 210000
self.bip34_height = 1
self.bip30_exceptions = []
# the default public signet's own value, not the empty one a
# custom signet's challenge carries: src/kernel/chainparams.cpp
# :457, at bitcoin/bitcoin@ca7162cde5, this class's own docstring
# above already arguing which of the two this leaf is
self.minimum_chain_work = int(
"00000000000000000000000000000000000000000000000000000b463ea0a4b8", 16
)
# src/kernel/chainparams.cpp:518, at bitcoin/bitcoin@ca7162cde5
self.prune_after_height = 1000
[docs]
@dataclass
class RegTest(Chain):
"""A local, disposable chain: no seeds, an easy target, no retargeting.
Every flag activates at height 0 and the difficulty never moves off
the genesis's own, so a functional test can mine past any of them
in as many blocks as it needs, on demand.
"""
# the class docstring above already says which network this is; the
# fields below are literal constants, not a decision this __init__
# makes that a docstring would need to explain
def __init__(self) -> None: # noqa: D107
self.name = "regtest"
self.port = 18444
self.rpc_port = 18443
self.addresses = []
self.genesis_block = create_genesis(1296688602, 2, 0x207FFFFF, 1, 50 * 10**8)
self.flags = [
(0, "P2SH"),
(0, "DERSIG"),
(0, "CHECKLOCKTIMEVERIFY"),
(0, "CHECKSEQUENCEVERIFY"),
(0, "WITNESS"),
(0, "NULLDUMMY"),
(0, "TAPROOT"),
]
self.pow_allow_min_difficulty_blocks = True
self.pow_no_retargeting = True
self.subsidy_halving_interval = 150
self.bip34_height = 1
self.bip30_exceptions = []
# src/kernel/chainparams.cpp:593, at bitcoin/bitcoin@ca7162cde5:
# an empty `uint256{}`, zero -- a regtest node counts as caught
# up on work alone, the way every regtest activation height
# already being 0 lets this leaf skip every other consensus
# ramp-up too
self.minimum_chain_work = 0
# src/kernel/chainparams.cpp:601, at bitcoin/bitcoin@ca7162cde5:
# `opts.fastprune ? 100 : 1000` -- this tree never passes
# `-fastprune`, Core's own knob for a lower regtest value in its
# test suite, so 1000 is the one value that applies here
self.prune_after_height = 1000