/** * What does it cost to store an artwork, and to serve it? * * npm run ceiling * SIZES=116,250,600,964 npm run ceiling * * For each image size, uploads a synthetic artwork both ways and measures the * gas `tokenURI` needs, on a local chain with the gas caps lifted (the `probe` * network) so the measurement reaches the renderer rather than the chain. * * pre-encoded the base64 of the image is stored, so rendering is a straight * copy — cheapest to serve, 33% more to store * raw the image is stored and base64'd at render time, one 3-aligned * chunk at a time — cheapest to store * * Two ceilings matter, and neither is the block gas limit: * - 16,777,216 gas — EIP-7825's per-transaction cap. Above this nothing can * call tokenURI from inside a transaction, and eth_estimateGas refuses. * - ~50,000,000 gas — the eth_call cap on a standard node, and what a * marketplace fetching tokenURI gets. Above it the token renders as broken. * * Run this before committing to an encode size, and again after any renderer * change. */ import { CHUNK, artworkKey, assertLocalChain, base64Chunks, connect, main, rawChunks } from "./_lib.js"; const TX_CAP = 16_777_216n; const CALL_CAP = 50_000_000n; main(async () => { const { ethers, net } = await connect(); await assertLocalChain(ethers, "scripts/render-ceiling.js"); if (net !== "probe") { console.log( "! run this against the uncapped local chain, otherwise the search stops\n" + " at the chain's own cap: npm run ceiling\n", ); } const [me] = await ethers.getSigners(); const blobs = await ethers.deployContract("ColorgasmStorage"); await blobs.waitForDeployment(); const ext = await ethers.deployContract("ColorgasmRedeemExtension", [me.address, 1, 100]); await ext.waitForDeployment(); await (await ext.setBlobStorage(await blobs.getAddress())).wait(); const renderers = {}; for (const preEncoded of [true, false]) { const r = await ethers.deployContract("ColorgasmRendererV3", [ await ext.getAddress(), await blobs.getAddress(), preEncoded, ]); await r.waitForDeployment(); renderers[preEncoded ? "pre" : "raw"] = r; } const maxGas = BigInt(process.env.MAX_GAS ?? 400_000_000); /** Smallest gas cap at which an eth_call to tokenURI succeeds. */ async function costOf(renderer, id) { const to = await renderer.getAddress(); const data = renderer.interface.encodeFunctionData("tokenURI", [me.address, id, id]); const worksAt = async (gas) => { try { await ethers.provider.send("eth_call", [ { to, data, gas: "0x" + gas.toString(16) }, "latest", ]); return true; } catch { return false; } }; let lo = 100_000n; let hi = maxGas; if (!(await worksAt(hi))) return null; while (hi - lo > 100_000n) { const mid = (lo + hi) / 2n; if (await worksAt(mid)) hi = mid; else lo = mid; } return hi; } const sizes = (process.env.SIZES ?? "100,250,400,600,964").split(",").map(Number); const shapes = (process.env.SHAPES ?? "pre,raw").split(","); let id = 0; console.log( " imageKB shape chunks storeGas render(utf8) render(b64) verdict", ); for (const kb of sizes) { const image = Buffer.alloc(kb * 1024); for (let i = 0; i < image.length; i++) image[i] = (i * 7 + 13) & 0xff; for (const shape of shapes) { id++; const key = artworkKey(id); const chunks = shape === "pre" ? base64Chunks(image) : rawChunks(image); let storeGas = 0n; for (const chunk of chunks) { storeGas += (await (await blobs.append(key, chunk)).wait()).gasUsed; } await (await ext.setArtwork(id, key, ethers.toUtf8Bytes('"name":"probe"'))).wait(); const renderer = renderers[shape]; await (await renderer.setBase64Json(false)).wait(); const utf8 = await costOf(renderer, id); await (await renderer.setBase64Json(true)).wait(); const b64 = process.env.SKIP_B64 === "1" ? undefined : await costOf(renderer, id); const best = utf8; const verdict = best === null ? "unrenderable" : best < TX_CAP ? "onchain-callable" : best < CALL_CAP ? "renders" : "TOO BIG"; console.log( ` ${String(kb).padStart(7)} ${shape === "pre" ? "pre-encoded" : "raw "} ` + `${String(chunks.length).padStart(6)} ${fmt(storeGas)} ${fmt(utf8)} ${fmt(b64)} ${verdict}`, ); } } console.log( `\nchunk size ${CHUNK} B (SSTORE2 max, rounded down to a multiple of 3)\n` + `onchain-callable = under EIP-7825's ${TX_CAP} gas per-transaction cap\n` + `renders = under a ~${CALL_CAP} gas eth_call cap (standard node default)\n` + `verdict uses the UTF-8 document, which is what you should serve.`, ); }); const fmt = (g) => g === undefined ? " skipped" : g === null ? " over cap" : (Number(g) / 1e6).toFixed(1).padStart(10) + "M";