An xOneRS producer builds an XDPoS v2 block in seven steps: wait for the slot, prepare the header, take the candidates from the pool, select and order them, execute, write the safety record, and sign. Each step follows a named geth-XDC function, and a test rebuilds 200 V1 and 200 V2 Apothem blocks from their own transactions to the canonical block hash. This post walks through the steps from the producer specs (spec/producer).
For the producer as an operator sees it, see the net2040 validator guide and signer setup.
When does the producer start a block?
When it is the leader of the round and the slot time has come (timing.md, seal-v2.md):
- The parent must be the block of the highest QC. Otherwise geth-XDC answers
ErrNotReadyToPropose, and xOne does not build. - V2 builds once
now - parent.time >= minePeriod(whole seconds), whereminePeriodcomes from the config row of the round. - The block time is
max(parent + minePeriod(round), now).
What goes into the header?
extraData: version byte 2, then RLP of{round, highestQC}.- Coinbase is the signer, difficulty 1, nonce and mix digest zero.
- At an epoch switch,
ValidatorsandPenaltiescarry the new masternode list and the penalties. - The gas limit follows geth-XDC's rule. See the gas limit rule.
- The base fee is the fixed tier of the block's own number: none before
Eip1559Block, 12.5 gwei from it, 625 gwei fromGas2500xBlock(gas-limit.md).
Which transactions does the producer take from the pool?
The executable set of the pool (Pool::pending). Since PR #1022 (2026-11-15), the build view copies the encodings of these candidates only, not every queued transaction. A held encoding cannot change during the build: a replacement goes in under a new hash.
In which order are they placed?
The selection follows geth-XDC's fillTransactions and commitTransactions (construction.md):
- Special transactions first. The unbroken run of transactions to
0x89or0x90at the front of each account's nonce queue goes first. They have gas price 0, so they skip the base-fee check. - Then by effective tip.
min(feeCap - baseFee, tipCap)at the block's base fee, then by the time first seen, then by sender. Each account keeps its nonce order. A transaction with a fee cap below the base fee drops its account. - Gas pool. The pool is the block's gas limit. A transaction that needs more than is left skips its account, not the block. The loop ends when less than 21,000 gas is left.
- Sign transaction filters. A sign transaction with calldata shorter than 68 bytes, or one that signs a block at or above the block being built, is skipped. At most
2 * maxMasternodesV2(256 when unset) go in one block.
The order is not consensus: another valid producer can order differently. Given the order, the bytes of the block are fixed.
What is the sign transaction at 0x89?
Each masternode sends a sign transaction for each block number divisible by 15: to 0x89, value 0, gas price 0, gas 200,000, data 0xe341eaa4 + the 32-byte block number + the block hash, as an EIP-155 legacy transaction signed by the validator key.
- The nonce is
max(pool nonce, head-state nonce). - A block more than 30 behind the head is not signed: the contract refuses a block older than two epochs, and a refused transaction at the lowest nonce jams the sender.
- A penalised node in the candidate pool still signs. This is how a penalised masternode comes back. See penalties and comeback.
- Since PR #1006 (2026-11-15), xOne broadcasts
0x89sign transactions to every peer.
How does execution fill the block?
The first selection counts each transaction at its declared gas. Since PR #963 (2026-11-14), the producer then selects again and charges each executed transaction the gas its receipt shows. This fills the room that declared limits left, and the larger body is executed. A refinement that fails execution or adds nothing is dropped, and the last executed body stands. So a block never goes over the gas limit and never carries a transaction that execution refused.
Execution then fills in the state root, transactions root, receipts root, bloom, gas used and the empty uncle hash (Finalize).
When is the safety record written?
Before the signature exists. The seal marks the round as proposed in the signer's safety record (the Propose slot), writes that to disk, and only then lets the signature leave the signing unit. If the node stops between the write and the send, it does not propose that round again after restart. See the safety record.
After the seal, the block is imported with the full header check and execution, sent to peers as NewBlock, and given to the BFT machine as a proposal. The machine votes through the same unit.
How do geth-XDC and xOne compare, step by step?
| Step | geth-XDC | xOneRS |
|---|---|---|
| Header | engine_v2.Prepare |
xone_builder::producer::seal_v2::prepare |
| Selection | miner/worker.go fillTransactions, commitTransactions |
producer::build::select |
| Sign transaction | contracts.CreateTransactionSign |
producer::signtx |
| Execution | FinalizeAndAssemble, Finalize |
xone_executor::execute_unsealed |
| Seal | engine_v2.Seal, then markSelfMined |
seal_v2::seal, with the Propose slot written before the signature |
How is it tested?
tests/producer_construction.rs: 200 V1 and 200 V2 Apothem blocks are rebuilt from their own transactions, timestamp, round and QC. Each equals the canonical header hash.tests/producer_vectors.rs: the real sign transactions of the archive (nonce, gas, price, data) equal the ones xOne builds.- Mutants that the tests must catch: special transactions not first, a price above zero, gas 100,000, no 15-block cadence, a wrong round in
extraData, a round proposed twice, signing before persisting.
The specs list what is not covered yet. Examples: the V1 checkpoint validator list for more than one masternode, and the timeout path across epoch boundaries.
Frequently asked questions
Does xOne order transactions the same way as geth-XDC?
It uses the same rule: special transactions first, then effective tip, time first seen and sender. The order is not consensus, so blocks from two producers can differ in order and both be valid.
Why do sign transactions have gas price 0?
geth-XDC builds them so (CreateTransactionSign). They go first in the block and skip the base-fee check.
Can a block go over its gas limit after the second packing pass?
No. A pass that fails execution or adds nothing is dropped, and the last executed body stands.
What stops a producer from proposing the same round twice after a crash?
The Propose slot of the safety record is written to disk before the signature is released.
Sources
This article describes the source and evidence at its review date. Check the linked repository for current implementation and qualification status.
Explore the engineering evidenceEdit this page on GitHub opens the Markdown source (content/blog/xone-block-building/index.md). GitHub may ask you to sign in. Report a correction opens a prefilled GitHub issue for this page.