diff --git a/packages/common/src/eips/1153.json b/packages/common/src/eips/1153.json new file mode 100644 index 0000000000..7d57dc67b8 --- /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 41310462a9..fa55016409 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 d567cb213f..cbd3860e0f 100644 --- a/packages/vm/src/evm/eei.ts +++ b/packages/vm/src/evm/eei.ts @@ -8,6 +8,7 @@ import { VmError, ERROR } from '../exceptions' import Message from './message' import EVM, { EVMResult } from './evm' import { Log } from './types' +import { TransientStorage } from '../state' const debugGas = createDebugLogger('vm:eei:gas') @@ -69,8 +70,16 @@ export default class EEI { _lastReturned: Buffer _common: Common _gasLeft: BN - - constructor(env: Env, state: StateManager, evm: EVM, common: Common, gasLeft: BN) { + _transientStorage: TransientStorage + + constructor( + env: Env, + state: StateManager, + evm: EVM, + common: Common, + gasLeft: BN, + transientStorage: TransientStorage + ) { this._env = env this._state = state this._evm = evm @@ -82,6 +91,7 @@ export default class EEI { returnValue: undefined, selfdestruct: {}, } + this._transientStorage = transientStorage } /** @@ -380,6 +390,23 @@ export default class EEI { } } + /** + * Store 256-bit a value in memory to transient storage. + * @param key - Storage key + * @param value - Storage value + */ + transientStorageStore(key: Buffer, value: Buffer): void { + return this._transientStorage.put(this._env.address, key, value) + } + + /** + * Loads a 256-bit value to memory from transient storage. + * @param key - Storage key + */ + transientStorageLoad(key: Buffer): Buffer { + return this._transientStorage.get(this._env.address, key) + } + /** * Returns the current gasCounter. */ diff --git a/packages/vm/src/evm/evm.ts b/packages/vm/src/evm/evm.ts index 42fa93e629..74be0c817b 100644 --- a/packages/vm/src/evm/evm.ts +++ b/packages/vm/src/evm/evm.ts @@ -20,6 +20,8 @@ import { short } from './opcodes/util' import * as eof from './opcodes/eof' import { Log } from './types' import { default as Interpreter, InterpreterOpts, RunState } from './interpreter' +import VM from '../index' +import { TransientStorage } from '../state' const debug = createDebugLogger('vm:evm') const debugGas = createDebugLogger('vm:evm:gas') @@ -130,7 +132,7 @@ export function VmErrorResult(error: VmError, gasUsed: BN): ExecResult { * @ignore */ export default class EVM { - _vm: any + _vm: VM _state: StateManager _tx: TxContext _block: Block @@ -138,13 +140,15 @@ export default class EVM { * Amount of gas to refund from deleting storage values */ _refund: BN + _transientStorage: TransientStorage - constructor(vm: any, txContext: TxContext, block: Block) { + constructor(vm: VM, txContext: TxContext, block: Block) { this._vm = vm this._state = this._vm.stateManager this._tx = txContext this._block = block this._refund = new BN(0) + this._transientStorage = new TransientStorage() } /** @@ -163,6 +167,8 @@ export default class EVM { const oldRefund = this._refund.clone() await this._state.checkpoint() + this._transientStorage.checkpoint() + if (this._vm.DEBUG) { debug('-'.repeat(100)) debug(`message checkpoint`) @@ -212,6 +218,7 @@ export default class EVM { if (this._vm._common.gteHardfork('homestead') || err.error != ERROR.CODESTORE_OUT_OF_GAS) { result.execResult.logs = [] await this._state.revert() + this._transientStorage.revert() if (this._vm.DEBUG) { debug(`message checkpoint reverted`) } @@ -219,12 +226,14 @@ export default class EVM { // we are in chainstart and the error was the code deposit error // we do like nothing happened. await this._state.commit() + this._transientStorage.commit() if (this._vm.DEBUG) { debug(`message checkpoint committed`) } } } else { await this._state.commit() + this._transientStorage.commit() if (this._vm.DEBUG) { debug(`message checkpoint committed`) } @@ -528,7 +537,14 @@ export default class EVM { contract: await this._state.getAccount(message.to || Address.zero()), codeAddress: message.codeAddress, } - const eei = new EEI(env, this._state, this, this._vm._common, message.gasLimit.clone()) + const eei = new EEI( + env, + this._state, + this, + this._vm._common, + message.gasLimit.clone(), + this._transientStorage + ) if (message.selfdestruct) { eei._result.selfdestruct = message.selfdestruct } diff --git a/packages/vm/src/evm/opcodes/codes.ts b/packages/vm/src/evm/opcodes/codes.ts index ef5a663a39..8b7c77ab17 100644 --- a/packages/vm/src/evm/opcodes/codes.ts +++ b/packages/vm/src/evm/opcodes/codes.ts @@ -252,6 +252,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 80b404afc6..ac9294c5ce 100644 --- a/packages/vm/src/evm/opcodes/functions.ts +++ b/packages/vm/src/evm/opcodes/functions.ts @@ -873,7 +873,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 6261e151cb..da0e2529a5 100644 --- a/packages/vm/src/index.ts +++ b/packages/vm/src/index.ts @@ -43,6 +43,7 @@ export interface VMOpts { * * ### Supported EIPs * + * - [EIP-1153](https://eips.ethereum.org/EIPS/eip-1153) - Transient Storage Opcodes (`experimental`) * - [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) - EIP-1559 Fee Market * - [EIP-2315](https://eips.ethereum.org/EIPS/eip-2315) - VM simple subroutines (`experimental`) * - [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537) - BLS12-381 precompiles (`experimental`) @@ -176,7 +177,7 @@ export default class VM extends AsyncEventEmitter { protected readonly _opts: VMOpts protected _isInitialized: boolean = false - protected readonly _allowUnlimitedContractSize: boolean + public readonly _allowUnlimitedContractSize: boolean // This opcode data is always set since `getActiveOpcodes()` is called in the constructor protected _opcodes!: OpcodeList protected _handlers!: Map @@ -207,7 +208,7 @@ export default class VM extends AsyncEventEmitter { * performance reasons to avoid string literal evaluation * @hidden */ - protected readonly DEBUG: boolean = false + readonly DEBUG: boolean = false /** * VM async constructor. Creates engine instance and initializes it. @@ -239,8 +240,8 @@ export default class VM extends AsyncEventEmitter { if (opts.common) { // Supported EIPs const supportedEIPs = [ - 1559, 2315, 2537, 2565, 2718, 2929, 2930, 3198, 3529, 3540, 3541, 3607, 3670, 3855, 3860, - 4399, + 1153, 1559, 2315, 2537, 2565, 2718, 2929, 2930, 3198, 3529, 3540, 3541, 3607, 3670, 3855, + 3860, 4399, ] for (const eip of opts.common.eips()) { if (!supportedEIPs.includes(eip)) { diff --git a/packages/vm/src/state/index.ts b/packages/vm/src/state/index.ts index 50a9960b28..5893f77e71 100644 --- a/packages/vm/src/state/index.ts +++ b/packages/vm/src/state/index.ts @@ -1,3 +1,4 @@ export { StateManager, EIP2929StateManager } from './interface' export { BaseStateManager } from './baseStateManager' export { default as DefaultStateManager, Proof } from './stateManager' +export { default as TransientStorage } from './transientStorage' diff --git a/packages/vm/src/state/transientStorage.ts b/packages/vm/src/state/transientStorage.ts new file mode 100644 index 0000000000..cec400b8c5 --- /dev/null +++ b/packages/vm/src/state/transientStorage.ts @@ -0,0 +1,147 @@ +import { Address } from 'ethereumjs-util' + +export type TStorage = Map> + +export interface TransientStorageModification { + addr: Address + key: Buffer + prevValue: Buffer +} + +export type Changeset = TStorage + +export interface TransientStorageOptions { + storage?: TStorage + changesets?: Changeset[] +} + +function copyTransientStorage(input: TStorage): TStorage { + const map: TStorage = new Map() + for (const [addr, storage] of input.entries()) { + const copy = new Map() + for (const [key, value] of storage.entries()) { + copy.set(key, value) + } + map.set(addr, copy) + } + return map +} + +export default class TransientStorage { + _storage: TStorage + _changesets: Changeset[] + + constructor(opts: TransientStorageOptions = {}) { + this._storage = opts.storage ?? new Map() + this._changesets = opts.changesets ?? [new Map()] + } + + private get latestChangeset(): Changeset { + if (this._changesets.length === 0) { + throw new Error('no changeset initialized') + } + return this._changesets[this._changesets.length - 1] + } + + private recordModification(modification: TransientStorageModification) { + const latest = this.latestChangeset + const addrString = modification.addr.toString() + if (!latest.has(addrString)) { + latest.set(addrString, new Map()) + } + const addrMap = latest.get(addrString)! + + const keyString = modification.key.toString('hex') + // we only need the previous value for the first time the addr-key has been changed since the last checkpoint + if (!addrMap.has(keyString)) { + addrMap.set(keyString, modification.prevValue) + } + } + + public get(addr: Address, key: Buffer): Buffer { + const map = this._storage.get(addr.toString()) + if (!map) { + return Buffer.alloc(32) + } + const value = map.get(key.toString('hex')) + if (!value) { + return Buffer.alloc(32) + } + return value + } + + public put(addr: Address, key: Buffer, value: Buffer) { + 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') + } + + if (!this._storage.has(addr.toString())) { + this._storage.set(addr.toString(), new Map()) + } + const map = this._storage.get(addr.toString())! + + const str = key.toString('hex') + const prevValue = map.get(str) ?? Buffer.alloc(32) + + this.recordModification({ + addr, + key, + prevValue, + }) + + map.set(str, value) + } + + public revert() { + const changeset = this._changesets.pop() + if (!changeset) { + throw new Error('cannot revert without a changeset') + } + + for (const [addr, map] of changeset.entries()) { + for (const [key, prevValue] of map.entries()) { + const storageMap = this._storage.get(addr)! + storageMap.set(key, prevValue) + } + } + } + + public 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() + } + + public checkpoint(): void { + this._changesets.push(new Map()) + } + + public toJSON(): { [address: string]: { [key: string]: string } } { + const obj: { [address: string]: { [key: string]: string } } = {} + for (const [address, map] of this._storage.entries()) { + obj[address.toString()] = {} + for (const [key, value] of map.entries()) { + obj[address.toString()][key] = value.toString('hex') + } + } + return obj + } + + public clear(): void { + this._storage = new Map() + this._changesets = [new Map()] + } + + public copy(): TransientStorage { + return new TransientStorage({ + storage: copyTransientStorage(this._storage), + changesets: this._changesets.slice().map(copyTransientStorage), + }) + } +} 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 0000000000..402efd683c --- /dev/null +++ b/packages/vm/tests/api/EIPs/eip-1153.spec.ts @@ -0,0 +1,663 @@ +import tape from 'tape' +import VM from '../../../src' +import Common, { Chain, Hardfork } from '@ethereumjs/common' +import { Address, BN } from 'ethereumjs-util' +import { Transaction } from '@ethereumjs/tx' + +interface Test { + steps: { expectedOpcode: string; expectedGasUsed: number; expectedStack: BN[] }[] + contracts: { code: string; address: Address }[] + transactions: Transaction[] +} + +const senderKey = Buffer.from( + 'e331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109', + 'hex' +) + +tape('EIP 1153: transient storage', (t) => { + const initialGas = new BN(0xffffffffff) + const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Berlin, eips: [1153] }) + + const runTest = async function (test: Test, 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++ + }) + + for (const { code, address } of test.contracts) { + await vm.stateManager.putContractCode(address, Buffer.from(code, 'hex')) + } + + const results = [] + for (const tx of test.transactions) { + const result = await vm.runTx({ tx, skipBalance: true }) + results.push(result) + } + + return results + } + + t.test('should tload and tstore', async (st) => { + const code = '60026001b46001b360005260206000F3' + const returndata = Buffer.alloc(32) + returndata[31] = 0x02 + + const address = new Address(Buffer.from('000000000000000000000000636F6E7472616374', 'hex')) + const tx = Transaction.fromTxData({ + gasLimit: new BN(21000 + 9000), + to: address, + value: new BN(1), + }).sign(senderKey) + + const test = { + contracts: [{ address, code }], + transactions: [tx], + 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[0].execResult.returnValue) + st.equal(undefined, result[0].execResult.exceptionError) + st.end() + }) + + t.test('should clear between transactions', async (st) => { + // If calldata size is 0, do a tload and return the value + // at key 0. If calldata size is nonzero, do a tstore at + // key 0. Send a transaction with nonzero calldata first + // and then send a second transaction with zero calldata + // and then assert that the returndata is 0. If the returndata + // is 0, then the transient storage is cleared between + // transactions + const code = '36600014630000001c5760016300000012575b60ff6000b4600080f35b6000b360005260206000f3' + const address = new Address(Buffer.from('000000000000000000000000636F6E7472616374', 'hex')) + + const test = { + contracts: [{ address, code }], + transactions: [ + Transaction.fromTxData({ + gasLimit: new BN(15000000), + to: address, + data: Buffer.alloc(32), + }).sign(senderKey), + Transaction.fromTxData({ + nonce: 1, + gasLimit: new BN(15000000), + to: address, + }).sign(senderKey), + ], + steps: [ + // first tx + { expectedOpcode: 'CALLDATASIZE', expectedGasUsed: 2, expectedStack: [] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(32)] }, + { expectedOpcode: 'EQ', expectedGasUsed: 3, expectedStack: [new BN(32), new BN(0)] }, + { expectedOpcode: 'PUSH4', expectedGasUsed: 3, expectedStack: [new BN(0)] }, + { expectedOpcode: 'JUMPI', expectedGasUsed: 10, expectedStack: [new BN(0), new BN(28)] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [] }, + { expectedOpcode: 'PUSH4', expectedGasUsed: 3, expectedStack: [new BN(1)] }, + { expectedOpcode: 'JUMPI', expectedGasUsed: 10, expectedStack: [new BN(1), new BN(18)] }, + { expectedOpcode: 'JUMPDEST', expectedGasUsed: 1, expectedStack: [] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(255)] }, + { expectedOpcode: 'TSTORE', expectedGasUsed: 100, expectedStack: [new BN(255), new BN(0)] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [] }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3, expectedStack: [new BN(0)] }, + { expectedOpcode: 'RETURN', expectedGasUsed: -278, expectedStack: [new BN(0), new BN(0)] }, + // second tx + { expectedOpcode: 'CALLDATASIZE', expectedGasUsed: 2, expectedStack: [] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(0)] }, + { expectedOpcode: 'EQ', expectedGasUsed: 3, expectedStack: [new BN(0), new BN(0)] }, + { expectedOpcode: 'PUSH4', expectedGasUsed: 3, expectedStack: [new BN(1)] }, + { expectedOpcode: 'JUMPI', expectedGasUsed: 10, expectedStack: [new BN(1), new BN(28)] }, + { expectedOpcode: 'JUMPDEST', expectedGasUsed: 1, expectedStack: [] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [] }, + { expectedOpcode: 'TLOAD', expectedGasUsed: 100, expectedStack: [new BN(0)] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(0)] }, + { expectedOpcode: 'MSTORE', expectedGasUsed: 6, expectedStack: [new BN(0), new BN(0)] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(32)] }, + { expectedOpcode: 'RETURN', expectedGasUsed: 3, expectedStack: [new BN(32), new BN(0)] }, + ], + } + + const [result1, result2] = await runTest(test, st) + st.equal(result1.execResult.exceptionError, undefined) + st.deepEqual(new BN(result2.execResult.returnValue).toNumber(), 0x00) + st.end() + }) + + t.test('tload should not keep reverted changes', async (st) => { + // logic address has a contract with transient storage logic in it + const logicAddress = new Address(Buffer.from('EA674fdDe714fd979de3EdF0F56AA9716B898ec8', 'hex')) + // calling address is the address that calls the logic address + const callingAddress = new Address(Buffer.alloc(20, 0xff)) + + // Perform 3 calls: + // - TSTORE, return + // - TSTORE, revert + // - TLOAD, return + // Then return the returndata from the final call and + // assert that the value that is returned is the value set in the + // first call. This asserts that reverts are handled correctly. + + const logicCode = + '36600080376000518063afc874d214630000003457806362fdb9be14630000003f57806343ac1c3914630000004a5760006000fd5b60ff6000b460006000fd5b60aa6000b460006000f35b6000b360005260206000f3' + const callingCode = + '6362fdb9be600052602060006020600060007f000000000000000000000000ea674fdde714fd979de3edf0f56aa9716b898ec861fffff163afc874d2600052602060006020600060007f000000000000000000000000ea674fdde714fd979de3edf0f56aa9716b898ec861fffff16343ac1c39600052602060006020600060007f000000000000000000000000ea674fdde714fd979de3edf0f56aa9716b898ec861fffff1366000803760206000f3' + + const unsignedTx = Transaction.fromTxData({ + gasLimit: new BN(15000000), + to: callingAddress, + }) + + const tx = unsignedTx.sign(senderKey) + + const test = { + contracts: [ + { address: logicAddress, code: logicCode }, + { address: callingAddress, code: callingCode }, + ], + transactions: [tx], + steps: [ + { expectedOpcode: 'PUSH4', expectedGasUsed: 3, expectedStack: [] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(1660795326)] }, + { + expectedOpcode: 'MSTORE', + expectedGasUsed: 6, + expectedStack: [new BN(1660795326), new BN(0)], + }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(32)] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(32), new BN(0)] }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(32), new BN(0), new BN(32)], + }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(32), new BN(0), new BN(32), new BN(0)], + }, + { + expectedOpcode: 'PUSH32', + expectedGasUsed: 3, + expectedStack: [new BN(32), new BN(0), new BN(32), new BN(0), new BN(0)], + }, + { + expectedOpcode: 'PUSH2', + expectedGasUsed: 3, + expectedStack: [ + new BN(32), + new BN(0), + new BN(32), + new BN(0), + new BN(0), + new BN('1338207774508379457866452578149304295121587113672'), + ], + }, + { + expectedOpcode: 'CALL', + expectedGasUsed: 14913432, + expectedStack: [ + new BN(32), + new BN(0), + new BN(32), + new BN(0), + new BN(0), + new BN('1338207774508379457866452578149304295121587113672'), + new BN('65535'), + ], + }, + { expectedOpcode: 'CALLDATASIZE', expectedGasUsed: 2, expectedStack: [] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(32)] }, + // + { expectedOpcode: 'DUP1', expectedGasUsed: 3, expectedStack: [new BN(32), new BN(0)] }, + { + expectedOpcode: 'CALLDATACOPY', + expectedGasUsed: 9, + expectedStack: [new BN(32), new BN(0), new BN(0)], + }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [] }, + { expectedOpcode: 'MLOAD', expectedGasUsed: 3, expectedStack: [new BN(0)] }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3, expectedStack: [new BN(1660795326)] }, + { + expectedOpcode: 'PUSH4', + expectedGasUsed: 3, + expectedStack: [new BN(1660795326), new BN(1660795326)], + }, + { + expectedOpcode: 'EQ', + expectedGasUsed: 3, + expectedStack: [new BN(1660795326), new BN(1660795326), new BN(2949149906)], + }, + { + expectedOpcode: 'PUSH4', + expectedGasUsed: 3, + expectedStack: [new BN(1660795326), new BN(0)], + }, + { + expectedOpcode: 'JUMPI', + expectedGasUsed: 10, + expectedStack: [new BN(1660795326), new BN(0), new BN(52)], + }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3, expectedStack: [new BN(1660795326)] }, + { + expectedOpcode: 'PUSH4', + expectedGasUsed: 3, + expectedStack: [new BN(1660795326), new BN(1660795326)], + }, + { + expectedOpcode: 'EQ', + expectedGasUsed: 3, + expectedStack: [new BN(1660795326), new BN(1660795326), new BN(1660795326)], + }, + { + expectedOpcode: 'PUSH4', + expectedGasUsed: 3, + expectedStack: [new BN(1660795326), new BN(1)], + }, + { + expectedOpcode: 'JUMPI', + expectedGasUsed: 10, + expectedStack: [new BN(1660795326), new BN(1), new BN(63)], + }, + { expectedOpcode: 'JUMPDEST', expectedGasUsed: 1, expectedStack: [new BN(1660795326)] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(1660795326)] }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(1660795326), new BN(170)], + }, + { + expectedOpcode: 'TSTORE', + expectedGasUsed: 100, + expectedStack: [new BN(1660795326), new BN(170), new BN(0)], + }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(1660795326)] }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(1660795326), new BN(0)], + }, + { + expectedOpcode: 'RETURN', + expectedGasUsed: -14910832, + expectedStack: [new BN(1660795326), new BN(0), new BN(0)], + }, + { expectedOpcode: 'PUSH4', expectedGasUsed: 3, expectedStack: [new BN(1)] }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(2949149906)], + }, + { + expectedOpcode: 'MSTORE', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(2949149906), new BN(0)], + }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(1)] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(1), new BN(32)] }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(32), new BN(0)], + }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(32), new BN(0), new BN(32)], + }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(32), new BN(0), new BN(32), new BN(0)], + }, + { + expectedOpcode: 'PUSH32', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(32), new BN(0), new BN(32), new BN(0), new BN(0)], + }, + { + expectedOpcode: 'PUSH2', + expectedGasUsed: 3, + expectedStack: [ + new BN(1), + new BN(32), + new BN(0), + new BN(32), + new BN(0), + new BN(0), + new BN('1338207774508379457866452578149304295121587113672'), + ], + }, + { + expectedOpcode: 'CALL', + expectedGasUsed: 14910622, + expectedStack: [ + new BN(1), + new BN(32), + new BN(0), + new BN(32), + new BN(0), + new BN(0), + new BN('1338207774508379457866452578149304295121587113672'), + new BN(0xffff), + ], + }, + { expectedOpcode: 'CALLDATASIZE', expectedGasUsed: 2, expectedStack: [] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(32)] }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3, expectedStack: [new BN(32), new BN(0)] }, + { + expectedOpcode: 'CALLDATACOPY', + expectedGasUsed: 9, + expectedStack: [new BN(32), new BN(0), new BN(0)], + }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [] }, + { expectedOpcode: 'MLOAD', expectedGasUsed: 3, expectedStack: [new BN(0)] }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3, expectedStack: [new BN(2949149906)] }, + { + expectedOpcode: 'PUSH4', + expectedGasUsed: 3, + expectedStack: [new BN(2949149906), new BN(2949149906)], + }, + { + expectedOpcode: 'EQ', + expectedGasUsed: 3, + expectedStack: [new BN(2949149906), new BN(2949149906), new BN(2949149906)], + }, + { + expectedOpcode: 'PUSH4', + expectedGasUsed: 3, + expectedStack: [new BN(2949149906), new BN(1)], + }, + { + expectedOpcode: 'JUMPI', + expectedGasUsed: 10, + expectedStack: [new BN(2949149906), new BN(1), new BN(52)], + }, + { expectedOpcode: 'JUMPDEST', expectedGasUsed: 1, expectedStack: [new BN(2949149906)] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(2949149906)] }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(2949149906), new BN(255)], + }, + { + expectedOpcode: 'TSTORE', + expectedGasUsed: 100, + expectedStack: [new BN(2949149906), new BN(255), new BN(0)], + }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(2949149906)] }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(2949149906), new BN(0)], + }, + { + expectedOpcode: 'REVERT', + expectedGasUsed: -14910522, + expectedStack: [new BN(2949149906), new BN(0), new BN(0)], + }, + { expectedOpcode: 'PUSH4', expectedGasUsed: 3, expectedStack: [new BN(1), new BN(0)] }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(0), new BN(1135352889)], + }, + { + expectedOpcode: 'MSTORE', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(0), new BN(1135352889), new BN(0)], + }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(1), new BN(0)] }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(0), new BN(32)], + }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(0), new BN(32), new BN(0)], + }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(0), new BN(32), new BN(0), new BN(32)], + }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(0), new BN(32), new BN(0), new BN(32), new BN(0)], + }, + { + expectedOpcode: 'PUSH32', + expectedGasUsed: 3, + expectedStack: [ + new BN(1), + new BN(0), + new BN(32), + new BN(0), + new BN(32), + new BN(0), + new BN(0), + ], + }, + { + expectedOpcode: 'PUSH2', + expectedGasUsed: 3, + expectedStack: [ + new BN(1), + new BN(0), + new BN(32), + new BN(0), + new BN(32), + new BN(0), + new BN(0), + new BN('1338207774508379457866452578149304295121587113672'), + ], + }, + { + expectedOpcode: 'CALL', + expectedGasUsed: 14910334, + expectedStack: [ + new BN(1), + new BN(0), + new BN(32), + new BN(0), + new BN(32), + new BN(0), + new BN(0), + new BN('1338207774508379457866452578149304295121587113672'), + new BN(0xffff), + ], + }, + { expectedOpcode: 'CALLDATASIZE', expectedGasUsed: 2, expectedStack: [] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(32)] }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3, expectedStack: [new BN(32), new BN(0)] }, + { + expectedOpcode: 'CALLDATACOPY', + expectedGasUsed: 9, + expectedStack: [new BN(32), new BN(0), new BN(0)], + }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [] }, + { expectedOpcode: 'MLOAD', expectedGasUsed: 3, expectedStack: [new BN(0)] }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3, expectedStack: [new BN(1135352889)] }, + { + expectedOpcode: 'PUSH4', + expectedGasUsed: 3, + expectedStack: [new BN(1135352889), new BN(1135352889)], + }, + { + expectedOpcode: 'EQ', + expectedGasUsed: 3, + expectedStack: [new BN(1135352889), new BN(1135352889), new BN(2949149906)], + }, + { + expectedOpcode: 'PUSH4', + expectedGasUsed: 3, + expectedStack: [new BN(1135352889), new BN(0)], + }, + { + expectedOpcode: 'JUMPI', + expectedGasUsed: 10, + expectedStack: [new BN(1135352889), new BN(0), new BN(52)], + }, + { + expectedOpcode: 'DUP1', + expectedGasUsed: 3, + expectedStack: [new BN(1135352889)], + }, + { + expectedOpcode: 'PUSH4', + expectedGasUsed: 3, + expectedStack: [new BN(1135352889), new BN(1135352889)], + }, + { + expectedOpcode: 'EQ', + expectedGasUsed: 3, + expectedStack: [new BN(1135352889), new BN(1135352889), new BN(1660795326)], + }, + { + expectedOpcode: 'PUSH4', + expectedGasUsed: 3, + expectedStack: [new BN(1135352889), new BN(0)], + }, + { + expectedOpcode: 'JUMPI', + expectedGasUsed: 10, + expectedStack: [new BN(1135352889), new BN(0), new BN(63)], + }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3, expectedStack: [new BN(1135352889)] }, + { + expectedOpcode: 'PUSH4', + expectedGasUsed: 3, + expectedStack: [new BN(1135352889), new BN(1135352889)], + }, + { + expectedOpcode: 'EQ', + expectedGasUsed: 3, + expectedStack: [new BN(1135352889), new BN(1135352889), new BN(1135352889)], + }, + { + expectedOpcode: 'PUSH4', + expectedGasUsed: 3, + expectedStack: [new BN(1135352889), new BN(1)], + }, + { + expectedOpcode: 'JUMPI', + expectedGasUsed: 10, + expectedStack: [new BN(1135352889), new BN(1), new BN(74)], + }, + { expectedOpcode: 'JUMPDEST', expectedGasUsed: 1, expectedStack: [new BN(1135352889)] }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(1135352889)] }, + { + expectedOpcode: 'TLOAD', + expectedGasUsed: 100, + expectedStack: [new BN(1135352889), new BN(0)], + }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(1135352889), new BN(170)], + }, + { + expectedOpcode: 'MSTORE', + expectedGasUsed: 3, + expectedStack: [new BN(1135352889), new BN(170), new BN(0)], + }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3, expectedStack: [new BN(1135352889)] }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(1135352889), new BN(32)], + }, + { + expectedOpcode: 'RETURN', + expectedGasUsed: -14910234, + expectedStack: [new BN(1135352889), new BN(32), new BN(0)], + }, + { + expectedOpcode: 'CALLDATASIZE', + expectedGasUsed: 2, + expectedStack: [new BN(1), new BN(0), new BN(1)], + }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(0), new BN(1), new BN(0)], + }, + { + expectedOpcode: 'DUP1', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(0), new BN(1), new BN(0), new BN(0)], + }, + { + expectedOpcode: 'CALLDATACOPY', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(0), new BN(1), new BN(0), new BN(0), new BN(0)], + }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(0), new BN(1)], + }, + { + expectedOpcode: 'PUSH1', + expectedGasUsed: 3, + expectedStack: [new BN(1), new BN(0), new BN(1), new BN(32)], + }, + { + expectedOpcode: 'RETURN', + expectedGasUsed: NaN, + expectedStack: [new BN(1), new BN(0), new BN(1), new BN(32), new BN(0)], + }, + ], + } + + const [result] = await runTest(test, st) + st.deepEqual(new BN(result.execResult.returnValue).toNumber(), 0xaa) + st.end() + }) +}) diff --git a/packages/vm/tests/api/evm/eei.spec.ts b/packages/vm/tests/api/evm/eei.spec.ts index 8fdd9ae0ba..a6dd84856c 100644 --- a/packages/vm/tests/api/evm/eei.spec.ts +++ b/packages/vm/tests/api/evm/eei.spec.ts @@ -2,12 +2,20 @@ import tape from 'tape' import { Account, Address } from 'ethereumjs-util' import EEI from '../../../src/evm/eei' import StateManager from '../../../src/state/stateManager' +import { TransientStorage } from '../../../src/state' const ZeroAddress = Address.zero() tape('EEI', (t) => { t.test('should return false on non-existing accounts', async (st) => { - const eei = new EEI(undefined!, new StateManager(), undefined!, undefined!, undefined!) // create a dummy EEI (no VM, no EVM, etc.) + const eei = new EEI( + undefined!, + new StateManager(), + undefined!, + undefined!, + undefined!, + undefined! + ) // create a dummy EEI (no VM, no EVM, etc.) st.notOk(await eei.accountExists(ZeroAddress)) st.ok(await eei.isAccountEmpty(ZeroAddress)) st.end() @@ -16,7 +24,14 @@ tape('EEI', (t) => { t.test( 'should return false on non-existing accounts which once existed in state but are now gone', async (st) => { - const eei = new EEI(undefined!, new StateManager(), undefined!, undefined!, undefined!) // create a dummy EEI (no VM, no EVM, etc.) + const eei = new EEI( + undefined!, + new StateManager(), + undefined!, + undefined!, + undefined!, + undefined! + ) // create a dummy EEI (no VM, no EVM, etc.) // create empty account await eei._state.putAccount(ZeroAddress, new Account()) st.ok(await eei.accountExists(ZeroAddress)) @@ -31,7 +46,14 @@ tape('EEI', (t) => { ) t.test('should return true on existing accounts', async (st) => { - const eei = new EEI(undefined!, new StateManager(), undefined!, undefined!, undefined!) // create a dummy EEI (no VM, no EVM, etc.) + const eei = new EEI( + undefined!, + new StateManager(), + undefined!, + undefined!, + undefined!, + undefined! + ) // create a dummy EEI (no VM, no EVM, etc.) // create empty account await eei._state.putAccount(ZeroAddress, new Account()) st.ok(await eei.accountExists(ZeroAddress)) // sanity check: account exists before we delete it @@ -41,4 +63,27 @@ tape('EEI', (t) => { st.ok(await eei.isAccountEmpty(ZeroAddress)) // account is empty st.end() }) + + t.test('should work with transient storage', async (st) => { + const eei = new EEI( + undefined!, + new StateManager(), + undefined!, + undefined!, + undefined!, + new TransientStorage() + ) // create a dummy EEI (no VM, no EVM, etc.) + // Set the caller to the zero address + ;(eei as any)._env = { address: ZeroAddress } + + // Put transient storage + const key = Buffer.alloc(32, 0x11) + const value = Buffer.alloc(32, 0x22) + ;(eei as any).transientStorageStore(key, value) + + // Get transient storage + const got = (eei as any).transientStorageLoad(key) + t.deepEqual(value, got) + st.end() + }) }) diff --git a/packages/vm/tests/api/state/transientStorage.spec.ts b/packages/vm/tests/api/state/transientStorage.spec.ts new file mode 100644 index 0000000000..482fcd2fb8 --- /dev/null +++ b/packages/vm/tests/api/state/transientStorage.spec.ts @@ -0,0 +1,232 @@ +import tape from 'tape' +import { Address } from 'ethereumjs-util' +import { TransientStorage } from '../../../src/state' + +tape('Transient Storage', (tester) => { + const it = tester.test + it('should set and get storage', (t) => { + const transientStorage = new TransientStorage() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value = Buffer.alloc(32, 0x99) + + transientStorage.put(address, key, value) + const got = transientStorage.get(address, key) + t.deepEqual(value, got) + t.end() + }) + + it('should return bytes32(0) if there is no key set', (t) => { + const transientStorage = new TransientStorage() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value = Buffer.alloc(32, 0x11) + + // No address set + const got = transientStorage.get(address, key) + t.deepEqual(Buffer.alloc(32, 0x00), got) + + // Address set, no key set + transientStorage.put(address, key, value) + const got2 = transientStorage.get(address, Buffer.alloc(32, 0x22)) + t.deepEqual(Buffer.alloc(32, 0x00), got2) + t.end() + }) + + it('should revert', (t) => { + const transientStorage = new TransientStorage() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value = Buffer.alloc(32, 0x99) + + transientStorage.put(address, key, value) + + transientStorage.checkpoint() + + const value2 = Buffer.alloc(32, 0x22) + transientStorage.put(address, key, value2) + const got = transientStorage.get(address, key) + t.deepEqual(got, value2) + + transientStorage.revert() + + const got2 = transientStorage.get(address, key) + t.deepEqual(got2, value) + t.end() + }) + + it('should commit', (t) => { + const transientStorage = new TransientStorage() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value = Buffer.alloc(32, 0x99) + + transientStorage.put(address, key, value) + + transientStorage.checkpoint() + transientStorage.commit() + + const got = transientStorage.get(address, key) + t.deepEqual(got, value) + t.end() + }) + + it('should copy', (t) => { + const transientStorage = new TransientStorage() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value = Buffer.alloc(32, 0x99) + + transientStorage.put(address, key, value) + + const transientStorage2 = transientStorage.copy() + transientStorage2.put(address, key, Buffer.alloc(32, 0x11)) + + const got = transientStorage.get(address, key) + const got2 = transientStorage2.get(address, key) + + t.notEqual(got.toString('hex'), got2.toString('hex')) + t.end() + }) + + it('should fail to commit without checkpoint', (t) => { + const transientStorage = new TransientStorage() + + t.throws(() => { + transientStorage.commit() + }, /trying to commit when not checkpointed/) + + t.end() + }) + + it('should fail to revert with empty changesets', (t) => { + const transientStorage = new TransientStorage() + transientStorage._changesets = [] + + t.throws(() => { + transientStorage.revert() + }, /cannot revert without a changeset/) + + t.end() + }) + + it('should fail to add storage with empty changesets', (t) => { + const transientStorage = new TransientStorage() + transientStorage._changesets = [] + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value = Buffer.alloc(32, 0x99) + + t.throws(() => { + transientStorage.put(address, key, value) + }, /no changeset initialized/) + + t.end() + }) + + it('should fail with wrong size key/value', (t) => { + const transientStorage = new TransientStorage() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + + t.throws(() => { + transientStorage.put(address, Buffer.alloc(10), Buffer.alloc(1)) + }, /Transient storage key must be 32 bytes long/) + + t.throws(() => { + transientStorage.put(address, Buffer.alloc(32), Buffer.alloc(100)) + }, /Transient storage value cannot be longer than 32 bytes/) + + t.end() + }) + + it('should clear', (t) => { + const transientStorage = new TransientStorage() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value = Buffer.alloc(32, 0x99) + + transientStorage.put(address, key, value) + + transientStorage.clear() + + const got = transientStorage.get(address, key) + t.deepEqual(got, Buffer.alloc(32, 0x00)) + t.end() + }) + + it('keys are stringified', (t) => { + const transientStorage = new TransientStorage() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value = Buffer.alloc(32, 0x99) + + transientStorage.put(address, key, value) + t.deepEqual( + transientStorage.get( + Address.fromString('0xff00000000000000000000000000000000000002'), + Buffer.alloc(32, 0xff) + ), + value + ) + t.end() + }) + + it('revert applies changes in correct order', (t) => { + const transientStorage = new TransientStorage() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value0 = Buffer.alloc(32, 0x00) + const value1 = Buffer.alloc(32, 0x01) + const value2 = Buffer.alloc(32, 0x02) + + transientStorage.put(address, key, value0) + transientStorage.checkpoint() + transientStorage.put(address, key, value1) + transientStorage.put(address, key, value2) + transientStorage.revert() + + t.deepEqual(transientStorage.get(address, key), value0) + t.end() + }) + + it('copies do not share changesets', (t) => { + const original = new TransientStorage() + + const address = Address.fromString('0xff00000000000000000000000000000000000002') + const key = Buffer.alloc(32, 0xff) + const value0 = Buffer.alloc(32, 0x00) + const value1 = Buffer.alloc(32, 0x01) + const value2 = Buffer.alloc(32, 0x02) + + original.put(address, key, value0) + original.checkpoint() + original.put(address, key, value1) + original.put(address, key, value2) + + const copy = original.copy() + + // they are not strictly equal + t.notStrictEqual(copy._changesets, original._changesets) + for (let i = 0; i < copy._changesets.length; i++) { + t.notStrictEqual(copy._changesets[i], original._changesets[i]) + } + t.notStrictEqual(copy._storage, original._storage) + + // however they are deeply equal + t.deepEqual(copy._storage, original._storage) + t.deepEqual(copy._changesets, original._changesets) + + t.deepEqual(copy.toJSON(), original.toJSON()) + t.end() + }) +})