Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VM, Common: Complex Genesis State tests #1757

Merged
merged 11 commits into from
Mar 9, 2022
22 changes: 20 additions & 2 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,30 @@ export interface CommonOpts extends BaseOpts {
* const common = new Common({ chain: 'myCustomChain1', customChains: [ myCustomChain1 ]})
* ```
*
* Pattern 2 (with genesis state, see {@link GenesisState} for format):
* Pattern 2 (with genesis state see {@link GenesisState} for format):
*
* ```javascript
* const simpleState = {
* '0x0...01': '0x100', // For EoA
* }
* import myCustomChain1 from '[PATH_TO_MY_CHAINS]/myCustomChain1.json'
* import chain1GenesisState from '[PATH_TO_GENESIS_STATES]/chain1GenesisState.json'
* const common = new Common({ chain: 'myCustomChain1', customChains: [ [ myCustomChain1, chain1GenesisState ] ]})
* const common = new Common({ chain: 'myCustomChain1', customChains: [ [ myCustomChain1, simpleState ] ]})
* ```
*
* Pattern 3 (with complex genesis state, containing contract accounts and storage).
* Note that in {@link AccountState} there are two
* accepted types. This allows to easily insert accounts in the genesis state:
*
* A complex genesis state with Contract and EoA states would have the following format:
*
* ```javascript
* const complexState = {
* '0x0...01': '0x100', // For EoA
* '0x0...02': ['0x1', '0xRUNTIME_BYTECODE', [[ keyOne, valueOne ], [ keyTwo, valueTwo ]]] // For contracts
* }
* import myCustomChain1 from '[PATH_TO_MY_CHAINS]/myCustomChain1.json'
* const common = new Common({ chain: 'myCustomChain1', customChains: [ [ myCustomChain1, complexState ] ]})
cbrzn marked this conversation as resolved.
Show resolved Hide resolved
ryanio marked this conversation as resolved.
Show resolved Hide resolved
* ```
*/
customChains?: IChain[] | [IChain, GenesisState][]
Expand Down
12 changes: 10 additions & 2 deletions packages/common/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BN } from 'ethereumjs-util'
import { BN, PrefixedHexString } from 'ethereumjs-util'
import { ConsensusAlgorithm, ConsensusType, Hardfork as HardforkName } from '.'

export interface genesisStatesType {
Expand Down Expand Up @@ -40,8 +40,16 @@ export interface Chain {
}
}

type StoragePair = [key: PrefixedHexString, value: PrefixedHexString]

export type AccountState = [
balance: PrefixedHexString,
code: PrefixedHexString,
storage: Array<StoragePair>
]

export interface GenesisState {
[key: string]: string | [string, [[string, string]]] // balance | [balance, code, [[storageKey, storageValue]]]
[key: PrefixedHexString]: PrefixedHexString | AccountState
}

export interface eipsType {
Expand Down
38 changes: 37 additions & 1 deletion packages/common/tests/customChains.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import testnet from './data/testnet.json'
import testnet2 from './data/testnet2.json'
import testnet3 from './data/testnet3.json'

import { Chain as IChain, GenesisState } from '../src/types'
import { AccountState, Chain as IChain, GenesisState } from '../src/types'

tape('[Common]: Custom chains', function (t: tape.Test) {
t.test(
Expand Down Expand Up @@ -208,6 +208,42 @@ tape('[Common]: Custom chains', function (t: tape.Test) {

st.equal(c.hardforks()[3].forkHash, '0x215201ca', 'forkhash should be calculated correctly')

const contractAccount = '0x96fb4792cf2B3A7EF9842D1Af74f8c99C6F4fF63'
const eoaAccount = '0x0000000000000000000000000000000000000002'
const storage: Array<[string, string]> = [
['0x000000000000000000000000000001', '0x3'],
['0x000000000000000000000000000002', '0x4'],
]

const contractState: AccountState = ['0x10000', '0xbca', storage]
const complexGenesisState = {
[contractAccount]: contractState,
[eoaAccount]: '0x100',
}

c = new Common({
chain: 'testnet',
customChains: [[testnet, complexGenesisState]],
})

// Retrieve balance from EoA
st.deepEqual(c.genesisState()[eoaAccount], complexGenesisState[eoaAccount])

// Retrieve code of the contract account
st.deepEqual(c.genesisState()[contractAccount][1], complexGenesisState[contractAccount][1])

// Retrieve value of first stored space in storage of account (state of contract)
st.deepEqual(
c.genesisState()[contractAccount][2][0][1],
complexGenesisState[contractAccount][2][0][1]
)

// Retrieve value of second stored space in storage of account (state of contract)
st.deepEqual(
c.genesisState()[contractAccount][2][1][1],
complexGenesisState[contractAccount][2][1][1]
)

st.end()
})
})
44 changes: 31 additions & 13 deletions packages/vm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![Discord][discord-badge]][discord-link]

| TypeScript implementation of the Ethereum VM. |
| --- |
| --------------------------------------------- |
cbrzn marked this conversation as resolved.
Show resolved Hide resolved

Note: this `README` reflects the state of the library from `v5.0.0` onwards. See `README` from the [standalone repository](https://github.com/ethereumjs/ethereumjs-vm) for an introduction on the last preceding release.

Expand Down Expand Up @@ -104,7 +104,7 @@ const hardforkByBlockNumber = true
const vm = new VM({ common, hardforkByBlockNumber })

const serialized = Buffer.from('f901f7a06bfee7294bf4457...', 'hex')
const block = Block.fromRLPSerializedBlock(serialized, { hardforkByBlockNumber })
const block = Block.fromRLPSerializedBlock(serialized, { hardforkByBlockNumber })
const result = await vm.runBlock(block)
```

Expand Down Expand Up @@ -140,6 +140,24 @@ const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Berlin })
const vm = new VM({ common })
```

## Custom genesis state support

If you want to create a new instance of the VM and add your own genesis state, you can do it by passing a `Common`
instance with [custom genesis state](../common/README.md#initialize-using-customchains-array) and passing the flag `activateGenesisState` in `VMOpts`, e.g.:

```typescript
import Common from '@ethereumjs/common'
import VM from '@ethereumjs/vm'
import myCustomChain1 from '[PATH_TO_MY_CHAINS]/myCustomChain1.json'
import chain1GenesisState from '[PATH_TO_GENESIS_STATES]/chain1GenesisState.json'

const common = new Common({
chain: 'myCustomChain1',
customChains: [[myCustomChain1, chain1GenesisState]],
})
const vm = new VM({ common, activateGenesisState: true })
```

## EIP Support

It is possible to individually activate EIP support in the VM by instantiate the `Common` instance passed
Expand Down Expand Up @@ -218,17 +236,17 @@ If you want to understand your VM runs we have added a hierarchically structured

The following loggers are currently available:

| Logger | Description |
| - | - |
| `vm:block` | Block operations (run txs, generating receipts, block rewards,...) |
| `vm:tx` | Transaction operations (account updates, checkpointing,...) |
| `vm:tx:gas` | Transaction gas logger |
| `vm:evm` | EVM control flow, CALL or CREATE message execution |
| `vm:evm:gas` | EVM gas logger |
| `vm:eei:gas` | EEI gas logger |
| `vm:state`| StateManager logger |
| `vm:ops` | Opcode traces |
| `vm:ops:[Lower-case opcode name]` | Traces on a specific opcode |
| Logger | Description |
| --------------------------------- | ------------------------------------------------------------------ |
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the comment above, should I remove these non-related changes?

Copy link
Contributor

@ryanio ryanio Mar 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is kind of annoying, I like the prettier format in the code sections especially, but could do without these table formatting. I wonder if we could turn off markdown table formatting specifically in our prettier root config file.

| `vm:block` | Block operations (run txs, generating receipts, block rewards,...) |
| `vm:tx` |  Transaction operations (account updates, checkpointing,...)  |
| `vm:tx:gas` |  Transaction gas logger |
| `vm:evm` |  EVM control flow, CALL or CREATE message execution |
| `vm:evm:gas` |  EVM gas logger |
| `vm:eei:gas` |  EEI gas logger |
| `vm:state` | StateManager logger |
| `vm:ops` |  Opcode traces |
| `vm:ops:[Lower-case opcode name]` | Traces on a specific opcode |

Here are some examples for useful logger combinations.

Expand Down
38 changes: 26 additions & 12 deletions packages/vm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ export interface VMOpts {
* Default: `false`
*/
activatePrecompiles?: boolean
/**
* If true, the state of the VM will add the genesis state given by {@link Common} to a new
* created state manager instance. Note that if stateManager option is also passed as argument
* this flag won't have any effect.
*
* Default: `false`
*/
activateGenesisState?: boolean
/**
* Allows unlimited contract sizes while debugging. By setting this to `true`, the check for
* contract size limit of 24KB (see [EIP-170](https://git.io/vxZkK)) is bypassed.
Expand Down Expand Up @@ -285,18 +293,24 @@ export default class VM extends AsyncEventEmitter {

await this.blockchain.initPromise

if (this._opts.activatePrecompiles && !this._opts.stateManager) {
await this.stateManager.checkpoint()
// put 1 wei in each of the precompiles in order to make the accounts non-empty and thus not have them deduct `callNewAccount` gas.
await Promise.all(
Object.keys(precompiles)
.map((k: string): Address => new Address(Buffer.from(k, 'hex')))
.map(async (address: Address) => {
const account = Account.fromAccountData({ balance: 1 })
await this.stateManager.putAccount(address, account)
})
)
await this.stateManager.commit()
if (!this._opts.stateManager) {
if (this._opts.activateGenesisState) {
await this.stateManager.generateCanonicalGenesis()
}

if (this._opts.activatePrecompiles) {
await this.stateManager.checkpoint()
// put 1 wei in each of the precompiles in order to make the accounts non-empty and thus not have them deduct `callNewAccount` gas.
await Promise.all(
Object.keys(precompiles)
.map((k: string): Address => new Address(Buffer.from(k, 'hex')))
.map(async (address: Address) => {
const account = Account.fromAccountData({ balance: 1 })
await this.stateManager.putAccount(address, account)
})
)
await this.stateManager.commit()
}
}

if (this._common.isActivatedEIP(2537)) {
Expand Down
135 changes: 135 additions & 0 deletions packages/vm/tests/api/customChain.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import tape from 'tape'
import Common, { Hardfork } from '@ethereumjs/common'
import testChain from './testdata/testnet.json'
import VM from '../../src'
import { TransactionFactory } from '@ethereumjs/tx'
import { Block } from '@ethereumjs/block'
import { AccountState } from '@ethereumjs/common/dist/types'
import { Interface } from '@ethersproject/abi'
import { Address } from 'ethereumjs-util'
import testnetMerge from './testdata/testnetMerge.json'

const storage: Array<[string, string]> = [
[
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000004',
],
]
const accountState: AccountState = [
'0x0',
'0x6080604052348015600f57600080fd5b506004361060285760003560e01c80632e64cec114602d575b600080fd5b60336047565b604051603e9190605d565b60405180910390f35b60008054905090565b6057816076565b82525050565b6000602082019050607060008301846050565b92915050565b600081905091905056fea2646970667358221220338001095242a334ada78025237955fa36b6f2f895ea7f297b69af72f8bc7fd164736f6c63430008070033',
storage,
]

/**
* The bytecode of this contract state represents:
* contract Storage {
* uint256 number = 4;
* function retrieve() public view returns (uint256){
* return number;
* }
* }
*/

const contractAddress = '0x3651539F2E119a27c606cF0cB615410eCDaAE62a'
const genesisState = {
'0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b': '0x6d6172697573766477000000',
'0xbe862ad9abfe6f22bcb087716c7d89a26051f74c': '0x6d6172697573766477000000',
[contractAddress]: accountState,
}

const common = new Common({
chain: 'testnet',
hardfork: 'london',
customChains: [[testChain, genesisState]],
})
const block = Block.fromBlockData(
{
header: {
gasLimit: 21_000,
baseFeePerGas: 7,
},
},
{
common,
}
)
const privateKey = Buffer.from(
'e331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109',
'hex'
)

tape('VM initialized with custom state ', (t) => {
t.test('should transfer eth from already existent account', async (t) => {
const vm = await VM.create({ common, activateGenesisState: true })

const to = '0x00000000000000000000000000000000000000ff'
const unsignedTransferTx = TransactionFactory.fromTxData(
{
type: 2,
to,
value: '0x1',
gasLimit: 21_000,
maxFeePerGas: 7,
},
{
common,
}
)
const tx = unsignedTransferTx.sign(privateKey)
const result = await vm.runTx({
tx,
block,
})
const toAddress = Address.fromString(to)
const receiverAddress = await vm.stateManager.getAccount(toAddress)

t.equal(result.gasUsed.toString(), '21000')
t.equal(receiverAddress.balance.toString(), '1')
t.end()
})

t.test('should retrieve value from storage', async (t) => {
const vm = await VM.create({ common, activateGenesisState: true })
const sigHash = new Interface(['function retrieve()']).getSighash('retrieve')

const callResult = await vm.runCall({
to: Address.fromString(contractAddress),
data: Buffer.from(sigHash.slice(2), 'hex'),
caller: Address.fromPrivateKey(privateKey),
})

const [, , storage] = genesisState[contractAddress]
// Returned value should be 4, because we are trying to trigger the method `retrieve`
// in the contract, which returns the variable stored in slot 0x00..00
t.equal(callResult.execResult.returnValue.toString('hex'), storage[0][1].slice(2))
t.end()
})
cbrzn marked this conversation as resolved.
Show resolved Hide resolved

t.test('hardforkByBlockNumber, hardforkByTD', async (st) => {
const customChains = [testnetMerge]
const common = new Common({ chain: 'testnetMerge', hardfork: Hardfork.Istanbul, customChains })

let vm = await VM.create({ common, hardforkByBlockNumber: true })
st.equal((vm as any)._hardforkByBlockNumber, true, 'should set hardforkByBlockNumber option')

vm = await VM.create({ common, hardforkByTD: 5001 })
st.equal((vm as any)._hardforkByTD, 5001, 'should set hardforkByTD option')

try {
await VM.create({ common, hardforkByBlockNumber: true, hardforkByTD: 3000 })
st.fail('should not reach this')
} catch (e: any) {
const msg =
'should throw if hardforkByBlockNumber and hardforkByTD options are used in conjunction'
st.ok(
e.message.includes(
`The hardforkByBlockNumber and hardforkByTD options can't be used in conjunction`
),
msg
)
}

st.end()
})
})
Loading