@ethereumjs/statemanager
Version:
An Ethereum statemanager implementation
265 lines (252 loc) • 8.76 kB
text/typescript
import {
createMPTFromProof,
createMerkleProof,
updateMPTFromMerkleProof,
verifyMerkleProof,
} from '@ethereumjs/mpt'
import { RLP } from '@ethereumjs/rlp'
import {
EthereumJSErrorWithoutCode,
KECCAK256_NULL,
KECCAK256_NULL_S,
KECCAK256_RLP,
KECCAK256_RLP_S,
bigIntToHex,
bytesToHex,
createAccountFromRLP,
createAddressFromString,
equalsBytes,
hexToBytes,
setLengthLeft,
unpadBytes,
} from '@ethereumjs/util'
import { MerkleStateManager } from '../merkleStateManager.ts'
import type { Proof, StorageProof } from '@ethereumjs/common'
import type { Address, PrefixedHexString } from '@ethereumjs/util'
import type { MerkleStateManagerOpts } from '../index.ts'
/**
* Get an EIP-1186 proof
* @param address address to get proof of
* @param storageSlots storage slots to get proof of
*/
export async function getMerkleStateProof(
sm: MerkleStateManager,
address: Address,
storageSlots: Uint8Array[] = [],
): Promise<Proof> {
await sm['flush']()
const account = await sm.getAccount(address)
if (!account) {
const returnValue: Proof = {
address: address.toString(),
balance: '0x0',
codeHash: KECCAK256_NULL_S,
nonce: '0x0',
storageHash: KECCAK256_RLP_S,
accountProof: (await createMerkleProof(sm['_trie'], address.bytes)).map((p) => bytesToHex(p)),
storageProof: [],
}
return returnValue
}
const accountProof: PrefixedHexString[] = (
await createMerkleProof(sm['_trie'], address.bytes)
).map((p) => bytesToHex(p))
const storageProof: StorageProof[] = []
const storageTrie = sm['_getStorageTrie'](address, account)
for (const storageKey of storageSlots) {
const proof = (await createMerkleProof(storageTrie, storageKey)).map((p) => bytesToHex(p))
const value = bytesToHex(await sm.getStorage(address, storageKey))
const proofItem: StorageProof = {
key: bytesToHex(storageKey),
value: value === '0x' ? '0x0' : value, // Return '0x' values as '0x0' since this is a JSON RPC response
proof,
}
storageProof.push(proofItem)
}
const returnValue: Proof = {
address: address.toString(),
balance: bigIntToHex(account.balance),
codeHash: bytesToHex(account.codeHash),
nonce: bigIntToHex(account.nonce),
storageHash: bytesToHex(account.storageRoot),
accountProof,
storageProof,
}
return returnValue
}
/**
* Adds a storage proof to the state manager
* @param storageProof The storage proof
* @param storageHash The root hash of the storage trie
* @param address The address
* @param safe Whether or not to verify if the reported roots match the current storage root
*/
export async function addMerkleStateStorageProof(
sm: MerkleStateManager,
storageProof: StorageProof[],
storageHash: PrefixedHexString,
address: Address,
safe: boolean = false,
) {
const trie = sm['_getStorageTrie'](address)
trie.root(hexToBytes(storageHash))
for (let i = 0; i < storageProof.length; i++) {
await updateMPTFromMerkleProof(
trie,
storageProof[i].proof.map((e) => hexToBytes(e)),
safe,
)
}
}
/**
* Create a StateManager and initialize this with proof(s) gotten previously from getProof
* This generates a (partial) StateManager where one can retrieve all items from the proof
* @param proof Either a proof retrieved from `getProof`, or an array of those proofs
* @param safe Whether or not to verify that the roots of the proof items match the reported roots
* @param opts a dictionary of StateManager opts
* @returns A new MerkleStateManager with elements from the given proof included in its backing state trie
*/
export async function fromMerkleStateProof(
proof: Proof | Proof[],
safe: boolean = false,
opts: MerkleStateManagerOpts = {},
): Promise<MerkleStateManager> {
if (Array.isArray(proof)) {
if (proof.length === 0) {
return new MerkleStateManager(opts)
} else {
const trie =
opts.trie ??
(await createMPTFromProof(
proof[0].accountProof.map((e) => hexToBytes(e)),
{ useKeyHashing: true },
))
const sm = new MerkleStateManager({ ...opts, trie })
const address = createAddressFromString(proof[0].address)
await addMerkleStateStorageProof(
sm,
proof[0].storageProof,
proof[0].storageHash,
address,
safe,
)
for (let i = 1; i < proof.length; i++) {
const proofItem = proof[i]
await addMerkleStateProofData(sm, proofItem, true)
}
await sm.flush() // TODO verify if this is necessary
return sm
}
} else {
return fromMerkleStateProof([proof], safe, opts)
}
}
/**
* Add proof(s) into an already existing trie
* @param proof The proof(s) retrieved from `getProof`
* @param verifyRoot verify that all proof root nodes match statemanager's stateroot - should be
* set to `false` when constructing a state manager where the underlying trie has proof nodes from different state roots
*/
export async function addMerkleStateProofData(
sm: MerkleStateManager,
proof: Proof | Proof[],
safe: boolean = false,
) {
if (Array.isArray(proof)) {
for (let i = 0; i < proof.length; i++) {
await updateMPTFromMerkleProof(
sm['_trie'],
proof[i].accountProof.map((e) => hexToBytes(e)),
safe,
)
await addMerkleStateStorageProof(
sm,
proof[i].storageProof,
proof[i].storageHash,
createAddressFromString(proof[i].address),
safe,
)
}
} else {
await addMerkleStateProofData(sm, [proof], safe)
}
}
/**
* Verify an EIP-1186 proof. Throws if proof is invalid, otherwise returns true.
* @param proof the proof to prove
*/
export async function verifyMerkleStateProof(
sm: MerkleStateManager,
proof: Proof,
): Promise<boolean> {
const key = hexToBytes(proof.address)
const accountProof = proof.accountProof.map((rlpString: PrefixedHexString) =>
hexToBytes(rlpString),
)
// This returns the account if the proof is valid.
// Verify that it matches the reported account.
const value = await verifyMerkleProof(key, accountProof, {
useKeyHashing: true,
})
if (value === null) {
// Verify that the account is empty in the proof.
const emptyBytes = new Uint8Array(0)
const notEmptyErrorMsg = 'Invalid proof provided: account is not empty'
const nonce = unpadBytes(hexToBytes(proof.nonce))
if (!equalsBytes(nonce, emptyBytes)) {
throw EthereumJSErrorWithoutCode(`${notEmptyErrorMsg} (nonce is not zero)`)
}
const balance = unpadBytes(hexToBytes(proof.balance))
if (!equalsBytes(balance, emptyBytes)) {
throw EthereumJSErrorWithoutCode(`${notEmptyErrorMsg} (balance is not zero)`)
}
const storageHash = hexToBytes(proof.storageHash)
if (!equalsBytes(storageHash, KECCAK256_RLP)) {
throw EthereumJSErrorWithoutCode(
`${notEmptyErrorMsg} (storageHash does not equal KECCAK256_RLP)`,
)
}
const codeHash = hexToBytes(proof.codeHash)
if (!equalsBytes(codeHash, KECCAK256_NULL)) {
throw EthereumJSErrorWithoutCode(
`${notEmptyErrorMsg} (codeHash does not equal KECCAK256_NULL)`,
)
}
} else {
const account = createAccountFromRLP(value)
const { nonce, balance, storageRoot, codeHash } = account
const invalidErrorMsg = 'Invalid proof provided:'
if (nonce !== BigInt(proof.nonce)) {
throw EthereumJSErrorWithoutCode(`${invalidErrorMsg} nonce does not match`)
}
if (balance !== BigInt(proof.balance)) {
throw EthereumJSErrorWithoutCode(`${invalidErrorMsg} balance does not match`)
}
if (!equalsBytes(storageRoot, hexToBytes(proof.storageHash))) {
throw EthereumJSErrorWithoutCode(`${invalidErrorMsg} storageHash does not match`)
}
if (!equalsBytes(codeHash, hexToBytes(proof.codeHash))) {
throw EthereumJSErrorWithoutCode(`${invalidErrorMsg} codeHash does not match`)
}
}
for (const stProof of proof.storageProof) {
const storageProof = stProof.proof.map((value: PrefixedHexString) => hexToBytes(value))
const storageValue = setLengthLeft(hexToBytes(stProof.value), 32)
const storageKey = hexToBytes(stProof.key)
const proofValue = await verifyMerkleProof(storageKey, storageProof, {
useKeyHashing: true,
})
const reportedValue = setLengthLeft(
RLP.decode(proofValue ?? new Uint8Array(0)) as Uint8Array,
32,
)
if (!equalsBytes(reportedValue, storageValue)) {
throw EthereumJSErrorWithoutCode(
`Reported trie value does not match storage, key: ${stProof.key}, reported: ${bytesToHex(
reportedValue,
)}, actual: ${bytesToHex(storageValue)}`,
)
}
}
return true
}