From eacb9a9a0765c3b17fb9d927fa629ba52736c85e Mon Sep 17 00:00:00 2001 From: William Cory Date: Tue, 9 Jul 2024 18:39:33 -0700 Subject: [PATCH 1/2] :white_check_mark: Test: MOre stateManager test coverage --- .../actions/__snapshots__/commit.spec.ts.snap | 32 ++++ .../__snapshots__/deepCopy.spec.ts.snap | 6 + .../generateCannonicalGenesis.spec.ts.snap | 8 + .../__snapshots__/getAccount.spec.ts.snap | 159 ++++++++++++++++++ .../getContractStorage.spec.ts.snap | 14 ++ .../putContractStorage.spec.ts.snap | 11 ++ packages/state/src/actions/commit.js | 2 - packages/state/src/actions/commit.spec.ts | 92 +++++++++- packages/state/src/actions/deepCopy.spec.ts | 24 +++ .../actions/generateCannonicalGenesis.spec.ts | 38 +++++ packages/state/src/actions/getAccount.spec.ts | 83 ++++++++- .../state/src/actions/getContractStorage.js | 3 +- .../src/actions/getContractStorage.spec.ts | 93 ++++++++++ .../state/src/actions/getForkBlockTag.spec.ts | 42 +++++ .../state/src/actions/putContractStorage.js | 11 +- .../src/actions/putContractStorage.spec.ts | 70 ++++++++ packages/state/src/utils/stripZeros.js | 12 ++ packages/state/src/utils/stripZeros.spec.ts | 34 ++++ 18 files changed, 713 insertions(+), 21 deletions(-) create mode 100644 packages/state/src/actions/__snapshots__/commit.spec.ts.snap create mode 100644 packages/state/src/actions/__snapshots__/deepCopy.spec.ts.snap create mode 100644 packages/state/src/actions/__snapshots__/generateCannonicalGenesis.spec.ts.snap create mode 100644 packages/state/src/actions/__snapshots__/getAccount.spec.ts.snap create mode 100644 packages/state/src/actions/__snapshots__/getContractStorage.spec.ts.snap create mode 100644 packages/state/src/actions/__snapshots__/putContractStorage.spec.ts.snap create mode 100644 packages/state/src/actions/generateCannonicalGenesis.spec.ts create mode 100644 packages/state/src/actions/getContractStorage.spec.ts create mode 100644 packages/state/src/actions/getForkBlockTag.spec.ts create mode 100644 packages/state/src/actions/putContractStorage.spec.ts create mode 100644 packages/state/src/utils/stripZeros.js create mode 100644 packages/state/src/utils/stripZeros.spec.ts diff --git a/packages/state/src/actions/__snapshots__/commit.spec.ts.snap b/packages/state/src/actions/__snapshots__/commit.spec.ts.snap new file mode 100644 index 0000000000..7f4de0eec4 --- /dev/null +++ b/packages/state/src/actions/__snapshots__/commit.spec.ts.snap @@ -0,0 +1,32 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`commit should clear all storage entries for the account corresponding to `address` 1`] = ` +[ + +Map { + "0000000000000000000000000000000004277dc9" => undefined, + } +, + Map {}, +] +`; + +exports[`commit should clear all storage entries for the account corresponding to `address` 2`] = ` +[ + +Map { + "0000000000000000000000000000000004277dc9" => undefined, + } +, + Map {}, +] +`; + +exports[`commit should clear all storage entries for the account corresponding to `address` 3`] = ` +[ + Map {}, + Map {}, +] +`; + +exports[`commit should clear all storage entries for the account corresponding to `address` 4`] = `"0x886f43e0144bf4f5748e999d0178ed7e4edea8ad708e0bf26a61341e8ae91d1e"`; diff --git a/packages/state/src/actions/__snapshots__/deepCopy.spec.ts.snap b/packages/state/src/actions/__snapshots__/deepCopy.spec.ts.snap new file mode 100644 index 0000000000..70d5282699 --- /dev/null +++ b/packages/state/src/actions/__snapshots__/deepCopy.spec.ts.snap @@ -0,0 +1,6 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`deepCopy should throw if uncommitted state 1`] = `[InternalError: Attempted to deepCopy state with uncommitted checkpoints + +Docs: https://tevm.sh/reference/tevm/errors/classes/internalerror/ +Version: 1.1.0.next-73]`; diff --git a/packages/state/src/actions/__snapshots__/generateCannonicalGenesis.spec.ts.snap b/packages/state/src/actions/__snapshots__/generateCannonicalGenesis.spec.ts.snap new file mode 100644 index 0000000000..f1ecbdf5b1 --- /dev/null +++ b/packages/state/src/actions/__snapshots__/generateCannonicalGenesis.spec.ts.snap @@ -0,0 +1,8 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`generateCanonicalGenesis should successfully generate canonical genesis 1`] = `{}`; + +exports[`generateCanonicalGenesis should throw if there are uncommitted checkpoints 1`] = `[InternalError: Attempted to generateCanonicalGenesis state with uncommitted checkpoints + +Docs: https://tevm.sh/reference/tevm/errors/classes/internalerror/ +Version: 1.1.0.next-73]`; diff --git a/packages/state/src/actions/__snapshots__/getAccount.spec.ts.snap b/packages/state/src/actions/__snapshots__/getAccount.spec.ts.snap new file mode 100644 index 0000000000..ba5008eb8a --- /dev/null +++ b/packages/state/src/actions/__snapshots__/getAccount.spec.ts.snap @@ -0,0 +1,159 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`getAccount forking Should fetch account from remote provider if not in cache and fork transport is provided 1`] = ` +Account { + "balance": 121136012589291788664n, + "codeHash": Uint8Array [ + 197, + 210, + 70, + 1, + 134, + 247, + 35, + 60, + 146, + 126, + 125, + 178, + 220, + 199, + 3, + 192, + 229, + 0, + 182, + 83, + 202, + 130, + 39, + 59, + 123, + 250, + 216, + 4, + 93, + 133, + 164, + 112, + ], + "nonce": 654430n, + "storageRoot": Uint8Array [ + 86, + 232, + 31, + 23, + 27, + 204, + 85, + 166, + 255, + 131, + 69, + 230, + 146, + 192, + 248, + 110, + 91, + 72, + 224, + 27, + 153, + 108, + 173, + 192, + 1, + 98, + 47, + 181, + 227, + 99, + 180, + 33, + ], +} +`; + +exports[`getAccount forking Should fetch account from remote provider if not in cache and fork transport is provided 2`] = ` +Account { + "balance": 121136012589291788664n, + "codeHash": Uint8Array [ + 197, + 210, + 70, + 1, + 134, + 247, + 35, + 60, + 146, + 126, + 125, + 178, + 220, + 199, + 3, + 192, + 229, + 0, + 182, + 83, + 202, + 130, + 39, + 59, + 123, + 250, + 216, + 4, + 93, + 133, + 164, + 112, + ], + "nonce": 654430n, + "storageRoot": Uint8Array [ + 86, + 232, + 31, + 23, + 27, + 204, + 85, + 166, + 255, + 131, + 69, + 230, + 146, + 192, + 248, + 110, + 91, + 72, + 224, + 27, + 153, + 108, + 173, + 192, + 1, + 98, + 47, + 181, + 227, + 99, + 180, + 33, + ], +} +`; + +exports[`getAccount forking Should return undefined and cache as undefined for empty remote account 1`] = ` +{ + "accountRLP": undefined, +} +`; + +exports[`getAccount forking Should return undefined and cache as undefined for empty remote account 2`] = `{}`; diff --git a/packages/state/src/actions/__snapshots__/getContractStorage.spec.ts.snap b/packages/state/src/actions/__snapshots__/getContractStorage.spec.ts.snap new file mode 100644 index 0000000000..d2b1908b48 --- /dev/null +++ b/packages/state/src/actions/__snapshots__/getContractStorage.spec.ts.snap @@ -0,0 +1,14 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`getContractStorage should throw an error if the key is not 32 bytes long 1`] = ` +"Storage key must be 32 bytes long. Received 2. If using numberToHex make the length 32. + +Docs: https://tevm.sh/reference/tevm/errors/classes/internalerror/ +Version: 1.1.0.next-73" +`; + +exports[`getContractStorage forking should fetch storage from remote provider if not in cache and fork transport is provided 1`] = ` +Uint8Array [ + 0, +] +`; diff --git a/packages/state/src/actions/__snapshots__/putContractStorage.spec.ts.snap b/packages/state/src/actions/__snapshots__/putContractStorage.spec.ts.snap new file mode 100644 index 0000000000..35ec7008a7 --- /dev/null +++ b/packages/state/src/actions/__snapshots__/putContractStorage.spec.ts.snap @@ -0,0 +1,11 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`putContractStorage should throw an error if the key is not 32 bytes long 1`] = `[InternalError: Storage key must be 32 bytes long. Received 18,52 + +Docs: https://tevm.sh/reference/tevm/errors/classes/internalerror/ +Version: 1.1.0.next-73]`; + +exports[`putContractStorage should throw an error if the account does not exist 1`] = `[InternalError: cannot putContractStorage on non existing acccount! Consider checking if account exists first + +Docs: https://tevm.sh/reference/tevm/errors/classes/internalerror/ +Version: 1.1.0.next-73]`; diff --git a/packages/state/src/actions/commit.js b/packages/state/src/actions/commit.js index 7dfa01dac8..2d07c2d1df 100644 --- a/packages/state/src/actions/commit.js +++ b/packages/state/src/actions/commit.js @@ -1,8 +1,6 @@ import { keccak256, numberToHex, toHex } from 'viem' import { dumpCanonicalGenesis } from './dumpCannonicalGenesis.js' -// TODO we might want to sometimes prune state roots - /** * Commits the current change-set to the instance since the * last call to checkpoint. diff --git a/packages/state/src/actions/commit.spec.ts b/packages/state/src/actions/commit.spec.ts index 9012b59a17..742c64425f 100644 --- a/packages/state/src/actions/commit.spec.ts +++ b/packages/state/src/actions/commit.spec.ts @@ -1,13 +1,18 @@ -import { describe, expect, it } from 'bun:test' +import { beforeEach, describe, expect, it, jest } from 'bun:test' +import { createAddress } from '@tevm/address' +import { EthjsAccount } from '@tevm/utils' import { createBaseState } from '../createBaseState.js' import { checkpoint } from './checkpoint.js' import { commit } from './commit.js' +import { putAccount } from './putAccount.js' describe(commit.name, () => { it('should clear all storage entries for the account corresponding to `address`', async () => { + // No mocks const baseState = createBaseState({ loggingLevel: 'warn', }) + await putAccount(baseState)(createAddress(69696969), EthjsAccount.fromAccountData({ balance: 20n })) expect(baseState.caches.storage._checkpoints).toBe(0) expect(baseState.caches.accounts._checkpoints).toBe(0) expect(baseState.caches.contracts._checkpoints).toBe(0) @@ -15,13 +20,86 @@ describe(commit.name, () => { expect(baseState.caches.storage._checkpoints).toBe(1) expect(baseState.caches.accounts._checkpoints).toBe(1) expect(baseState.caches.contracts._checkpoints).toBe(1) - expect(baseState.caches.accounts._diffCache).toEqual([new Map(), new Map()]) + expect(baseState.caches.accounts._diffCache).toEqual([ + new Map( + Object.entries({ + '0000000000000000000000000000000004277dc9': undefined, + }), + ), + new Map(), + ]) expect(baseState.caches.storage._diffCache).toEqual([new Map(), new Map()]) + await commit(baseState)(true) + expect(baseState.caches.storage._checkpoints).toBe(1) + expect(baseState.caches.accounts._checkpoints).toBe(1) + expect(baseState.caches.contracts._checkpoints).toBe(1) + expect(baseState.caches.accounts._diffCache).toEqual([ + new Map( + Object.entries({ + '0000000000000000000000000000000004277dc9': undefined, + }), + ), + new Map(), + ]) + expect(baseState.caches.storage._diffCache).toEqual([new Map(), new Map()]) + expect(baseState.getCurrentStateRoot()).toEqual( + '0x886f43e0144bf4f5748e999d0178ed7e4edea8ad708e0bf26a61341e8ae91d1e', + ) + }) + + let baseState: ReturnType + + beforeEach(() => { + baseState = createBaseState({ + loggingLevel: 'warn', + }) + baseState.getCurrentStateRoot = jest.fn(() => 'existingStateRoot') as any + baseState.setCurrentStateRoot = jest.fn() + baseState.logger.debug = jest.fn() + baseState.options.onCommit = jest.fn() + baseState.stateRoots.set = jest.fn() + baseState.caches.accounts.commit = jest.fn() + baseState.caches.contracts.commit = jest.fn() + baseState.caches.storage.commit = jest.fn() + }) + + it('should commit to existing state root', async () => { + await checkpoint(baseState)() await commit(baseState)() - expect(baseState.caches.storage._checkpoints).toBe(0) - expect(baseState.caches.accounts._checkpoints).toBe(0) - expect(baseState.caches.contracts._checkpoints).toBe(0) - expect(baseState.caches.accounts._diffCache).toEqual([new Map()]) - expect(baseState.caches.storage._diffCache).toEqual([new Map()]) + + expect(baseState.getCurrentStateRoot).toHaveBeenCalled() + expect(baseState.setCurrentStateRoot).toHaveBeenCalledWith('existingStateRoot') + expect(baseState.logger.debug).toHaveBeenCalledWith('Comitting to existing state root...') + expect(baseState.stateRoots.set).toHaveBeenCalledWith('existingStateRoot', expect.any(Object)) + expect(baseState.caches.accounts.commit).toHaveBeenCalled() + expect(baseState.caches.contracts.commit).toHaveBeenCalled() + expect(baseState.caches.storage.commit).toHaveBeenCalled() + expect(baseState.options.onCommit).toHaveBeenCalledWith(baseState) + }) + + it('should commit to a new state root', async () => { + await checkpoint(baseState)() + await commit(baseState)(true) + + expect(baseState.setCurrentStateRoot).toHaveBeenCalledWith(expect.any(String)) + expect(baseState.logger.debug).toHaveBeenCalledWith( + expect.objectContaining({ root: expect.any(String) }), + 'Committing to new state root...', + ) + expect(baseState.stateRoots.set).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) + expect(baseState.caches.accounts.commit).toHaveBeenCalled() + expect(baseState.caches.contracts.commit).toHaveBeenCalled() + expect(baseState.caches.storage.commit).toHaveBeenCalled() + expect(baseState.options.onCommit).toHaveBeenCalledWith(baseState) + }) + + it('should handle onCommit callback correctly', async () => { + const onCommitMock = jest.fn() + baseState.options.onCommit = onCommitMock + + await checkpoint(baseState)() + await commit(baseState)() + + expect(onCommitMock).toHaveBeenCalledWith(baseState) }) }) diff --git a/packages/state/src/actions/deepCopy.spec.ts b/packages/state/src/actions/deepCopy.spec.ts index 8ae2c0a481..6305d690f4 100644 --- a/packages/state/src/actions/deepCopy.spec.ts +++ b/packages/state/src/actions/deepCopy.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'bun:test' +import { InternalError } from '@tevm/errors' import { EthjsAccount, EthjsAddress, hexToBytes, keccak256 } from '@tevm/utils' import { createBaseState } from '../createBaseState.js' import { checkpoint } from './checkpoint.js' @@ -41,4 +42,27 @@ describe(deepCopy.name, () => { expect(await getAccount(newState)(address)).toEqual(account) expect(await getContractCode(newState)(address)).toEqual(hexToBytes(deployedBytecode)) }) + + it('should throw if uncommitted state', async () => { + const baseState = createBaseState({ + loggingLevel: 'warn', + }) + + const address = EthjsAddress.fromString(`0x${'01'.repeat(20)}`) + + const nonce = 2n + const balance = 420n + const account = new EthjsAccount(nonce, balance, undefined, hexToBytes(keccak256(deployedBytecode))) + + await putAccount(baseState)(address, account) + + await putContractCode(baseState)(address, hexToBytes(deployedBytecode)) + + await checkpoint(baseState)() + + const error = await deepCopy(baseState)().catch((e) => e) + + expect(error).toBeInstanceOf(InternalError) + expect(error).toMatchSnapshot() + }) }) diff --git a/packages/state/src/actions/generateCannonicalGenesis.spec.ts b/packages/state/src/actions/generateCannonicalGenesis.spec.ts new file mode 100644 index 0000000000..0759d58681 --- /dev/null +++ b/packages/state/src/actions/generateCannonicalGenesis.spec.ts @@ -0,0 +1,38 @@ +import { beforeEach, describe, expect, it } from 'bun:test' +import { createAddress } from '@tevm/address' +import { InternalError } from '@tevm/errors' +import { EthjsAccount } from '@tevm/utils' +import { createBaseState } from '../createBaseState.js' +import type { TevmState } from '../state-types/TevmState.js' +import { dumpCanonicalGenesis } from './dumpCannonicalGenesis.js' +import { generateCanonicalGenesis } from './generateCannonicalGenesis.js' +import { putAccount } from './putAccount.js' + +describe(generateCanonicalGenesis.name, () => { + let baseState: ReturnType + let state: TevmState + + beforeEach(async () => { + baseState = createBaseState({}) + + state = await (async () => { + const state = createBaseState({}) + await putAccount(state)(createAddress(69), EthjsAccount.fromAccountData({ balance: 20n, nonce: 2n })) + return dumpCanonicalGenesis(baseState)() + })() + }) + + it('should successfully generate canonical genesis', async () => { + await generateCanonicalGenesis(baseState)(state) + expect(await dumpCanonicalGenesis(baseState)()).toMatchSnapshot() + }) + + it('should throw if there are uncommitted checkpoints', async () => { + baseState.caches.accounts._checkpoints = 1 + + const error = await generateCanonicalGenesis(baseState)(state).catch((e) => e) + + expect(error).toBeInstanceOf(InternalError) + expect(error).toMatchSnapshot() + }) +}) diff --git a/packages/state/src/actions/getAccount.spec.ts b/packages/state/src/actions/getAccount.spec.ts index 879465e8d9..d4f320f9f0 100644 --- a/packages/state/src/actions/getAccount.spec.ts +++ b/packages/state/src/actions/getAccount.spec.ts @@ -1,9 +1,16 @@ -import { describe, expect, it } from 'bun:test' +import { afterEach, beforeEach, describe, expect, it, jest, mock } from 'bun:test' +import { createAddress } from '@tevm/address' +import { transports } from '@tevm/test-utils' import { EthjsAccount, EthjsAddress } from '@tevm/utils' import { createBaseState } from '../createBaseState.js' +import { dumpCanonicalGenesis } from './dumpCannonicalGenesis.js' import { getAccount } from './getAccount.js' import { putAccount } from './putAccount.js' +afterEach(() => { + jest.restoreAllMocks() +}) + describe(getAccount.name, () => { it('Should get an account', async () => { const baseState = createBaseState({ @@ -23,3 +30,77 @@ describe(getAccount.name, () => { expect(await getAccount(baseState)(address)).toEqual(account) }) }) + +describe(`${getAccount.name} forking`, () => { + let baseState: ReturnType + let address: EthjsAddress + let account: EthjsAccount + + const knownAccount = createAddress('0x9430801EBAf509Ad49202aaBC5F5Bc6fd8A3dAf8') + + beforeEach(() => { + baseState = createBaseState({ + loggingLevel: 'warn', + fork: { + transport: transports.optimism, + blockTag: 122486679n, + }, + }) + + address = createAddress(`0x${'01'.repeat(20)}`) + account = EthjsAccount.fromAccountData({ + balance: 420n, + nonce: 2n, + }) + }) + + it('Should get an account', async () => { + await putAccount(baseState)(address, account) + expect(await getAccount(baseState)(address)).toEqual(account) + }) + + it('Should return undefined if account is not in cache and no fork transport', async () => { + expect(await getAccount(createBaseState({}))(address)).toBeUndefined() + }) + + it('Should return undefined if skipFetchingFromFork is true and account is not in cache', async () => { + expect(await getAccount(baseState, true)(knownAccount)).toBeUndefined() + }) + + it('Should fetch account from remote provider if not in cache and fork transport is provided', async () => { + const result = await getAccount(baseState)(knownAccount) + expect(result).toBeDefined() + expect(result).toMatchSnapshot() + const cachedResult = await getAccount(baseState)(knownAccount) + expect(cachedResult).toEqual(result as any) + // test that it indead is cached and we didn't fetch twice + expect(await getAccount(baseState)(knownAccount)).toMatchSnapshot() + }) + + it('Should return undefined and cache as undefined for empty remote account', async () => { + mock.module('./getAccountFromProvider.js', () => { + return { + getAccountFromProvider: () => async () => { + return EthjsAccount.fromAccountData({ + balance: 0n, + nonce: 0n, + codeHash: new Uint8Array(32), + storageRoot: new Uint8Array(32), + }) + }, + } + }) + + const baseStateWithFork = createBaseState({ + loggingLevel: 'warn', + fork: { + transport: transports.optimism, + blockTag: 122486679n, + }, + }) + + expect(await getAccount(baseStateWithFork)(address)).toBeUndefined() + expect(baseStateWithFork.caches.accounts.get(address)).toMatchSnapshot() + expect(await dumpCanonicalGenesis(baseStateWithFork)()).toMatchSnapshot() + }) +}) diff --git a/packages/state/src/actions/getContractStorage.js b/packages/state/src/actions/getContractStorage.js index 5b5c5b7458..50c8714e1d 100644 --- a/packages/state/src/actions/getContractStorage.js +++ b/packages/state/src/actions/getContractStorage.js @@ -1,3 +1,4 @@ +import { InternalError } from '@tevm/errors' import { bytesToHex, hexToBytes } from 'viem' import { getAccount } from './getAccount.js' import { getForkBlockTag } from './getForkBlockTag.js' @@ -17,7 +18,7 @@ export const getContractStorage = (baseState) => async (address, key) => { } = baseState // Check storage slot in cache if (key.length !== 32) { - throw new Error( + throw new InternalError( `Storage key must be 32 bytes long. Received ${key.length}. If using numberToHex make the length 32.`, ) } diff --git a/packages/state/src/actions/getContractStorage.spec.ts b/packages/state/src/actions/getContractStorage.spec.ts new file mode 100644 index 0000000000..7524ca1336 --- /dev/null +++ b/packages/state/src/actions/getContractStorage.spec.ts @@ -0,0 +1,93 @@ +import { beforeEach, describe, expect, it } from 'bun:test' +import { createAddress } from '@tevm/address' +import { transports } from '@tevm/test-utils' +import { EthjsAccount, EthjsAddress, hexToBytes, toBytes } from '@tevm/utils' +import type { BaseState } from '../BaseState.js' +import { createBaseState } from '../createBaseState.js' +import { getContractStorage } from './getContractStorage.js' +import { putAccount } from './putAccount.js' +import { putContractStorage } from './putContractStorage.js' + +describe('getContractStorage', () => { + let baseState: BaseState + let address: EthjsAddress + let key: Uint8Array + let value: Uint8Array + let account: EthjsAccount + + beforeEach(async () => { + baseState = createBaseState({ + loggingLevel: 'warn', + }) + + address = createAddress('01'.repeat(20)) + key = hexToBytes(`0x${'02'.repeat(32)}`) + value = hexToBytes('0x1234') + account = EthjsAccount.fromAccountData({ + balance: 420n, + nonce: 2n, + }) + + await putAccount(baseState)(address, account) + await putContractStorage(baseState)(address, key, value) + }) + + it('should get the storage value associated with the provided address and key', async () => { + expect(await getContractStorage(baseState)(address, key)).toEqual(value) + }) + + it('should return empty Uint8Array if the storage does not exist', async () => { + const newKey = hexToBytes(`0x${'03'.repeat(32)}`) + expect(await getContractStorage(baseState)(address, newKey)).toEqual(Uint8Array.from([0])) + }) + + it('should throw an error if the key is not 32 bytes long', async () => { + const invalidKey = hexToBytes('0x1234') + const err = await getContractStorage(baseState)(address, invalidKey).catch((e) => e) + expect(err).toBeInstanceOf(Error) + expect(err.message).toMatchSnapshot() + }) + + it('should return empty Uint8Array if the account is not a contract', async () => { + const newAddress = EthjsAddress.fromString(`0x${'02'.repeat(20)}`) + await putAccount(baseState)(newAddress, EthjsAccount.fromAccountData({ balance: 100n, nonce: 1n })) + expect(await getContractStorage(baseState)(newAddress, key)).toEqual(Uint8Array.from([0])) + }) +}) + +describe('getContractStorage forking', () => { + let baseState: BaseState + let knownContractAddress: EthjsAddress + let knownStorageKey: Uint8Array + + beforeEach(() => { + baseState = createBaseState({ + loggingLevel: 'warn', + fork: { + transport: transports.optimism, + blockTag: 122488188n, + }, + }) + + knownContractAddress = EthjsAddress.fromString('0x4200000000000000000000000000000000000042') + knownStorageKey = toBytes(1, { size: 32 }) + }) + + it('should fetch storage from remote provider if not in cache and fork transport is provided', async () => { + const result = await getContractStorage(baseState)(knownContractAddress, knownStorageKey) + expect(result).toBeDefined() + expect(result.length).toBeGreaterThan(0) + expect(result).toMatchSnapshot() + const cachedResult = await getContractStorage(baseState)(knownContractAddress, knownStorageKey) + expect(cachedResult).toEqual(result) + }) + + it('should return empty Uint8Array if the account does not exist and no fork transport', async () => { + const noForkBaseState = createBaseState({ + loggingLevel: 'warn', + }) + expect(await getContractStorage(noForkBaseState)(knownContractAddress, knownStorageKey)).toEqual( + Uint8Array.from([0]), + ) + }) +}) diff --git a/packages/state/src/actions/getForkBlockTag.spec.ts b/packages/state/src/actions/getForkBlockTag.spec.ts new file mode 100644 index 0000000000..f4fb5f4666 --- /dev/null +++ b/packages/state/src/actions/getForkBlockTag.spec.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'bun:test' +import { getForkBlockTag } from './getForkBlockTag.js' + +describe('getForkBlockTag', () => { + it('should return undefined if fork option is not provided', () => { + const baseState = { + options: {}, + } as any + expect(getForkBlockTag(baseState)).toBeUndefined() + }) + + it('should return { blockTag: "latest" } if blockTag is undefined', () => { + const baseState = { + options: { + fork: {}, + }, + } as any + expect(getForkBlockTag(baseState)).toEqual({ blockTag: 'latest' }) + }) + + it('should return { blockNumber } if blockTag is a bigint', () => { + const baseState = { + options: { + fork: { + blockTag: 123456789n, + }, + }, + } as any + expect(getForkBlockTag(baseState)).toEqual({ blockNumber: 123456789n }) + }) + + it('should return { blockTag } if blockTag is a string', () => { + const baseState = { + options: { + fork: { + blockTag: '0xabcdef', + }, + }, + } as any + expect(getForkBlockTag(baseState)).toEqual({ blockTag: '0xabcdef' } as any) + }) +}) diff --git a/packages/state/src/actions/putContractStorage.js b/packages/state/src/actions/putContractStorage.js index 32440d7a20..69aa4b4065 100644 --- a/packages/state/src/actions/putContractStorage.js +++ b/packages/state/src/actions/putContractStorage.js @@ -1,16 +1,7 @@ import { InternalError } from '@tevm/errors' +import { stripZeros } from '../utils/stripZeros.js' import { getAccount } from './getAccount.js' -/** - * @param {Uint8Array} bytes - * @returns {Uint8Array} - */ -const stripZeros = (bytes) => { - if (!(bytes instanceof Uint8Array)) { - throw new InternalError('Unexpected type') - } - return bytes.slice(bytes.findIndex(/** @param {number} entry*/ (entry) => entry !== 0)) -} /** * Adds value to the cache for the `account` * corresponding to `address` at the provided `key`. diff --git a/packages/state/src/actions/putContractStorage.spec.ts b/packages/state/src/actions/putContractStorage.spec.ts new file mode 100644 index 0000000000..ae9845af67 --- /dev/null +++ b/packages/state/src/actions/putContractStorage.spec.ts @@ -0,0 +1,70 @@ +import { beforeEach, describe, expect, it } from 'bun:test' +import { InternalError } from '@tevm/errors' +import { EthjsAccount, EthjsAddress, hexToBytes } from '@tevm/utils' +import type { BaseState } from '../BaseState.js' +import { createBaseState } from '../createBaseState.js' +import { getContractStorage } from './getContractStorage.js' +import { putAccount } from './putAccount.js' +import { putContractStorage } from './putContractStorage.js' + +describe('putContractStorage', () => { + let baseState: BaseState + let address: EthjsAddress + let key: Uint8Array + let value: Uint8Array + let account: EthjsAccount + + beforeEach(async () => { + baseState = createBaseState({ + loggingLevel: 'warn', + }) + + address = EthjsAddress.fromString(`0x${'01'.repeat(20)}`) + key = hexToBytes(`0x${'02'.repeat(32)}`) + value = hexToBytes('0x1234') + account = EthjsAccount.fromAccountData({ + balance: 420n, + nonce: 2n, + }) + + await putAccount(baseState)(address, account) + }) + + it('should add value to the cache for the account at the provided key', async () => { + await putContractStorage(baseState)(address, key, value) + expect(await getContractStorage(baseState)(address, key)).toEqual(value) + }) + + it('should strip leading zeros from the value', async () => { + const valueWithLeadingZeros = hexToBytes('0x00001234') + const strippedValue = hexToBytes('0x1234') + await putContractStorage(baseState)(address, key, valueWithLeadingZeros) + expect(await getContractStorage(baseState)(address, key)).toEqual(strippedValue) + }) + + it('should throw an error if the key is not 32 bytes long', async () => { + const invalidKey = hexToBytes('0x1234') + const err = await putContractStorage(baseState)(address, invalidKey, value).catch((e) => e) + expect(err).toBeInstanceOf(InternalError) + expect(err).toMatchSnapshot() + }) + + it('should throw an error if the account does not exist', async () => { + const newAddress = EthjsAddress.fromString(`0x${'02'.repeat(20)}`) + const err = await putContractStorage(baseState)(newAddress, key, value).catch((e) => e) + expect(err).toBeInstanceOf(InternalError) + expect(err).toMatchSnapshot() + }) + + it('should delete the value if it is empty or filled with zeros', async () => { + const emptyValue = hexToBytes('0x') + await putContractStorage(baseState)(address, key, emptyValue) + expect(await getContractStorage(baseState)(address, key)).toEqual(new Uint8Array()) + }) + + it('should delete the value if it is filled with zeros', async () => { + const zeroValue = hexToBytes(`0x${'00'.repeat(32)}`) + await putContractStorage(baseState)(address, key, zeroValue) + expect(await getContractStorage(baseState)(address, key)).toEqual(Uint8Array.from([0])) + }) +}) diff --git a/packages/state/src/utils/stripZeros.js b/packages/state/src/utils/stripZeros.js new file mode 100644 index 0000000000..fb192235cd --- /dev/null +++ b/packages/state/src/utils/stripZeros.js @@ -0,0 +1,12 @@ +import { InternalError } from '@tevm/errors' + +/** + * @param {Uint8Array} bytes + * @returns {Uint8Array} + */ +export const stripZeros = (bytes) => { + if (!(bytes instanceof Uint8Array)) { + throw new InternalError('Unexpected type') + } + return bytes.slice(bytes.findIndex(/** @param {number} entry*/ (entry) => entry !== 0)) +} diff --git a/packages/state/src/utils/stripZeros.spec.ts b/packages/state/src/utils/stripZeros.spec.ts new file mode 100644 index 0000000000..224550598e --- /dev/null +++ b/packages/state/src/utils/stripZeros.spec.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'bun:test' +import { InternalError } from '@tevm/errors' +import { stripZeros } from './stripZeros.js' + +describe('stripZeros', () => { + it('should strip leading zeros from the Uint8Array', () => { + const input = new Uint8Array([0, 0, 0, 1, 2, 3]) + const expected = new Uint8Array([1, 2, 3]) + expect(stripZeros(input)).toEqual(expected) + }) + + it('should return the same array if there are no leading zeros', () => { + const input = new Uint8Array([1, 2, 3]) + expect(stripZeros(input)).toEqual(input) + }) + + it('should return an 1 0 if all elements are zero', () => { + const input = new Uint8Array([0, 0, 0]) + const expected = new Uint8Array([0]) + expect(stripZeros(input)).toEqual(expected) + }) + + it('should return an empty array if the input is already empty', () => { + const input = new Uint8Array([]) + const expected = new Uint8Array([]) + expect(stripZeros(input)).toEqual(expected) + }) + + it('should throw an InternalError if the input is not a Uint8Array', () => { + expect(() => stripZeros({} as any)).toThrow(InternalError) + expect(() => stripZeros('string' as any)).toThrow(InternalError) + expect(() => stripZeros(123 as any)).toThrow(InternalError) + }) +}) From a783e0682254111fe750e684c9f7ea46f7a9baa3 Mon Sep 17 00:00:00 2001 From: William Cory Date: Tue, 9 Jul 2024 18:51:19 -0700 Subject: [PATCH 2/2] :recycle: Regenerate --- docs/src/content/docs/reference/@tevm/state/functions/commit.md | 2 +- .../docs/reference/@tevm/state/functions/getContractStorage.md | 2 +- .../docs/reference/@tevm/state/functions/putContractStorage.md | 2 +- packages/state/docs/functions/commit.md | 2 +- packages/state/docs/functions/getContractStorage.md | 2 +- packages/state/docs/functions/putContractStorage.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/src/content/docs/reference/@tevm/state/functions/commit.md b/docs/src/content/docs/reference/@tevm/state/functions/commit.md index 6555263da8..058e9a4b92 100644 --- a/docs/src/content/docs/reference/@tevm/state/functions/commit.md +++ b/docs/src/content/docs/reference/@tevm/state/functions/commit.md @@ -40,4 +40,4 @@ This API should not be used in production and may be trimmed from a public relea ## Defined in -[packages/state/src/actions/commit.js:11](https://github.com/evmts/tevm-monorepo/blob/main/packages/state/src/actions/commit.js#L11) +[packages/state/src/actions/commit.js:9](https://github.com/evmts/tevm-monorepo/blob/main/packages/state/src/actions/commit.js#L9) diff --git a/docs/src/content/docs/reference/@tevm/state/functions/getContractStorage.md b/docs/src/content/docs/reference/@tevm/state/functions/getContractStorage.md index a756d2bbf9..a04c6a22a0 100644 --- a/docs/src/content/docs/reference/@tevm/state/functions/getContractStorage.md +++ b/docs/src/content/docs/reference/@tevm/state/functions/getContractStorage.md @@ -34,4 +34,4 @@ If this does not exist an empty `Uint8Array` is returned. ## Defined in -[packages/state/src/actions/getContractStorage.js:14](https://github.com/evmts/tevm-monorepo/blob/main/packages/state/src/actions/getContractStorage.js#L14) +[packages/state/src/actions/getContractStorage.js:15](https://github.com/evmts/tevm-monorepo/blob/main/packages/state/src/actions/getContractStorage.js#L15) diff --git a/docs/src/content/docs/reference/@tevm/state/functions/putContractStorage.md b/docs/src/content/docs/reference/@tevm/state/functions/putContractStorage.md index 600e2e3494..1b8cc35487 100644 --- a/docs/src/content/docs/reference/@tevm/state/functions/putContractStorage.md +++ b/docs/src/content/docs/reference/@tevm/state/functions/putContractStorage.md @@ -36,4 +36,4 @@ If it is empty or filled with zeros, deletes the value. ## Defined in -[packages/state/src/actions/putContractStorage.js:21](https://github.com/evmts/tevm-monorepo/blob/main/packages/state/src/actions/putContractStorage.js#L21) +[packages/state/src/actions/putContractStorage.js:12](https://github.com/evmts/tevm-monorepo/blob/main/packages/state/src/actions/putContractStorage.js#L12) diff --git a/packages/state/docs/functions/commit.md b/packages/state/docs/functions/commit.md index e2fa8dfebf..8954c7473a 100644 --- a/packages/state/docs/functions/commit.md +++ b/packages/state/docs/functions/commit.md @@ -39,4 +39,4 @@ This api is not stable ## Defined in -[packages/state/src/actions/commit.js:11](https://github.com/evmts/tevm-monorepo/blob/main/packages/state/src/actions/commit.js#L11) +[packages/state/src/actions/commit.js:9](https://github.com/evmts/tevm-monorepo/blob/main/packages/state/src/actions/commit.js#L9) diff --git a/packages/state/docs/functions/getContractStorage.md b/packages/state/docs/functions/getContractStorage.md index 324abb69f2..ea1499994e 100644 --- a/packages/state/docs/functions/getContractStorage.md +++ b/packages/state/docs/functions/getContractStorage.md @@ -35,4 +35,4 @@ If this does not exist an empty `Uint8Array` is returned. ## Defined in -[packages/state/src/actions/getContractStorage.js:14](https://github.com/evmts/tevm-monorepo/blob/main/packages/state/src/actions/getContractStorage.js#L14) +[packages/state/src/actions/getContractStorage.js:15](https://github.com/evmts/tevm-monorepo/blob/main/packages/state/src/actions/getContractStorage.js#L15) diff --git a/packages/state/docs/functions/putContractStorage.md b/packages/state/docs/functions/putContractStorage.md index 9a8dd09f87..e87ac445d2 100644 --- a/packages/state/docs/functions/putContractStorage.md +++ b/packages/state/docs/functions/putContractStorage.md @@ -37,4 +37,4 @@ If it is empty or filled with zeros, deletes the value. ## Defined in -[packages/state/src/actions/putContractStorage.js:21](https://github.com/evmts/tevm-monorepo/blob/main/packages/state/src/actions/putContractStorage.js#L21) +[packages/state/src/actions/putContractStorage.js:12](https://github.com/evmts/tevm-monorepo/blob/main/packages/state/src/actions/putContractStorage.js#L12)