ETH2015

DEVELOPER FAQ

How minting actually works

For the plain English version see Plain English. For a machine readable specification see LLM instructions.

How much work is a mint?

Each record contains its deployment block's historical difficulty. The NFT contract's requiredDifficulty(record.difficulty) defines the accepted threshold; the collection's standard rule returns historical difficulty unchanged. Difficulty is the expected attempt count, not a required number of iterations. Across the collection it is about 1.46 to 9.44 trillion.

Every additional level uses the same difficulty. No multiplier is applied as the level increases.

Which Ethash epochs does the collection cover?

The 6,187 NFTs span 25 Ethash epochs, numbered 1–25. Each epoch covers 30,000 Ethereum blocks: the epoch number is the deployment block number divided by 30,000, rounded down. Each NFT uses the DAG for its recorded epoch.

NFTs in the same epoch share a DAG, but their difficulty can differ because historical difficulty changes from block to block. The table averages the recorded deployment-block difficulty of the NFTs in each epoch, then divides by the epoch 1 average. Epoch 1 = 1.00× represents about 1.59 trillion expected attempts.

Collection averages · epoch 1 difficulty normalized to 1
Ethash epochNFTsRelative difficulty
11251.00×
21721.43×
31102.66×
41933.25×
52414.60×
62543.99×
71654.24×
82853.75×
92034.35×
101193.75×
111223.89×
122643.92×
132633.71×
145883.94×
152894.64×
164024.76×
173004.88×
181965.27×
192144.75×
201294.88×
214334.90×
223285.11×
233165.48×
241815.36×
252955.37×
Total6,18725 epochs

These are averages over the NFTs in this collection, not over every Ethereum block in an epoch. Each NFT still uses its own exact recorded difficulty. The figures compare the normal historical-work rule; they are not replacement difficulty values for a miner.

For example, epoch 25 averages about 5.37 times the work of epoch 1. At the same hash rate, that means about 5.37 times as many nonce attempts and as much search time on average. Actual solution times vary; DAG preparation and differences in device performance are not included. Difficulty does not rise steadily from epoch to epoch.

Source: the epoch and difficulty fields in the collection records. The same relative work applies to level-ups, since each level uses its NFT’s original difficulty.

What is being verified on chain?

A complete Ethash hashimoto round, not a signature over an off chain result. The contract recomputes the seed, performs all 64 adaptive dataset accesses, authenticates every accessed page against a committed DAG root, recomputes the mix and the final digest, and compares it to the record's historical difficulty target. Calldata size and gas depend on the epoch and operation; estimate the exact transaction before sending.

Nothing security relevant is taken on trust from the caller. The difficulty, the DAG root, the page count and the historical address all live inside a Merkle leaf committed at deployment, so supplying an easier record is a proof failure rather than a cheaper mint.

Is there a cheaper way to submit? (optional zk path)

Why it exists: the pages path re-executes the full hashimoto round on chain and carries about 43 KB of calldata, about 1.8 million gas per mint. At 0.2 gwei that is about 0.00036 ETH, so it stays the default and the simple choice. Gas has been far higher before and may be again: at 20 gwei a pages mint costs about 0.036 ETH. The zk path keeps minting affordable in that case, at about 395,000 gas (about 0.008 ETH at 20 gwei), in exchange for more setup and heavy proving hardware. It is an alternative, not a replacement: both paths stay open, and the browser always uses the pages path.

Yes. mintZK and levelUpZK take an SP1 Groth16 proof (356 bytes) that the ETH2015 SP1 program ran the same hashimoto round and Merkle checks off chain and accepted, plus its 224-byte ABI-encoded statement (challenge, nonce, dagRoot, pageCount, difficulty, mixDigest, result). A mint costs about 395,000 gas instead of 1.85 million, and a level-up about 312,000 instead of 1.73 million.

The contract verifies the proof through Succinct's SP1 verifier gateway (zkVerifier()), which routes it to the v6.1.0 Groth16 verifier and refuses any SP1 version Succinct has frozen (RouteIsFrozen). It accepts only the pinned program (zkProgramVKey()), derives the challenge itself, requires the record's dagRoot and pageCount, and checks result against requiredDifficulty on chain. Both paths share usedLevelNonces and emit the same WorkAccepted. The trade is hardware: about 27 GB of RAM and about 5 minutes on 16 cores per proof, with SP1 SDK 6.8.1 and its 6.2 GB circuit files, or Succinct's prover network. As a fallback, owner() can call disableZK() once to close the zk path for good (zkDisabled() reports it); the pages path is unaffected either way.

How is the challenge derived?

The contract computes it from authenticated state:

DOMAIN = keccak256("ETH2015_ACTION_WORK_V3")
action = 0 for mint, 1 for level-up
boundRecipient = mint recipient for action 0, address(0) for action 1
challenge = keccak256(abi.encode(
    DOMAIN, block.chainid, address(this), collectionRoot,
    tokenId, boundRecipient, action))

The ABI types are (bytes32,uint256,address,bytes32,uint256,address,uint8). Check WORK_VERSION() == 3 and compare your result with challengeFor(action, tokenId, recipient). For level-up, pass the zero address; the contract ignores that parameter for action 1 and requires the NFT to exist.

Mint work fixes its recipient. Level-up work fixes its NFT, with no owner, level or transfer counter in the challenge. Both fix the chain and deployment address, so work cannot be carried to another deployment or action.

How do level-ups prevent replay?

levelUp rejects a nonce already recorded in usedLevelNonces(tokenId, nonce). After full verification it marks the nonce used and increments the current level by one. Failed verification does not consume a nonce.

Different solutions found at the same level remain valid in either submission order. Transfers do not change the challenge or clear used nonces. Anyone may mine and relay a level-up; the current owner benefits. A copied solution can be accepted only once, regardless of who submits it.

What is the DAG commitment?

Each record commits to the Ethash dataset for its epoch as a positional Merkle tree over 128 byte pages, padded to 2**ceil(log2(pageCount)) leaves.

leaf(i < pageCount) = keccak256(0x00 || uint32be(i) || page[128])
leaf(i >= pageCount) = keccak256(0x02 || uint32be(i))
node                 = keccak256(0x01 || left || right)

Three details matter. Hashing is positional, not sorted, so OpenZeppelin's commutative multiproof does not apply; the contract takes its own positional multiproof, below. The index is inside the leaf, so a page cannot be moved to a different index. And padding is index dependent, so padding subtree roots cannot be precomputed once per level.

What does a witness contain?

For one (challenge, nonce) pair, the miner produces:

The contract takes those branches packed into one positional multiproof, so hashes shared between branches are sent and checked once:

At depth 24 that is about 1,040 hashes, since the 64 branches share their upper levels. packProof in mint/ethash.js builds it from a verified witness and walks it to the root before anything is sent. The contract rejects an unused, missing or extra hash, an order that is not a sorted permutation, and a repeated page with different bytes.

Where does the dataset come from?

The device builds it. A per epoch seed of 16.9 to 20.1 MiB is fetched from https://seed.eth2015.com/<epoch>/cache and expanded on the GPU into the full 1.08 to 1.28 GiB dataset, in 8 MiB pieces each checked against chunkSHA256 in the epoch manifest before use or persistence. Whole dataset downloads are not served: the seed is 60 times smaller for a byte identical result.

The Merkle tree over those pages is built once per epoch, not per nonce. Persisting only the upper levels keeps it to about 512 KiB, and each witness then recomputes 64 small bottom subtrees.

What is the contract interface?

mint(Record record, bytes32[] collectionProof, address recipient,
     uint64 nonce, bytes pages, bytes order, bytes dagProof,
     bytes32 card, bytes32[] cardProof)

levelUp(Record record, bytes32[] collectionProof,
        uint64 nonce, bytes pages, bytes order, bytes dagProof)

// optional zk path
mintZK(Record record, bytes32[] collectionProof, address recipient,
       bytes32 card, bytes32[] cardProof, bytes publicValues, bytes proof)
levelUpZK(Record record, bytes32[] collectionProof, bytes publicValues, bytes proof)

Record is (uint256 tokenId, address historicalContract, uint256 deploymentBlock, bytes32 blockHash, uint256 difficulty, uint256 epoch, uint32 pageCount, bytes32 dagRoot). Its collection proof contains 13 siblings.

Mint also authenticates the packed artwork word against cardRoot, using a 13-sibling proof. The static shards at /mint/card-proofs/<floor((tokenId - 1) / 128)>.json provide data and proof. Level-up uses the card already stored by acquisition.

A mint uses about 1.8 million gas and a level-up about 1.75 million. With about 43 KB of calldata, a transaction sits close to the EIP-7623 calldata floor, so proof bytes matter as much as execution. The optional zk path costs about 395,000 and 312,000 gas with about 2 KB of calldata. Simulate with the actual sender before submission. WorkAlreadyUsed means that solution already counted. A level-up has no recipient or expected-level arguments. Read the resulting level from the receipt's WorkAccepted event or level(tokenId).

A mint does not roll. Each level-up of a card that is not golden rolls a 1 in 5 golden chance (GOLDEN_ODDS) from the hash of the next block: uint256(keccak256(abi.encode(blockhash(rollBlock + 1), tokenId))) % 5 == 0. finish(tokenId) returns 0 regular, 1 rolling, 2 won and waiting, 3 golden, with the roll's block. A won roll is made golden by claimGolden(tokenId) (any sender; emits GoldenClaimed) or by the card's next level-up, within 8,191 blocks.

Is the artwork on chain?

Yes, including the JSON. tokenURI returns a base64 data: URI whose image is a base64 SVG composed in the contract: a 1:1 frame, the padded token number, the assigned symbol and the historical contract address. There is no HTTP host or IPFS gateway. The drawing is done by a renderer contract that owner() can replace with setRenderer(newRenderer) until lockRenderer() fixes it for good; a switch emits BatchMetadataUpdate(1, 6187). Card data, levels, finish and ownership stay in the NFT contract. The browser composes the identical string locally from the same generated fragments, and the two are compared byte for byte in the test suite.

Who can withdraw contract balances?

owner() can, on both the NFT contract and its Ethash verifier. It can withdraw ETH, ERC20 tokens, ERC721 NFTs and ERC1155 tokens sent to either contract by mistake. It cannot take assets from users’ wallets, move or change any card, change mining difficulty or replace the verifier. owner() starts as the deploying address, and transferOwnership hands recovery on together with its other rights: replacing the renderer until it is locked, disableZK, and setContractURI. That last one replaces the collection metadata marketplaces read (ERC-7572, emits ContractURIUpdated), for example a new website link. An empty value restores the built-in on-chain JSON. The verifier has no owner of its own: its owner() reads the NFT's, and collection() names that NFT.

Recovery uses recoverETH(recipient, amount), recoverERC20(token, recipient, amount), recoverERC721(token, recipient, tokenId) and recoverERC1155(token, recipient, tokenId, amount). Amounts are raw base units. Recovery requires a transaction from the current owner() (otherwise NotOwner); ordinary mint and level-up proofs can still be submitted by anyone. These recovery methods require a new deployment and are not added to an existing contract by updating the website.

What are the known sharp edges?

Back to the collection