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

Update EthersStateManager to be RpcStateManager under the hood #3167

Merged
merged 18 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

22 changes: 18 additions & 4 deletions packages/statemanager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,24 +62,38 @@ First, a simple example of usage:
```typescript
import { Account, Address } from '@ethereumjs/util'
import { EthersStateManager } from '@ethereumjs/statemanager'
import { ethers } from 'ethers'

const provider = new ethers.providers.JsonRpcProvider('https://path.to.my.provider.com')
const provider = 'https://path.to.my.provider.com'
const stateManager = new EthersStateManager({ 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 `EthersStateManager` can be be used with any JSON-RPC provider that supports the `eth` namespace. 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.

**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!

### Points on usage:

#### Instantiating the EVM

In order to have a fully functioning EVM instance, you must also instantiate the `EthersStateManager` and the `RpcBlockChain` and use that when instantiating your EVM as below:

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

const blockchain = new RPCBlockChain({}, provider)
const blockTag = 1n
const state = new EthersStateManager({ 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 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
95 changes: 64 additions & 31 deletions packages/statemanager/src/ethersStateManager.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
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
provider: string
blockTag: bigint | 'earliest'
}

export class EthersStateManager implements EVMStateManagerInterface {
protected _provider: ethers.JsonRpcProvider
protected _provider: string
protected _contractCache: Map<string, Uint8Array>
protected _storageCache: StorageCache
protected _blockTag: string
Expand All @@ -34,12 +45,10 @@ export class EthersStateManager implements EVMStateManagerInterface {
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) {
if (typeof opts.provider === 'string' && opts.provider.startsWith('http')) {
Copy link
Member

Choose a reason for hiding this comment

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

This is fine, but just noting: we thus do not have websocket support. (Also does not seem that fetchFromProvider accepts this)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct. This only works over HTTP. If anyone ever asks about websocket support, we can explore it. I'm not sure if node's native fetch instance supports WS anyway.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If ws are added in future you might need to use an isomorphic library or dependency inject it to get it to work in both client and node

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 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 @@ -405,3 +417,24 @@ export class EthersStateManager implements EVMStateManagerInterface {
return Promise.resolve()
}
}

export class RPCBlockChain {
provider: string
constructor(opts: {}, 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