Skip to content

Commit

Permalink
E2e test with real verification after forking (#133)
Browse files Browse the repository at this point in the history
/**

This test simulates the first proof after a fork
The stateRoot and the globalExitRoot of the old state was calculated with an old chainID: 1000
The new chainID is 1001. The test shows that the newly generated proof is valid
on the real verifier contract. To show this a polygonZkEVM contract is set up
with the old state root, as it is done in a fork scenario via forkingManager contract.
After this a new batch is sequenced and the proof is verified.
The old state root was generated with the zkevm state tool from common-js. See this fork:
https://github.com/RealityETH/zkevm-commonjs/pull/1/files
and especially this line:
https://github.com/RealityETH/zkevm-commonjs/pull/1/files#diff-4d0a534c9be88e9cfd08ea7c2e6d412b9ad1d6501db1900339f19c02fb689922R147
It took the genesis state from our deployment script and the first two transactions are
some transfers on the chain.
Note that we run this "deterministic test" without a fork introduced by the forkingManager,
as this would require us to generate a proof for specific inputs (stateHash, globalExitRoot, etc.)
that would require us to prover each new test-run (unless one is able to set up the test perfectly deterministic as well).
This would be additional work and is not necessary to verify that we can
continue to use the old state root and globalExitRoot with a new chainID - as it is done in this test
*/

test can be run via:
npx hardhat test test/e2e/forking.t.js
  • Loading branch information
josojo authored Dec 28, 2023
1 parent 8909ee3 commit 5d9f881
Show file tree
Hide file tree
Showing 6 changed files with 4,211 additions and 2 deletions.
5 changes: 4 additions & 1 deletion hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,16 @@ module.exports = {
dependencyCompiler: {
paths: [
'@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol',
'@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol',
'@openzeppelin/contracts/mocks/ERC20PermitMock.sol',
'@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol',
'@RealityETH/zkevm-contracts/contracts/deployment/PolygonZkEVMDeployer.sol',
'@RealityETH/zkevm-contracts/contracts/PolygonZkEVMGlobalExitRootL2.sol',
'@RealityETH/zkevm-contracts/contracts/PolygonZkEVMTimelock.sol',
'@RealityETH/zkevm-contracts/contracts/mocks/VerifierRollupHelperMock.sol',
'@RealityETH/zkevm-contracts/contracts/mocks/PolygonZkEVMGlobalExitRootMock.sol',
'@RealityETH/zkevm-contracts/contracts/mocks/PolygonZkEVMMock.sol',
'@RealityETH/zkevm-contracts/contracts/verifiers/FflonkVerifier.sol',
'@RealityETH/zkevm-contracts/contracts/PolygonZkEVMBridgeWrapper.sol',
], // ,
// keep: true
},
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"hardhat": "^2.13.0"
},
"devDependencies": {
"@0xpolygonhermez/zkevm-commonjs": "github:0xPolygonHermez/zkevm-commonjs#v2.0.0-fork.5",
"@0xpolygonhermez/zkevm-commonjs": "github:0xPolygonHermez/zkevm-commonjs#v3.0.0-fork.6",
"@nomiclabs/hardhat-ethers": "^2.2.2",
"@nomiclabs/hardhat-etherscan": "^3.1.7",
"@nomiclabs/hardhat-waffle": "^2.0.6",
Expand Down
281 changes: 281 additions & 0 deletions test/e2e/forking.t.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
/* eslint-disable no-await-in-loop */

const { expect } = require('chai');
const { ethers, upgrades } = require('hardhat');
const { Scalar } = require('ffjavascript');

const { contractUtils } = require('@0xpolygonhermez/zkevm-commonjs');

const { generateSolidityInputs } = contractUtils;

const { calculateSnarkInput, calculateBatchHashData, calculateAccInputHash } = contractUtils;

const proofJson = require('./proof.json');
const input = require('./public.json');
const inputJson = require('./input.json');

/**
* This test simulates the first proof after a fork
* The stateRoot and the globalExitRoot of the old state was calculated with an old chainID: 1000
* The new chainID is 1001. The test shows that the newly generated proof is valid
* on the real verifier contract. To show this a polygonZkEVM contract is set up
* with the old state root, as it is done in a fork scenario via forkingManager contract.
* After this a new batch is sequenced and the proof is verified.
*
* The old state root was generated with the zkevm state tool from common-js. See this fork:
* https://github.com/RealityETH/zkevm-commonjs/pull/1/files
* and especially these lines:
* https://github.com/RealityETH/zkevm-commonjs/blob/main/test/processor-forking.test.js#L148-L154
* It took the genesis state from our deployment script and the first two transactions are
* some transfers on the chain.
*
* Note that we run this "deterministic test" without a fork introduced by the forkingManager,
* as this would require us to generate a proof for specific inputs (stateHash, globalExitRoot, etc.)
* that would require us to prover each new test-run (unless one is able to set up the test perfectly deterministic as well).
* This would be additional work and is not necessary to verify that we can
* continue to use the old state root and globalExitRoot with a new chainID - as it is done in this test
*/

describe('Simulating first proof after a fork', () => {
let verifierContract;
let maticTokenContract;
let polygonZkEVMBridgeContract;
let polygonZkEVMContract;
let polygonZkEVMGlobalExitRoot;
let deployer;
let trustedSequencer;
let trustedAggregator;
let admin;

const maticTokenName = 'Fork Token';
const maticTokenSymbol = 'FORK';
const maticTokenInitialBalance = ethers.utils.parseEther('20000000');

const genesisRoot = inputJson.oldStateRoot;

const networkIDMainnet = 0;

const urlSequencer = 'http://zkevm-json-rpc:8123';
const { chainID } = inputJson;
const networkName = 'zkevm';
const version = '0.0.1';
const forkID = 6;
const pendingStateTimeoutDefault = 10;
const trustedAggregatorTimeoutDefault = 10;
const depositBranches = new Array(32).fill(ethers.constants.HashZero);

beforeEach('Deploy contract', async () => {
upgrades.silenceWarnings();

// load signers
[deployer, trustedAggregator, admin] = await ethers.getSigners();

/*
* fund trustedAggregator address
* Could be different address theortically but for now it's fine
*/
const trustedSequencerAddress = inputJson.singleBatchData[0].sequencerAddr;
await ethers.provider.send('hardhat_impersonateAccount', [trustedSequencerAddress]);
trustedSequencer = await ethers.getSigner(trustedSequencerAddress);
await deployer.sendTransaction({
to: trustedSequencerAddress,
value: ethers.utils.parseEther('4'),
});

// deploy real verifier
const VerifierRollupHelperFactory = await ethers.getContractFactory(
'FflonkVerifier',
);
verifierContract = await VerifierRollupHelperFactory.deploy();

// deploy MATIC
const maticTokenFactory = await ethers.getContractFactory('ERC20PermitMock');
maticTokenContract = await maticTokenFactory.deploy(
maticTokenName,
maticTokenSymbol,
deployer.address,
maticTokenInitialBalance,
);
await maticTokenContract.deployed();

// deploy global exit root manager
await upgrades.deployProxyAdmin();

// deploy global exit root manager
const polygonZkEVMGlobalExitRootFactory = await ethers.getContractFactory('PolygonZkEVMGlobalExitRootMock');
polygonZkEVMGlobalExitRoot = await upgrades.deployProxy(polygonZkEVMGlobalExitRootFactory, [], { initializer: false });

// deploy PolygonZkEVMBridge
const polygonZkEVMBridgeFactory = await ethers.getContractFactory('PolygonZkEVMBridgeWrapper');
polygonZkEVMBridgeContract = await upgrades.deployProxy(polygonZkEVMBridgeFactory, [], { initializer: false });

// deploy PolygonZkEVMMock
const PolygonZkEVMFactory = await ethers.getContractFactory('PolygonZkEVMMock');
polygonZkEVMContract = await upgrades.deployProxy(PolygonZkEVMFactory, [], {
initializer: false,
});

// initialize contracts
await polygonZkEVMGlobalExitRoot.initialize(polygonZkEVMContract.address, polygonZkEVMBridgeContract.address);
await polygonZkEVMBridgeContract.initialize(
networkIDMainnet,
polygonZkEVMGlobalExitRoot.address,
polygonZkEVMContract.address,
maticTokenContract.address,
true,
0,
depositBranches,
);
await polygonZkEVMContract.initialize(
{
admin: admin.address,
trustedSequencer: trustedSequencer.address,
pendingStateTimeout: pendingStateTimeoutDefault,
trustedAggregator: trustedAggregator.address,
trustedAggregatorTimeout: trustedAggregatorTimeoutDefault,
chainID,
forkID,
},
genesisRoot,
urlSequencer,
networkName,
version,
polygonZkEVMGlobalExitRoot.address,
maticTokenContract.address,
verifierContract.address,
polygonZkEVMBridgeContract.address,
);

// fund sequencer address with Matic tokens
await maticTokenContract.transfer(trustedSequencer.address, ethers.utils.parseEther('1000'));
});

it('Test verifying of first batch after a fork', async () => {
const batchesData = inputJson.singleBatchData;
const batchesNum = batchesData.length;

// Approve tokens
const maticAmount = await polygonZkEVMContract.batchFee();
await expect(
maticTokenContract.connect(trustedSequencer).approve(polygonZkEVMContract.address, maticAmount.mul(batchesNum)),
).to.emit(maticTokenContract, 'Approval');

// prepare PolygonZkEVMMock
await polygonZkEVMContract.setVerifiedBatch(inputJson.oldNumBatch);
await polygonZkEVMContract.setSequencedBatch(inputJson.oldNumBatch);
const lastTimestamp = batchesData[batchesNum - 1].timestamp;
await ethers.provider.send('evm_setNextBlockTimestamp', [lastTimestamp]);

for (let i = 0; i < batchesNum; i++) {
// set timestamp for the sendBatch call
const currentBatchData = batchesData[i];

const currentSequence = {
transactions: currentBatchData.batchL2Data,
globalExitRoot: currentBatchData.globalExitRoot,
timestamp: currentBatchData.timestamp,
minForcedTimestamp: 0,
};

const batchAccInputHashJs = calculateAccInputHash(
currentBatchData.oldAccInputHash,
calculateBatchHashData(currentBatchData.batchL2Data),
currentBatchData.globalExitRoot,
currentBatchData.timestamp,
currentBatchData.sequencerAddr, // fix
);
expect(batchAccInputHashJs).to.be.eq(currentBatchData.newAccInputHash);

// prepare globalExitRoot
const randomTimestamp = 1001;
const { globalExitRoot } = batchesData[0];
await polygonZkEVMGlobalExitRoot.setGlobalExitRoot(globalExitRoot, randomTimestamp);

const lastBatchSequenced = await polygonZkEVMContract.lastBatchSequenced();

// check trusted sequencer
const trustedSequencerAddress = inputJson.singleBatchData[i].sequencerAddr;
if (trustedSequencer.address !== trustedSequencerAddress) {
await polygonZkEVMContract.connect(admin).setTrustedSequencer(trustedSequencerAddress);
await ethers.provider.send('hardhat_impersonateAccount', [trustedSequencerAddress]);
trustedSequencer = await ethers.getSigner(trustedSequencerAddress);
await deployer.sendTransaction({
to: trustedSequencerAddress,
value: ethers.utils.parseEther('4'),
});
await expect(
maticTokenContract.connect(trustedSequencer).approve(polygonZkEVMContract.address, maticAmount.mul(batchesNum)),
).to.emit(maticTokenContract, 'Approval');
await maticTokenContract.transfer(trustedSequencer.address, ethers.utils.parseEther('100'));
}

// Sequence Batches
await expect(polygonZkEVMContract.connect(trustedSequencer).sequenceBatches([currentSequence], trustedSequencer.address))
.to.emit(polygonZkEVMContract, 'SequenceBatches')
.withArgs(Number(lastBatchSequenced) + 1);
}

// Set state and exit root
await polygonZkEVMContract.setStateRoot(inputJson.oldStateRoot, inputJson.oldNumBatch);

const { aggregatorAddress } = inputJson;
await ethers.provider.send('hardhat_impersonateAccount', [aggregatorAddress]);
const aggregator = await ethers.getSigner(aggregatorAddress);
await deployer.sendTransaction({
to: aggregatorAddress,
value: ethers.utils.parseEther('4'),
});
await polygonZkEVMContract.connect(admin).setTrustedAggregator(aggregatorAddress);

const batchAccInputHash = (await polygonZkEVMContract.sequencedBatches(inputJson.newNumBatch)).accInputHash;
expect(batchAccInputHash).to.be.equal(inputJson.newAccInputHash);

const proof = generateSolidityInputs(proofJson);

// Verify snark input
const circuitInputStarkJS = await calculateSnarkInput(
inputJson.oldStateRoot,
inputJson.newStateRoot,
inputJson.newLocalExitRoot,
inputJson.oldAccInputHash,
inputJson.newAccInputHash,
inputJson.oldNumBatch,
inputJson.newNumBatch,
inputJson.chainID,
inputJson.aggregatorAddress,
forkID,
);

expect(circuitInputStarkJS).to.be.eq(Scalar.e(input[0]));

// aggregator forge the batch
const { newLocalExitRoot } = inputJson;
const { newStateRoot } = inputJson;
const { oldNumBatch } = inputJson;
const { newNumBatch } = inputJson;
const pendingStateNum = 0;

// Verify batch
await expect(
polygonZkEVMContract.connect(aggregator).verifyBatchesTrustedAggregator(
pendingStateNum,
oldNumBatch,
newNumBatch,
newLocalExitRoot,
newStateRoot,
Array.from(Array(24)).fill('0x0e7073c1e73dfb716c35623b741e4ccfc6290d943f3df60377ad8799373ae439'), // this is arbitrary data
),
).to.be.revertedWith('InvalidProof');
await expect(
polygonZkEVMContract.connect(aggregator).verifyBatchesTrustedAggregator(
pendingStateNum,
oldNumBatch,
newNumBatch,
newLocalExitRoot,
newStateRoot,
proof,
),
).to.emit(polygonZkEVMContract, 'VerifyBatchesTrustedAggregator')
.withArgs(newNumBatch, newStateRoot, aggregator.address);
});
});
Loading

0 comments on commit 5d9f881

Please sign in to comment.