a manifold extension to port fully onchain an existing collection minted with offchain content
clgsms/scripts/render-ceiling.js
JavaScript · 145 lines · 5 KBRAWDOWNLOAD
/**
* 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";