Eric Mastro ad040cfee6 [marketplace] extend proof ending
Allow proof ending to be extending once a contract is started, so that all filled slots share an ending time that is equal to the contract end time. Added tests.

Add a mapping for proof id to endId so that proof expectations can be extended for all proofs that share a given endId.

Add function modifiers that require the request state to allow proofs, with accompanying tests.

General clean up of each function’s request state context, with accompanying tests.

General clean up of all tests, including state change “wait” functions and normalising the time advancement functions.
2022-10-25 12:38:19 +11:00

317 lines
11 KiB
JavaScript

const { expect } = require("chai")
const { ethers, deployments } = require("hardhat")
const { BigNumber } = ethers
const { hexlify, randomBytes } = ethers.utils
const { AddressZero } = ethers.constants
const { exampleRequest } = require("./examples")
const { advanceTime, advanceTimeTo, currentTime, mine } = require("./evm")
const { requestId, slotId } = require("./ids")
const { periodic } = require("./time")
const { price } = require("./price")
const {
waitUntilCancelled,
waitUntilStarted,
waitUntilFinished,
} = require("./marketplace")
describe("Storage", function () {
const proof = hexlify(randomBytes(42))
let storage
let token
let client, host
let request
let collateralAmount, slashMisses, slashPercentage
let slot
function switchAccount(account) {
token = token.connect(account)
storage = storage.connect(account)
}
beforeEach(async function () {
;[client, host] = await ethers.getSigners()
await deployments.fixture(["TestToken", "TestStorage"])
token = await ethers.getContract("TestToken")
storage = await ethers.getContract("TestStorage")
await token.mint(client.address, 1_000_000_000)
await token.mint(host.address, 1_000_000_000)
collateralAmount = await storage.collateralAmount()
slashMisses = await storage.slashMisses()
slashPercentage = await storage.slashPercentage()
minCollateralThreshold = await storage.minCollateralThreshold()
request = exampleRequest()
request.client = client.address
slot = {
request: requestId(request),
index: request.ask.slots / 2,
}
switchAccount(client)
await token.approve(storage.address, price(request))
await storage.requestStorage(request)
switchAccount(host)
await token.approve(storage.address, collateralAmount)
await storage.deposit(collateralAmount)
})
it("can retrieve storage requests", async function () {
const id = requestId(request)
const retrieved = await storage.getRequest(id)
expect(retrieved.client).to.equal(request.client)
expect(retrieved.expiry).to.equal(request.expiry)
expect(retrieved.nonce).to.equal(request.nonce)
})
it("can retrieve host that filled slot", async function () {
expect(await storage.getHost(slotId(slot))).to.equal(AddressZero)
await storage.fillSlot(slot.request, slot.index, proof)
expect(await storage.getHost(slotId(slot))).to.equal(host.address)
})
describe("ending the contract", function () {
it("unlocks the host collateral", async function () {
await storage.fillSlot(slot.request, slot.index, proof)
await waitUntilFinished(storage, slotId(slot))
await expect(storage.withdraw()).not.to.be.reverted
})
})
describe("missing proofs", function () {
let period, periodOf, periodEnd
beforeEach(async function () {
period = (await storage.proofPeriod()).toNumber()
;({ periodOf, periodEnd } = periodic(period))
})
async function waitUntilProofIsRequired(id) {
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
while (
!(
(await storage.isProofRequired(id)) &&
(await storage.getPointer(id)) < 250
)
) {
await advanceTime(period)
}
}
describe("slashing when missing proofs", function () {
it("reduces collateral when too many proofs are missing", async function () {
const id = slotId(slot)
await storage.fillSlot(slot.request, slot.index, proof)
for (let i = 0; i < slashMisses; i++) {
await waitUntilProofIsRequired(id)
let missedPeriod = periodOf(await currentTime())
await advanceTime(period)
await storage.markProofAsMissing(id, missedPeriod)
}
const expectedBalance =
(collateralAmount * (100 - slashPercentage)) / 100
expect(await storage.balanceOf(host.address)).to.equal(expectedBalance)
})
})
describe("freeing a slot", function () {
it("frees slot when collateral slashed below minimum threshold", async function () {
const id = slotId(slot)
await waitUntilAllSlotsFilled(
storage,
request.ask.slots,
slot.request,
proof
)
// max slashes before dropping below collateral threshold
const maxSlashes = 10
for (let i = 0; i < maxSlashes; i++) {
for (let j = 0; j < slashMisses; j++) {
await waitUntilProofIsRequired(id)
let missedPeriod = periodOf(await currentTime())
await advanceTime(period)
if (i === maxSlashes - 1 && j === slashMisses - 1) {
await expect(
await storage.markProofAsMissing(id, missedPeriod)
).to.emit(storage, "SlotFreed")
await expect(storage.getSlot(id)).to.be.revertedWith("Slot empty")
} else {
await storage.markProofAsMissing(id, missedPeriod)
}
}
}
})
})
})
describe("contract state", function () {
let period, periodOf, periodEnd
beforeEach(async function () {
period = (await storage.proofPeriod()).toNumber()
;({ periodOf, periodEnd } = periodic(period))
})
async function waitUntilProofWillBeRequired(id) {
while (!(await storage.willProofBeRequired(id))) {
await mine()
}
}
async function waitUntilProofIsRequired(id) {
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
while (
!(
(await storage.isProofRequired(id)) &&
(await storage.getPointer(id)) < 250
)
) {
await advanceTime(period)
}
}
it("fails to mark proof as missing when cancelled", async function () {
await storage.fillSlot(slot.request, slot.index, proof)
await waitUntilCancelled(request.expiry)
let missedPeriod = periodOf(await currentTime())
await expect(
storage.markProofAsMissing(slotId(slot), missedPeriod)
).to.be.revertedWith("Slot not accepting proofs")
})
it("will not require proofs once cancelled", async function () {
const id = slotId(slot)
await storage.fillSlot(slot.request, slot.index, proof)
await waitUntilProofWillBeRequired(id)
await expect(await storage.willProofBeRequired(id)).to.be.true
await advanceTimeTo(request.expiry + 1)
await expect(await storage.willProofBeRequired(id)).to.be.false
})
it("does not require proofs once cancelled", async function () {
const id = slotId(slot)
await storage.fillSlot(slot.request, slot.index, proof)
await waitUntilProofIsRequired(id)
await expect(await storage.isProofRequired(id)).to.be.true
await advanceTimeTo(request.expiry + 1)
await expect(await storage.isProofRequired(id)).to.be.false
})
it("does not provide challenges once cancelled", async function () {
const id = slotId(slot)
await storage.fillSlot(slot.request, slot.index, proof)
await waitUntilProofIsRequired(id)
const challenge1 = await storage.getChallenge(id)
expect(BigNumber.from(challenge1).gt(0))
await advanceTimeTo(request.expiry + 1)
const challenge2 = await storage.getChallenge(id)
expect(BigNumber.from(challenge2).isZero())
})
it("does not provide pointer once cancelled", async function () {
const id = slotId(slot)
await storage.fillSlot(slot.request, slot.index, proof)
await waitUntilProofIsRequired(id)
const challenge1 = await storage.getChallenge(id)
expect(BigNumber.from(challenge1).gt(0))
await advanceTimeTo(request.expiry + 1)
const challenge2 = await storage.getChallenge(id)
expect(BigNumber.from(challenge2).isZero())
})
})
describe("freeing a slot", function () {
beforeEach(async function () {
period = (await storage.proofPeriod()).toNumber()
;({ periodOf, periodEnd } = periodic(period))
})
async function waitUntilProofIsRequired(id) {
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
while (
!(
(await storage.isProofRequired(id)) &&
(await storage.getPointer(id)) < 250
)
) {
await advanceTime(period)
}
}
async function markProofAsMissing(slotId, onMarkAsMissing) {
for (let i = 0; i < slashMisses; i++) {
await waitUntilProofIsRequired(slotId)
let missedPeriod = periodOf(await currentTime())
await advanceTime(period)
if (i === slashMisses - 1 && typeof onMarkAsMissing === "function") {
onMarkAsMissing(missedPeriod)
} else await storage.markProofAsMissing(slotId, missedPeriod)
}
}
it("frees slot when collateral slashed below minimum threshold", async function () {
const id = slotId(slot)
await waitUntilStarted(storage, request.ask.slots, slot.request, proof)
while (true) {
await markProofAsMissing(id)
let balance = await storage.balanceOf(host.address)
let slashAmount = await storage.slashAmount(
host.address,
slashPercentage
)
if (balance - slashAmount < minCollateralThreshold) {
break
}
}
let onMarkAsMissing = async function (missedPeriod) {
await expect(
await storage.markProofAsMissing(id, missedPeriod)
).to.emit(storage, "SlotFreed")
}
await markProofAsMissing(id, onMarkAsMissing)
await expect(storage.getSlot(id)).to.be.revertedWith("Slot empty")
})
})
describe("contract state", function () {
it("isCancelled is true once request is cancelled", async function () {
await expect(await storage.isCancelled(slot.request)).to.equal(false)
await waitUntilCancelled(request.expiry)
await expect(await storage.isCancelled(slot.request)).to.equal(true)
})
it("isSlotCancelled fails when slot is empty", async function () {
await expect(storage.isSlotCancelled(slotId(slot))).to.be.revertedWith(
"Slot empty"
)
})
it("isSlotCancelled is true once request is cancelled", async function () {
await storage.fillSlot(slot.request, slot.index, proof)
await waitUntilCancelled(request.expiry)
await expect(await storage.isSlotCancelled(slotId(slot))).to.equal(true)
})
it("isFinished is true once started and contract duration lapses", async function () {
await expect(await storage.isFinished(slot.request)).to.be.false
// fill all slots, should change state to RequestState.Started
await waitUntilStarted(storage, request.ask.slots, slot.request, proof)
await expect(await storage.isFinished(slot.request)).to.be.false
advanceTime(request.ask.duration + 1)
await expect(await storage.isFinished(slot.request)).to.be.true
})
})
})
// TODO: implement checking of actual proofs of storage, instead of dummy bool
// TODO: allow other host to take over contract when too many missed proofs
// TODO: small partial payouts when proofs are being submitted
// TODO: reward caller of markProofAsMissing