feat: move to hardhat ignition for deployments (#231)

* Move to ethers 6 and use hardhat ignition for deployments

* Update prettier to the last version

* Remove comment

* Remove npx call in package.json

* Remove useless comment

* Add localhost configuration for hardhat ignition

* Use contract initial balance instead of const value

* Update dependencies and use extract mining to a script in order to use hardhat ignition deploy command

* Fix deployment modules for local env

* Remove unused function

* Export contract deployment custom error assert into a function

* Refactoring

* Remove old deployments folder

* Add process names when running concurrently

* Remove conditional allowBlocksWithSameTimestamp, set true everytime

* Update dependencies

* Update Vault tests for ignition

* Update token description

* Add vault ignition module

* Remove old .tool-versions

* Fix formatting

* Remove deployments folder and add README for previous files references

* Put back the comment related to hardhat automine

* Set hardhat gas limit to auto and restore manual mining for Vault tests

* Apply prettier formatting and bug test with ignition syntax

* Add deployments artifacts

* Fix build-info ignore

* Use HARDHAT_NETWORK env variable to deploy marketplace contract

* Add guard to check that configs has tags

* Add testnet deployment addresses

* Add TOKEN_ADDRESS to reuse the token contract deployed

* Fix token deployment with contractAt

* Remove localhost deployment artifacts

* Add section in README for deployments

* Ignore localhost deployments in git

* Set mine script for localhost deployment only and add deploy reset command

* Remove previous deployment scripts

* Fix typo in documentation

* Add log when reusing token address

* Update testnet artifact reference

* Remove HARDHAT_NETWORK and update documentation

* fix md format

* Npm audit fix

* Update dependencies

* Remove default deployer

* Update commit for last testnet artifacts

* Remove deployments files from linea and testnet and update the last commit hashes to those artifacts
This commit is contained in:
Arnaud 2025-06-20 16:05:57 +02:00 committed by GitHub
parent 2dddc26015
commit dee3d7b654
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
69 changed files with 8116 additions and 40131 deletions

View File

@ -10,7 +10,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }} group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
formatting: formatting:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -26,10 +25,9 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
# workaround for https://github.com/NomicFoundation/hardhat/issues/3877
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 18.15 node-version: 22
- run: npm install - run: npm install
- run: npm test - run: npm test
- uses: actions/cache@v4 - uses: actions/cache@v4
@ -53,9 +51,9 @@ jobs:
- name: Install Java - name: Install Java
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'zulu' distribution: "zulu"
java-version: '11' java-version: "11"
java-package: 'jre' java-package: "jre"
- name: Install Certora CLI - name: Install Certora CLI
run: pip3 install certora-cli==7.10.2 run: pip3 install certora-cli==7.10.2
@ -88,4 +86,3 @@ jobs:
rule: rule:
- verify:marketplace - verify:marketplace
- verify:state_changes - verify:state_changes

7
.gitignore vendored
View File

@ -4,3 +4,10 @@ artifacts
deployment-localhost.json deployment-localhost.json
crytic-export crytic-export
.certora_internal .certora_internal
coverage
coverage.json
# Ignore localhost deployments files
ignition/deployments/chain-31337
ignition/deployments/**/build-info

View File

@ -1 +0,0 @@
nodejs 18.15.0

View File

@ -22,9 +22,28 @@ To start a local Ethereum node with the contracts deployed, execute:
npm start npm start
This will create a `deployment-localhost.json` file containing the addresses of Deployment
the deployed contracts. ----------
To deploy the marketplace, you need to specify the network using `--network MY_NETWORK`:
```bash
npm run deploy -- --network localhost
```
Hardhat uses [reconciliation](https://hardhat.org/ignition/docs/advanced/reconciliation) to recover from
errors or resume a previous deployment. In our case, we will likely redeploy a new contract every time,
so we will need to [clear the previous deployment](https://hardhat.org/ignition/docs/guides/modifications#clearing-an-existing-deployment-with-reset):
```bash
npm run deploy -- --network testnet --reset
```
To reuse a previously deployed `Token` contract, define the environment variable `TOKEN_ADDRESS`.
The deployment script will use `contractAt` from Hardhat Ignition to retrieve the existing contract
instead of deploying a new one.
The deployment files are kept under version control [as recommended by Hardhat](https://hardhat.org/ignition/docs/advanced/versioning), except the build files, which are 18 MB.
Running the prover Running the prover
------------------ ------------------

View File

@ -1,53 +0,0 @@
const { loadZkeyHash } = require("../verifier/verifier.js")
const { loadConfiguration } = require("../configuration/configuration.js")
async function mine256blocks({ network, ethers }) {
if (network.tags.local) {
await ethers.provider.send("hardhat_mine", ["0x100"])
}
}
// deploys a marketplace with a real Groth16 verifier
async function deployMarketplace({ deployments, getNamedAccounts }) {
const token = await deployments.get("TestToken")
const verifier = await deployments.get("Groth16Verifier")
const zkeyHash = loadZkeyHash(network.name)
let configuration = loadConfiguration(network.name)
configuration.proofs.zkeyHash = zkeyHash
const args = [configuration, token.address, verifier.address]
const { deployer: from } = await getNamedAccounts()
const marketplace = await deployments.deploy("Marketplace", { args, from })
console.log("Deployed Marketplace with Groth16 Verifier at:")
console.log(marketplace.address)
console.log()
}
// deploys a marketplace with a testing verifier
async function deployTestMarketplace({
network,
deployments,
getNamedAccounts,
}) {
if (network.tags.local) {
const token = await deployments.get("TestToken")
const verifier = await deployments.get("TestVerifier")
const zkeyHash = loadZkeyHash(network.name)
let configuration = loadConfiguration(network.name)
configuration.proofs.zkeyHash = zkeyHash
const args = [configuration, token.address, verifier.address]
const { deployer: from } = await getNamedAccounts()
const marketplace = await deployments.deploy("Marketplace", { args, from })
console.log("Deployed Marketplace with Test Verifier at:")
console.log(marketplace.address)
console.log()
}
}
module.exports = async (environment) => {
await mine256blocks(environment)
await deployMarketplace(environment)
await deployTestMarketplace(environment)
}
module.exports.tags = ["Marketplace"]
module.exports.dependencies = ["TestToken", "Verifier"]

View File

@ -1,36 +0,0 @@
const MINTED_TOKENS = 1_000_000_000_000_000
module.exports = async ({
deployments,
getNamedAccounts,
getUnnamedAccounts,
network,
}) => {
const { deployer } = await getNamedAccounts()
const tokenDeployment = await deployments.deploy("TestToken", {
from: deployer,
skipIfAlreadyDeployed: true,
})
const token = await hre.ethers.getContractAt(
"TestToken",
tokenDeployment.address
)
const accounts = [
...Object.values(await getNamedAccounts()),
...(await getUnnamedAccounts()),
]
if (network.tags.local) {
for (const account of accounts) {
console.log(`Minting ${MINTED_TOKENS} tokens to address ${account}`)
const transaction = await token.mint(account, MINTED_TOKENS, {
from: deployer,
})
await transaction.wait()
}
console.log()
}
}
module.exports.tags = ["TestToken"]

View File

@ -1,24 +0,0 @@
const { loadVerificationKey } = require("../verifier/verifier.js")
async function deployVerifier({ deployments, getNamedAccounts }) {
const { deployer } = await getNamedAccounts()
const verificationKey = loadVerificationKey(network.name)
await deployments.deploy("Groth16Verifier", {
args: [verificationKey],
from: deployer,
})
}
async function deployTestVerifier({ network, deployments, getNamedAccounts }) {
if (network.tags.local) {
const { deployer } = await getNamedAccounts()
await deployments.deploy("TestVerifier", { from: deployer })
}
}
module.exports = async (environment) => {
await deployVerifier(environment)
await deployTestVerifier(environment)
}
module.exports.tags = ["Verifier"]

View File

@ -1,2 +0,0 @@
localhost
codexdisttestnetwork

View File

@ -1 +0,0 @@
789987

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
1660990954

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
167005

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,5 @@
require("@nomiclabs/hardhat-waffle") require("@nomicfoundation/hardhat-toolbox")
require("hardhat-deploy") require("@nomicfoundation/hardhat-ignition-ethers")
require("hardhat-deploy-ethers")
module.exports = { module.exports = {
solidity: { solidity: {
@ -25,6 +24,10 @@ module.exports = {
hardhat: { hardhat: {
tags: ["local"], tags: ["local"],
allowBlocksWithSameTimestamp: true, allowBlocksWithSameTimestamp: true,
gas: "auto",
},
localhost: {
tags: ["local"],
}, },
codexdisttestnetwork: { codexdisttestnetwork: {
url: `${process.env.DISTTEST_NETWORK_URL}`, url: `${process.env.DISTTEST_NETWORK_URL}`,

33
ignition/README.md Normal file
View File

@ -0,0 +1,33 @@
# Deployment
Hardhat Ignition is now used to deploy the contracts, so the old
deployment files are no longer relevant.
However, the ABI of the contracts has changed due to an OpenZeppelin update.
If we ever need to recreate the artifacts from the previous ABI contracts (for any reason),
we can do so using a small script that imports the previously generated files.
Here is an example:
```js
module.exports = buildModule("Token", (m) => {
const previousJsonFile = path.join(__dirname, "./TestToken.json");
const artifact = JSON.parse(fs.readFileSync(previousJsonFile, "utf8"));
const address = artifact.address;
const token = m.contractAt("TestToken", address, {});
return { token };
});
```
Then we can run:
```bash
npx hardhat ignition deploy ignition/modules/migration/token.js --network taiko_test
```
**Note:** Check [this comment](https://github.com/codex-storage/codex-contracts-eth/pull/231#issuecomment-2808996517) for more context.
Here is the list of previous commits containing the ABI contracts that were deployed:
- [Taiko](https://github.com/codex-storage/codex-contracts-eth/commit/1854dfba9991a25532de5f6a53cf50e66afb3c8b)
- [Testnet](https://github.com/codex-storage/codex-contracts-eth/commit/449d64ffc0dc1478d0690d36f037358084a17b09)
- [Linea](https://github.com/codex-storage/codex-contracts-eth/pull/226/commits/2dddc260152b6e9c24ae372397f9b9b2d27ce8e4)

View File

@ -0,0 +1,5 @@
{
"Token#TestToken": "0x34a22f3911De437307c6f4485931779670f78764",
"Verifier#Groth16Verifier": "0x1f60B2329775545AaeF743dbC3571e699405263e",
"Marketplace#Marketplace": "0xDB2908d724a15d05c0B6B8e8441a8b36E67476d3"
}

View File

@ -0,0 +1,9 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
module.exports = buildModule("Endian", (m) => {
const endian = m.contract("Endian", [], {})
const testEndian = m.contract("TestEndian", [], {})
return { endian, testEndian }
})

View File

@ -0,0 +1,43 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
const { loadZkeyHash } = require("../../verifier/verifier.js")
const { loadConfiguration } = require("../../configuration/configuration.js")
const TokenModule = require("./token.js")
const VerifierModule = require("./verifier.js")
function getDefaultConfig() {
const zkeyHash = loadZkeyHash(hre.network.name)
const config = loadConfiguration(hre.network.name)
config.proofs.zkeyHash = zkeyHash
return config
}
module.exports = buildModule("Marketplace", (m) => {
const { token } = m.useModule(TokenModule)
const { verifier } = m.useModule(VerifierModule)
const configuration = m.getParameter("configuration", getDefaultConfig())
const marketplace = m.contract(
"Marketplace",
[configuration, token, verifier],
{},
)
let testMarketplace
const config = hre.network.config
if (config && config.tags && config.tags.includes("local")) {
const { testVerifier } = m.useModule(VerifierModule)
testMarketplace = m.contract(
"TestMarketplace",
[configuration, token, testVerifier],
{},
)
}
return {
marketplace,
testMarketplace,
token,
}
})

View File

@ -0,0 +1,9 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
module.exports = buildModule("Periods", (m) => {
const secondsPerPeriod = m.getParameter("secondsPerPeriod", 0)
const periods = m.contract("Periods", [secondsPerPeriod], {})
return { periods }
})

View File

@ -0,0 +1,11 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
const VerifierModule = require("./verifier.js")
module.exports = buildModule("Proofs", (m) => {
const { verifier } = m.useModule(VerifierModule)
const configuration = m.getParameter("configuration", null)
const testProofs = m.contract("TestProofs", [configuration, verifier], {})
return { testProofs }
})

View File

@ -0,0 +1,13 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
module.exports = buildModule("SlotReservations", (m) => {
const configuration = m.getParameter("configuration", null)
const testSlotReservations = m.contract(
"TestSlotReservations",
[configuration],
{},
)
return { testSlotReservations }
})

31
ignition/modules/token.js Normal file
View File

@ -0,0 +1,31 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
const MAX_ACCOUNTS = 20
const MINTED_TOKENS = 1_000_000_000_000_000n
module.exports = buildModule("Token", (m) => {
let token
if (process.env.TOKEN_ADDRESS) {
console.log(
"Using existing TestToken on address: ",
process.env.TOKEN_ADDRESS,
)
token = m.contractAt("TestToken", process.env.TOKEN_ADDRESS, {})
} else {
token = m.contract("TestToken", [], {})
}
const config = hre.network.config
if (config && config.tags && config.tags.includes("local")) {
for (let i = 0; i < MAX_ACCOUNTS; i++) {
const account = m.getAccount(i)
m.call(token, "mint", [account, MINTED_TOKENS], {
id: `SendingTestTokens_${i}`,
})
}
}
return { token }
})

10
ignition/modules/vault.js Normal file
View File

@ -0,0 +1,10 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
const TokenModule = require("./token.js")
module.exports = buildModule("Vault", (m) => {
const { token } = m.useModule(TokenModule)
const vault = m.contract("Vault", [token], {})
return { vault, token }
})

View File

@ -0,0 +1,11 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
const { loadVerificationKey } = require("../../verifier/verifier.js")
module.exports = buildModule("Verifier", (m) => {
const verificationKey = loadVerificationKey(hre.network.name)
const verifier = m.contract("Groth16Verifier", [verificationKey], {})
const testVerifier = m.contract("TestVerifier", [], {})
return { verifier, testVerifier }
})

36014
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,29 +4,31 @@
"scripts": { "scripts": {
"test": "npm run lint && hardhat test", "test": "npm run lint && hardhat test",
"fuzz": "hardhat compile && fuzzing/fuzz.sh", "fuzz": "hardhat compile && fuzzing/fuzz.sh",
"start": "hardhat node --export deployment-localhost.json", "start": "concurrently --names \"hardhat,deployment\" --prefix \"[{time} {name}]\" \"hardhat node\" \"sleep 2 && npm run mine && npm run deploy -- --network localhost\"",
"compile": "hardhat compile", "compile": "hardhat compile",
"format": "prettier --write contracts/*.sol contracts/**/*.sol test/**/*.js", "format": "prettier --write test/**/*.js --plugin=prettier-plugin-solidity contracts/**/*.sol ",
"format:check": "prettier --check contracts/*.sol contracts/**/*.sol test/**/*.js", "format:check": "prettier --check test/**/*.js --plugin=prettier-plugin-solidity contracts/**/*.sol",
"lint": "solhint contracts/**.sol", "lint": "solhint contracts/**.sol",
"deploy": "hardhat deploy", "deploy": "hardhat ignition deploy ignition/modules/marketplace.js",
"mine": "hardhat run scripts/mine.js --network localhost",
"verify": "npm run verify:marketplace && npm run verify:state_changes", "verify": "npm run verify:marketplace && npm run verify:state_changes",
"verify:marketplace": "certoraRun certora/confs/Marketplace.conf", "verify:marketplace": "certoraRun certora/confs/Marketplace.conf",
"verify:state_changes": "certoraRun certora/confs/StateChanges.conf" "verify:state_changes": "certoraRun certora/confs/StateChanges.conf",
"coverage": "hardhat coverage",
"gas:report": "REPORT_GAS=true hardhat test"
}, },
"devDependencies": { "devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.2.1",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"@openzeppelin/contracts": "^5.3.0", "@openzeppelin/contracts": "^5.3.0",
"@stdlib/stats-binomial-test": "^0.0.7", "@nomicfoundation/hardhat-chai-matchers": "^2.0.8",
"chai": "^4.3.7", "@nomicfoundation/hardhat-ignition-ethers": "^0.15.11",
"ethereum-waffle": "^3.4.4", "@nomicfoundation/hardhat-toolbox": "^5.0.0",
"ethers": "^5.7.2", "@stdlib/stats-binomial-test": "^0.2.2",
"hardhat": "^2.24.2", "chai": "^4.5.0",
"hardhat-deploy": "^0.11.34", "ethers": "6.14.4",
"hardhat-deploy-ethers": "^0.3.0-beta.13", "hardhat": "^2.24.3",
"prettier": "^2.8.2", "prettier": "^3.5.3",
"prettier-plugin-solidity": "^1.4.2", "prettier-plugin-solidity": "^1.4.3",
"solhint": "^5.0.5" "solhint": "^5.1.0",
"concurrently": "^9.1.2"
} }
} }

7
scripts/mine.js Normal file
View File

@ -0,0 +1,7 @@
const { mine } = require("@nomicfoundation/hardhat-network-helpers")
async function main() {
await mine(256)
}
main().catch(console.error)

View File

@ -1,5 +1,5 @@
const { expect } = require("chai") const { expect } = require("chai")
const { ethers } = require("hardhat") const EndianModule = require("../ignition/modules/endian")
describe("Endian", function () { describe("Endian", function () {
const big = const big =
@ -10,8 +10,8 @@ describe("Endian", function () {
let endian let endian
beforeEach(async function () { beforeEach(async function () {
let Endian = await ethers.getContractFactory("TestEndian") const { testEndian } = await ignition.deploy(EndianModule)
endian = await Endian.deploy() endian = testEndian
}) })
it("converts from little endian to big endian", async function () { it("converts from little endian to big endian", async function () {

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,32 @@
const { expect } = require("chai") const { expect } = require("chai")
const { ethers } = require("hardhat") const PeriodsModule = require("../ignition/modules/periods")
const { assertDeploymentRejectedWithCustomError } = require("./helpers")
describe("Periods", function () { describe("Periods", function () {
it("should revert when secondsPerPeriod is 0", async function () { it("should revert when secondsPerPeriod is 0", async function () {
const PeriodsContract = await ethers.getContractFactory("Periods") const promise = ignition.deploy(PeriodsModule, {
await expect(PeriodsContract.deploy(0)).to.be.revertedWith( parameters: {
"Periods_InvalidSecondsPerPeriod" Periods: {
secondsPerPeriod: 0,
},
},
})
assertDeploymentRejectedWithCustomError(
"Periods_InvalidSecondsPerPeriod",
promise,
) )
}) })
it("should not revert when secondsPerPeriod more than 0", async function () { it("should not revert when secondsPerPeriod more than 0", async function () {
const PeriodsContract = await ethers.getContractFactory("Periods") const promise = ignition.deploy(PeriodsModule, {
await expect(PeriodsContract.deploy(10)).not.to.be.reverted parameters: {
Periods: {
secondsPerPeriod: 10,
},
},
})
await expect(promise).not.to.be.rejected
}) })
}) })

View File

@ -1,20 +1,21 @@
const { expect } = require("chai") const { expect } = require("chai")
const { ethers, deployments } = require("hardhat") const { ethers } = require("hardhat")
const { hexlify, randomBytes } = ethers.utils const { hexlify, randomBytes } = ethers
const { const {
snapshot, snapshot,
revert, revert,
mine,
ensureMinimumBlockHeight, ensureMinimumBlockHeight,
currentTime, currentTime,
advanceTime, advanceTime,
advanceTimeTo, advanceTimeTo,
mine,
} = require("./evm") } = require("./evm")
const { periodic } = require("./time") const { periodic } = require("./time")
const { loadProof, loadPublicInput } = require("../verifier/verifier") const { loadProof, loadPublicInput } = require("../verifier/verifier")
const { SlotState } = require("./requests") const { SlotState } = require("./requests")
const binomialTest = require("@stdlib/stats-binomial-test") const binomialTest = require("@stdlib/stats-binomial-test")
const { exampleProof } = require("./examples") const { exampleProof } = require("./examples")
const ProofsModule = require("../ignition/modules/proofs")
describe("Proofs", function () { describe("Proofs", function () {
const slotId = hexlify(randomBytes(32)) const slotId = hexlify(randomBytes(32))
@ -30,13 +31,22 @@ describe("Proofs", function () {
beforeEach(async function () { beforeEach(async function () {
await snapshot() await snapshot()
await ensureMinimumBlockHeight(256) await ensureMinimumBlockHeight(256)
const Proofs = await ethers.getContractFactory("TestProofs")
await deployments.fixture(["Verifier"]) const { testProofs } = await ignition.deploy(ProofsModule, {
const verifier = await deployments.get("Groth16Verifier") parameters: {
proofs = await Proofs.deploy( Proofs: {
{ period, timeout, downtime, zkeyHash: "", downtimeProduct }, configuration: {
verifier.address period,
) timeout,
downtime,
zkeyHash: "",
downtimeProduct,
},
},
},
})
proofs = testProofs
}) })
afterEach(async function () { afterEach(async function () {
@ -113,7 +123,7 @@ describe("Proofs", function () {
let previous = await proofs.getPointer(slotId) let previous = await proofs.getPointer(slotId)
await mine() await mine()
let current = await proofs.getPointer(slotId) let current = await proofs.getPointer(slotId)
expect(current).to.equal((previous + 1) % 256) expect(current).to.equal((previous + 1n) % 256n)
} }
}) })
}) })
@ -202,15 +212,15 @@ describe("Proofs", function () {
it("fails proof submission when proof is incorrect", async function () { it("fails proof submission when proof is incorrect", async function () {
let invalid = exampleProof() let invalid = exampleProof()
await expect( await expect(
proofs.proofReceived(slotId, invalid, pubSignals) proofs.proofReceived(slotId, invalid, pubSignals),
).to.be.revertedWith("Proofs_InvalidProof") ).to.be.revertedWithCustomError(proofs, "Proofs_InvalidProof")
}) })
it("fails proof submission when public input is incorrect", async function () { it("fails proof submission when public input is incorrect", async function () {
let invalid = [1, 2, 3] let invalid = [1, 2, 3]
await expect( await expect(
proofs.proofReceived(slotId, proof, invalid) proofs.proofReceived(slotId, proof, invalid),
).to.be.revertedWith("Proofs_InvalidProof") ).to.be.revertedWithCustomError(proofs, "Proofs_InvalidProof")
}) })
it("emits an event when proof was submitted", async function () { it("emits an event when proof was submitted", async function () {
@ -222,8 +232,8 @@ describe("Proofs", function () {
it("fails proof submission when already submitted", async function () { it("fails proof submission when already submitted", async function () {
await proofs.proofReceived(slotId, proof, pubSignals) await proofs.proofReceived(slotId, proof, pubSignals)
await expect( await expect(
proofs.proofReceived(slotId, proof, pubSignals) proofs.proofReceived(slotId, proof, pubSignals),
).to.be.revertedWith("Proofs_ProofAlreadySubmitted") ).to.be.revertedWithCustomError(proofs, "Proofs_ProofAlreadySubmitted")
}) })
it("marks a proof as missing", async function () { it("marks a proof as missing", async function () {
@ -239,8 +249,8 @@ describe("Proofs", function () {
await waitUntilProofIsRequired(slotId) await waitUntilProofIsRequired(slotId)
let currentPeriod = periodOf(await currentTime()) let currentPeriod = periodOf(await currentTime())
await expect( await expect(
proofs.markProofAsMissing(slotId, currentPeriod) proofs.markProofAsMissing(slotId, currentPeriod),
).to.be.revertedWith("Proofs_PeriodNotEnded") ).to.be.revertedWithCustomError(proofs, "Proofs_PeriodNotEnded")
}) })
it("does not mark a proof as missing after timeout", async function () { it("does not mark a proof as missing after timeout", async function () {
@ -248,8 +258,8 @@ describe("Proofs", function () {
let currentPeriod = periodOf(await currentTime()) let currentPeriod = periodOf(await currentTime())
await advanceTimeTo(periodEnd(currentPeriod) + timeout + 1) await advanceTimeTo(periodEnd(currentPeriod) + timeout + 1)
await expect( await expect(
proofs.markProofAsMissing(slotId, currentPeriod) proofs.markProofAsMissing(slotId, currentPeriod),
).to.be.revertedWith("Proofs_ValidationTimedOut") ).to.be.revertedWithCustomError(proofs, "Proofs_ValidationTimedOut")
}) })
it("does not mark a received proof as missing", async function () { it("does not mark a received proof as missing", async function () {
@ -258,8 +268,8 @@ describe("Proofs", function () {
await proofs.proofReceived(slotId, proof, pubSignals) await proofs.proofReceived(slotId, proof, pubSignals)
await advanceTimeTo(periodEnd(receivedPeriod) + 1) await advanceTimeTo(periodEnd(receivedPeriod) + 1)
await expect( await expect(
proofs.markProofAsMissing(slotId, receivedPeriod) proofs.markProofAsMissing(slotId, receivedPeriod),
).to.be.revertedWith("Proofs_ProofNotMissing") ).to.be.revertedWithCustomError(proofs, "Proofs_ProofNotMissing")
}) })
it("does not mark proof as missing when not required", async function () { it("does not mark proof as missing when not required", async function () {
@ -269,8 +279,8 @@ describe("Proofs", function () {
let currentPeriod = periodOf(await currentTime()) let currentPeriod = periodOf(await currentTime())
await advanceTimeTo(periodEnd(currentPeriod) + 1) await advanceTimeTo(periodEnd(currentPeriod) + 1)
await expect( await expect(
proofs.markProofAsMissing(slotId, currentPeriod) proofs.markProofAsMissing(slotId, currentPeriod),
).to.be.revertedWith("Proofs_ProofNotRequired") ).to.be.revertedWithCustomError(proofs, "Proofs_ProofNotRequired")
}) })
it("does not mark proof as missing twice", async function () { it("does not mark proof as missing twice", async function () {
@ -279,8 +289,11 @@ describe("Proofs", function () {
await advanceTimeTo(periodEnd(missedPeriod) + 1) await advanceTimeTo(periodEnd(missedPeriod) + 1)
await proofs.markProofAsMissing(slotId, missedPeriod) await proofs.markProofAsMissing(slotId, missedPeriod)
await expect( await expect(
proofs.markProofAsMissing(slotId, missedPeriod) proofs.markProofAsMissing(slotId, missedPeriod),
).to.be.revertedWith("Proofs_ProofAlreadyMarkedMissing") ).to.be.revertedWithCustomError(
proofs,
"Proofs_ProofAlreadyMarkedMissing",
)
}) })
it("requires no proofs when slot is finished", async function () { it("requires no proofs when slot is finished", async function () {

View File

@ -3,6 +3,7 @@ const { ethers } = require("hardhat")
const { exampleRequest, exampleConfiguration } = require("./examples") const { exampleRequest, exampleConfiguration } = require("./examples")
const { requestId, slotId } = require("./ids") const { requestId, slotId } = require("./ids")
const { SlotState } = require("./requests") const { SlotState } = require("./requests")
const SlotReservationsModule = require("../ignition/modules/slot-reservations")
describe("SlotReservations", function () { describe("SlotReservations", function () {
let reservations let reservations
@ -15,10 +16,18 @@ describe("SlotReservations", function () {
const config = exampleConfiguration() const config = exampleConfiguration()
beforeEach(async function () { beforeEach(async function () {
let SlotReservations = await ethers.getContractFactory( const { testSlotReservations } = await ignition.deploy(
"TestSlotReservations" SlotReservationsModule,
{
parameters: {
SlotReservations: {
configuration: config.reservations,
},
},
},
) )
reservations = await SlotReservations.deploy(config.reservations)
reservations = testSlotReservations
;[provider, address1, address2, address3] = await ethers.getSigners() ;[provider, address1, address2, address3] = await ethers.getSigners()
request = await exampleRequest() request = await exampleRequest()
@ -76,8 +85,11 @@ describe("SlotReservations", function () {
it("cannot reserve a slot more than once", async function () { it("cannot reserve a slot more than once", async function () {
await reservations.reserveSlot(reqId, slotIndex) await reservations.reserveSlot(reqId, slotIndex)
await expect(reservations.reserveSlot(reqId, slotIndex)).to.be.revertedWith( await expect(
"SlotReservations_ReservationNotAllowed" reservations.reserveSlot(reqId, slotIndex),
).to.be.revertedWithCustomError(
reservations,
"SlotReservations_ReservationNotAllowed",
) )
expect(await reservations.length(id)).to.equal(1) expect(await reservations.length(id)).to.equal(1)
}) })
@ -95,8 +107,11 @@ describe("SlotReservations", function () {
switchAccount(address3) switchAccount(address3)
await reservations.reserveSlot(reqId, slotIndex) await reservations.reserveSlot(reqId, slotIndex)
switchAccount(provider) switchAccount(provider)
await expect(reservations.reserveSlot(reqId, slotIndex)).to.be.revertedWith( await expect(
"SlotReservations_ReservationNotAllowed" reservations.reserveSlot(reqId, slotIndex),
).to.be.revertedWithCustomError(
reservations,
"SlotReservations_ReservationNotAllowed",
) )
expect(await reservations.length(id)).to.equal(3) expect(await reservations.length(id)).to.equal(3)
expect(await reservations.contains(id, provider.address)).to.be.false expect(await reservations.contains(id, provider.address)).to.be.false
@ -115,8 +130,11 @@ describe("SlotReservations", function () {
it("cannot reserve a slot if not free or not in repair", async function () { it("cannot reserve a slot if not free or not in repair", async function () {
await reservations.setSlotState(id, SlotState.Filled) await reservations.setSlotState(id, SlotState.Filled)
await expect(reservations.reserveSlot(reqId, slotIndex)).to.be.revertedWith( await expect(
"SlotReservations_ReservationNotAllowed" reservations.reserveSlot(reqId, slotIndex),
).to.be.revertedWithCustomError(
reservations,
"SlotReservations_ReservationNotAllowed",
) )
expect(await reservations.length(id)).to.equal(0) expect(await reservations.length(id)).to.equal(0)
}) })
@ -139,7 +157,7 @@ describe("SlotReservations", function () {
it("should not emit an event when reservations are not full", async function () { it("should not emit an event when reservations are not full", async function () {
await expect(reservations.reserveSlot(reqId, slotIndex)).to.not.emit( await expect(reservations.reserveSlot(reqId, slotIndex)).to.not.emit(
reservations, reservations,
"SlotReservationsFull" "SlotReservationsFull",
) )
}) })
}) })

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,27 @@
const { ethers } = require("hardhat") const {
time,
mine,
takeSnapshot,
} = require("@nomicfoundation/hardhat-network-helpers")
const hre = require("hardhat")
const provider = hre.network.provider
let snapshots = [] const snapshots = []
async function snapshot() { async function snapshot() {
const id = await ethers.provider.send("evm_snapshot") const snapshot = await takeSnapshot()
const time = await currentTime() const time = await currentTime()
const automine = await ethers.provider.send("hardhat_getAutomine") const automine = await provider.send("hardhat_getAutomine")
snapshots.push({ id, time, automine }) snapshots.push({ snapshot, automine, time })
} }
async function revert() { async function revert() {
const { id, time, automine } = snapshots.pop() const { snapshot, time, automine } = snapshots.pop()
await ethers.provider.send("evm_revert", [id]) if (snapshot) {
await snapshot.restore()
const current = await currentTime() await setNextBlockTimestamp(time)
const nextTime = Math.max(time + 1, current + 1) await provider.send("evm_setAutomine", [automine])
}
await ethers.provider.send("evm_setNextBlockTimestamp", [nextTime])
await ethers.provider.send("evm_setAutomine", [automine])
} }
/** /**
@ -26,38 +30,33 @@ async function revert() {
* When automine mode is disabled, transactions that revert are silently ignored! * When automine mode is disabled, transactions that revert are silently ignored!
*/ */
async function setAutomine(enabled) { async function setAutomine(enabled) {
await ethers.provider.send("evm_setAutomine", [enabled]) await provider.send("evm_setAutomine", [enabled])
}
async function mine() {
await ethers.provider.send("evm_mine")
} }
async function ensureMinimumBlockHeight(height) { async function ensureMinimumBlockHeight(height) {
while ((await ethers.provider.getBlockNumber()) < height) { while ((await time.latestBlock()) < height) {
await mine() await mine()
} }
} }
async function setNextBlockTimestamp(timestamp) {
return time.setNextBlockTimestamp(timestamp)
}
async function currentTime() { async function currentTime() {
let block = await ethers.provider.getBlock("latest") return time.latest()
return block.timestamp
} }
async function advanceTime(seconds) { async function advanceTime(seconds) {
await ethers.provider.send("evm_increaseTime", [seconds]) await time.increase(seconds)
await mine() await mine()
} }
async function advanceTimeTo(timestamp) { async function advanceTimeTo(timestamp) {
await setNextBlockTimestamp(timestamp) await time.setNextBlockTimestamp(timestamp)
await mine() await mine()
} }
async function setNextBlockTimestamp(timestamp) {
await ethers.provider.send("evm_setNextBlockTimestamp", [timestamp])
}
module.exports = { module.exports = {
snapshot, snapshot,
revert, revert,

View File

@ -1,6 +1,5 @@
const { ethers } = require("hardhat")
const { hours } = require("./time") const { hours } = require("./time")
const { hexlify, randomBytes } = ethers.utils const { hexlify, randomBytes } = require("hardhat").ethers
const exampleConfiguration = () => ({ const exampleConfiguration = () => ({
collateral: { collateral: {
@ -36,7 +35,7 @@ const exampleRequest = async () => {
}, },
content: { content: {
cid: Buffer.from("zb2rhheVmk3bLks5MgzTqyznLu1zqGH5jrfTA1eAZXrjx7Vob"), cid: Buffer.from("zb2rhheVmk3bLks5MgzTqyznLu1zqGH5jrfTA1eAZXrjx7Vob"),
merkleRoot: Array.from(randomBytes(32)), merkleRoot: randomBytes(32),
}, },
expiry: hours(1), expiry: hours(1),
nonce: hexlify(randomBytes(32)), nonce: hexlify(randomBytes(32)),

15
test/helpers.js Normal file
View File

@ -0,0 +1,15 @@
module.exports = {
assertDeploymentRejectedWithCustomError: async function (
customError,
deploymentPromise,
) {
const error = await expect(deploymentPromise).to.be.rejected
expect(error)
.to.have.property("message")
.that.contains(
customError,
`Expected error ${expectedError}, but got ${error.message}`,
)
},
}

View File

@ -1,12 +1,11 @@
const { ethers } = require("hardhat") const { keccak256, AbiCoder } = require("hardhat").ethers
const { keccak256, defaultAbiCoder } = ethers.utils
function requestId(request) { function requestId(request) {
const Ask = "tuple(uint256, uint256, uint256, uint64, uint64, uint64, int64)" const Ask = "tuple(uint256, uint256, uint256, uint64, uint64, uint64, int64)"
const Content = "tuple(bytes, bytes32)" const Content = "tuple(bytes, bytes32)"
const Request = const Request =
"tuple(address, " + Ask + ", " + Content + ", uint64, bytes32)" "tuple(address, " + Ask + ", " + Content + ", uint64, bytes32)"
return keccak256(defaultAbiCoder.encode([Request], requestToArray(request))) return keccak256(new AbiCoder().encode([Request], requestToArray(request)))
} }
function askToArray(ask) { function askToArray(ask) {
@ -40,7 +39,7 @@ function requestToArray(request) {
function slotId(slot) { function slotId(slot) {
const types = "tuple(bytes32, uint256)" const types = "tuple(bytes32, uint256)"
const values = [slot.request, slot.index] const values = [slot.request, slot.index]
const encoding = defaultAbiCoder.encode([types], [values]) const encoding = new AbiCoder().encode([types], [values])
return keccak256(encoding) return keccak256(encoding)
} }

View File

@ -4,16 +4,16 @@ const { payoutForDuration } = require("./price")
const { collateralPerSlot } = require("./collateral") const { collateralPerSlot } = require("./collateral")
async function waitUntilCancelled(contract, request) { async function waitUntilCancelled(contract, request) {
const expiry = (await contract.requestExpiry(requestId(request))).toNumber() const expiry = await contract.requestExpiry(requestId(request))
// We do +1, because the expiry check in contract is done as `>` and not `>=`. // We do +1, because the expiry check in contract is done as `>` and not `>=`.
await advanceTimeTo(expiry + 1) return advanceTimeTo(expiry + 1n)
} }
async function waitUntilSlotsFilled(contract, request, proof, token, slots) { async function waitUntilSlotsFilled(contract, request, proof, token, slots) {
let collateral = collateralPerSlot(request) let collateral = collateralPerSlot(request)
await token.approve(contract.address, collateral * slots.length) await token.approve(await contract.getAddress(), collateral * slots.length)
let requestEnd = (await contract.requestEnd(requestId(request))).toNumber() let requestEnd = await contract.requestEnd(requestId(request))
const payouts = [] const payouts = []
for (let slotIndex of slots) { for (let slotIndex of slots) {
await contract.reserveSlot(requestId(request), slotIndex) await contract.reserveSlot(requestId(request), slotIndex)
@ -22,7 +22,7 @@ async function waitUntilSlotsFilled(contract, request, proof, token, slots) {
payouts[slotIndex] = payoutForDuration( payouts[slotIndex] = payoutForDuration(
request, request,
await currentTime(), await currentTime(),
requestEnd requestEnd,
) )
} }
@ -35,14 +35,14 @@ async function waitUntilStarted(contract, request, proof, token) {
request, request,
proof, proof,
token, token,
Array.from({ length: request.ask.slots }, (_, i) => i) Array.from({ length: request.ask.slots }, (_, i) => i),
) )
} }
async function waitUntilFinished(contract, requestId) { async function waitUntilFinished(contract, requestId) {
const end = (await contract.requestEnd(requestId)).toNumber() const end = await contract.requestEnd(requestId)
// We do +1, because the end check in contract is done as `>` and not `>=`. // We do +1, because the end check in contract is done as `>` and not `>=`.
await advanceTimeTo(end + 1) await advanceTimeTo(end + 1n)
} }
async function waitUntilFailed(contract, request) { async function waitUntilFailed(contract, request) {
@ -72,7 +72,7 @@ function patchOverloads(contract) {
if (logicalXor(rewardRecipient, collateralRecipient)) { if (logicalXor(rewardRecipient, collateralRecipient)) {
// XOR, if exactly one is truthy // XOR, if exactly one is truthy
throw new Error( throw new Error(
"Invalid freeSlot overload, you must specify both `rewardRecipient` and `collateralRecipient` or neither." "Invalid freeSlot overload, you must specify both `rewardRecipient` and `collateralRecipient` or neither.",
) )
} }
@ -96,6 +96,11 @@ function patchOverloads(contract) {
} }
} }
function littleEndianToBigInt(littleEndian) {
const buffer = Buffer.from(littleEndian)
return BigInt(`0x${buffer.toString("hex")}`)
}
module.exports = { module.exports = {
waitUntilCancelled, waitUntilCancelled,
waitUntilStarted, waitUntilStarted,
@ -104,4 +109,5 @@ module.exports = {
waitUntilFailed, waitUntilFailed,
waitUntilSlotFailed, waitUntilSlotFailed,
patchOverloads, patchOverloads,
littleEndianToBigInt,
} }

View File

@ -9,7 +9,21 @@ function maxPrice(request) {
} }
function payoutForDuration(request, start, end) { function payoutForDuration(request, start, end) {
return (end - start) * pricePerSlotPerSecond(request) return (Number(end) - Number(start)) * pricePerSlotPerSecond(request)
} }
module.exports = { maxPrice, pricePerSlotPerSecond, payoutForDuration } function calculatePartialPayout(request, expiresAt, filledAt) {
return (Number(expiresAt) - Number(filledAt)) * pricePerSlotPerSecond(request)
}
function calculateBalance(balance, reward) {
return BigInt(balance) + BigInt(reward)
}
module.exports = {
maxPrice,
pricePerSlotPerSecond,
payoutForDuration,
calculatePartialPayout,
calculateBalance,
}

View File

@ -1,5 +1,4 @@
const { Assertion } = require("chai") const { Assertion } = require("chai")
const { currentTime } = require("./evm")
const RequestState = { const RequestState = {
New: 0, New: 0,
@ -10,13 +9,13 @@ const RequestState = {
} }
const SlotState = { const SlotState = {
Free: 0, Free: 0n,
Filled: 1, Filled: 1n,
Finished: 2, Finished: 2n,
Failed: 3, Failed: 3n,
Paid: 4, Paid: 4n,
Cancelled: 5, Cancelled: 5n,
Repair: 6, Repair: 6n,
} }
function enableRequestAssertions() { function enableRequestAssertions() {
@ -29,21 +28,21 @@ function enableRequestAssertions() {
"expected request #{this} to have client #{exp} but got #{act}", "expected request #{this} to have client #{exp} but got #{act}",
"expected request #{this} to not have client #{act}, expected #{exp}", "expected request #{this} to not have client #{act}, expected #{exp}",
request.client, // expected request.client, // expected
actual.client // actual actual.client, // actual
) )
this.assert( this.assert(
actual.expiry == request.expiry, actual.expiry == request.expiry,
"expected request #{this} to have expiry #{exp} but got #{act}", "expected request #{this} to have expiry #{exp} but got #{act}",
"expected request #{this} to not have expiry #{act}, expected #{exp}", "expected request #{this} to not have expiry #{act}, expected #{exp}",
request.expiry, // expected request.expiry, // expected
actual.expiry // actual actual.expiry, // actual
) )
this.assert( this.assert(
actual.nonce === request.nonce, actual.nonce === request.nonce,
"expected request #{this} to have nonce #{exp} but got #{act}", "expected request #{this} to have nonce #{exp} but got #{act}",
"expected request #{this} to not have nonce #{act}, expected #{exp}", "expected request #{this} to not have nonce #{act}, expected #{exp}",
request.nonce, // expected request.nonce, // expected
actual.nonce // actual actual.nonce, // actual
) )
}) })
} }

View File

@ -0,0 +1,28 @@
{
"pi_a": [
"6534256117371673392078914918983405561950705741555607896616281548993166767050",
"4683704071819826577549511087725295557628226569657977399743888281350488008160",
"1"
],
"pi_b": [
[
"5214601005301447749408976517903523691026761713935248596363525026084207179898",
"12928862018496028019248584699701543976607635004280021323367162829172676142063"
],
[
"6802359022534661924490571652741723715650845080843101596088257121470226373081",
"13026065833109947056007317243769626467054545801812125911606045634450472294131"
],
[
"1",
"0"
]
],
"pi_c": [
"4064922718155280187684708354118623661512911161238889435233679092743470899962",
"16109406845320108508342512724551388830826795687369147885401665396548555026064",
"1"
],
"protocol": "groth16",
"curve": "bn128"
}

View File

@ -0,0 +1,5 @@
[
"10941033813",
"16074246370508166450132968585287196391860062495017081813239200574579640171677",
"3"
]

View File

@ -0,0 +1,4 @@
pragma circom 2.0.0;
include "sample_cells.circom";
// SampleAndProven( maxDepth, maxLog2NSlots, blockTreeDepth, nFieldElemsPerCell, nSamples )
component main {public [entropy,dataSetRoot,slotIndex]} = SampleAndProve(32, 8, 5, 67, 5);

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,104 @@
{
"protocol": "groth16",
"curve": "bn128",
"nPublic": 3,
"vk_alpha_1": [
"20491192805390485299153009773594534940189261866228447918068658471970481763042",
"9383485363053290200918347156157836566562967994039712273449902621266178545958",
"1"
],
"vk_beta_2": [
[
"6375614351688725206403948262868962793625744043794305715222011528459656738731",
"4252822878758300859123897981450591353533073413197771768651442665752259397132"
],
[
"10505242626370262277552901082094356697409835680220590971873171140371331206856",
"21847035105528745403288232691147584728191162732299865338377159692350059136679"
],
[
"1",
"0"
]
],
"vk_gamma_2": [
[
"10857046999023057135944570762232829481370756359578518086990519993285655852781",
"11559732032986387107991004021392285783925812861821192530917403151452391805634"
],
[
"8495653923123431417604973247489272438418190587263600148770280649306958101930",
"4082367875863433681332203403145435568316851327593401208105741076214120093531"
],
[
"1",
"0"
]
],
"vk_delta_2": [
[
"3431611495862121747865613070227194870328323956412989443038622984801922101560",
"1839332345901382467668376261123176838724068493614256702976446713083234015641"
],
[
"4267694917739299737209021244200092851467599237028086467255985366439671917679",
"14005704598922119245150389324584568926199987543485693236347173598336567030859"
],
[
"1",
"0"
]
],
"vk_alphabeta_12": [
[
[
"2029413683389138792403550203267699914886160938906632433982220835551125967885",
"21072700047562757817161031222997517981543347628379360635925549008442030252106"
],
[
"5940354580057074848093997050200682056184807770593307860589430076672439820312",
"12156638873931618554171829126792193045421052652279363021382169897324752428276"
],
[
"7898200236362823042373859371574133993780991612861777490112507062703164551277",
"7074218545237549455313236346927434013100842096812539264420499035217050630853"
]
],
[
[
"7077479683546002997211712695946002074877511277312570035766170199895071832130",
"10093483419865920389913245021038182291233451549023025229112148274109565435465"
],
[
"4595479056700221319381530156280926371456704509942304414423590385166031118820",
"19831328484489333784475432780421641293929726139240675179672856274388269393268"
],
[
"11934129596455521040620786944827826205713621633706285934057045369193958244500",
"8037395052364110730298837004334506829870972346962140206007064471173334027475"
]
]
],
"IC": [
[
"11919420103024546168896650006162652130022732573970705849225139177428442519914",
"17747753383929265689844293401689552935018333420134132157824903795680624926572",
"1"
],
[
"13158415194355348546090070151711085027834066488127676886518524272551654481129",
"18831701962118195025265682681702066674741422770850028135520336928884612556978",
"1"
],
[
"20882269691461568155321689204947751047717828445545223718893788782534717197527",
"11996193054822748526485644723594571195813487505803351159052936325857690315211",
"1"
],
[
"18155166643053044822201627105588517913195535693446564472247126736722594445000",
"13816319482622393060406816684195314200198627617641073470088058848129378231754",
"1"
]
]
}

View File

@ -0,0 +1 @@
"65dfa139e1c6dae5f50102cca2a856b759723c6ec6029343c2c6e926b855a6ca"