Bitcoin-Lite Node
Capstone: Bitcoin-Lite Node
Build a minimal Bitcoin-like node with:
- UTXO validation
- P2PKH-lite script checking (HASH256)
- Mempool selection by fee rate
- Proof-of-work mining (fixed difficulty)
- Chain selection and reorgs (longest chain)
This is a toy node but must preserve core invariants.
Types
type Outpoint struct {
TxID [32]byte
Index uint32
}
type TxIn struct {
Prev Outpoint
ScriptSig []byte
}
type TxOut struct {
Value uint64
ScriptPubKey []byte
}
type Transaction struct {
Version int32
Inputs []TxIn
Outputs []TxOut
LockTime uint32
}
type BlockHeader struct {
Version uint32
PrevHash [32]byte
MerkleRoot [32]byte
Timestamp uint32
Bits uint32
Nonce uint32
}
type Block struct {
Header BlockHeader
Txs []Transaction
}
type UTXOSet map[Outpoint]TxOut
type UndoEntry struct {
Outpoint Outpoint
Output TxOut
}
type Node struct {
Tip [32]byte
Blocks map[[32]byte]Block
Height map[[32]byte]int
UTXO UTXOSet
Undo map[[32]byte][]UndoEntry
Mempool map[[32]byte]Transaction
Bits uint32
Subsidy uint64
MaxBlockSz int
MinerScript []byte
}
func NewNode(genesis Block, bits uint32, subsidy uint64, maxBlockSize int, minerScript []byte) *Node
func (n *Node) SubmitTx(tx Transaction) bool
func (n *Node) MineBlock(timestamp uint32, maxNonce uint32) (Block, bool)
func (n *Node) ReceiveBlock(block Block) bool
Initialization and model boundaries
NewNode receives a valid, trusted genesis with zero PrevHash; it initializes
height zero and its outputs. The constructor does not revalidate genesis against
the configured difficulty or subsidy. All received new blocks must have a known,
nonzero parent. Configuration uses a positive byte budget, a fixed valid Bits
value, and nonnegative chain heights representable by the API. Callers do not
mutate accepted transactions/blocks, configuration, or the node's exposed maps.
This is a single-threaded in-memory model; there is no network or disk API.
Canonical bytes and scripts
- Serialize the earlier legacy transaction layout, using
fffffffffor every input's Sequence (the capstone type omits that field). Values are uint64 LE; counts/script lengths use canonical CompactSize; txid is double-SHA256. - Merkle leaves are txids, without an extra leaf hash; duplicate the final node at odd levels. Header serialization is the earlier 80-byte layout.
- Use the earlier unsigned 24-bit compact mantissa and raw big-endian hash integer for PoW. These are course conventions, not full Bitcoin consensus semantics.
- Support direct byte pushes 1..75, DUP (76 hex), HASH256 (aa), EQUALVERIFY (88),
and CHECKSIG (ac), rejecting malformed scripts, unsupported opcodes, and stack
underflow. Evaluate ScriptSig then the referenced ScriptPubKey on one stack;
its final top must be nonzero. CHECKSIG uses ASN.1 ECDSA with P-256, uncompressed
65-byte public keys, and this exact digest:
doubleSHA256(serializeTxWithAllScriptSigsEmpty || uint32LE(inputIndex) || referencedScriptPubKey). Output scripts remain intact. This replaces the standalone script task's supplied-ID signing rule; a txid including the signature would be circular.
Transaction admission
SubmitTx accepts only ordinary transactions with at least one input. Every
input must exist in the current confirmed UTXO map, appear once, and pass script
validation. Checked uint64 input/output sums must satisfy inputs >= outputs.
Reject duplicate mempool txids and conflicts with already admitted inputs.
Admission changes only Mempool. Unconfirmed children are not admitted in this
version. The fee is the checked input sum minus the checked output sum.
Mining
Sort eligible mempool candidates once by exact fee/serialized-size descending, then fee descending, then raw txid bytes lexicographically ascending. Avoid rounded division and overflowing cross-products. Scan once against an evolving private UTXO view; skip invalid or oversized candidates without side effects.
The budget is the sum of serialized transaction bytes, including the actual coinbase, excluding the block header and outer transaction count. Exact fits are allowed. Coinbase has Version=1, zero inputs, one output to MinerScript with value subsidy+selected fees, and LockTime equal to the new height. This height convention makes consecutive rewards distinct; LockTime has no scheduling semantics here. If height cannot fit uint32, or coinbase cannot fit the budget, mining fails. Skip candidates that would overflow cumulative fees or reward.
Mine nonces 0..maxNonce inclusively using configured Bits. On exhausted search
return (Block{}, false) without changing node state. Pass a found candidate
through the same acceptance path as a received block.
Block validation and branch selection
For a new block:
- Require fixed Bits, an indexed nonzero parent, matching Merkle root, valid PoW, a nonempty transaction list, and a transaction-byte sum within MaxBlockSz.
- Build a private UTXO view of its parent branch. Require coinbase first (zero inputs, at least one output), no duplicate txids in the block, and no output that overwrites a live outpoint in the parent view.
- Apply ordinary transactions in order with the same script/value checks. Outputs from earlier ordinary transactions are available. This block's coinbase is not available to its own transactions.
- Sum fees and coinbase outputs with checked uint64 arithmetic. Require total coinbase value <= subsidy+fees, also rejecting overflow. Then add coinbase outputs. There is no later coinbase-maturity delay in this model.
Failed acceptance leaves all node state unchanged. A known header returns true only when the supplied transaction body matches the stored body; a changed body under the same header returns false.
Store a valid side branch even when it does not win. Fixed Bits means longest chain determines preference; switch only for a strictly greater height, keeping the existing tip on ties. Publish Tip and UTXO for the same selected branch. Populate Undo for its blocks with spent-outpoint records. Replaying the selected path from genesis is an acceptable baseline; incremental undo/reapply is an optional optimization. Genesis is the trusted exception to reward checks.
After a tip change, remove confirmed transactions and mempool transactions that are invalid under the new confirmed view. Re-admission of transactions from a disconnected branch is outside this exercise. On one branch, ordinary input spends and coinbase rewards must never silently overwrite live value.