LithosDocs
ProtocolPayoutsTokenStatsDocs
Start mining
Documentation home
Protocol
  • Lithos Protocol
  • LFSM
    • Rollups
    • NISPs
    • Emissions
    • Mining on Lithos
    • Candidate building
    • Ergo Voting
  • LithosDex
    • Trading with orders
    • Providing liquidity
Run Lithos
  • Startup guide
    • Setting up an Ergo testnet node
  • Configuration
    • Node connection and security
    • Stratum and difficulty
    • Block packages
    • Batching and orders
    • Collateral and emission
    • Wallet and consolidation
    • Statistics and web panels
    • Synchronization and storage
    • Tasks and dispatchers
    • Failure modes
    • Reporting issues
Client API
  • Overview
    • getLithos client information
    • getList UTXO ids for blocks
    • getUTXO ids by block height
    • getBlock info by UTXO id
    • postBlock info for multiple UTXO ids
    • getGet a block's miner set
    • getList all pool blocks
    • getGet Lithos payouts
    • getStatistics overview
    • getMining and payment totals
    • getMining statistics by hour or day
    • getEstimate Lithos hashrate
    • getNetwork difficulty by epoch
    • getThis client's own hashrate
    • getThis client's hashrate over time
    • getThis client's payouts and claims
    • getCollateral inventory
    • getLocal mining activity
    • getCollateral market snapshot
    • getList queue boxes
    • getList the active set
    • getPermit price history
    • getYour locked block rewards
    • postClaim unlocked block rewards
    • postQuote joining the queue
    • postJoin the collateral queue
    • getLithosDex pool state
    • getLithosDex fee vault state
    • getList your outstanding orders
    • postQuote a swap order
    • postPlace a swap order
    • postQuote a deposit order
    • postPlace a deposit order
    • postQuote a redeem order
    • postPlace a redeem order
    • postCancel an order
    • postQuote a swap
    • postQuote a swap by desired output
    • postExecute a swap
    • postQuote a liquidity deposit
    • postDeposit liquidity
    • postQuote a liquidity redemption
    • postRedeem liquidity
    • getList liquidity provisions
    • postClaim settled fees for a provision
    • postQuote a provision resize
    • postResize a provision
    • postCheck a pending fee flush
    • postFlush pending fees to the vault
    • getPool-wide fee history
    • getPool price history
    • getRecent pool activity
    • getFee history for one provision
    • getWallet balances and lending status
    • getGet Stratum information
    • getGet candidate settings
    • getGet difficulty commitment
    • postCommit a difficulty
    • getGet best NISP at height
Lithos

A decentralized mining pool protocol on Ergo. Miners build their own blocks; contracts pay them for proven work.

Live on Ergo mainnet
Protocol
  • How Lithos works
  • Mining on Lithos
  • NISPs
  • Emissions
  • LFSM contracts
Run Lithos
  • Startup guide
  • Configuration
  • LithosDex
  • Client API
  • Failure modes
Network
  • Live stats
  • Public stats API
  • Ergo Platform↗
  • SOAT Miner↗
Community
  • Telegram↗
  • X / Twitter↗
  • GitHub↗
Lithos
© 2026 Lithos Protocol. Open source under the MIT licence.λίθος — stone
Docs/Protocol

LFSM

The contract set that runs Lithos, its parameters, and its trust boundaries.

Expert23 min readAssumes ErgoScript, Ergo box registers, context variables and AVL trees. For the protocol without the contracts, read Lithos Protocol.

The Lithos Finite State Machine (LFSM) is the contract set that runs Lithos. It governs LIT emission, the collateral queue, the rollups that collect and score miner work, the fraud proofs that remove invalid work, and the payout that ends each rollup.

Nothing off-chain holds authority. The Lithos client builds transactions and chooses when to send them, but every rule that decides whether a transaction is valid lives in a contract. A patched client changes what its operator attempts, never what the protocol accepts.

This page maps the contract set, its parameters and its trust boundaries. The rollup phases and their transformations are covered in detail in Rollups. The LithosDex is a separate product and is not part of the LFSM.

Info
Values on this page are testnet values

Token IDs, contract addresses and genesis constants in the current build are placeholders and will change before mainnet. Do not record any of them as final.

The four contract groups#

The LFSM divides into four groups, each owning one part of the protocol:

GroupContractsGoverns
collateral/Emission_Guard, LIT_Emissions, Collateral_Enforcer, Emission_Config_Mainnet, Emission_Gate, Collateral_MainnetLIT emission, the collateral queue, and the boxes miners spend to make a block a Lithos block
rollups/Holding_Guard, Holding_Logic, Evaluation, Payout, FP_Control_MainnetThe three-phase lifecycle of one block's payout
fraudproofs/Nine FP_* contractsRemoving NISPs that do not hold up
dictionaries/MinerDictionary, MinerData_Guard, MinerData_LogicMiner identity and difficulty commitments

How the groups connect#

A single collateral box travels through most of the protocol. The following shows its path, and where each group acts on it:

The queue side, where the emission box authorises Join, Activate and Clear to move a position from queue box to collateral box, and the rollup side, where a spent collateral box becomes a holding box, then an evaluation box, then a payout box. The miner dictionary supplies the commitments that fraud proofs check scores against.

The two halves are joined by exactly one event: a finder spending a collateral box.

Before that spend, a position is queue policy and the enforcer governs it. After the spend, the value belongs to a rollup and the queue never sees it again.

The emission and queue subsystem#

The emission box is a singleton authenticated by its own NFT. It holds the entire mined LIT supply, the set of currently active lender keys, and the queue's head and tail counters.

Its script is Emission_Guard, which is about 26 lines. The Guard authenticates, then executes two scripts that ride in context variables:

CheckForm
authenticConfigThe config NFT sits on dataInputs(0)
authenticEMScriptblake2b256(var 64) == CONST_EM_SCRIPT_HASH, a Guard constant
authenticEnforcerScriptblake2b256(var 65) == config.R5, replaceable through the config box
authenticGuardSELF.tokens(0)._1 == CONST_EMISSION_NFT

That split is the design. Emission arithmetic is pinned by a constant inside the Guard, so it is fixed at genesis. Queue policy is pinned by a config register, so it can be replaced by a vote. Any spend of the config box needs 3 of the 5 voting tokens in its inputs, and a vote may also move the config NFT to another script, such as a DAO or a NISP-based voting mechanism.

authenticGuard carries more weight than it looks. Without it, anyone could build a box under the public Guard script and run the full emission logic against tokens of their own minting.

Three operations, permanently#

CTX_OP is a byte in context variable 0, taking the value 0, 1 or 2, with sigmaProp(false) as the else branch. A fourth operation cannot be added after genesis.

OpNameEffect
0JoinA lender locks principal in ERG plus a permit in LIT into a queue box at the tail
1ActivateThe head queue box becomes a live collateral box, and this block's LIT is emitted into it
2ClearA queue box is removed because its lender key is already in the active set

Ordering is not in the emission contract. LIT_Emissions.ergo declares queueHead, queueTail, nextHead and nextTail, and reads none of them on any of the three paths. Queue order exists only in Collateral_Enforcer.ergo, which is the replaceable half.

Danger
Clear does not refund

A cleared box is forfeited, principal and permit, to whoever builds the clearing transaction. Not refunding is deliberate, because a refund would make queue spam free.

The client consequence is severe: use a fresh lender key for every queue box. A lender who joins with a key still present in the active set loses that box outright.

The permit thermostat#

The permit is a quantity of LIT locked alongside the ERG principal and returned when the collateral box is spent. It is never consumed. The enforcer evaluates the required amount at Join, from the current queue backlog:

scala
backlog        = queueTail - queueHead
scaled         = permitFloor.toBigInt + (permitSlope.toBigInt * backlog.toBigInt)
permitRequired = if (scaled >= permitCeiling.toBigInt) permitCeiling else scaled.toLong

Three properties are worth not undoing:

  • The intermediate is a BigInt, and the clamp happens before narrowing. An overflow here would not fail one transaction. It would make every Join unsatisfiable until a config fix.
  • The comparison is ==, not >=. Over-posting is harmless to the pool, but it makes boxes heterogeneous, which is permit-as-bid by accident.
  • The function is self-damping. Stuffing the queue raises the permit for every later join, including the spammer's own.

The function is frozen in the enforcer hash. The three numbers live in config R4 and are tunable by a 3-of-5 vote with no redeploy.

What the enforcer checks at Join#

Every queue-side check happens at Join and nowhere later:

CheckForm
Principal floorqueueOut.value >= BLOCK_REWARD - FEE_CAP + DUST_BUDGET
Dust budgetqueueOut.R4 == CONST_DUST_BUDGET
Permit amountqueueLIT._2 == permitRequired
Permit identityqueueLIT._1 == CONST_LIT_ID
Dlog companionqueueProp.propBytes == proveDlog(CTX_GE).propBytes
Not the identity pointCTX_GE.getEncoded(0) != 0.toByte
Extension flagqueueOut.R6[Byte] == 0x00
Queue positionqueueOut.R7 == queueTail, and the tail advances by exactly 1

The dlog and identity-point checks exist because a lender key that cannot receive a coinbase can never satisfy lenderIsBlockMiner. Such a box would sit at the head of the queue and refuse to activate, which stalls the queue permanently. The same reasoning applies to the extension flag: a nonzero flag routes the spend through a script named by config R6, and a rejecting script there makes the box unspendable forever.

Anything that makes Activate refuse the head stalls the queue. Every such condition is therefore checked at Join, where refusing costs the joiner a transaction rather than costing everyone the queue.

The rollup chain#

Every Lithos block spawns one rollup. Three contracts run it, and each hands the box to the next:

PhaseContractLengthAccepts
HoldingHolding_Guard plus Holding_LogicHOLDING_PERIOD blocksNISP submissions and their bonds
EvaluationEvaluation.ergoEVAL_PERIOD blocksFraud proofs
PayoutPayout.ergoUntil drainedOne miner per transaction

Holding is split in two, and the reason is NISP size#

A holding box carries only Holding_Guard, 137 bytes that check the logic's hash, dispatch on an op byte, and execute the real rules out of context variable 64. Holding_Logic holds every condition and never sits on a box at all.

That split exists because only the box's propositionBytes ride in the genesis transaction, and the genesis transaction is embedded in all ten super shares of every NISP. One byte of script becomes ten bytes of NISP ceiling and thirty bytes of worst-case fraud proof. Moving 653 bytes of logic into a context variable takes them out of that multiplication entirely, and the submission transaction pays for them instead, where nothing multiplies.

The guard stays 137 bytes however far the logic grows, so a new holding rule now costs the protocol nothing in NISP size.

Registers#

All three phases share one layout:

RegisterTypeMeaning
R4AvlTreeThe NISP tree, keyed by miner hash. Each value is a 40-byte commitment, not a NISP. Starts empty
R5IntMiner count. Incremented per insert in Holding, decremented per removal in Evaluation
R6BigIntSum of submitted scores. This is a sum of Long scores held in a BigInt, not a tau value
R7Coll[Long]Exactly three slots of Long state. See below
R8, R9anyFree for extensions

R7 carries three Longs rather than spending a register on each, which is what leaves R8 and R9 open. Its length is pinned everywhere because every rollup contract and every fraud proof reads these by index, and Coll is the one register type carrying no length of its own. Slots 0 and 1 change meaning at the payout boundary:

SlotHoldingEvaluationPayout
0Period startPeriod startTotal block reward
1Block heightBlock heightTotal LIT reward
2Bond totalBond totalBond total

Every rollup box also carries the rollup NFT at token index 0, with LIT behind it at index 1. The NFT is minted from the collateral box's own ID, since that box is input 0 of the genesis transaction, so holding it proves a box is the rollup a real collateral spend opened. LIT sits at index 1 because the NFT is always present and its index must never shift. Payout burns the NFT when it consumes the box.

Two structural properties follow:

  • AVL insert fails on a duplicate key, so one identity submits at most one NISP per rollup. No counter is needed to enforce it.
  • Payout removes each miner from the tree as it pays them, so a batch cannot pay the same miner twice.

A rollup stays challengeable for HOLDING_PERIOD + EVAL_PERIOD, which is 720 blocks. A super share may be up to NISP_WINDOW blocks older than the block that opened the rollup, so the full span any commitment must survive is 780 blocks.

The tree stores a commitment, not the NISP#

A NISP runs up to 29,629 bytes. Putting one in every tree leaf would make every AVL proof carry NISPs around, so the tree holds a 40-byte commitment instead:

miner hash  ->  [score: 8 bytes][blake2b256(NISP): 32 bytes]

The score sits in the clear because Holding needs it to price the bond and add to the running total, and because a proof that only reads a score never has to supply a NISP at all.

The submitting transaction still carries the whole NISP, since the contract has to hash the bytes it is committing to. Everything after the insert works from the commitment. When a fraud proof needs the real NISP, the prover supplies it in a context variable and the contract hashes it back against the leaf:

scala
val nispEntry = nispTree.get(CTX_MINER_BYTES, CTX_LOOKUP_PROOF).getOrElse(Coll[Byte]())
val minerNISP = getVarFromInput[Coll[Byte]](1, 4).getOrElse(Coll[Byte]())
if (nispEntry.size == 40 && minerNISP.size >= 8 &&
    minerNISP.slice(0, 8) == nispEntry.slice(0, 8) &&
    blake2b256(minerNISP) == nispEntry.slice(8, 40)) { ... }

That check runs before any parsing, which matters because the bytes come from the accused. An unauthenticated preimage is rejected rather than parsed, so nobody can steer a proof into throwing on a payload the tree never committed to. The commitment binds the exact submitted bytes including malformed ones, which is what lets a proof establish that a NISP was malformed in the first place.

Three consequences, all of which used to be limits:

If the tree held NISPsWith commitments
AVL proof sizeGrew with the NISP, because the leaf was the NISPIndependent of NISP size entirely
A fraud proof at the maximum NISPAbout 90 KB, carrying the removed leaf and its neighbourAbout 31 KB, carrying the NISP once
Miners settled per payout transactionOne, because two keys exceeded Ergo's collection limitBatches of up to 256 keys serialize

A fraud proof now grows one byte per byte of NISP rather than two, and at 31 KB it sits well inside Ergo's 98,304-byte transaction limit rather than near it.

For the transformations, their guards and the payout paths, read Rollups.

The submission bond#

A submission posts a refundable bond alongside its NISP:

bond = max(0.002 ERG, score / 25)

Three contracts touch it, and each does exactly one thing:

ContractDoes
Holding_LogicCollects it. The box value and R7 slot 2 both rise by exactly bondForScore(score)
EvaluationSlashes it. Value and ledger both fall by the removed entry's bond, paid to the fraud proof's submitter
PayoutRefunds it. Each miner receives their reward share plus their own bond

The same formula is injected into all three from MIN_ENTRY_BOND and BOND_DIVISOR, so changing either moves all three contract addresses together.

Two problems share this one mechanism. Writing to the NISP tree used to cost a single minimum fee, which put dictionary-cache saturation within a block reward of free. And a miner proven fraudulent had nothing to forfeit, so whoever proved it was paid only by the dilution of everyone else's share, which thins out as the pool grows. Pricing the bond from the miner's own claimed score fixes both: a large claim is not cheap to post, and the prover is paid by the fraudster.

The slash lives in Evaluation, not in the nine fraud proofs. Each proof establishes which miner and which score are removed and nothing else. One formula, one place to audit, and a newly whitelisted proof inherits the slash rather than reimplementing it.

Miner identity and commitments#

A miner's payout identity is blake2b256(propBytes). The stratum address and worker name are cosmetic and carry no weight anywhere in the protocol.

MinerDictionary.ergo is a singleton authenticated by its own NFT, holding one AVL tree in R4. Each entry is 40 bytes:

blake2b256(miner propBytes)  ->  [credentialId: 32][validUntil: 8]

It has three byte-dispatched operations:

OpEffectWho may spend it
0 registerMints one credential into a new MinerData box and inserts the entryThe miner
1 evictRemoves an entry long past its expiry, touching no data boxAnyone
2 removeCo-spends the miner's data box, burns the credential, removes the entryThe miner

The credential takes the dictionary box's ID at the moment of registration, so every miner's credential is distinct, and the dictionary is the only record of which one belongs to whom.

There is no renewal. A registration expires after DATA_LIFETIME, and an expiring miner removes themselves and registers again. A renewal would either leave the commitments untouched, which protects nothing, or move them mid-evaluation, which is exactly what a fraud proof is comparing against.

MinerData is also two contracts, split for the same reason Holding is but toward the opposite goal. This is the one box in the protocol designed to sit untouched for years, and Ergo prices storage rent by box length, so bytes moved into a context variable are bytes the box does not pay rent on.

FileBytesWhere it lives
MinerData_Guard.ergo73On the box
MinerData_Logic.ergo330In context variable 64

The guard binds the signer and runs the logic, and nothing else. Its registers:

RegisterTypeMeaning
R4Coll[(Int, Long)]One or two commitments as (commitHeight, score), index 0 newest
R5Coll[Byte]Exactly 32 bytes: blake2b256(miner propBytes). Never changes
R6Refused on every path that produces a successor

Every changeDiff must produce exactly two commitments, with the previous newest preserved at index 1. The box therefore always carries the commitment currently in effect and the one before it, and never more history than that.

The box holds no value beyond its floor. It is pinned at MIN_DATA_BOX_AMNT, 0.001 ERG, with no draw-down path. Bonds are posted per NISP on the rollup instead, so nothing here holds value on a miner's behalf.

Registrations expire, and the timing is what makes a cheap box safe#

A MinerData box at 0.001 ERG is under water from its first storage period: at Ergo's rent rate its structural maximum of 233 bytes costs about 0.29 ERG per period. Ergo's rent path preserves a collected box's script, tokens and registers only while its value covers the fee, and here it never does, so the box becomes spendable with no constraint at all and whoever spends it keeps the credential.

Flooring the box above the rent would cost about 0.29 ERG per registered miner, permanently locked, at exactly the scale where registration should be free. Ordering buys the same guarantee for nothing:

ConstantBlocksMeaning
DATA_LIFETIME919,800How long a registration is honoured. About 3.5 years at 2-minute blocks
STORAGE_PERIOD1,051,200Ergo's own, after which a box is collectible
EVICT_DELAY131,400The gap between the two, about six months

DATA_LIFETIME + EVICT_DELAY equals STORAGE_PERIOD exactly, so a credential can only reach a storage collector 131,400 blocks after the entry naming it stopped being honoured. A leaked credential buys nothing, because FP_NonMatchingCommitment refuses an expired entry.

Eviction exists because the collector breaks the removal path. A miner whose box has been collected cannot spend remove, since there is no data box to co-spend, and therefore cannot register again, because the tree key is their identity and inserting on a key already present throws. Eviction is permissionless and takes no data box for exactly that reason.

Why the commitment exists#

A score chosen after the shares are known measures luck, not hashrate.

Without a binding commitment, a miner picks the highest score whose super-share threshold their best 10 shares still clear, after seeing them. That is post-hoc target fitting. It reintroduces exactly the variance problem NISPs exist to remove, and it rewards whoever tunes hardest rather than whoever mines most.

FP_InvalidDiff does not close this. It proves the 10 shares meet the threshold of the score claimed in that very NISP, not the score the miner committed to. Only FP_NonMatchingCommitment closes it.

The governing commitment#

Two timing rules live in changeDiff and they do different jobs:

  1. A new commitment names a height at least NISP_WINDOW = 60 blocks past the block that includes it, and takes effect NISP_WINDOW blocks after that named height. This is the notice period that makes a commitment binding. The reference client names a height 65 blocks ahead, so a commitment it sends binds 125 blocks later.
  2. A commitment cannot be replaced sooner than NISP_WINDOW + ROLLUP_LIFETIME = 780 blocks after the height the last one named, which is 720 blocks after it took effect. This keeps the commitment governing an older rollup present in one of the two slots for as long as that rollup can still be challenged.

Rule 2 is load-bearing. Loosen the spacing and a miner evicts the governing commitment by churning, which turns the proof into one that either declines to fire or compares against the wrong score.

FP_NonMatchingCommitment anchors on currentBlock, the rollup's own block height from R7 slot 1, rather than on submission height. Submission height is not recorded on-chain and is miner-controlled, so anchoring there would let a miner sit on a NISP until a different commitment took effect. The rule it applies:

scala
if (currentBlock - commitments(0)._1 >= NISP_WINDOW) commitments(0)._2
else                                                commitments.getOrElse(1, (0, -1L))._2

The -1L fallback covers a miner holding only a registration commitment that had not yet taken effect at currentBlock. No NISP can carry a score of -1, because Holding accepts only a score above zero, so any submission in that state is fraudulent by construction.

Fraud proofs#

Nine proof types are live. Each spends the evaluation box, removes one NISP from the tree, decrements R5 and R6, and pays the removed entry's bond to whoever submitted the proof.

They run in a fixed order, and the order is a dependency graph rather than a convention:

#ProofProves that the minerDepends on
0FP_NonMatchingCommitmentSubmitted a score that is not the commitment in effect at the rollup's block heightNothing
1FP_InvalidFormatSubmitted a NISP whose byte layout is malformed or out of boundsNothing
2FP_MalformedGESubmitted a header carrying an invalid group element for the miner public key1
3FP_NotInWindowSubmitted at least one share outside the NISP window1, 2
4FP_NonUniqueHeadersSubmitted two or more shares with identical header IDs1, 2
5FP_IncorrectNSubmitted a super share whose N value does not match its height1, 2
6FP_InvalidDiffSubmitted a share that does not reach the super-share threshold of the score claimed1, 2, 5
7FP_TransactionNotIncludedSubmitted a collateral transaction and proof that do not match the header's Merkle root1, 2
8FP_MalformedGenesisSubmitted super shares built from someone else's genesis transaction1

Why the order matters#

A proof has two acceptable answers, fires or declines. A third thing can happen: it throws while evaluating, and that is not an answer. Nothing can distinguish a proof that threw from one that was never reached, so a NISP that throws in every proof belongs to a miner nobody can slash. The accused writes the NISP, so that is a fraud that pays.

The set closes it by ordering rather than by defending each proof separately. Each proof reads bytes on the assumption that the ones before it declined. Three edges carry the weight:

  1. FP_InvalidFormat cannot throw on anything Holding admitted. Its slices clamp and a failed predicate poisons the accumulator, so later iterations index nothing.
  2. FP_MalformedGE declining implies the miner key decompresses, which means the header parses. It works on raw bytes for exactly that reason, and it is the last proof that may not parse a header.
  3. FP_IncorrectN must precede FP_InvalidDiff. N is the one field of a super share the header does not carry, and it feeds straight into the proof-of-work hit, so an unpinned N answers a different puzzle.

FP_MalformedGenesis is what makes a NISP non-transferable#

Every other proof asks whether the ten super shares are good work. None asks whose. The header's miner key belongs to the lender, shared by every miner drawing that collateral box, so without this proof a NISP is ten shares anyone could copy out of the public tree and be paid for.

What separates two miners is that each builds their own genesis transaction, whose holding output carries their own hash. This proof walks the serialized transaction inside the share down to that register and compares it against the accused.

FP_MalformedGenesis and FP_TransactionNotIncluded are jointly necessary and individually insufficient. A thief who rewrites the register to their own hash changes the transaction's hash and fails inclusion. One who leaves it alone fails this proof. Neither closes the theft alone.

Two properties govern the whole set#

  • A false negative is fatal and a false positive is not. A proof that declines to fire lets theft through permanently. A proof that fires wrongly removes one honest NISP from one rollup. Proofs must therefore err toward firing.
  • FP_CONTROL is immutable at launch. There is no key and no DAO. It whitelists nine proof hashes, and changing the set requires a hardfork to a new NFT, so the audit has to be complete before launch and there is no recovery from an incomplete one.

Parameters#

The following parameters are compiled into contracts or injected at compile time. Values are from LFSMHelpers.scala and the contracts themselves:

ParameterValueUsage
TARGET_MAX_LITHOS2^256 - 1The numerator in every score and tau conversion
HOLDING_PERIOD360 blocksLength of the Holding phase. About 12 hours at mainnet's 2-minute blocks, 4.5 hours at testnet's 45-second blocks
EVAL_PERIOD360 blocksLength of the Evaluation phase, same durations
NISP_WINDOW60 blocksHow far before a block a super share may have been mined and still count. The same 60 blocks on both networks, which is about 45 minutes on testnet and 2 hours on mainnet
ROLLUP_LIFETIME720 blocksHOLDING_PERIOD + EVAL_PERIOD. How long a rollup stays challengeable
NISP_COEFFICIENT10,000Divides a miner's tau to give the super-share threshold
NISP_MAX29,629 bytesMaximum NISP size, enforced by Holding_Logic. Derived, not chosen: 8 + 10 × (4 + 221 + 3 + 2174 + 528 + 32) + 1
NISP_MIN7,948 bytesMinimum NISP size. Derived as 8 + 10 × (4 + 220 + 3 + 502 + 33 + 32). Stops a short NISP from causing an index error when a fraud proof iterates its 10 shares
TX_PROOF_MIN502 bytesMinimum size of a super share's transaction proof, as 2 + TX_SIZE_MIN + COLLAT_BOX_MIN
TX_PROOF_MAX2,174 bytesMaximum size of a super share's transaction proof. The worst honest proof plus a margin reserved for a larger replacement holding contract
TX_SIZE_MIN450 bytesMinimum size of the collateral transaction inside a transaction proof
COLLAT_BOX_MIN50 bytesMinimum size of the collateral box inside that transaction
NUM_LVLS_MAX16Maximum Merkle proof depth for a transaction proof
MIN_ENTRY_BOND2,000,000 nanoERGFloor on a submission's refundable bond, 0.002 ERG
BOND_DIVISOR25Score units per nanoERG of proportional bond
MIN_DATA_BOX_AMNT1,000,000 nanoERGValue of a MinerData box, pinned rather than floored
DATA_LIFETIME919,800 blocksHow long a registration is honoured
EVICT_DELAY131,400 blocksHow long after expiry an entry may be evicted
PERMIT_FLOOR2,000 LITPermit at an empty queue
PERMIT_CEIL40,000 LITPermit ceiling, whatever the backlog
PERMIT_SLOPE20 LIT per queued boxSlope of the thermostat between the two. The thermostat saturates at a backlog of 1,900

There is no separate maximum for the collateral box or the transaction inside a proof, and that is deliberate. A collateral box's size is the proof size minus the transaction size, so TX_PROOF_MAX and TX_SIZE_MIN already bound it from both ends. A redundant cap can only tighten a sound bound, and one derived slightly too small would reject honest shares forever.

Score and tau#

A miner's score is the difficulty value their NISP claims, and it is the only input to their share of a payout. Score and tau are reciprocal through TARGET_MAX_LITHOS:

score = (2^256 - 1) / tau
tau   = (2^256 - 1) / score

The same function converts in both directions, which is why LFSMHelpers.convertTauOrScore takes one argument and names neither.

Two derived quantities matter to the contracts:

  • The super-share threshold is minerTau / NISP_COEFFICIENT, which is tau / 10,000. A share at or below it counts as a super share. FP_InvalidDiff recomputes this from the score in the NISP and checks all 10 shares against it.
  • A payment is paymentFromScore(score, totalScore, totalValue), evaluated as (totalValue * score) / totalScore in BigInt and narrowed to Long. The truncation deficit is under one nanoERG per miner.

Difficulty strings in the client's configuration, such as "4.0G" or "1.52M", are parsed into tau by parseDiffValueForStratum, which supports K, M, G, T and P.

What can change and what cannot#

The LFSM deliberately draws its upgrade boundary in one place. Knowing which side a thing sits on decides whether it is a design decision or a deployment one:

Fixed at genesisReplaceable
The emission arithmetic in LIT_Emissions.ergo, pinned by a Guard constantThe queue policy in Collateral_Enforcer.ergo, pinned by config R5
The three operation bytes, with sigmaProp(false) as the else branchThe permit floor, ceiling and slope, held in config R4
lenderAlreadyActive as the condition for ClearWhich queue box Clear may target, which is enforcer policy
The collateral contract hash, held inside the immutable emission scriptThe collateral extension script, named by config R6
The accepted fraud proof set, held by an immutable FP_CONTROLThe rollup holding hash, stamped from config R7 at activation

Replacing the enforcer is not a hot swap. The queue has to drain first, because boxes joined under one policy and activated under another can disagree about what the old boxes promised.

Three changes in the right-hand column are hardforks anyway#

FP_CONTROL whitelists nine proof hashes and cannot be amended, so anything that moves one of those hashes needs a new NFT rather than a contract swap. Three changes that look routine do exactly that:

ChangeWhy it moves a hashWhat happens if it ships anyway
Enabling collateral extensionsThe extension branch needs a context variable and a data input on the genesis spend, and FP_MalformedGenesis requires both absentEvery miner drawing an extension-bearing box becomes slashable by anyone
Rotating the rollup holding hash through config R7FP_MalformedGenesis injects the holding contract's whole tree, not its hashEvery miner on the new generation becomes slashable
An Ergo header version bump that populates the header's unparsed-bytes fieldFP_InvalidFormat accepts only version 4 or above with that count at zeroEvery honest miner at once

All three fail toward slashing honest miners, and that direction is deliberate. A header a proof cannot size is one the header-reading proofs cannot read either, and a proof that throws is indistinguishable from one that never ran, which would leave the miner unslashable instead. Firing is the safe half of a bad choice. It does not make the trigger less real.

The practical consequence is that the collateral box's rollup-hash register exists so the rollup chain can be replaced without reissuing the emission box, and that independence is now nominal.

Next steps#

  • Rollups: the rollup contracts, their transformations and the payout paths.
  • NISPs: the proof format, super shares and how a NISP is built.
  • Lithos Protocol: the same system without the contracts.
Edit this page on GitHubsynced from e0ca86a
← PreviousLithos ProtocolNext →Rollups
On this page
  • The four contract groups
  • How the groups connect
  • The emission and queue subsystem
  • Three operations, permanently
  • The permit thermostat
  • What the enforcer checks at Join
  • The rollup chain
  • Holding is split in two, and the reason is NISP size
  • Registers
  • The tree stores a commitment, not the NISP
  • The submission bond
  • Miner identity and commitments
  • Registrations expire, and the timing is what makes a cheap box safe
  • Why the commitment exists
  • The governing commitment
  • Fraud proofs
  • Why the order matters
  • FP_MalformedGenesis is what makes a NISP non-transferable
  • Two properties govern the whole set
  • Parameters
  • Score and tau
  • What can change and what cannot
  • Three changes in the right-hand column are hardforks anyway
  • Next steps