This document specifies the Ferrous Network consensus rules: block and transaction formats, proof-of-work, difficulty adjustment, UTXO and script validation, chain selection, and network parameters.
Blocks consist of:
- 88-byte block header.
- Compact-size VarInt for transaction count.
- Serialized transactions (with witnesses).
The block header is defined in block.rs:
version: u32prev_block_hash: Hash256(32 bytes)merkle_root: Hash256(32 bytes, over legacy txids)timestamp: u64(seconds since UNIX epoch)n_bits: u32(compact difficulty target)nonce: u64
The total encoded header size is fixed at 88 bytes (HEADER_SIZE).
The block hash is:
hash = sha256d(header_bytes)
where header_bytes is the little-endian encoding of the fields above.
Transactions are defined in transaction.rs:
version: u32inputs: Vec<TxInput>outputs: Vec<TxOutput>witnesses: Vec<Witness>locktime: u32
TxInput:
prev_txid: Hash256(32-byte little-endian transaction id)prev_index: u32(output index)script_sig: Vec<u8>(script for legacy spends / height commitment)sequence: u32
Coinbase inputs are recognized by:
prev_txid == [0u8; 32]prev_index == 0xFFFF_FFFF
TxOutput:
value: u64(satoshis)script_pubkey: Vec<u8>(locking script, e.g. P2PKH or P2WPKH)
The total supply is bounded by MAX_MONEY:
MAX_MONEY = 21_000_000 * 100_000_000
Witness:
stack_items: Vec<Vec<u8>>
Witness serialization:
- VarInt count of stack items.
- For each item, a VarInt length plus raw bytes.
Witnesses are stored in Transaction::witnesses with the same length as inputs.
The transaction implements custom binary encoding via Encode/Decode.
encode_without_witness()encodes version, inputs, outputs, and locktime.encode_with_witness()additionally appends the witness vector.
Identifiers:
txid = sha256d(encode_without_witness())wtxid = sha256d(encode_with_witness())
The compact target (n_bits) is decoded by BlockHeader::target():
exponent = n_bits >> 24(high byte)mantissa = n_bits & 0x00FF_FFFFtarget = mantissa * 256^(exponent - 3)(subject to bounds)
Constraints:
mantissa < 0x0080_0000(non-negative encoding)exponent <= 32(fits into 256 bits)
The full 256-bit target is represented as U256([u8; 32]) in little-endian order.
The block hash is computed as:
hash = sha256d(header_bytes)
Proof-of-work validation (check_proof_of_work):
- Convert
hashtoU256(little-endian). - Compute
targetfromn_bits. - Accept if
hash_value <= target.
Difficulty retargeting is implemented in difficulty.rs and depends on ChainParams.
Inputs:
prev_header: previous block header.current_timestamp: proposed timestamp for the new block.params.target_block_time: target spacing in seconds.params.max_target: maximum allowed difficulty target.params.difficulty_adjustment: enable/disable adjustment.
Steps:
- Decode the previous target from
prev_header.n_bits. - If
params.difficulty_adjustmentis false, reuse the previous target. - Compute
actual_timespan = current_timestamp - prev_header.timestamp(saturating). - Clamp timespan:
min_timespan = target_block_time / 4max_timespan = target_block_time * 4actual_timespan = clamp(actual_timespan, min_timespan, max_timespan)
- Scale target linearly:
new_target = prev_target * actual_timespan / target_block_time
- Ensure bounds:
- If
new_targetis zero, set to minimum (1). - If
new_target > max_target, set tomax_target.
- If
- Convert back to compact form (
u256_to_compact) and then toU256.
Effectively, Ferrous uses a per-block proportional adjustment with a factor bounded to [1/4, 4] of the target spacing.
validate_difficulty recomputes the expected target for each block and rejects blocks whose decoded target does not match.
validate_block in validation.rs enforces:
- At least one transaction.
- First transaction must be coinbase; no other coinbase transactions.
- Merkle root of
txids matchesheader.merkle_root. - Proof-of-work is valid.
- Block weight does not exceed
MAX_BLOCK_WEIGHT = 4_000_000. - Each transaction passes
check_structure. - No duplicate
txids within the block.
The block subsidy is defined by calculate_subsidy:
INITIAL_SUBSIDY = 50 * 100_000_000HALVING_INTERVAL = 840_000blockssubsidy(height) = INITIAL_SUBSIDY >> (height / HALVING_INTERVAL)
validate_coinbase_reward:
- Computes
max_reward = subsidy + block_fees. - Sums all coinbase outputs.
- Ensures the coinbase value is:
- ≤
MAX_MONEY. - ≤
max_reward.
- ≤
COINBASE_MATURITY = 100 confirms:
- Coinbase outputs must not be spent until at least 100 blocks after inclusion.
- Enforcement is implemented inside the UTXO and spending logic.
The UtxoSet tracks unspent outputs for the active chain and:
- Fails if a transaction attempts to spend a non-existent or already spent outpoint.
- Applies and rolls back UTXOs when reorganizations occur.
validate_timestamp enforces:
- For non-genesis blocks, the block timestamp must be strictly greater than the MedianTimePast of the last up to 11 blocks.
- The timestamp must not be more than 2 hours (7200 seconds) into the future compared to the local system time.
MedianTimePast is computed both in validate_timestamp and in ChainState::median_time_past to drive mining.
Scripts are executed by execute_script in engine.rs.
Supported features:
- Push operations: direct pushes (1–75 bytes) and
OP_PUSHDATA1/2/4. - Small integer constants:
OP_0,OP_1NEGATE,OP_1–OP_16. - Stack operations:
OP_DUP,OP_DROP. - Hashing:
OP_HASH160. - Comparisons:
OP_EQUAL,OP_EQUALVERIFY. - Verification:
OP_VERIFY. - Signature checks:
OP_CHECKSIG,OP_CHECKSIGVERIFY. - Termination:
OP_RETURN(immediate failure).
Errors are captured as ScriptError variants (e.g. StackUnderflow, InvalidOpcode, VerifyFailed).
Patterns:
- P2PKH scriptPubKey:
OP_DUP OP_HASH160 <pubkey_hash> OP_EQUALVERIFY OP_CHECKSIG
- P2WPKH scriptPubKey:
0 <20-byte pubkey_hash>(witness program)
The engine uses ScriptContext to compute the correct sighash and verify signatures based on:
- The full transaction.
- The input index.
- The set of spent outputs.
Signatures are currently verified using secp256k1 ECDSA as a placeholder, with a 64-byte signature format similar to Schnorr.
validate_witness_commitment enforces a BIP141-style witness commitment when any transaction has witness data:
- The coinbase must contain an
OP_RETURNoutput with:0x6a(OP_RETURN)0x24(36-byte push)- Magic bytes
0xaa 0x21 0xa9 0xed - 32-byte witness commitment.
- The commitment is:
sha256d(witness_merkle_root || reserved_value)witness_merkle_rootis computed fromwtxids of all non-coinbase transactions.reserved_valueis taken from the first element of the coinbase witness stack if present, or zero.
If witness data exists but no valid commitment is found, the block is rejected.
ChainState maintains:
- A map of block hash →
BlockData(header, transactions, height, cumulative work). - The current tip hash.
Chain selection:
- Each block’s cumulative work is computed from the difficulty target.
- The best chain is the one with the greatest cumulative work.
Reorganization:
- When a competing chain with greater work is found,
ChainState:- Identifies the fork point.
- Rolls back UTXOs along the old branch.
- Applies UTXOs along the new branch.
This ensures deterministic selection of the heaviest chain.
Network parameters are defined in params.rs via ChainParams:
target_block_time: u64max_target: U256difficulty_adjustment: boolallow_min_difficulty_blocks: bool(reserved for future behavior)
Networks:
- Mainnet
target_block_time = 150max_target = MAINNET_MAX_TARGETdifficulty_adjustment = trueallow_min_difficulty_blocks = false
- Testnet
target_block_time = 150max_target = TESTNET_MAX_TARGETdifficulty_adjustment = trueallow_min_difficulty_blocks = true(not yet used in consensus rules)
- Regtest
target_block_time = 1max_target = REGTEST_MAX_TARGETdifficulty_adjustment = false(fixed difficulty)allow_min_difficulty_blocks = true
All networks currently share the same genesis block parameters; differences arise only from difficulty behavior and target spacing.