Skip to content

Commit 44e069e

Browse files
Trie/StateManager: create partial state tries / state managers from proofs (#3186)
* trie: add methods to create and update tries from proofs * stateManager: add fromProof support * Add readme updates for new fromProof constructors * Apply suggested changes * Add missing line * statemanager/trie: further update readme examples --------- Co-authored-by: acolytec3 <[email protected]>
1 parent d79a2e3 commit 44e069e

File tree

7 files changed

+453
-22
lines changed

7 files changed

+453
-22
lines changed

packages/statemanager/README.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ Note: this library was part of the [@ethereumjs/vm](../vm/) package up till VM `
2525

2626
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/)).
2727

28-
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.
28+
The library includes a TypeScript interface `StateManager` to ensure a unified interface (e.g. when passed to the VM), a concrete Trie-based `DefaultStateManager` implementation, as well as an `RPCStateManager` implementation that sources state and history data from an external JSON-RPC provider.
2929

3030
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.
3131

32-
### `DefaultStateManager` Example
32+
### `DefaultStateManager`
33+
34+
#### Usage example
3335

3436
```typescript
3537
import { Account, Address } from '@ethereumjs/util'
@@ -45,7 +47,7 @@ await stateManager.commit()
4547
await stateManager.flush()
4648
```
4749

48-
### Account, Storage and Code Caches
50+
#### Account, Storage and Code Caches
4951

5052
Starting with the v2 release and complemented by the v2.1 release the StateManager comes with a significantly more elaborate caching mechanism for account, storage and code caches.
5153

@@ -55,6 +57,25 @@ Caches now "survive" a flush operation and especially long-lived usage scenarios
5557

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

60+
#### Instantiating from a proof
61+
62+
The `DefaultStateManager` has a static constructor `fromProof` that accepts one or more [EIP-1186](https://eips.ethereum.org/EIPS/eip-1186) [proofs](./src/stateManager.ts) and will instantiate a `DefaultStateManager` with a partial trie containing the state provided by the proof(s). See below example:
63+
64+
```typescript
65+
// setup `stateManager` with some existing address
66+
const proof = await stateManager.getProof(address)
67+
const proofWithStorage = await stateManger.getProof(contractAddress, [storageKey1, storageKey2])
68+
69+
const partialStateManager = await DefaultStateManager.fromProof(proof)
70+
// To add more proof data, use `addProofData`
71+
await partialStateManager.addProofData(proofWithStorage)
72+
const accountFromNewSM = await partialStateManager.getAccount(address)
73+
const accountFromOldSM = await stateManager.getAccount(address)
74+
console.log(accountFromNewSM, accountFromOldSM) // should match
75+
const slot1FromNewSM = await stateManager.getContractStorage(contractAddress, storageKey1)
76+
const slot2FromNewSM = await stateManager.getContractStorage(contractAddress, storageKey1) // should also match
77+
```
78+
5879
### `RPCStateManager`
5980

6081
First, a simple example of usage:
@@ -74,9 +95,9 @@ The `RPCStateManager` can be be used with any JSON-RPC provider that supports th
7495

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

77-
### Points on usage:
98+
#### Points on `RPCStateManager` usage
7899

79-
#### Instantiating the EVM
100+
##### Instantiating the EVM
80101

81102
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:
82103

@@ -92,24 +113,24 @@ const evm = new EVM({ blockchain, stateManager: state })
92113

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

95-
#### Provider selection
116+
##### Provider selection
96117

97118
- The provider you select must support the `eth_getProof`, `eth_getCode`, and `eth_getStorageAt` RPC methods.
98119
- 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.
99120

100-
#### Block Tag selection
121+
##### Block Tag selection
101122

102123
- You have to pass a block number or `earliest` in the constructor that specifies the block height you want to pull state from.
103124
- The `latest`/`pending` values supported by the Ethereum JSON-RPC are not supported as longer running scripts run the risk of state values changing as blocks are mined while your script is running.
104125
- If using a very recent block as your block tag, be aware that reorgs could occur and potentially alter the state you are interacting with.
105126
- If you want to rerun transactions from block X or run block X, you need to specify the block tag as X-1 in the state manager constructor to ensure you are pulling the state values at the point in time the transactions or block was run.
106127

107-
#### Potential gotchas
128+
##### Potential gotchas
108129

109130
- 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.
110131
- 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.
111132

112-
#### Further reference
133+
##### Further reference
113134

114135
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.
115136

packages/statemanager/src/stateManager.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,7 @@ export class DefaultStateManager implements EVMStateManagerInterface {
705705
* @param storageSlots storage slots to get proof of
706706
*/
707707
async getProof(address: Address, storageSlots: Uint8Array[] = []): Promise<Proof> {
708+
await this.flush()
708709
const account = await this.getAccount(address)
709710
if (!account) {
710711
// throw new Error(`getProof() can only be called for an existing account`)
@@ -748,6 +749,93 @@ export class DefaultStateManager implements EVMStateManagerInterface {
748749
return returnValue
749750
}
750751

752+
/**
753+
* Create a StateManager and initialize this with proof(s) gotten previously from getProof
754+
* This generates a (partial) StateManager where one can retrieve all items from the proof
755+
* @param proof Either a proof retrieved from `getProof`, or an array of those proofs
756+
* @param safe Wether or not to verify that the roots of the proof items match the reported roots
757+
* @param verifyRoot verify that all proof root nodes match statemanager's stateroot - should be
758+
* set to `false` when constructing a state manager where the underlying trie has proof nodes from different state roots
759+
* @returns A new DefaultStateManager with elements from the given proof included in its backing state trie
760+
*/
761+
static async fromProof(
762+
proof: Proof | Proof[],
763+
safe: boolean = false,
764+
opts: DefaultStateManagerOpts = {}
765+
): Promise<DefaultStateManager> {
766+
if (Array.isArray(proof)) {
767+
if (proof.length === 0) {
768+
return new DefaultStateManager(opts)
769+
} else {
770+
const trie =
771+
opts.trie ??
772+
(await Trie.createTrieFromProof(
773+
proof[0].accountProof.map((e) => hexToBytes(e)),
774+
{ useKeyHashing: true }
775+
))
776+
const sm = new DefaultStateManager({ ...opts, trie })
777+
const address = Address.fromString(proof[0].address)
778+
await sm.addStorageProof(proof[0].storageProof, proof[0].storageHash, address, safe)
779+
for (let i = 1; i < proof.length; i++) {
780+
const proofItem = proof[i]
781+
await sm.addProofData(proofItem, true)
782+
}
783+
await sm.flush() // TODO verify if this is necessary
784+
return sm
785+
}
786+
} else {
787+
return DefaultStateManager.fromProof([proof])
788+
}
789+
}
790+
791+
/**
792+
* Adds a storage proof to the state manager
793+
* @param storageProof The storage proof
794+
* @param storageHash The root hash of the storage trie
795+
* @param address The address
796+
* @param safe Whether or not to verify if the reported roots match the current storage root
797+
*/
798+
private async addStorageProof(
799+
storageProof: StorageProof[],
800+
storageHash: string,
801+
address: Address,
802+
safe: boolean = false
803+
) {
804+
const trie = this._getStorageTrie(address)
805+
trie.root(hexToBytes(storageHash))
806+
for (let i = 0; i < storageProof.length; i++) {
807+
await trie.updateTrieFromProof(
808+
storageProof[i].proof.map((e) => hexToBytes(e)),
809+
safe
810+
)
811+
}
812+
}
813+
814+
/**
815+
* Add proof(s) into an already existing trie
816+
* @param proof The proof(s) retrieved from `getProof`
817+
* @param verifyRoot verify that all proof root nodes match statemanager's stateroot - should be
818+
* set to `false` when constructing a state manager where the underlying trie has proof nodes from different state roots
819+
*/
820+
async addProofData(proof: Proof | Proof[], safe: boolean = false) {
821+
if (Array.isArray(proof)) {
822+
for (let i = 0; i < proof.length; i++) {
823+
await this._trie.updateTrieFromProof(
824+
proof[i].accountProof.map((e) => hexToBytes(e)),
825+
safe
826+
)
827+
await this.addStorageProof(
828+
proof[i].storageProof,
829+
proof[i].storageHash,
830+
Address.fromString(proof[i].address),
831+
safe
832+
)
833+
}
834+
} else {
835+
await this.addProofData([proof], safe)
836+
}
837+
}
838+
751839
/**
752840
* Verify an EIP-1186 proof. Throws if proof is invalid, otherwise returns true.
753841
* @param proof the proof to prove

packages/statemanager/test/proofStateManager.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
Address,
55
bytesToHex,
66
bytesToUnprefixedHex,
7+
equalsBytes,
78
hexToBytes,
89
randomBytes,
910
zeros,
@@ -28,6 +29,20 @@ describe('ProofStateManager', () => {
2829
assert.equal(proof.nonce, '0x0', 'Nonce is in quantity-encoded RPC representation')
2930
})
3031

32+
it(`should correctly return the right storage root / account root`, async () => {
33+
const address = Address.zero()
34+
const key = zeros(32)
35+
const stateManager = new DefaultStateManager()
36+
37+
await stateManager.putAccount(address, new Account(BigInt(100), BigInt(200)))
38+
const storageRoot = (await stateManager.getAccount(address))!.storageRoot
39+
40+
await stateManager.putContractStorage(address, key, new Uint8Array([10]))
41+
42+
const proof = await stateManager.getProof(address, [key])
43+
assert.ok(!equalsBytes(hexToBytes(proof.storageHash), storageRoot))
44+
})
45+
3146
it(`should return quantity-encoded RPC representation for existing accounts`, async () => {
3247
const address = Address.zero()
3348
const key = zeros(32)
@@ -132,7 +147,6 @@ describe('ProofStateManager', () => {
132147
await trie._db.put(key, bufferData)
133148
}
134149
trie.root(stateRoot!)
135-
await stateManager.putAccount(address, new Account())
136150
const proof = await stateManager.getProof(address)
137151
assert.deepEqual((ropsten_nonexistentAccount as any).default, proof)
138152
assert.ok(await stateManager.verifyProof(ropsten_nonexistentAccount))

0 commit comments

Comments
 (0)