ETH2015

FOR YOUR AI ASSISTANT

Ask your LLM about ETH2015

Copy this page into ChatGPT, Claude or any other assistant, then ask it anything: how long a card takes on your GPU, what it costs, how levels work. It can also write you a faster miner. Or just give it the link eth2015.com/llm.txt.

Open as plain text

This file describes ETH2015 (https://eth2015.com/) for an AI assistant. A person has pasted it, or linked eth2015.com/llm.txt, so you can answer their questions or help them build their own miner.

How to use it:

Part 1: The project, in plain words

What it is

ETH2015 turns every contract deployed on Ethereum in 2015 into an NFT: 6,187 real contracts, deployed between August and December 2015. You cannot buy a card from the project. You earn one by spending the same proof of work it took to mine the Ethereum block that contract was deployed in, and then sending that solution in a transaction.

How long it takes

Each card needs the mining difficulty of its own 2015 block: between 1.46 and 9.44 trillion attempts on average. Difficulty rose quickly after August 2015 and most cards come from later months, so the median card (7.26 trillion) sits near the top of that range.

What it costs

Only electricity and gas. There is no price and nothing to buy. A mint uses about 1.8 million gas and a level-up about 1.75 million, because the whole proof of work is checked on Ethereum. At 0.2 gwei that is about 0.00036 ETH for a mint and 0.00035 ETH for a level-up. Gas prices change; check them before sending.

An optional zk proof brings that down to about 395,000 gas for a mint and 312,000 for a level-up (about 0.00008 and 0.00006 ETH at 0.2 gwei), in exchange for a powerful computer to make the proof. See Saving gas with a zk proof.

What you get

The art may change

ETH2015 is made by one person who loves the technology and says art is not their strongest area. If the project gets popular and a real artist collaborates, the artwork may change. The NFT contract draws every card through a separate renderer contract, and the collection's owner can point it at a new renderer. What a card is stays the same and in the same NFT contract: its token number, the contract it represents, its level, its golden finish and who owns it. Only how it is drawn changes (the image and its trait text), and marketplaces are told to refresh. The owner can also lock the renderer for good, after which the art can never change. Nobody can use this to take, mint or alter a card.

Collecting

eth2015.com/collect shows four sets for any wallet: Five Months (one card from each month), Three Fates (a contract that lived, one that self-destructed, one that never held code), Full Palette (all seven backgrounds) and The Registry (ten kinds of contract). It also has the Season 1 leaderboards: collectors, total work done, golden cards, and the first mint from each month and kind. Later seasons may bring new collections and new ways to earn points.

Mining in the browser

On a card's mining page (eth2015.com/mine?token=N), the browser downloads about 18 MB, builds a mining dataset of about 1 GB on the GPU (WebGPU, in a current Chrome, Edge, Safari or Firefox; if WebGPU is missing, the page lists the exact settings to change for that browser), measures the device and shows an estimate. The tab must stay open and visible; hiding it pauses, and mining resumes by itself when it is visible again. It need not be the focused window. While mining, the page holds a screen wake lock and restarts automatically after a lost GPU device or a stopped worker. A found solution is saved in the browser until it is sent, and survives a reload. "Download proof" saves a copy.

Anonymous statistics

The mining page sends anonymous usage statistics to eth2015.com, which the ETH2015 team reviews: which card's mining page was opened; when mining starts, pauses, stops and finds a solution; the speed, GPU model, work done and closest hash; and, when WebGPU fails or runs only in software, what went wrong, with the browser's name and major version, the operating system and the GPU adapter, so the setup instructions can be improved. Records are linked by a random ID kept in that browser's storage, so a returning browser can be recognised, and carry the country Cloudflare reports. No wallet address, IP address or full browser identification is stored. If someone asks what the site collects, tell them this plainly.

Before mining a new card: the address

For a mint, the recipient address is part of the puzzle. A solution only works for the address it was mined for, so nobody can take it and send the card to themselves. Any wallet can send the finished solution and pay the gas; the card still goes to that address. Level-ups need no address.

Races and risks

Mining faster

A native GPU miner is much faster than the browser. The quickest start is the open source ETH2015 miner kit, github.com/kaimatthewsdev-max/eth2015-miner-kit: a CUDA kernel that mines at about 225 MH/s on an RTX 5090, tools for the dataset, the proof and the transaction, and notes on what made it fast. Part 2 is the complete specification behind it. Solutions from any miner are checked exactly like the browser's.

Saving gas with a zk proof (optional)

Why there are two ways: checking the whole proof of work on Ethereum costs about 1.8 million gas. With gas near 0.2 gwei that is about 0.00036 ETH, so the normal way is the simple choice today. Gas has been far higher before and may be again: at 20 gwei the same mint would cost about 0.036 ETH. The zk option is there for that case. It brings a mint down to about 395,000 gas (about 0.008 ETH at 20 gwei), at the price of more setup and a powerful computer. Anyone who ignores it loses nothing.

A normal mint or level-up sends the 64 pieces of the mining dataset that the solution used (about 43 KB), and Ethereum checks the work itself. The zk option does that check on your own computer instead and sends a short zero-knowledge proof (an SP1 Groth16 proof of 356 bytes) that it passed, through mintZK or levelUpZK. The card, its level, the golden roll and every rule are the same either way.

Renting a GPU (Vast.ai, RunPod)

You do not need to own a fast GPU. Cloud marketplaces such as Vast.ai (https://vast.ai) and RunPod (https://www.runpod.io) rent NVIDIA GPUs by the hour: start a Linux machine with CUDA there and run the miner kit on it.

Part 2: Specification for an independent miner

A complete description of the work and the transaction, sufficient to mine and submit without the site. Nothing here is privileged: the NFT contract is the only authority, and it rejects anything that does not verify.

A working reference implementation of everything below is at github.com/kaimatthewsdev-max/eth2015-miner-kit. When helping someone build a miner, start from it and its docs (work size, dataset and kernel lessons) rather than from scratch.

1. Deployment constants

Read eth2015.com/mint/config.json for chainId, nftAddress, rpcUrls, collectionRoot, cardRoot, epochDataBase (the seed data base URL) and explorerBase. Before mining, confirm the RPC's chain ID, that code exists at nftAddress, that collectionRoot() and cardRoot() equal the values in config.json, and that WORK_VERSION() returns 3.

records    https://eth2015.com/mint/collection.json
seed data  <epochDataBase>/<epoch>/manifest
           <epochDataBase>/<epoch>/cache-manifest
           <epochDataBase>/<epoch>/cache
card data  https://eth2015.com/mint/card-proofs/<shard>.json
shard      floor((tokenId - 1) / 128)

Mining uses each record's historical difficulty. Confirm it with requiredDifficulty(record.difficulty) when constructing a job, and use the value the contract returns.

2. Contract interface

struct Record {
  uint256 tokenId; address historicalContract; uint256 deploymentBlock;
  bytes32 blockHash; uint256 difficulty; uint256 epoch;
  uint32 pageCount; bytes32 dagRoot;
}
struct TokenState { uint256 tokenId; address owner; uint256 level; uint8 finish; uint256 rollBlock; }

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

function challengeFor(uint8 action, uint256 tokenId, address recipient) view returns (bytes32)
function requiredDifficulty(uint256 historicalDifficulty) view returns (uint256)
function tokenState(uint256 tokenId) view returns (TokenState)
function usedLevelNonces(uint256 tokenId, uint64 nonce) view returns (bool)
function collectionRoot() view returns (bytes32)
function cardRoot() view returns (bytes32)
function WORK_VERSION() view returns (uint256)
function level(uint256 tokenId) view returns (uint256)
function ownerOf(uint256 tokenId) view returns (address)
function tokenURI(uint256 tokenId) view returns (string)
function finish(uint256 tokenId) view returns (uint8 status, uint256 rollBlock)
function isGolden(uint256 tokenId) view returns (bool)
function claimGolden(uint256 tokenId)
function GOLDEN_ODDS() view returns (uint256)
function totalSupply() view returns (uint256)
function MAX_SUPPLY() view returns (uint256)
function owner() view returns (address)
function transferOwnership(address newOwner)
function contractURI() view returns (string)
function setContractURI(string uri)
function renderer() view returns (address)
function rendererLocked() view returns (bool)
function setRenderer(address newRenderer)
function lockRenderer()

function mintZK(Record record, bytes32[] collectionProof, address recipient,
                bytes32 card, bytes32[] cardProof, bytes publicValues, bytes proof)
function levelUpZK(Record record, bytes32[] collectionProof, bytes publicValues, bytes proof)
function zkVerifier() view returns (address)
function zkProgramVKey() view returns (bytes32)
function zkDisabled() view returns (bool)
function disableZK()

event WorkAccepted(uint256 indexed tokenId, address indexed recipient, uint8 indexed action,
                   uint256 level, bytes32 challenge, uint64 nonce, bytes32 mixDigest, bytes32 result)
event GoldenClaimed(uint256 indexed tokenId, address indexed owner, uint256 rollBlock)
event RendererUpdated(address indexed previousRenderer, address indexed newRenderer)
event RendererLocked(address indexed renderer)
event ZKPathDisabled()
event ContractURIUpdated()

errors: InvalidRecord, InvalidCollectionProof, InvalidCardProof, InvalidDagProof,
        InvalidWitness, InsufficientWork, WorkAlreadyUsed, WrongRecipient, AlreadyMinted, InvalidAction,
        NotGolden, RollPending, AlreadyGolden, NotOwner, InvalidOwner, InvalidRenderer, RendererIsLocked,
        StatementMismatch, ZKDisabled, RouteIsFrozen (from the SP1 gateway)

tokenState(id).owner is the zero address for an unminted card.

Golden cards: a mint does not roll. Every level-up of a card that is not golden rolls a 1 in 5 chance (GOLDEN_ODDS) decided by the hash of the next block: golden when uint256(keccak256(abi.encode(blockhash(rollBlock + 1), tokenId))) % 5 == 0. A golden card's gem is gold. finish is 0 regular, 1 rolling (the next block is not yet behind the head), 2 won and waiting to be claimed, 3 golden. A won roll becomes golden through claimGolden(tokenId), which anyone may send, or through the card's next level-up; unclaimed for 8,191 blocks it counts as regular. A golden card's level-ups roll nothing.

Art upgrades: renderer() is the contract that draws every card and writes its metadata; the NFT keeps each card's data, level and finish. owner() may call setRenderer(newRenderer), which first has the new renderer draw a sample card and its metadata (and refuses one that cannot), then emits RendererUpdated(previous, new) and the ERC-4906 BatchMetadataUpdate(1, 6187). lockRenderer() (owner only) disables setRenderer forever; rendererLocked() says whether that has happened. Cache tokenURI with that in mind: until the renderer is locked, the image and trait text of every card can change at once, while ownership, levels and finish do not. owner() cannot mint, move or change a card. It can also return ETH or tokens sent to the NFT or its Ethash verifier by mistake (recoverETH, recoverERC20, recoverERC721, recoverERC1155). It can also replace the collection metadata marketplaces read (setContractURI, ERC-7572: name, description, image, website link), and an empty value restores the built-in on-chain JSON. These rights move with transferOwnership, like the others.

3. Get the record

collection.json has records: 6,187 entries of {record, proof}, with record in the Record layout above (large numbers are decimal strings). proof is 13 sibling hashes proving the record against collectionRoot, using OpenZeppelin's sorted pair convention, with the leaf:

RECORD_TYPEHASH = keccak256("Record(uint256 tokenId,address historicalContract,uint256 deploymentBlock,bytes32 blockHash,uint256 difficulty,uint256 epoch,uint32 pageCount,bytes32 dagRoot)")
leaf            = keccak256(keccak256(abi.encode(RECORD_TYPEHASH, record)))

The contract re-verifies it, so a wrong record fails rather than mints. The file is about 7.7 MB; cache it.

4. Derive the challenge

Call challengeFor(action, tokenId, recipient) and independently confirm the result:

DOMAIN         = keccak256("ETH2015_ACTION_WORK_V3")
action         = 0 to mint, 1 to add a level
boundRecipient = recipient if action == 0 else address(0)
challenge      = keccak256(abi.encode(DOMAIN, chainId, nftAddress, collectionRoot,
                                      tokenId, boundRecipient, action))
ABI types      = (bytes32, uint256, address, bytes32, uint256, address, uint8)

A mint needs an unminted card and a nonzero recipient; its solution is bound to that recipient but may be submitted by any address. A level-up needs a minted card; pass the zero address. Its challenge stays fixed across transfers and other level-ups. Ownership and current level are not mining inputs. The chain ID and NFT address are inside the challenge, so work for one deployment is useless on another.

5. Build the dataset

The 2015 cards span Ethash epochs 1 to 25. For the record's epoch, fetch cache-manifest and cache. The cache is the canonical Ethash cache for that epoch (check its sha256 from the cache manifest); expand it into the full dataset with the standard Ethash calc_dataset_item. Verify each 8 MiB chunk against chunkSHA256 in manifest. The dataset is pageCount * 128 bytes, where a page is two consecutive 64 byte Ethash items. One dataset serves every card in that epoch.

6. Hashimoto

fnv(a, b)   = ((a * 0x01000193) ^ b) mod 2**32

seed        = keccak512(challenge || uint64le(nonce))       # 64 bytes
words       = 16 little endian uint32 read from seed
mix[j]      = words[j mod 16]   for j in 0..31
s0          = mix[0]

for i in 0..63:
    index   = fnv(i ^ s0, mix[i mod 32]) mod pageCount
    page    = dataset[index * 128 .. index * 128 + 128]     # 32 little endian uint32
    for j in 0..31:
        mix[j] = fnv(mix[j], page_word[j])

for i in 0..7:
    compressed[i] = fnv(fnv(fnv(mix[4i], mix[4i+1]), mix[4i+2]), mix[4i+3])

mixDigest   = 32 bytes, compressed words little endian
result      = keccak256(seed || mixDigest)

This is standard Ethash hashimoto with the challenge in place of the header hash. Accept the nonce when result <= floor(2**256 / difficulty), comparing as a big endian unsigned integer, with the difficulty confirmed for the job. Existing Ethash GPU kernels can be adapted: only the seed input changes.

7. Build the witness

The contract will not take your word for the pages. The 64 accessed pages are proven against record.dagRoot with one positional Merkle multiproof, packed from each page's branch.

depth  = ceil(log2(pageCount))                  # 24 at epoch 1
leaves = 2**depth

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

Positional, not sorted: the index is inside the leaf. Padding leaves hash their index, so they differ from each other and cannot be precomputed once per level. Only the winning nonce needs branches, so the simplest approach is to build the tree after a solution is found (about 1 GiB of hashes at depth 24). Keep it if you will mine more cards from the same epoch. Check your root equals dagRoot before relying on it.

Each accessed page has a branch: depth sibling hashes from the leaf up. Pack them into the multiproof the contract takes:

pages    = 8192 bytes: the 64 accessed pages in access order
order    = 64 bytes: the access numbers 0..63 sorted by page index (equal indices: lower access first)

nodes = the distinct pages in `order`, as (index, leaf hash)      # a repeated page appears once
dagProof = []
for level in 0 .. depth - 1:
    next = []; k = 0
    while k < len(nodes):
        x = nodes[k]
        if x.index is even and k + 1 < len(nodes) and nodes[k+1].index == x.index + 1:
            parent = node(x.hash, nodes[k+1].hash);  k += 2          # both children known
        else:
            s = the sibling at this level from the branch of any access that reaches x
            append s to dagProof
            parent = node(s, x.hash) if x.index is odd else node(x.hash, s);  k += 1
        next.append((x.index >> 1, parent))
    nodes = next
# nodes is now [(0, dagRoot)]; dagProof is packed 32-byte hashes, about 1,040 at depth 24

The contract replays exactly this walk and rejects any unused, missing or extra dagProof hash, an order that is not a permutation sorted by page index, and a repeated page whose bytes differ.

8. Submit

For a mint, card and cardProof come from the card shard: the entry with this tokenId has data (the card) and proof (13 hashes). Its leaf is keccak256(keccak256(abi.encode(keccak256("Card(uint256 tokenId,bytes32 data)"), tokenId, card))), with sorted pairs against cardRoot().

mint(record, collectionProof, recipient, nonce, pages, order, dagProof, card, cardProof)
levelUp(record, collectionProof, nonce, pages, order, dagProof)

mintZK and levelUpZK are optional, cheaper alternatives to sections 7 and 8; see section 11.

Before a level-up, check usedLevelNonces(tokenId, nonce). Simulate with eth_call and eth_estimateGas from the actual sender, and allow for about 1.8 million gas (mint) or 1.75 million (level-up). State can change after simulation, so check the receipt: a successful one has a WorkAccepted event. Any address may send either action; a mint benefits its bound recipient and a level-up benefits whoever holds the card.

9. Search and persistence

Start from a cryptographically random uint64 nonce and search sequentially, with wraparound. Give parallel workers disjoint ranges. Save found solutions keyed by deployment, token, action, challenge and difficulty. Keep unused level-up solutions: they stay valid across transfers and level changes. Skip nonces already accepted on-chain. Failed attempts do not accumulate, so restarting from a fresh range costs nothing.

10. Things that will waste your time

11. Optional: submit with a zk proof

mintZK and levelUpZK replace pages, order and dagProof with a proof that the work check passed off chain. Sections 1 to 6 (record, challenge, dataset, hashimoto) are unchanged, and a level-up nonce counts once whichever path sends it: both share usedLevelNonces.

struct WorkStatement {
  bytes32 challenge; uint64 nonce; bytes32 dagRoot; uint32 pageCount;
  uint256 difficulty; bytes32 mixDigest; bytes32 result;
}
publicValues = abi.encode(WorkStatement)       # 224 bytes
proof        = SP1 Groth16 proof               # 356 bytes, begins with the verifier's 4-byte VERIFIER_HASH prefix

mintZK(record, collectionProof, recipient, card, cardProof, publicValues, proof)
levelUpZK(record, collectionProof, publicValues, proof)

The proof is of the ETH2015 SP1 program, whose verification key is zkProgramVKey() (also zkProgramVKey in config.json). That program takes the challenge, nonce, dagRoot, pageCount, difficulty, the 64 accessed pages in access order and each access's full Merkle branch from leaf to root (section 7, before packing). It reruns hashimoto, checks every page against dagRoot, checks the result meets the difficulty, and commits the WorkStatement. A proof of any other program is rejected. Proofs are made with SP1 SDK 6.8.1 in Groth16 mode. zkVerifier() is Succinct's SP1 verifier gateway: it reads the first 4 bytes of the proof, sends it to the verifier for that SP1 version (v6.1.0, selector 0x4388a21c) and refuses versions Succinct has frozen (RouteIsFrozen).

The contract verifies the proof against zkProgramVKey, then requires the statement's challenge to equal challengeFor(action, tokenId, recipient) and its dagRoot and pageCount to equal the record's (StatementMismatch otherwise). It then checks result against requiredDifficulty(record.difficulty) itself (InsufficientWork otherwise): the difficulty inside the statement is informational.