Block packages
How the client assembles the blocks it mines, and what each transaction source may contribute.
Everything under stratum.candidate controls the blocks this client builds: which collateral box it
draws, how long it waits for the genesis transaction, and what other transactions it inserts. This page
covers that block and the five transaction sources beneath it.
Inserting transactions is on by default, from every source except storage rent. The settings that control it are in Transaction sources. What the client does with them, and where the revenue they earn goes, is explained in Candidate building.
Collateral selection#
The following settings decide which collateral box your block spends:
| Setting | Default | Range | What it does |
|---|---|---|---|
stratum.candidate.collateralPoolSize | 100 | 1 to 100 | Collateral boxes kept pre-loaded in memory |
stratum.candidate.collateralRefreshInterval | 60000 | 100 to 3600000 ms | How often the collateral set is refreshed. It also refreshes on every new block |
stratum.candidate.collateralStrategy | highestFee | highestFee or random | How a box is drawn from the set |
stratum.candidate.clearanceAge | 7200 | 100 to 14400 blocks | With highestFee, how long a box may stay live before it is drawn ahead of every priority fee |
The active set never holds more than 100 boxes protocol-wide, which is why the pool size stops there. Fetching a box at mining time would put an indexer round trip on the critical path of every block, so the whole set is held in memory instead.
The refresh interval is a floor rather than a schedule. Every new block triggers one anyway, since that is the only moment a collateral box can be spent.
Which box your block spends#
The box is chosen afresh at every block height and kept for the rest of that height. With the default
highestFee strategy, the client draws one box from the set by the following rules:
- If any box has been live for
clearanceAgeblocks or more, the oldest of those is drawn, whatever the others offer. - Otherwise the box offering the finder the largest priority fee is drawn, and the oldest breaks a tie.
The default of 7200 blocks is about 10 days on mainnet. Until a box reaches it, lenders compete on priority fee alone, which is what makes paying one worth anything. The age rule then clears boxes that offer nothing, so no box waits in the active set forever.
A lower value clears idle boxes sooner and weakens priority fees. At the minimum of 100 blocks, about 3 hours 20 minutes on mainnet, a box that bids nothing outranks every bid after waiting only that long. The maximum of 14400 blocks, about 20 days, lets bids decide for longest. The rule is this client's own choice, not a contract rule, so different miners may run different values.
random draws uniformly from every usable box, ignoring priority fees and age. Because it ignores the
bids, it usually earns you a smaller finder's share than highestFee.
Boxes your block never spends#
The client leaves a box out of the draw in the following cases:
| Case | What happens |
|---|---|
| The box pays into a rollup chain this release does not build | Skipped with a warning. The chain is named by the box's R8, stamped from the emission config when the box was activated |
| No box is left after that | Blocks are mined solo, with a warning, until you update the client |
| The node left the box's genesis transaction out of its candidate | Skipped with a warning for up to three blocks, and the next build uses another box. At most three rebuilds per block |
| The box was spent in a recent block | Skipped for up to three blocks, because the indexer can still report it unspent for a few seconds |
A box skipped for the first reason stays skipped, because this release cannot pay into its chain at all. Updating to a release that builds that chain brings it back.
Building the genesis transaction#
| Setting | Default | Range | What it does |
|---|---|---|---|
stratum.candidate.genesisWaitMs | 1500 | 0 to 60000 ms | How long to wait for candidate sources before a normal genesis transaction is sent |
stratum.candidate.waitForBlockPackage | true | true or false | Wait for other sources to contribute before sending the genesis transaction, up to genesisWaitMs |
The genesis transaction is what makes a block a Lithos block. The candidate builder will wait for the genesis and other sources to arrive,
but on a new height build, it will wait at-most genesisWaitMs before it sends a solo-candidate. If waitForBlockPackage is off, the client
will first create the genesis transaction and send it out as a new job, before sending a following job containing package sources.
This can potentially slow down mining, especially if the two jobs are sent in quick succession.
Refreshing mid-block#
| Setting | Default | Range | What it does |
|---|---|---|---|
stratum.candidate.mempoolRefreshMs | 20000 | 0 to 3600000 ms | Milliseconds between mid-block package rebuilds. 0 mines the block's initial transaction set and never rebuilds |
stratum.candidate.minCandidateChangeRevenue | 1000000 | 0 to 9223372036854775807 nanoERG | Extra package revenue required before a mid-block job change is worth making |
stratum.candidate.refreshForProtocolTxs | true | true or false | Also change the job when a rebuilt package adds rollup or emission transactions |
stratum.candidate.minNewProtocolTxs | 1 | 1 to 1000 | Rollup or emission transactions a rebuilt package must add before the job changes |
stratum.candidate.blockTxTimeout | 20000 | 0 to 600000 ms | How long to wait for sources to build. Not validated separately from its range |
A rebuild picks up transactions that arrived since the last one, which can raise what the block earns.
Rebuilding too often can slow down your rig's hashrate, so feel free to adjust mempoolRefreshMs in order
to control how often rebuilds happen.
minCandidateChangeRevenue is what stops a trivial gain from being worth that window. The client
compares a rebuilt package against the published one and keeps the old job unless the difference clears
this threshold. Tokens are not valued, so a package whose only gain is tokens never clears it.
Initial packages and required fallbacks bypass the threshold entirely.
Rollup and emission transactions earn no ERG, so minCandidateChangeRevenue alone never changes the
job for them. With refreshForProtocolTxs on, the client also changes the job when the rebuilt package
carries at least minNewProtocolTxs rollup or emission transactions that the current job does not.
Unconfirmed transactions carried ahead of them do not count. Each change is a new job for your mining
software, and at most one happens per mempoolRefreshMs.
The client keeps the current job, whatever the rebuild adds, when a source that contributed to the current job did not answer the rebuild in time. Switching would drop that source's work from the block.
blockTxTimeout tells you how long the client will wait before sending its best effort candidate to the stratum.
In some cases, transaction sources can take a while, so before blockTxTimeout is hit, the candidate builder will
collect transactions from the sources that did respond in time, and send just those. Later sources will still show up
in refreshes once they respond.
The package budget#
| Setting | Default | Range | What it does |
|---|---|---|---|
stratum.candidate.blockShare | 0.5 | 0.0 to 1.0 | Fraction of the node's block byte and cost limits this client's whole package may claim |
The default is well under 1 on purpose. The node adds transactions this client never sees and picks a remainder you have no visibility into, and an unconfirmed ancestor carried into the package reports no execution cost at all. Claiming the whole block would mean the node dropping part of what you built.
Keeping this below 0.8 is recommended. The genesis transaction is charged against the budget first, so it is never the thing that gets dropped.
Transaction sources#
| Setting | Default | Range | What it does |
|---|---|---|---|
stratum.candidate.blockTransactions | true | true or false. Not validated | The master switch. With it off, no source contributes anything |
Each source is bounded on its own before the package is bounded as a whole, so a busy source cannot crowd out a quieter one that matters more. A source that is off is never asked, so it costs no build and no node read.
Revenue the package earns goes to the pool, not to you alone. Executor fees and collected storage rent are added to the holding box your block's genesis transaction creates, so every miner who submits to that rollup is paid a share of them by score. You submit to it too.
Every source takes the same four keys under stratum.candidate.sources.<name>:
| Key | Range | What it does |
|---|---|---|
enabled | true or false | Whether this source is asked to contribute |
maxTxs | 0 to 100 | Transactions it may place in one block |
maxBytes | 0 to 8388608 | Serialized bytes it may claim, counting unconfirmed ancestors it carries |
maxCost | 0 to 100000000 | Execution cost it may claim |
The five sources and what they ship as:
| Source | Ships | maxTxs | What it inserts |
|---|---|---|---|
rollups | on | 5 | Non-Interactive Share Proof (NISP) submissions, phase transforms, fraud proofs and payouts |
emissions | on | 20 | Clear and Activate on the collateral queue. Activates alone stop at emission.candidateActivates |
lithosdex | on | 20 | LithosDex order executions, their placements and the closing flush |
ergodex | on | 20 | Native ErgoDEX swaps, deposits and redemptions, including ancestors |
rent | off | 1 | Storage rent collection |
maxBytes and maxCost ship at 262144 and 1000000 for all five.
Rollup work#
The rollups source takes one extra key:
| Setting | Default | Range | What it does |
|---|---|---|---|
stratum.candidate.sources.rollups.maxAncestorTxs | 4 | 0 to 64 | Unconfirmed transactions one rollup transaction may carry into your block ahead of itself |
A rollup transaction often spends a box created by a transaction still in the mempool, usually another
miner's NISP submission to the same rollup. Your block has to carry that parent too, and every parent
counts towards maxTxs. Work that needs more parents than maxAncestorTxs stays out of your block
until they confirm, and still reaches the mempool as usual. 0 offers only work that spends a confirmed
rollup box.
Rollup work your block carries is not also sent with a fee while that block is mined. Unless a block lands the work first, the client sends every rollup transaction to the mempool with a fee. The following rules decide when:
- If your block carries the transaction, the fee-paying copy waits while that block is mined.
- If that block passes without the work landing, the fee-paying copy goes out at the client's next chain check. Checks run once a minute, so this is about one block later.
- A rollup transaction that appears mid-block waits up to
mempoolRefreshMsfor the next rebuild, so your block gets it first. - NISP submissions and fraud proofs with 10 blocks or fewer left in their window are always sent at once. That is about 20 minutes at Ergo's 2-minute block time.
A transaction that appears mid-block waits only if a rebuild can still bring it into the job. That needs
mempoolRefreshMs above 0, and either refreshForProtocolTxs on or minCandidateChangeRevenue at
0. While your client is not building candidates, nothing waits.
A NISP submission in your block posts its bond from one confirmed wallet box worth at least the bond plus 0.001 ERG. If no such box is free, the submission is left to the mempool for that block.
Setting boxes aside for fraud proofs#
One setting funds fraud proofs inside your own block:
| Setting | Default | Range | What it does |
|---|---|---|---|
stratum.candidate.pinnedInputs | 1 | 0 to 16 | Wallet boxes set aside to fund fraud proofs inside your own block |
A fraud proof needs one wallet input of yours, and its reward, the slashed bond, is paid to that input's address. In your own block that input is a pinned box: a wallet box the client sets aside and never sends anywhere. Each fraud proof returns the pinned box whole as change, so one pinned box funds every fraud proof a block can carry.
The client pins the smallest boxes that qualify, which are confirmed plain boxes of yours with no tokens and at least 0.001 ERG. It never pins your last such box, so the mempool path always has one. A pinned box stays out of the client's other transactions and out of your reported spendable balance until one of your blocks spends it. The client then pins a replacement. If your wallet cannot supply enough boxes, the client logs a warning once.
pinnedInputs applies only while all four of the following are on:
- The stratum task,
lithos-tasks.stratum-server - Rollup processing, which
state.disableTransforms = trueturns off stratum.candidate.blockTransactions- The
rollupssource
0 turns pinning off. Fraud proofs then reach the chain through the mempool alone.
Storage rent#
Storage rent collects boxes nobody has touched for four years, which Ergo permits anyone to spend. It ships off, and it has three settings of its own:
| Setting | Default | Range | What it does |
|---|---|---|---|
stratum.candidate.sources.rent.startHeight | 800000 | 0 to 100000000 | Inclusion height the walk begins at |
stratum.candidate.sources.rent.scanIntervalMs | 30000 | 1000 to 3600000 ms | Gap between scan passes |
stratum.candidate.sources.rent.blocksPerScan | 200 | 1 to 10000 | Blocks one pass reads. Two node calls each |
Discovery walks the chain by inclusion height rather than reading the whole UTXO set. A box created at
height h becomes collectable at h + 1051200, so the walk trails four years behind the tip and every
box it meets there is old enough by construction.
Set startHeight to roughly where collection has already caught up, not to zero. The early chain
was swept long ago and walking it costs node reads for nothing.
Intermediate outputs#
| Setting | Default | Range | What it does |
|---|---|---|---|
stratum.candidate.useTruePropCollection | false | true or false | Guard the intermediate outputs a candidate creates for its own later transactions with TrueProp instead of your own key |
A package often creates an output that a later transaction in the same package spends. Guarding those
with TrueProp rather than your P2PK is cheaper in both validation cost and byte size, which lets more
transactions fit in the block.
Logging#
| Setting | Default | Range | What it does |
|---|---|---|---|
stratum.candidate.logTimings | false | true or false | Append how long each stage took to the candidate log lines |
stratum.candidate.logBudgets | false | true or false | Log package and source bytes and cost against their budgets |
Both are diagnostic and ship off. logTimings breaks a candidate down into building the genesis
transaction, collecting the extra transactions, and the wait before miners are given the job, which is
what to turn on when a block seems slow to reach your rigs. logBudgets shows which source is using
the package budget, and what was refused for want of room.
Next steps#
- Candidate building: how a package is assembled, and what happens when part of it fails.
- Batching and orders: the two sources that fill other people's orders.
- Collateral and emission: what the
emissionssource is doing.