From cfeb752911c09b65276118cea44655d101f2a9d3 Mon Sep 17 00:00:00 2001 From: Mark Tyneway Date: Sun, 6 Mar 2022 09:08:50 -0800 Subject: [PATCH] feat: eip1153 Implement EIP 1153 - transient storage TLOAD 0xb3 TSTORE 0xb4 cleanup remove dead comment remove diff --- packages/common/src/eips/1153.json | 22 +++ packages/common/src/eips/index.ts | 1 + packages/vm/src/evm/eei.ts | 8 + packages/vm/src/evm/opcodes/codes.ts | 7 + packages/vm/src/evm/opcodes/functions.ts | 28 +++ packages/vm/src/index.ts | 3 +- packages/vm/src/state/interface.ts | 2 + packages/vm/src/state/stateManager.ts | 32 ++++ packages/vm/src/state/transientStorage.ts | 109 ++++++++++++ packages/vm/tests/api/EIPs/eip-1153.spec.ts | 101 +++++++++++ .../vm/tests/api/state/stateManager.spec.ts | 162 ++++++++++++++++++ 11 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 packages/common/src/eips/1153.json create mode 100644 packages/vm/src/state/transientStorage.ts create mode 100644 packages/vm/tests/api/EIPs/eip-1153.spec.ts diff --git a/packages/common/src/eips/1153.json b/packages/common/src/eips/1153.json new file mode 100644 index 00000000000..5dbb643e67f --- /dev/null +++ b/packages/common/src/eips/1153.json @@ -0,0 +1,22 @@ +{ + "name": "EIP-1153", + "number": 1153, + "comment": "Transient Storage", + "url": "https://eips.ethereum.org/EIPS/eip-1153", + "status": "Review", + "minimumHardfork": "chainstart", + "requiredEIPs": [], + "gasConfig": {}, + "gasPrices": { + "tstore": { + "v": 100, + "d": "Base fee of the TSTORE opcode" + }, + "tload": { + "v": 100, + "d": "Base fee of the TLOAD opcode" + } + }, + "vm": {}, + "pow": {} +} diff --git a/packages/common/src/eips/index.ts b/packages/common/src/eips/index.ts index c8e4b7ee0ed..83e64c637b4 100644 --- a/packages/common/src/eips/index.ts +++ b/packages/common/src/eips/index.ts @@ -1,6 +1,7 @@ import { eipsType } from './../types' export const EIPs: eipsType = { + 1153: require('./1153.json'), 1559: require('./1559.json'), 2315: require('./2315.json'), 2537: require('./2537.json'), diff --git a/packages/vm/src/evm/eei.ts b/packages/vm/src/evm/eei.ts index d567cb213f6..30555d0b144 100644 --- a/packages/vm/src/evm/eei.ts +++ b/packages/vm/src/evm/eei.ts @@ -380,6 +380,14 @@ export default class EEI { } } + transientStorageStore(key: Buffer, value: Buffer): void { + return this._state.putContractTransientStorage(this._env.address, key, value) + } + + transientStorageLoad(key: Buffer): Buffer { + return this._state.getContractTransientStorage(this._env.address, key) + } + /** * Returns the current gasCounter. */ diff --git a/packages/vm/src/evm/opcodes/codes.ts b/packages/vm/src/evm/opcodes/codes.ts index be230ff569f..07bbd835675 100644 --- a/packages/vm/src/evm/opcodes/codes.ts +++ b/packages/vm/src/evm/opcodes/codes.ts @@ -249,6 +249,13 @@ const hardforkOpcodes: { hardforkName: string; opcodes: OpcodeEntry }[] = [ ] const eipOpcodes: { eip: number; opcodes: OpcodeEntry }[] = [ + { + eip: 1153, + opcodes: { + 0xb3: { name: 'TLOAD', isAsync: false, dynamicGas: false }, + 0xb4: { name: 'TSTORE', isAsync: false, dynamicGas: false }, + }, + }, { eip: 2315, opcodes: { diff --git a/packages/vm/src/evm/opcodes/functions.ts b/packages/vm/src/evm/opcodes/functions.ts index 840f6139be3..62fbb3723a2 100644 --- a/packages/vm/src/evm/opcodes/functions.ts +++ b/packages/vm/src/evm/opcodes/functions.ts @@ -867,7 +867,35 @@ export const handlers: Map = new Map([ runState.eei.log(mem, topicsCount, topicsBuf) }, ], + // 0xb3: TLOAD + [ + 0xb3, + function (runState) { + const key = runState.stack.pop() + const keyBuf = key.toArrayLike(Buffer, 'be', 32) + const value = runState.eei.transientStorageLoad(keyBuf) + const valueBN = value.length ? new BN(value) : new BN(0) + runState.stack.push(valueBN) + }, + ], + // 0xb4: TSTORE + [ + 0xb4, + function (runState) { + const [key, val] = runState.stack.popN(2) + const keyBuf = key.toArrayLike(Buffer, 'be', 32) + // NOTE: this should be the shortest representation + let value + if (val.isZero()) { + value = Buffer.from([]) + } else { + value = val.toArrayLike(Buffer, 'be') + } + + runState.eei.transientStorageStore(keyBuf, value) + }, + ], // '0xf0' range - closures // 0xf0: CREATE [ diff --git a/packages/vm/src/index.ts b/packages/vm/src/index.ts index 6756e92e28c..cf4805c5b35 100644 --- a/packages/vm/src/index.ts +++ b/packages/vm/src/index.ts @@ -41,6 +41,7 @@ export interface VMOpts { * * ### Supported EIPs * + * - [EIP-1153](https://eips.ethereum.org/EIPS/eip-1153) - Transient Storage Opcodes * - [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) - Fee Market * - [EIP-2315](https://eips.ethereum.org/EIPS/eip-2315) - VM simple subroutines * - [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537) (`experimental`) - BLS12-381 precompiles @@ -205,7 +206,7 @@ export default class VM extends AsyncEventEmitter { if (opts.common) { // Supported EIPs const supportedEIPs = [ - 1559, 2315, 2537, 2565, 2718, 2929, 2930, 3198, 3529, 3541, 3607, 3855, 3860, + 1153, 1559, 2315, 2537, 2565, 2718, 2929, 2930, 3198, 3529, 3541, 3607, 3855, 3860, ] for (const eip of opts.common.eips()) { if (!supportedEIPs.includes(eip)) { diff --git a/packages/vm/src/state/interface.ts b/packages/vm/src/state/interface.ts index 1e9179a5148..d89deb5e0bf 100644 --- a/packages/vm/src/state/interface.ts +++ b/packages/vm/src/state/interface.ts @@ -21,6 +21,8 @@ export interface StateManager { getOriginalContractStorage(address: Address, key: Buffer): Promise putContractStorage(address: Address, key: Buffer, value: Buffer): Promise clearContractStorage(address: Address): Promise + getContractTransientStorage(address: Address, key: Buffer): Buffer + putContractTransientStorage(address: Address, key: Buffer, value: Buffer): void checkpoint(): Promise commit(): Promise revert(): Promise diff --git a/packages/vm/src/state/stateManager.ts b/packages/vm/src/state/stateManager.ts index 9142cc96bef..4c1f5de3093 100644 --- a/packages/vm/src/state/stateManager.ts +++ b/packages/vm/src/state/stateManager.ts @@ -18,6 +18,7 @@ import Common from '@ethereumjs/common' import { StateManager, StorageDump } from './interface' import Cache, { getCb, putCb } from './cache' import { BaseStateManager } from './' +import TransientStorage from './transientStorage' import { short } from '../evm/opcodes' type StorageProof = { @@ -48,6 +49,8 @@ export interface DefaultStateManagerOpts { * A {@link SecureTrie} instance */ trie?: Trie + + transientStorage?: TransientStorage } /** @@ -63,6 +66,7 @@ export interface DefaultStateManagerOpts { export default class DefaultStateManager extends BaseStateManager implements StateManager { _trie: Trie _storageTries: { [key: string]: Trie } + _transientStorage: TransientStorage /** * Instantiate the StateManager interface. @@ -72,6 +76,7 @@ export default class DefaultStateManager extends BaseStateManager implements Sta this._trie = opts.trie ?? new Trie() this._storageTries = {} + this._transientStorage = new TransientStorage() /* * For a custom StateManager implementation adopt these @@ -103,6 +108,7 @@ export default class DefaultStateManager extends BaseStateManager implements Sta return new DefaultStateManager({ trie: this._trie.copy(false), common: this._common, + transientStorage: this._transientStorage.copy(), }) } @@ -261,6 +267,29 @@ export default class DefaultStateManager extends BaseStateManager implements Sta }) } + getContractTransientStorage(address: Address, key: Buffer): Buffer { + if (key.length !== 32) { + throw new Error('Storage key must be 32 bytes long') + } + return this._transientStorage.get(address, key) + } + + putContractTransientStorage(address: Address, key: Buffer, value: Buffer): void { + if (this.DEBUG) { + this._debug(`Update transient storage for account ${address} to ${short(value)}`) + } + + if (key.length !== 32) { + throw new Error('Transient storage key must be 32 bytes long') + } + + if (value.length > 32) { + throw new Error('Transient storage value cannot be longer than 32 bytes') + } + + this._transientStorage.put(address, key, value) + } + /** * Clears all storage entries for the account corresponding to `address`. * @param address - Address to clear the storage of @@ -279,6 +308,7 @@ export default class DefaultStateManager extends BaseStateManager implements Sta */ async checkpoint(): Promise { this._trie.checkpoint() + this._transientStorage.checkpoint() await super.checkpoint() } @@ -289,6 +319,7 @@ export default class DefaultStateManager extends BaseStateManager implements Sta async commit(): Promise { // setup trie checkpointing await this._trie.commit() + this._transientStorage.commit() await super.commit() } @@ -299,6 +330,7 @@ export default class DefaultStateManager extends BaseStateManager implements Sta async revert(): Promise { // setup trie checkpointing await this._trie.revert() + this._transientStorage.revert() this._storageTries = {} await super.revert() } diff --git a/packages/vm/src/state/transientStorage.ts b/packages/vm/src/state/transientStorage.ts new file mode 100644 index 00000000000..26dddc65862 --- /dev/null +++ b/packages/vm/src/state/transientStorage.ts @@ -0,0 +1,109 @@ +import { Address } from 'ethereumjs-util' + +export interface TransientStorageOps { + storage?: Map> + changesets?: Changeset[][] +} + +export type TStorage = Map> + +export interface Changeset { + addr: Address + key: Buffer + prevValue: Buffer +} + +const copyStorage = (input: TStorage): TStorage => { + const map = new Map() + for (const [addr, storage] of input.entries()) { + const copy = new Map() + for (const [key, value] of storage.entries()) { + copy.set(key, Buffer.from(value)) + } + map.set(addr, copy) + } + return map +} + +export default class TransientStorage { + _storage: TStorage + _changesets: Changeset[][] + + constructor(opts: TransientStorageOps = {}) { + this._storage = opts.storage ?? new Map() + this._changesets = opts.changesets ?? [[]] + } + + private _addChangeset(changeset: Changeset) { + const latest = this._changesets[this._changesets.length - 1] + if (!latest) { + throw new Error('no changeset initialized') + } + latest.push(changeset) + } + + get(addr: Address, key: Buffer): Buffer { + const map = this._storage.get(addr) + if (!map) { + return Buffer.alloc(32, 0x00) + } + const value = map.get(key.toString('hex')) + if (!value) { + return Buffer.alloc(32, 0x00) + } + return value + } + + put(addr: Address, key: Buffer, value: Buffer) { + if (!this._storage.has(addr)) { + this._storage.set(addr, new Map()) + } + const map = this._storage.get(addr) + + const str = key.toString('hex') + const prevValue = map?.get(str) ?? Buffer.alloc(32, 0x00) + + this._addChangeset({ + addr, + key, + prevValue, + }) + + map?.set(str, value) + } + + revert() { + const changeset = this._changesets.pop() + if (!changeset) { + throw new Error('cannot revert without a changeset') + } + for (const change of changeset) { + const map = this._storage.get(change.addr) + map?.set(change.key.toString('hex'), change.prevValue) + } + } + + commit(): void { + // Don't allow there to be no changeset + if (this._changesets.length <= 1) { + throw new Error('trying to commit when not checkpointed') + } + this._changesets.pop() + } + + checkpoint(): void { + this._changesets.push([]) + } + + clear(): void { + this._storage = new Map() + this._changesets = [[]] + } + + copy(): TransientStorage { + return new TransientStorage({ + storage: copyStorage(this._storage), + changesets: this._changesets.slice(), + }) + } +} diff --git a/packages/vm/tests/api/EIPs/eip-1153.spec.ts b/packages/vm/tests/api/EIPs/eip-1153.spec.ts new file mode 100644 index 00000000000..b01343d0f42 --- /dev/null +++ b/packages/vm/tests/api/EIPs/eip-1153.spec.ts @@ -0,0 +1,101 @@ +import tape from 'tape' +import VM from '../../../src' +import Common, { Chain, Hardfork } from '@ethereumjs/common' +import { Account, Address, BN } from 'ethereumjs-util' +import { Transaction } from '@ethereumjs/tx' + +const TLOAD = 'b3' +const TSTORE = 'b4' + +tape('EIP 1153: transient storage', (t) => { + const initialGas = new BN(0xffffffffff) + const address = new Address(Buffer.from('000000000000000000000000636F6E7472616374', 'hex')) + const senderKey = Buffer.from( + 'e331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109', + 'hex' + ) + const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Berlin, eips: [1153] }) + + const runTest = async function (test: any, st: tape.Test) { + let i = 0 + let currentGas = initialGas + const vm = new VM({ common }) + + vm.on('step', function (step: any) { + const gasUsed = currentGas.sub(step.gasLeft) + currentGas = step.gasLeft + + st.equal( + step.opcode.name, + test.steps[i].expectedOpcode, + `Expected Opcode: ${test.steps[i].expectedOpcode}` + ) + + st.deepEqual( + step.stack.map((e: BN) => e.toString()), + test.steps[i].expectedStack.map((e: BN) => e.toString()), + `Expected stack: ${step.stack}` + ) + + if (i > 0) { + const expectedGasUsed = new BN(test.steps[i - 1].expectedGasUsed) + st.equal( + true, + gasUsed.eq(expectedGasUsed), + `Opcode: ${ + test.steps[i - 1].expectedOpcode + }, Gas Used: ${gasUsed}, Expected: ${expectedGasUsed}` + ) + } + i++ + }) + + await vm.stateManager.putContractCode(address, Buffer.from(test.code, 'hex')) + + const unsignedTx = Transaction.fromTxData({ + gasLimit: new BN(21000 + 9000), + to: address, + value: new BN(1), + }) + + const initialBalance = new BN(10).pow(new BN(18)) + const account = await vm.stateManager.getAccount(address) + + await vm.stateManager.putAccount( + Address.fromPrivateKey(senderKey), + Account.fromAccountData({ ...account, balance: initialBalance }) + ) + + const tx = unsignedTx.sign(senderKey) + + const result = await vm.runTx({ tx }) + return result + } + + t.test('should tload and tstore', async (st) => { + const code = '60026001' + TSTORE + '6001' + TLOAD + '600052' + '602060' + '00F3' + const returndata = Buffer.alloc(32) + returndata[31] = 0x02 + + const test = { + code: code, + steps: [ + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(2)] }, + { expectedOpcode: 'TSTORE', expectedGasUsed: 100, expectedStack: [new BN(2), new BN(1)] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [] }, + { expectedOpcode: 'TLOAD', expectedGasUsed: 100, expectedStack: [new BN(1)] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(2)] }, + { expectedOpcode: 'MSTORE', expectedGasUsed: 6, expectedStack: [new BN(2), new BN(0)] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(32)] }, + { expectedOpcode: 'RETURN', expectedGasUsed: NaN, expectedStack: [new BN(32), new BN(0)] }, + ], + } + + const result = await runTest(test, st) + st.deepEqual(returndata, result.execResult.returnValue) + st.equal(undefined, result.execResult.exceptionError) + st.end() + }) +}) diff --git a/packages/vm/tests/api/state/stateManager.spec.ts b/packages/vm/tests/api/state/stateManager.spec.ts index e1136f5de14..c88095abe69 100644 --- a/packages/vm/tests/api/state/stateManager.spec.ts +++ b/packages/vm/tests/api/state/stateManager.spec.ts @@ -832,3 +832,165 @@ tape('StateManager - generateAccessList', (tester) => { t.end() }) }) + +tape.only('StateManager - Transient storage', (tester) => { + const it = tester.test + it('should set and get storage', (t) => { + const stateManager = new DefaultStateManager() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value = Buffer.alloc(32, 0x99) + + stateManager.putContractTransientStorage(address, key, value) + const got = stateManager.getContractTransientStorage(address, key) + t.deepEqual(value, got) + t.end() + }) + + it('should return bytes32(0) if there is no key set', (t) => { + const stateManager = new DefaultStateManager() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value = Buffer.alloc(32, 0x11) + + // No address set + const got = stateManager.getContractTransientStorage(address, key) + t.deepEqual(Buffer.alloc(32, 0x00), got) + + // Address set, no key set + stateManager.putContractTransientStorage(address, key, value) + const got2 = stateManager.getContractTransientStorage(address, Buffer.alloc(32, 0x22)) + t.deepEqual(Buffer.alloc(32, 0x00), got2) + + t.end() + }) + + it('should revert', async (t) => { + const stateManager = new DefaultStateManager() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value = Buffer.alloc(32, 0x99) + + stateManager.putContractTransientStorage(address, key, value) + + await stateManager.checkpoint() + + const value2 = Buffer.alloc(32, 0x22) + stateManager.putContractTransientStorage(address, key, value2) + const got = stateManager.getContractTransientStorage(address, key) + t.deepEqual(got, value2) + + await stateManager.revert() + + const got2 = stateManager.getContractTransientStorage(address, key) + t.deepEqual(got2, value) + t.end() + }) + + it('should commit', async (t) => { + const stateManager = new DefaultStateManager() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value = Buffer.alloc(32, 0x99) + + stateManager.putContractTransientStorage(address, key, value) + + await stateManager.checkpoint() + await stateManager.commit() + + const got = stateManager.getContractTransientStorage(address, key) + t.deepEqual(got, value) + t.end() + }) + + it('should copy', (t) => { + const stateManager = new DefaultStateManager() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value = Buffer.alloc(32, 0x99) + + stateManager.putContractTransientStorage(address, key, value) + + const stateManager2 = stateManager.copy() + stateManager2.putContractTransientStorage(address, key, Buffer.alloc(32, 0x11)) + + const got = stateManager.getContractTransientStorage(address, key) + const got2 = stateManager2.getContractTransientStorage(address, key) + + t.notEqual(got.toString('hex'), got2.toString('hex')) + t.end() + }) + + it('should fail to commit without checkpoint', (t) => { + const stateManager = new DefaultStateManager() + + t.throws(() => { + stateManager._transientStorage.commit() + }, /trying to commit when not checkpointed/) + + t.end() + }) + + it('should fail to revert with empty changesets', (t) => { + const stateManager = new DefaultStateManager() + stateManager._transientStorage._changesets = [] + + t.throws(() => { + stateManager._transientStorage.revert() + }, /cannot revert without a changeset/) + + t.end() + }) + + it('should fail to add storage with empty changesets', (t) => { + const stateManager = new DefaultStateManager() + stateManager._transientStorage._changesets = [] + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value = Buffer.alloc(32, 0x99) + + t.throws(() => { + stateManager.putContractTransientStorage(address, key, value) + }, /no changeset initialized/) + + t.end() + }) + + it('should fail with wrong size key/value', (t) => { + const stateManager = new DefaultStateManager() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + + t.throws(() => { + stateManager.putContractTransientStorage(address, Buffer.alloc(10), Buffer.alloc(1)) + }, /Transient storage key must be 32 bytes long/) + + t.throws(() => { + stateManager.putContractTransientStorage(address, Buffer.alloc(32), Buffer.alloc(100)) + }, /Transient storage value cannot be longer than 32 bytes/) + + t.end() + }) + + it('should clear', (t) => { + const stateManager = new DefaultStateManager() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value = Buffer.alloc(32, 0x99) + + stateManager.putContractTransientStorage(address, key, value) + + stateManager._transientStorage.clear() + + const got = stateManager.getContractTransientStorage(address, key) + t.deepEqual(got, Buffer.alloc(32, 0x00)) + t.end() + }) +})