Skip to content

Commit

Permalink
Update EthersStateManager to be RpcStateManager under the hood (#3167)
Browse files Browse the repository at this point in the history
* Add ESMBlockchain

* Add comment on test

* Fix bytecode

* Make RpcBlockChain class much better

* Remove ethers from ESM

* Fix mock provider and tests

* update readme

* stateManager: make rpcblockchain only have one param in constructor

* Address feedback

* stateManager: rename ethers -> rpc

* Add test plugins for wasm

* Cleanup rpcStateManager test and skip in browser

* Remove obsolete function

* Address feedback

* address feedback

* statemanager: update readme

---------

Co-authored-by: Jochem Brouwer <[email protected]>
  • Loading branch information
acolytec3 and jochem-brouwer authored Nov 30, 2023
1 parent 5d12ee6 commit 019084d
Show file tree
Hide file tree
Showing 9 changed files with 533 additions and 517 deletions.
16 changes: 11 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 25 additions & 11 deletions packages/statemanager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Note: this library was part of the [@ethereumjs/vm](../vm/) package up till VM `

The `StateManager` provides high-level access and manipulation methods to and for the Ethereum state, thinking in terms of accounts or contract code rather then the storage operations of the underlying data structure (e.g. a [Trie](../trie/)).

The library includes a TypeScript interface `StateManager` to ensure a unified interface (e.g. when passed to the VM) as well as a concrete Trie-based implementation `DefaultStateManager` as well as an `EthersStateManager` implementation that sources state and history data from an external `ethers` provider.
The library includes a TypeScript interface `StateManager` to ensure a unified interface (e.g. when passed to the VM) as well as a concrete Trie-based implementation `DefaultStateManager` as well as an `RPCStateManager` implementation that sources state and history data from an external JSON-RPC provider.

It also includes a checkpoint/revert/commit mechanism to either persist or revert state changes and provides a sophisticated caching mechanism under the hood to reduce the need for direct state accesses.

Expand Down Expand Up @@ -55,31 +55,45 @@ Caches now "survive" a flush operation and especially long-lived usage scenarios

Have a loot at the extended `CacheOptions` on how to use and leverage the new cache system.

### `EthersStateManager`
### `RPCStateManager`

First, a simple example of usage:

```typescript
import { Account, Address } from '@ethereumjs/util'
import { EthersStateManager } from '@ethereumjs/statemanager'
import { ethers } from 'ethers'
import { RPCStateManager } from '@ethereumjs/statemanager'

const provider = new ethers.providers.JsonRpcProvider('https://path.to.my.provider.com')
const stateManager = new EthersStateManager({ provider, blockTag: 500000n })
const provider = 'https://path.to.my.provider.com'
const stateManager = new RPCStateManager({ provider, blockTag: 500000n })
const vitalikDotEth = Address.fromString('0xd8da6bf26964af9d7eed9e03e53415d37aa96045')
const account = await stateManager.getAccount(vitalikDotEth)
console.log('Vitalik has a current ETH balance of ', account.balance)
```

The `EthersStateManager` can be be used with an `ethers` `JsonRpcProvider` or one of its subclasses. Instantiate the `VM` and pass in an `EthersStateManager` to run transactions against accounts sourced from the provider or to run blocks pulled from the provider at any specified block height.
The `RPCStateManager` can be be used with any JSON-RPC provider that supports the `eth` namespace. Instantiate the `VM` and pass in an `RPCStateManager` to run transactions against accounts sourced from the provider or to run blocks pulled from the provider at any specified block height.

**Note:** Usage of this StateManager can cause a heavy load regarding state request API calls, so be careful (or at least: aware) if used in combination with an Ethers provider connecting to a third-party API service like Infura!
**Note:** Usage of this StateManager can cause a heavy load regarding state request API calls, so be careful (or at least: aware) if used in combination with a JSON-RPC provider connecting to a third-party API service like Infura!

### Points on usage:

#### Instantiating the EVM

In order to have an EVM instance that supports the BLOCKHASH opcode (which requires access to block history), you must instantiate both the `RPCStateManager` and the `RpcBlockChain` and use that when initalizing your EVM instance as below:

```js
import { RPCStateManager, RPCBlockChain } from '../src/rpcStateManager.js'
import { EVM } from '@ethereumjs/evm'

const blockchain = new RPCBlockChain({}, provider)
const blockTag = 1n
const state = new RPCStateManager({ provider, blockTag })
const evm = new EVM({ blockchain, stateManager: state })
```

Note: Failing to provide the `RPCBlockChain` instance when instantiating the EVM means that the `BLOCKHASH` opcode will fail to work correctly during EVM execution.

#### Provider selection

- If you don't have access to a provider, you can use the `CloudFlareProvider` from the `@ethersproject/providers` module to get a quickstart.
- The provider you select must support the `eth_getProof`, `eth_getCode`, and `eth_getStorageAt` RPC methods.
- Not all providers support retrieving state from all block heights so refer to your provider's documentation. Trying to use a block height not supported by your provider (e.g. any block older than the last 256 for CloudFlare) will result in RPC errors when using the state manager.

Expand All @@ -92,12 +106,12 @@ The `EthersStateManager` can be be used with an `ethers` `JsonRpcProvider` or on

#### Potential gotchas

- The Ethers State Manager cannot compute valid state roots when running blocks as it does not have access to the entire Ethereum state trie so can not compute correct state roots, either for the account trie or for storage tries.
- The RPC State Manager cannot compute valid state roots when running blocks as it does not have access to the entire Ethereum state trie so can not compute correct state roots, either for the account trie or for storage tries.
- If you are replaying mainnet transactions and an account or account storage is touched by multiple transactions in a block, you must replay those transactions in order (with regard to their position in that block) or calculated gas will likely be different than actual gas consumed.

#### Further reference

Refer to [this test script](./test/ethersStateManager.spec.ts) for complete examples of running transactions and blocks in the `vm` with data sourced from a provider.
Refer to [this test script](./test/rpcStateManager.spec.ts) for complete examples of running transactions and blocks in the `vm` with data sourced from a provider.

## Browser

Expand Down
1 change: 0 additions & 1 deletion packages/statemanager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
"@ethereumjs/util": "^9.0.1",
"debug": "^4.3.3",
"ethereum-cryptography": "^2.1.2",
"ethers": "^6.4.0",
"js-sdsl": "^4.1.4",
"lru-cache": "^10.0.0"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/statemanager/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './cache/index.js'
export * from './ethersStateManager.js'
export * from './rpcStateManager.js'
export * from './stateManager.js'
Original file line number Diff line number Diff line change
@@ -1,45 +1,54 @@
import { Trie } from '@ethereumjs/trie'
import { Account, bigIntToHex, bytesToBigInt, bytesToHex, toBytes } from '@ethereumjs/util'
import {
Account,
bigIntToHex,
bytesToHex,
fetchFromProvider,
hexToBytes,
intToHex,
toBytes,
} from '@ethereumjs/util'
import debugDefault from 'debug'
import { keccak256 } from 'ethereum-cryptography/keccak.js'
import { ethers } from 'ethers'

import { AccountCache, CacheType, OriginalStorageCache, StorageCache } from './cache/index.js'

import type { Proof } from './index.js'
import type { AccountFields, EVMStateManagerInterface, StorageDump } from '@ethereumjs/common'
import type { StorageRange } from '@ethereumjs/common/src'
import type {
AccountFields,
EVMStateManagerInterface,
StorageDump,
StorageRange,
} from '@ethereumjs/common'
import type { Address } from '@ethereumjs/util'
import type { Debugger } from 'debug'
const { debug: createDebugLogger } = debugDefault

export interface EthersStateManagerOpts {
provider: string | ethers.JsonRpcProvider
export interface RPCStateManagerOpts {
provider: string
blockTag: bigint | 'earliest'
}

export class EthersStateManager implements EVMStateManagerInterface {
protected _provider: ethers.JsonRpcProvider
export class RPCStateManager implements EVMStateManagerInterface {
protected _provider: string
protected _contractCache: Map<string, Uint8Array>
protected _storageCache: StorageCache
protected _blockTag: string
protected _accountCache: AccountCache
originalStorageCache: OriginalStorageCache
protected _debug: Debugger
protected DEBUG: boolean
constructor(opts: EthersStateManagerOpts) {
constructor(opts: RPCStateManagerOpts) {
// Skip DEBUG calls unless 'ethjs' included in environmental DEBUG variables
// Additional window check is to prevent vite browser bundling (and potentially other) to break
this.DEBUG =
typeof window === 'undefined' ? process?.env?.DEBUG?.includes('ethjs') ?? false : false

this._debug = createDebugLogger('statemanager:ethersStateManager')
if (typeof opts.provider === 'string') {
this._provider = new ethers.JsonRpcProvider(opts.provider)
} else if (opts.provider instanceof ethers.JsonRpcProvider) {
this._debug = createDebugLogger('statemanager:rpcStateManager')
if (typeof opts.provider === 'string' && opts.provider.startsWith('http')) {
this._provider = opts.provider
} else {
throw new Error(`valid JsonRpcProvider or url required; got ${opts.provider}`)
throw new Error(`valid RPC provider url required; got ${opts.provider}`)
}

this._blockTag = opts.blockTag === 'earliest' ? opts.blockTag : bigIntToHex(opts.blockTag)
Expand All @@ -54,10 +63,10 @@ export class EthersStateManager implements EVMStateManagerInterface {
/**
* Note that the returned statemanager will share the same JsonRpcProvider as the original
*
* @returns EthersStateManager
* @returns RPCStateManager
*/
shallowCopy(): EthersStateManager {
const newState = new EthersStateManager({
shallowCopy(): RPCStateManager {
const newState = new RPCStateManager({
provider: this._provider,
blockTag: BigInt(this._blockTag),
})
Expand Down Expand Up @@ -103,7 +112,10 @@ export class EthersStateManager implements EVMStateManagerInterface {
async getContractCode(address: Address): Promise<Uint8Array> {
let codeBytes = this._contractCache.get(address.toString())
if (codeBytes !== undefined) return codeBytes
const code = await this._provider.getCode(address.toString(), this._blockTag)
const code = await fetchFromProvider(this._provider, {
method: 'eth_getCode',
params: [address.toString(), this._blockTag],
})
codeBytes = toBytes(code)
this._contractCache.set(address.toString(), codeBytes)
return codeBytes
Expand Down Expand Up @@ -141,11 +153,10 @@ export class EthersStateManager implements EVMStateManagerInterface {
}

// Retrieve storage slot from provider if not found in cache
const storage = await this._provider.getStorage(
address.toString(),
bytesToBigInt(key),
this._blockTag
)
const storage = await fetchFromProvider(this._provider, {
method: 'eth_getStorageAt',
params: [address.toString(), bytesToHex(key), this._blockTag],
})
value = toBytes(storage)

await this.putContractStorage(address, key, value)
Expand Down Expand Up @@ -206,11 +217,10 @@ export class EthersStateManager implements EVMStateManagerInterface {
const localAccount = this._accountCache.get(address)
if (localAccount !== undefined) return true
// Get merkle proof for `address` from provider
const proof = await this._provider.send('eth_getProof', [
address.toString(),
[],
this._blockTag,
])
const proof = await fetchFromProvider(this._provider, {
method: 'eth_getProof',
params: [address.toString(), [] as any, this._blockTag],
})

const proofBuf = proof.accountProof.map((proofNode: string) => toBytes(proofNode))

Expand Down Expand Up @@ -247,11 +257,10 @@ export class EthersStateManager implements EVMStateManagerInterface {
*/
async getAccountFromProvider(address: Address): Promise<Account> {
if (this.DEBUG) this._debug(`retrieving account data from ${address.toString()} from provider`)
const accountData = await this._provider.send('eth_getProof', [
address.toString(),
[],
this._blockTag,
])
const accountData = await fetchFromProvider(this._provider, {
method: 'eth_getProof',
params: [address.toString(), [] as any, this._blockTag],
})
const account = Account.fromAccountData({
balance: BigInt(accountData.balance),
nonce: BigInt(accountData.nonce),
Expand Down Expand Up @@ -334,11 +343,14 @@ export class EthersStateManager implements EVMStateManagerInterface {
*/
async getProof(address: Address, storageSlots: Uint8Array[] = []): Promise<Proof> {
if (this.DEBUG) this._debug(`retrieving proof from provider for ${address.toString()}`)
const proof = await this._provider.send('eth_getProof', [
address.toString(),
[storageSlots.map((slot) => bytesToHex(slot))],
this._blockTag,
])
const proof = await fetchFromProvider(this._provider, {
method: 'eth_getProof',
params: [
address.toString(),
[storageSlots.map((slot) => bytesToHex(slot))],
this._blockTag,
] as any,
})

return proof
}
Expand Down Expand Up @@ -383,19 +395,19 @@ export class EthersStateManager implements EVMStateManagerInterface {
}

/**
* @deprecated This method is not used by the Ethers State Manager and is a stub required by the State Manager interface
* @deprecated This method is not used by the RPC State Manager and is a stub required by the State Manager interface
*/
getStateRoot = async () => {
return new Uint8Array(32)
}

/**
* @deprecated This method is not used by the Ethers State Manager and is a stub required by the State Manager interface
* @deprecated This method is not used by the RPC State Manager and is a stub required by the State Manager interface
*/
setStateRoot = async (_root: Uint8Array) => {}

/**
* @deprecated This method is not used by the Ethers State Manager and is a stub required by the State Manager interface
* @deprecated This method is not used by the RPC State Manager and is a stub required by the State Manager interface
*/
hasStateRoot = () => {
throw new Error('function not implemented')
Expand All @@ -405,3 +417,24 @@ export class EthersStateManager implements EVMStateManagerInterface {
return Promise.resolve()
}
}

export class RPCBlockChain {
readonly provider: string
constructor(provider: string) {
if (provider === undefined || provider === '') throw new Error('provider URL is required')
this.provider = provider
}
async getBlock(blockId: number) {
const block = await fetchFromProvider(this.provider, {
method: 'eth_getBlockByNumber',
params: [intToHex(blockId), false],
})
return {
hash: () => hexToBytes(block.hash),
}
}

shallowCopy() {
return this
}
}
Loading

0 comments on commit 019084d

Please sign in to comment.