From 92b796f537999e2c2f31b0aad9f6b9248080c6d4 Mon Sep 17 00:00:00 2001 From: markspanbroek Date: Wed, 13 Aug 2025 16:36:57 +0200 Subject: [PATCH] fix(tests): maximum gas deviation for reserveSlot (#250) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adam Uhlíř --- test/GasUsage.test.js | 181 ++++++++++++++++++++++++++++++++++ test/Marketplace.test.js | 4 +- test/SlotReservations.test.js | 2 +- test/examples.js | 2 +- 4 files changed, 185 insertions(+), 4 deletions(-) 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..9a19c1c --- /dev/null +++ b/test/GasUsage.test.js @@ -0,0 +1,181 @@ +const { ethers, ignition } = 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") +const MarketplaceModule = require("../ignition/modules/marketplace") + +// 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 requestStorage() { + const request = exampleRequest() + request.client = signer.address + await token.approve(await marketplace.getAddress(), 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( + await marketplace.getAddress(), + collateralPerSlot(request), + ) + await marketplace.fillSlot(id, i, exampleProof()) + } + } + + beforeEach(async function () { + await snapshot() + await ensureMinimumBlockHeight(256) + + signer = (await ethers.getSigners())[0] + + const { testMarketplace, token: _token } = await ignition.deploy( + MarketplaceModule, + { + parameters: { + Marketplace: { + configuration: exampleConfiguration(), + }, + }, + }, + ) + + marketplace = testMarketplace + patchOverloads(marketplace) + + token = _token + for (let account of await ethers.getSigners()) { + await token.mint(account.address, 1_000_000_000_000_000n) + } + }) + + afterEach(async function () { + await revert() + }) + + describe("reserveSlot", function () { + it("has at most 25% deviation in gas usage", async function () { + const request = await requestStorage() + const id = requestId(request) + const gasUsage = [] + for (let signer of await ethers.getSigners()) { + marketplace = marketplace.connect(signer) + for (let i = 0; i < request.ask.slots; i++) { + try { + const transaction = await marketplace.reserveSlot(id, i) + const receipt = await transaction.wait() + gasUsage.push(Number(receipt.gasUsed)) + } catch (exception) { + // ignore: reservations can be full + } + } + } + const deviation = Math.max(...gasUsage) / Math.min(...gasUsage) - 1.0 + expect(deviation).to.be.gt(0) + expect(deviation).to.be.lte(0.25) + }) + }) + + 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( + await marketplace.getAddress(), + collateralPerSlot(request), + ) + const transaction = await marketplace.fillSlot(id, i, exampleProof()) + const receipt = await transaction.wait() + gasUsage.push(Number(receipt.gasUsed)) + } + 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(Number(receipt.gasUsed)) + } + 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 = Number(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(Number(receipt.gasUsed)) + } 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) + }) + }) +}) diff --git a/test/Marketplace.test.js b/test/Marketplace.test.js index 153f9a1..ee3311a 100644 --- a/test/Marketplace.test.js +++ b/test/Marketplace.test.js @@ -168,7 +168,7 @@ describe("Marketplace", function () { patchOverloads(marketplace) - request = await exampleRequest() + request = exampleRequest() request.client = client.address slot = { @@ -426,7 +426,7 @@ describe("Marketplace", function () { }) it("is rejected when request is unknown", async function () { - let unknown = await exampleRequest() + let unknown = exampleRequest() await expect( marketplace.fillSlot(requestId(unknown), 0, proof), ).to.be.revertedWithCustomError(marketplace, "Marketplace_UnknownRequest") diff --git a/test/SlotReservations.test.js b/test/SlotReservations.test.js index 5817888..4d2d1b3 100644 --- a/test/SlotReservations.test.js +++ b/test/SlotReservations.test.js @@ -30,7 +30,7 @@ describe("SlotReservations", function () { reservations = testSlotReservations ;[provider, address1, address2, address3] = await ethers.getSigners() - request = await exampleRequest() + request = exampleRequest() reqId = requestId(request) slotIndex = request.ask.slots / 2 slot = { diff --git a/test/examples.js b/test/examples.js index 98d2fb8..e6d173a 100644 --- a/test/examples.js +++ b/test/examples.js @@ -21,7 +21,7 @@ const exampleConfiguration = () => ({ requestDurationLimit: 60 * 60 * 24 * 30, // 30 days }) -const exampleRequest = async () => { +const exampleRequest = () => { return { client: hexlify(randomBytes(20)), ask: {