forked from ethereumjs/ethereumjs-monorepo
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrpcStateManager.ts
380 lines (339 loc) · 12.5 KB
/
rpcStateManager.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
import { Common, Mainnet } from '@ethereumjs/common'
import { RLP } from '@ethereumjs/rlp'
import {
Account,
bigIntToHex,
bytesToHex,
createAccount,
createAccountFromRLP,
equalsBytes,
fetchFromProvider,
hexToBytes,
intToHex,
toBytes,
} from '@ethereumjs/util'
import debugDefault from 'debug'
import { keccak256 } from 'ethereum-cryptography/keccak.js'
import { Caches, OriginalStorageCache } from './cache/index.js'
import { modifyAccountFields } from './util.js'
import type { RPCStateManagerOpts } from './index.js'
import type { AccountFields, StateManagerInterface, StorageDump } from '@ethereumjs/common'
import type { Address } from '@ethereumjs/util'
import type { Debugger } from 'debug'
const KECCAK256_RLP_EMPTY_ACCOUNT = RLP.encode(new Account().serialize()).slice(2)
export class RPCStateManager implements StateManagerInterface {
protected _provider: string
protected _caches: Caches
protected _blockTag: string
originalStorageCache: OriginalStorageCache
protected _debug: Debugger
protected DEBUG: boolean
private keccakFunction: Function
public readonly common: Common
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 = debugDefault('statemanager:rpc')
if (typeof opts.provider === 'string' && opts.provider.startsWith('http')) {
this._provider = opts.provider
} else {
throw new Error(`valid RPC provider url required; got ${opts.provider}`)
}
this._blockTag = opts.blockTag === 'earliest' ? opts.blockTag : bigIntToHex(opts.blockTag)
this._caches = new Caches({ storage: { size: 100000 }, code: { size: 100000 } })
this.originalStorageCache = new OriginalStorageCache(this.getStorage.bind(this))
this.common = opts.common ?? new Common({ chain: Mainnet })
this.keccakFunction = opts.common?.customCrypto.keccak256 ?? keccak256
}
/**
* Note that the returned statemanager will share the same JSONRPCProvider as the original
*
* @returns RPCStateManager
*/
shallowCopy(): RPCStateManager {
const newState = new RPCStateManager({
provider: this._provider,
blockTag: BigInt(this._blockTag),
})
newState._caches = new Caches({ storage: { size: 100000 } })
return newState
}
/**
* Sets the new block tag used when querying the provider and clears the
* internal cache.
* @param blockTag - the new block tag to use when querying the provider
*/
setBlockTag(blockTag: bigint | 'earliest'): void {
this._blockTag = blockTag === 'earliest' ? blockTag : bigIntToHex(blockTag)
this.clearCaches()
if (this.DEBUG) this._debug(`setting block tag to ${this._blockTag}`)
}
/**
* Clears the internal cache so all accounts, contract code, and storage slots will
* initially be retrieved from the provider
*/
clearCaches(): void {
this._caches.clear()
}
/**
* Gets the code corresponding to the provided `address`.
* @param address - Address to get the `code` for
* @returns {Promise<Uint8Array>} - Resolves with the code corresponding to the provided address.
* Returns an empty `Uint8Array` if the account has no associated code.
*/
async getCode(address: Address): Promise<Uint8Array> {
let codeBytes = this._caches.code?.get(address)?.code
if (codeBytes !== undefined) return codeBytes
const code = await fetchFromProvider(this._provider, {
method: 'eth_getCode',
params: [address.toString(), this._blockTag],
})
codeBytes = toBytes(code)
this._caches.code?.put(address, codeBytes)
return codeBytes
}
async getCodeSize(address: Address): Promise<number> {
const contractCode = await this.getCode(address)
return contractCode.length
}
/**
* Adds `value` to the state trie as code, and sets `codeHash` on the account
* corresponding to `address` to reference this.
* @param address - Address of the `account` to add the `code` for
* @param value - The value of the `code`
*/
async putCode(address: Address, value: Uint8Array): Promise<void> {
// Store contract code in the cache
this._caches.code?.put(address, value)
}
/**
* Gets the storage value associated with the provided `address` and `key`. This method returns
* the shortest representation of the stored value.
* @param address - Address of the account to get the storage for
* @param key - Key in the account's storage to get the value for. Must be 32 bytes long.
* @returns {Uint8Array} - The storage value for the account
* corresponding to the provided address at the provided key.
* If this does not exist an empty `Uint8Array` is returned.
*/
async getStorage(address: Address, key: Uint8Array): Promise<Uint8Array> {
// Check storage slot in cache
if (key.length !== 32) {
throw new Error('Storage key must be 32 bytes long')
}
let value = this._caches.storage?.get(address, key)
if (value !== undefined) {
return value
}
// Retrieve storage slot from provider if not found in cache
const storage = await fetchFromProvider(this._provider, {
method: 'eth_getStorageAt',
params: [address.toString(), bytesToHex(key), this._blockTag],
})
value = toBytes(storage)
await this.putStorage(address, key, value)
return value
}
/**
* Adds value to the cache for the `account`
* corresponding to `address` at the provided `key`.
* @param address - Address to set a storage value for
* @param key - Key to set the value at. Must be 32 bytes long.
* @param value - Value to set at `key` for account corresponding to `address`.
* Cannot be more than 32 bytes. Leading zeros are stripped.
* If it is empty or filled with zeros, deletes the value.
*/
async putStorage(address: Address, key: Uint8Array, value: Uint8Array): Promise<void> {
this._caches.storage?.put(address, key, value)
}
/**
* Clears all storage entries for the account corresponding to `address`.
* @param address - Address to clear the storage of
*/
async clearStorage(address: Address): Promise<void> {
this._caches.storage?.clearStorage(address)
}
/**
* Dumps the RLP-encoded storage values for an `account` specified by `address`.
* @param address - The address of the `account` to return storage for
* @returns {Promise<StorageDump>} - The state of the account as an `Object` map.
* Keys are the storage keys, values are the storage values as strings.
* Both are represented as `0x` prefixed hex strings.
*/
dumpStorage(address: Address): Promise<StorageDump> {
const storageMap = this._caches.storage?.dump(address)
const dump: StorageDump = {}
if (storageMap !== undefined) {
for (const slot of storageMap) {
dump[slot[0]] = bytesToHex(slot[1])
}
}
return Promise.resolve(dump)
}
/**
* Gets the account associated with `address` or `undefined` if account does not exist
* @param address - Address of the `account` to get
*/
async getAccount(address: Address): Promise<Account | undefined> {
const elem = this._caches.account?.get(address)
if (elem !== undefined) {
return elem.accountRLP !== undefined ? createAccountFromRLP(elem.accountRLP) : undefined
}
const accountFromProvider = await this.getAccountFromProvider(address)
const account =
equalsBytes(accountFromProvider.codeHash, new Uint8Array(32)) ||
equalsBytes(accountFromProvider.serialize(), KECCAK256_RLP_EMPTY_ACCOUNT)
? undefined
: createAccountFromRLP(accountFromProvider.serialize())
this._caches.account?.put(address, account)
return account
}
/**
* Retrieves an account from the provider and stores in the local trie
* @param address Address of account to be retrieved from provider
* @private
*/
async getAccountFromProvider(address: Address): Promise<Account> {
if (this.DEBUG) this._debug(`retrieving account data from ${address.toString()} from provider`)
const accountData = await fetchFromProvider(this._provider, {
method: 'eth_getProof',
params: [address.toString(), [] as any, this._blockTag],
})
const account = createAccount({
balance: BigInt(accountData.balance),
nonce: BigInt(accountData.nonce),
codeHash: toBytes(accountData.codeHash),
storageRoot: toBytes(accountData.storageHash),
})
return account
}
/**
* Saves an account into state under the provided `address`.
* @param address - Address under which to store `account`
* @param account - The account to store
*/
async putAccount(address: Address, account: Account | undefined): Promise<void> {
if (this.DEBUG) {
this._debug(
`Save account address=${address} nonce=${account?.nonce} balance=${
account?.balance
} contract=${account && account.isContract() ? 'yes' : 'no'} empty=${
account && account.isEmpty() ? 'yes' : 'no'
}`,
)
}
if (account !== undefined) {
this._caches.account!.put(address, account)
} else {
this._caches.account!.del(address)
}
}
/**
* Gets the account associated with `address`, modifies the given account
* fields, then saves the account into state. Account fields can include
* `nonce`, `balance`, `storageRoot`, and `codeHash`.
* @param address - Address of the account to modify
* @param accountFields - Object containing account fields and values to modify
*/
async modifyAccountFields(address: Address, accountFields: AccountFields): Promise<void> {
if (this.DEBUG) {
this._debug(`modifying account fields for ${address.toString()}`)
this._debug(
JSON.stringify(
accountFields,
(k, v) => {
if (k === 'nonce') return v.toString()
return v
},
2,
),
)
}
await modifyAccountFields(this, address, accountFields)
}
/**
* Deletes an account from state under the provided `address`.
* @param address - Address of the account which should be deleted
*/
async deleteAccount(address: Address) {
if (this.DEBUG) {
this._debug(`deleting account corresponding to ${address.toString()}`)
}
this._caches.account?.del(address)
}
/**
* Returns the applied key for a given address
* Used for saving preimages
* @param address - The address to return the applied key
* @returns {Uint8Array} - The applied key (e.g. hashed address)
*/
getAppliedKey(address: Uint8Array): Uint8Array {
return this.keccakFunction(address)
}
/**
* Checkpoints the current state of the StateManager instance.
* State changes that follow can then be committed by calling
* `commit` or `reverted` by calling rollback.
*/
async checkpoint(): Promise<void> {
this._caches.checkpoint()
}
/**
* Commits the current change-set to the instance since the
* last call to checkpoint.
*
* Partial implementation, called from the subclass.
*/
async commit(): Promise<void> {
// setup cache checkpointing
this._caches.account?.commit()
}
/**
* Reverts the current change-set to the instance since the
* last call to checkpoint.
*
* Partial implementation , called from the subclass.
*/
async revert(): Promise<void> {
this._caches.revert()
}
async flush(): Promise<void> {
this._caches.account?.flush()
}
/**
* @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 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 RPC State Manager and is a stub required by the State Manager interface
*/
hasStateRoot = () => {
throw new Error('function not implemented')
}
}
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
}
}