From 78334edccf39ec9eb921f4881e0b62cf9e79775b Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Mon, 16 Jun 2025 15:18:49 +0200 Subject: [PATCH] feat(tests): calculate maximum deviation for gas estimates --- test/GasUsage.test.js | 171 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 test/GasUsage.test.js diff --git a/test/GasUsage.test.js b/test/GasUsage.test.js new file mode 100644 index 0000000..5a5d53c --- /dev/null +++ b/test/GasUsage.test.js @@ -0,0 +1,171 @@ +const { ethers } = require("hardhat") +const { + exampleRequest, + exampleProof, + exampleConfiguration, +} = require("./examples") +const { maxPrice } = require("./price") +const { collateralPerSlot } = require("./collateral") +const { requestId, slotId } = require("./ids") +const { + revert, + snapshot, + ensureMinimumBlockHeight, + advanceTimeTo, + currentTime, +} = require("./evm") +const { expect } = require("chai") +const { patchOverloads } = require("./marketplace") +const { periodic } = require("./time") +const { RequestState } = require("./requests") + +// Ensures that the actual gas costs can never deviate from the gas estimate by +// more than a certain percentage. +// The percentages from these tests should be used to pad the gas estimates in +// market.nim, for example when calling fillSlot: +// https://github.com/codex-storage/nim-codex/blob/6db6bf5f72a0038b77d02f48dcf128b4d77b469a/codex/contracts/market.nim#L278 +describe("Marketplace gas estimates", function () { + let marketplace + let token + let signer + + async function setupToken() { + const Token = await ethers.getContractFactory("TestToken") + const token = await Token.deploy() + await token.mint(signer.address, 1_000_000_000_000_000) + return token + } + + async function setupMarketplace() { + const Marketplace = await ethers.getContractFactory("Marketplace") + const Verifier = await ethers.getContractFactory("TestVerifier") + const verifier = await Verifier.deploy() + await ensureMinimumBlockHeight(256) + const marketplace = await Marketplace.deploy( + exampleConfiguration(), + token.address, + verifier.address + ) + patchOverloads(marketplace) + return marketplace + } + + async function requestStorage() { + const request = exampleRequest() + request.client = signer.address + await token.approve(marketplace.address, maxPrice(request)) + await marketplace.requestStorage(request) + return request + } + + async function startRequest(request) { + const id = requestId(request) + for (let i = 0; i < request.ask.slots; i++) { + await marketplace.reserveSlot(id, i) + await token.approve(marketplace.address, collateralPerSlot(request)) + await marketplace.fillSlot(id, i, exampleProof()) + } + } + + beforeEach(async function () { + await snapshot() + signer = (await ethers.getSigners())[0] + token = await setupToken() + marketplace = await setupMarketplace() + }) + + afterEach(async function () { + await revert() + }) + + describe("reserveSlot", function () { + it("has at most 1% deviation in gas usage", async function () { + const request = await requestStorage() + const id = requestId(request) + const gasUsage = [] + for (let i = 0; i < request.ask.slots; i++) { + const transaction = await marketplace.reserveSlot(id, i) + const receipt = await transaction.wait() + gasUsage.push(receipt.gasUsed.toNumber()) + await token.approve(marketplace.address, collateralPerSlot(request)) + await marketplace.fillSlot(id, i, exampleProof()) + } + const deviation = Math.max(...gasUsage) / Math.min(...gasUsage) - 1.0 + expect(deviation).to.be.gt(0) + expect(deviation).to.be.lte(0.01) + }) + }) + + describe("fillSlot", function () { + it("has at most 10% deviation in gas usage", async function () { + const request = await requestStorage() + const id = requestId(request) + const gasUsage = [] + for (let i = 0; i < request.ask.slots; i++) { + await marketplace.reserveSlot(id, i) + await token.approve(marketplace.address, collateralPerSlot(request)) + const transaction = await marketplace.fillSlot(id, i, exampleProof()) + const receipt = await transaction.wait() + gasUsage.push(receipt.gasUsed.toNumber()) + } + const deviation = Math.max(...gasUsage) / Math.min(...gasUsage) - 1.0 + expect(deviation).to.be.gt(0) + expect(deviation).to.be.lte(0.1) + }) + }) + + describe("freeSlot", function () { + it("has at most 200% deviation in gas usage", async function () { + const request = await requestStorage() + const id = requestId(request) + await startRequest(request) + const gasUsage = [] + for (let i = 0; i < request.ask.slots; i++) { + const slot = { request: id, index: i } + const transaction = await marketplace.freeSlot(slotId(slot)) + const receipt = await transaction.wait() + gasUsage.push(receipt.gasUsed.toNumber()) + } + const deviation = Math.max(...gasUsage) / Math.min(...gasUsage) - 1.0 + expect(deviation).to.be.gt(0) + expect(deviation).to.be.lte(2.0) + }) + }) + + describe("markProofAsMissing", function () { + let period, periodOf, periodEnd + + beforeEach(async function () { + const configuration = await marketplace.configuration() + period = configuration.proofs.period + ;({ periodOf, periodEnd } = periodic(period)) + }) + + it("has at most 50% deviation in gas usage", async function () { + const request = await requestStorage() + const id = requestId(request) + await startRequest(request) + const gasUsage = [] + while ((await marketplace.requestState(id)) != RequestState.Failed) { + const missingPeriod = periodOf(await currentTime()) + await advanceTimeTo(periodEnd(missingPeriod) + 1) + for (let i = 0; i < request.ask.slots; i++) { + try { + const slot = { request: id, index: i } + const transaction = await marketplace.markProofAsMissing( + slotId(slot), + missingPeriod + ) + const receipt = await transaction.wait() + gasUsage.push(receipt.gasUsed.toNumber()) + } catch (exception) { + // ignore: proof might not be missing + } + } + } + const deviation = Math.max(...gasUsage) / Math.min(...gasUsage) - 1.0 + expect(deviation).to.be.gt(0) + expect(deviation).to.be.lte(0.5) + }) + }) +})