diff --git a/packages/contracts/contracts/test-helpers/Helper_GasMeasurer.sol b/packages/contracts/contracts/test-helpers/Helper_GasMeasurer.sol new file mode 100644 index 000000000000..a7a381d62293 --- /dev/null +++ b/packages/contracts/contracts/test-helpers/Helper_GasMeasurer.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.7.0; + +contract Helper_GasMeasurer { + function measureCallGas( + address _target, + bytes memory _data + ) + public + returns ( uint ) + { + uint gasBefore; + uint gasAfter; + + uint calldataStart; + uint calldataLength; + assembly { + calldataStart := add(_data,0x20) + calldataLength := mload(_data) + } + + bool success; + assembly { + gasBefore := gas() + success := call(gas(), _target, 0, calldataStart, calldataLength, 0, 0) + gasAfter := gas() + } + require(success, "Call failed, but calls we want to measure gas for should succeed!"); + + return gasBefore - gasAfter; + } +} diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 399a5ca3204a..8f1b60affde0 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -19,9 +19,9 @@ "build:typechain": "buidler typechain", "test": "yarn run test:contracts", "test:contracts": "buidler test --show-stack-traces", + "test:gas": "buidler test \"test/contracts/OVM/execution/OVM_StateManager.gas-spec.ts\" --no-compile --show-stack-traces", "lint": "yarn run lint:typescript", "lint:typescript": "tslint --format stylish --project .", - "lint:fix": "yarn run lint:fix:typescript", "lint:fix:typescript": "prettier --config prettier-config.json --write \"buidler.config.ts\" \"{src,test}/**/*.ts\"", "clean": "rm -rf ./artifacts ./build ./cache", "deploy": "./bin/deploy.js" diff --git a/packages/contracts/test/contracts/OVM/execution/OVM_StateManager.gas-spec.ts b/packages/contracts/test/contracts/OVM/execution/OVM_StateManager.gas-spec.ts new file mode 100644 index 000000000000..7c701c648096 --- /dev/null +++ b/packages/contracts/test/contracts/OVM/execution/OVM_StateManager.gas-spec.ts @@ -0,0 +1,527 @@ +import { expect } from '../../../setup' + +/* External Imports */ +import { ethers } from '@nomiclabs/buidler' +import { Contract, ContractFactory, Signer, BigNumber } from 'ethers' +import _ from 'lodash' + +/* Internal Imports */ +import { DUMMY_ACCOUNTS, DUMMY_BYTES32, ZERO_ADDRESS, EMPTY_ACCOUNT_CODE_HASH, NON_ZERO_ADDRESS, NON_NULL_BYTES32, STORAGE_XOR_VALUE } from '../../../helpers' + +const DUMMY_ACCOUNT = DUMMY_ACCOUNTS[0] +const DUMMY_KEY = DUMMY_BYTES32[0] +const DUMMY_VALUE_1 = DUMMY_BYTES32[1] +const DUMMY_VALUE_2 = DUMMY_BYTES32[2] + +describe('OVM_StateManager gas consumption', () => { + let owner: Signer + before(async () => { + ;[owner] = await ethers.getSigners() + }) + + let Factory__OVM_StateManager: ContractFactory + let Helper_GasMeasurer: Contract + before(async () => { + Factory__OVM_StateManager = await ethers.getContractFactory( + 'OVM_StateManager' + ) + + Helper_GasMeasurer = await (await (await ethers.getContractFactory( + 'Helper_GasMeasurer' + )).deploy()).connect(owner) + }) + + let OVM_StateManager: Contract + beforeEach(async () => { + OVM_StateManager = ( + await Factory__OVM_StateManager.deploy(await owner.getAddress()) + ).connect(owner) + + await OVM_StateManager.setExecutionManager(Helper_GasMeasurer.address) + }) + + const measure = ( + methodName: string, + methodArgs: Array = [], + doFirst: () => Promise = async () => {return} + ) => { + it('measured consumption!', async () => { + await doFirst() + await getSMGasCost(methodName, methodArgs) + }) + } + + const getSMGasCost = async (methodName: string, methodArgs: Array = []): Promise => { + const gasCost: number = await Helper_GasMeasurer.callStatic.measureCallGas( + OVM_StateManager.address, + OVM_StateManager.interface.encodeFunctionData( + methodName, + methodArgs + ) + ) + console.log(` calculated gas cost of ${gasCost}`) + + return gasCost + } + + const setupFreshAccount = async () => { + await OVM_StateManager.putAccount(DUMMY_ACCOUNT.address, { + ...DUMMY_ACCOUNT.data, + isFresh: true, + }) + } + const setupNonFreshAccount = async () => { + await OVM_StateManager.putAccount( + DUMMY_ACCOUNT.address, + DUMMY_ACCOUNT.data + ) + } + const putSlot = async (value: string) => { + await OVM_StateManager.putContractStorage( + DUMMY_ACCOUNT.address, + DUMMY_KEY, + value + ) + } + + describe('ItemState testAndSetters', () => { + describe('testAndSetAccountLoaded', () => { + describe('when account ItemState is ITEM_UNTOUCHED', () => { + measure( + 'testAndSetAccountLoaded', + [NON_ZERO_ADDRESS] + ) + }) + describe('when account ItemState is ITEM_LOADED', () => { + measure( + 'testAndSetAccountLoaded', + [NON_ZERO_ADDRESS], + async () => { + await OVM_StateManager.testAndSetAccountLoaded(NON_ZERO_ADDRESS) + } + ) + }) + describe('when account ItemState is ITEM_CHANGED', () => { + measure( + 'testAndSetAccountLoaded', + [NON_ZERO_ADDRESS], + async () => { + await OVM_StateManager.testAndSetAccountChanged(NON_ZERO_ADDRESS) + } + ) + }) + }) + + describe('testAndSetAccountChanged', () => { + describe('when account ItemState is ITEM_UNTOUCHED', () => { + measure( + 'testAndSetAccountChanged', + [NON_ZERO_ADDRESS] + ) + }) + describe('when account ItemState is ITEM_LOADED', () => { + measure( + 'testAndSetAccountChanged', + [NON_ZERO_ADDRESS], + async () => { + await OVM_StateManager.testAndSetAccountLoaded(NON_ZERO_ADDRESS) + } + ) + }) + describe('when account ItemState is ITEM_CHANGED', () => { + measure( + 'testAndSetAccountChanged', + [NON_ZERO_ADDRESS], + async () => { + await OVM_StateManager.testAndSetAccountChanged(NON_ZERO_ADDRESS) + } + ) + }) + }) + + describe('testAndSetContractStorageLoaded', () => { + describe('when storage ItemState is ITEM_UNTOUCHED', () => { + measure( + 'testAndSetContractStorageLoaded', + [NON_ZERO_ADDRESS, DUMMY_KEY] + ) + }) + describe('when storage ItemState is ITEM_LOADED', () => { + measure( + 'testAndSetContractStorageLoaded', + [NON_ZERO_ADDRESS, DUMMY_KEY], + async () => { + await OVM_StateManager.testAndSetContractStorageLoaded(NON_ZERO_ADDRESS, DUMMY_KEY) + } + ) + }) + describe('when storage ItemState is ITEM_CHANGED', () => { + measure( + 'testAndSetContractStorageLoaded', + [NON_ZERO_ADDRESS, DUMMY_KEY], + async () => { + await OVM_StateManager.testAndSetContractStorageChanged(NON_ZERO_ADDRESS, DUMMY_KEY) + } + ) + }) + }) + + describe('testAndSetContractStorageChanged', () => { + describe('when storage ItemState is ITEM_UNTOUCHED', () => { + measure( + 'testAndSetContractStorageChanged', + [NON_ZERO_ADDRESS, DUMMY_KEY] + ) + }) + describe('when storage ItemState is ITEM_LOADED', () => { + measure( + 'testAndSetContractStorageChanged', + [NON_ZERO_ADDRESS, DUMMY_KEY], + async () => { + await OVM_StateManager.testAndSetContractStorageLoaded(NON_ZERO_ADDRESS, DUMMY_KEY) + } + ) + }) + describe('when storage ItemState is ITEM_CHANGED', () => { + measure( + 'testAndSetContractStorageChanged', + [NON_ZERO_ADDRESS, DUMMY_KEY], + async () => { + await OVM_StateManager.testAndSetContractStorageChanged(NON_ZERO_ADDRESS, DUMMY_KEY) + } + ) + }) + }) + }) + + describe('incrementTotalUncommittedAccounts', () => { + describe('when totalUncommittedAccounts is 0', () => { + measure('incrementTotalUncommittedAccounts') + }) + describe('when totalUncommittedAccounts is nonzero', () => { + const doFirst = async () => { + await OVM_StateManager.incrementTotalUncommittedAccounts() + } + measure('incrementTotalUncommittedAccounts', [], doFirst) + }) + }) + + describe('incrementTotalUncommittedContractStorage', () => { + describe('when totalUncommittedContractStorage is 0', () => { + measure('incrementTotalUncommittedContractStorage') + }) + describe('when totalUncommittedContractStorage is nonzero', () => { + const doFirst = async () => { + await OVM_StateManager.incrementTotalUncommittedContractStorage() + } + measure('incrementTotalUncommittedContractStorage', [], doFirst) + }) + }) + + describe('hasAccount', () => { + describe('when it does have the account', () => { + const doFirst = async () => { + await OVM_StateManager.putAccount( + DUMMY_ACCOUNT.address, + DUMMY_ACCOUNT.data + ) + } + measure( + 'hasAccount', + [DUMMY_ACCOUNT.address], + doFirst + ) + }) + }) + + describe('hasEmptyAccount', () => { + describe('when it does have an empty account', () => { + measure( + 'hasEmptyAccount', + [DUMMY_ACCOUNT.address], + async () => { + await OVM_StateManager.putAccount(DUMMY_ACCOUNT.address, { + ...DUMMY_ACCOUNT.data, + codeHash: EMPTY_ACCOUNT_CODE_HASH, + }) + } + ) + }) + describe('when it has an account which is not emtpy', () => { + measure( + 'hasEmptyAccount', + [DUMMY_ACCOUNT.address], + async () => { + await OVM_StateManager.putAccount( + DUMMY_ACCOUNT.address, + DUMMY_ACCOUNT.data + ) + } + ) + }) + }) + + describe('setAccountNonce', () => { + describe('when the nonce is 0 and set to 0', () => { + measure( + 'setAccountNonce', + [DUMMY_ACCOUNT.address, 0], + async () => { + await OVM_StateManager.putAccount( + DUMMY_ACCOUNT.address, + { + ...DUMMY_ACCOUNT.data, + nonce: 0 + } + ) + } + ) + }) + describe('when the nonce is 0 and set to nonzero', () => { + measure( + 'setAccountNonce', + [DUMMY_ACCOUNT.address, 1], + async () => { + await OVM_StateManager.putAccount( + DUMMY_ACCOUNT.address, + { + ...DUMMY_ACCOUNT.data, + nonce: 0 + } + ) + } + ) + }) + describe('when the nonce is nonzero and set to 0', () => { + measure( + 'setAccountNonce', + [DUMMY_ACCOUNT.address, 0], + async () => { + await OVM_StateManager.putAccount( + DUMMY_ACCOUNT.address, + { + ...DUMMY_ACCOUNT.data, + nonce: 1 + } + ) + } + ) + }) + describe('when the nonce is nonzero and set to nonzero', () => { + measure( + 'setAccountNonce', + [DUMMY_ACCOUNT.address, 2], + async () => { + await OVM_StateManager.putAccount( + DUMMY_ACCOUNT.address, + { + ...DUMMY_ACCOUNT.data, + nonce: 1 + } + ) + } + ) + }) + }) + + describe('getAccountNonce', () => { + describe('when the nonce is 0', () => { + measure( + 'getAccountNonce', + [DUMMY_ACCOUNT.address] + ) + }) + describe('when the nonce is nonzero', () => { + measure( + 'getAccountNonce', + [DUMMY_ACCOUNT.address], + async () => { + await OVM_StateManager.putAccount( + DUMMY_ACCOUNT.address, + { + ...DUMMY_ACCOUNT.data, + nonce: 1 + } + ) + } + ) + }) + }) + + describe('getAccountEthAddress', () => { + describe('when the ethAddress is a random address', () => { + measure( + 'getAccountEthAddress', + [DUMMY_ACCOUNT.address], + async () => { + await OVM_StateManager.putAccount( + DUMMY_ACCOUNT.address, + { + ...DUMMY_ACCOUNT.data, + ethAddress: NON_ZERO_ADDRESS + } + ) + } + ) + }) + describe('when the ethAddress is zero', () => { + measure( + 'getAccountEthAddress', + [DUMMY_ACCOUNT.address], + async () => { + await OVM_StateManager.putAccount( + DUMMY_ACCOUNT.address, + { + ...DUMMY_ACCOUNT.data, + ethAddress: ZERO_ADDRESS + } + ) + } + ) + }) + }) + + describe('initPendingAccount', () => { + // note: this method should only be accessibl if _hasEmptyAccount is true, so it should always be empty nonce etc + measure( + 'initPendingAccount', + [NON_ZERO_ADDRESS] + ) + }) + + describe('commitPendingAccount', () => { + // this should only set ethAddress and codeHash from ZERO to NONZERO, so one case should be sufficient + measure( + 'commitPendingAccount', + [ + NON_ZERO_ADDRESS, + NON_ZERO_ADDRESS, + NON_NULL_BYTES32 + ], + async () => { + await OVM_StateManager.initPendingAccount(NON_ZERO_ADDRESS) + } + ) + }) + + describe('getContractStorage', () => { + // confirm with kelvin that this covers all cases + describe('when the account isFresh', () => { + describe('when the storage slot value has not been set', () => { + measure( + 'getContractStorage', + [DUMMY_ACCOUNT.address, DUMMY_KEY], + setupFreshAccount + ) + }) + describe('when the storage slot has already been set', () => { + describe('when the storage slot value is STORAGE_XOR_VALUE', () => { + measure( + 'getContractStorage', + [DUMMY_ACCOUNT.address, DUMMY_KEY], + async () => { + await setupFreshAccount() + await putSlot(STORAGE_XOR_VALUE) + } + ) + }) + describe('when the storage slot value is something other than STORAGE_XOR_VALUE', () => { + measure( + 'getContractStorage', + [DUMMY_ACCOUNT.address, DUMMY_KEY], + async () => { + await setupFreshAccount() + await putSlot(DUMMY_VALUE_1) + } + ) + }) + }) + }) + describe('when the account is not fresh', () => { + describe('when the storage slot value is STORAGE_XOR_VALUE', () => { + measure( + 'getContractStorage', + [DUMMY_ACCOUNT.address, DUMMY_KEY], + async () => { + await setupNonFreshAccount() + await putSlot(STORAGE_XOR_VALUE) + } + ) + }) + describe('when the storage slot value is something other than STORAGE_XOR_VALUE', () => { + measure( + 'getContractStorage', + [DUMMY_ACCOUNT.address, DUMMY_KEY], + async () => { + await setupNonFreshAccount() + await putSlot(DUMMY_VALUE_1) + } + ) + }) + }) + }) + + describe('putContractStorage', () => { + const relevantValues = [ + DUMMY_VALUE_1, + DUMMY_VALUE_2, + STORAGE_XOR_VALUE + ] + for (let preValue of relevantValues) { + for (let postValue of relevantValues) { + describe(`when overwriting ${preValue} with ${postValue}`, () => { + measure( + 'putContractStorage', + [NON_ZERO_ADDRESS, DUMMY_KEY, postValue], + async () => { + await OVM_StateManager.putContractStorage( + NON_ZERO_ADDRESS, + DUMMY_KEY, + preValue + ) + } + ) + }) + } + } + }) + + describe('hasContractStorage', () => { + describe('when the account is fresh', () => { + describe('when the storage slot has not been set', () => { + measure( + 'hasContractStorage', + [DUMMY_ACCOUNT.address, DUMMY_KEY], + setupFreshAccount + ) + }) + describe('when the slot has already been set', () => { + measure( + 'hasContractStorage', + [DUMMY_ACCOUNT.address, DUMMY_KEY], + async () => { + await setupFreshAccount() + await OVM_StateManager.putContractStorage( + DUMMY_ACCOUNT.address, + DUMMY_KEY, + DUMMY_VALUE_1 + ) + } + ) + }) + }) + describe('when the account is not fresh', () => { + measure( + 'hasContractStorage', + [DUMMY_ACCOUNT.address, DUMMY_KEY], + async () => { + await OVM_StateManager.putContractStorage( + DUMMY_ACCOUNT.address, + DUMMY_KEY, + DUMMY_VALUE_1 + ) + } + ) + }) + }) +}) \ No newline at end of file diff --git a/packages/contracts/test/contracts/OVM/execution/OVM_StateManager.spec.ts b/packages/contracts/test/contracts/OVM/execution/OVM_StateManager.spec.ts index 2eb625604aa8..6dc41d97caa4 100644 --- a/packages/contracts/test/contracts/OVM/execution/OVM_StateManager.spec.ts +++ b/packages/contracts/test/contracts/OVM/execution/OVM_StateManager.spec.ts @@ -6,12 +6,9 @@ import { Contract, ContractFactory, Signer, BigNumber } from 'ethers' import _ from 'lodash' /* Internal Imports */ -import { DUMMY_ACCOUNTS, DUMMY_BYTES32, ZERO_ADDRESS } from '../../../helpers' +import { DUMMY_ACCOUNTS, DUMMY_BYTES32, ZERO_ADDRESS, EMPTY_ACCOUNT_CODE_HASH, KECCAK_256_NULL } from '../../../helpers' + -const EMPTY_ACCOUNT_CODE_HASH = - '0x00004B1DC0DE000000004B1DC0DE000000004B1DC0DE000000004B1DC0DE0000' -const KECCAK_256_NULL = - '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470' describe('OVM_StateManager', () => { let signer1: Signer diff --git a/packages/contracts/test/helpers/constants.ts b/packages/contracts/test/helpers/constants.ts index 1f20984472ae..12328dd797f1 100644 --- a/packages/contracts/test/helpers/constants.ts +++ b/packages/contracts/test/helpers/constants.ts @@ -26,6 +26,9 @@ export const NON_ZERO_ADDRESS = makeAddress('11') export const VERIFIED_EMPTY_CONTRACT_HASH = '0x00004B1DC0DE000000004B1DC0DE000000004B1DC0DE000000004B1DC0DE0000' +export const STORAGE_XOR_VALUE = + '0xFEEDFACECAFEBEEFFEEDFACECAFEBEEFFEEDFACECAFEBEEFFEEDFACECAFEBEEF' + export const NUISANCE_GAS_COSTS = { NUISANCE_GAS_SLOAD: 20000, NUISANCE_GAS_SSTORE: 20000, @@ -42,3 +45,8 @@ export const STORAGE_XOR = export const getStorageXOR = (key: string): string => { return toHexString(xor(fromHexString(key), fromHexString(STORAGE_XOR))) } + +export const EMPTY_ACCOUNT_CODE_HASH = + '0x00004B1DC0DE000000004B1DC0DE000000004B1DC0DE000000004B1DC0DE0000' +export const KECCAK_256_NULL = + '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'