Skip to content

Commit

Permalink
Implement eth_getProof (#1590)
Browse files Browse the repository at this point in the history
* vm: start on getProof

* vm: stateManager: add getProof EIP-1186

* vm: stateManager: add verifyProof

* vm: add getProof tests

* vm: start on getProof

* vm: stateManager: add getProof EIP-1186

* vm: stateManager: add verifyProof

* vm: move changes of old state manager into new one

* vm: fix proof test

* vm: make getProof better readable

* vm: stateManager cleanup verifyProof

* vm: make proof/verifyproof optional

* stateManager: add ropsten stateManager tests
stateManager: fix getProof EIP1178 field

* stateManager: more getProof tests / ensure geth compatibility

* stateManager: partially fix verifyProof

* stateManager: fix empty account proofs
stateManager: fix storage slot proofs

* stateManager: add tests for tampered proofs

* stateManager: use Proof type of stateManage not MPT

* client: add getProof endpoint
client: add tests for getProof

* vm: state: update interface

* review - vm:
  * bolster invalid proof error messages
  * extract ProofStateManager tests to own file
  * move testdata files to own folder and use import syntax for improved type info

* review - client:
  * already have array validator (usage: `validators.array(validators.hex)`)
  * add typedoc for getProof (thanks @gabrocheleau)
  * update getProof.spec.ts to match future test setup from PR #1598 (this slightly modifies the returned accountProof)

Co-authored-by: Ryan Ghods <ryan@ryanio.com>
  • Loading branch information
jochem-brouwer and ryanio authored Dec 9, 2021
1 parent 164989b commit bffbc03
Show file tree
Hide file tree
Showing 10 changed files with 587 additions and 4 deletions.
41 changes: 41 additions & 0 deletions packages/client/lib/rpc/modules/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
TxReceipt,
} from '@ethereumjs/vm/dist/types'
import type { Log } from '@ethereumjs/vm/dist/evm/types'
import type { Proof, ProofStateManager } from '@ethereumjs/vm/dist/state'
import type { EthereumClient } from '../..'
import type { Chain } from '../../blockchain'
import type { EthProtocol } from '../../net/protocol'
Expand Down Expand Up @@ -362,6 +363,12 @@ export class Eth {
this.protocolVersion = middleware(this.protocolVersion.bind(this), 0, [])

this.syncing = middleware(this.syncing.bind(this), 0, [])

this.getProof = middleware(this.getProof.bind(this), 3, [
[validators.address],
[validators.array(validators.hex)],
[validators.blockOption],
])
}

/**
Expand Down Expand Up @@ -957,6 +964,40 @@ export class Eth {

return bufferToHex(tx.hash())
}

/**
* Returns an account object along with data about the proof.
* @param params An array of three parameters:
* 1. address of the account
* 2. array of storage keys which should be proofed and included
* 3. integer block number, or the string "latest" or "earliest"
* @returns The {@link Proof}
*/
async getProof(params: [string, string[], string]): Promise<Proof> {
const [addressHex, slotsHex, blockOption] = params

if (!this._vm) {
throw new Error('missing vm')
}

// use a copy of the vm in case new blocks are executed
const vm = this._vm.copy()
if (blockOption !== 'latest') {
const latest = await vm.blockchain.getLatestHeader()
if (blockOption !== bnToHex(latest.number)) {
throw {
code: INVALID_PARAMS,
message: `Currently only "latest" block supported`,
}
}
}

const address = Address.fromString(addressHex)
const slots = slotsHex.map((slotHex) => setLengthLeft(toBuffer(slotHex), 32))
const proof = await (vm.stateManager as ProofStateManager).getProof(address, slots)
return proof
}

/**
* Returns an object with data about the sync status or false.
* @param params An empty array
Expand Down
127 changes: 127 additions & 0 deletions packages/client/test/rpc/eth/getProof.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import tape from 'tape'
import { Block } from '@ethereumjs/block'
import Blockchain from '@ethereumjs/blockchain'
import { Transaction } from '@ethereumjs/tx'
import { Address, BN, bnToHex } from 'ethereumjs-util'
import { FullSynchronizer } from '../../../lib/sync'
import { startRPC, createManager, createClient, params, baseRequest } from '../helpers'

const method = 'eth_getProof'

const expectedProof = {
address: '0x9288f8f702cbfb8cc5890819c1c1e2746e684d07',
balance: '0x0',
codeHash: '0x05698751a8fe928d7049ee0af6927f3ff6e398d7d11293ea4c6786d7cfc3dbd4',
nonce: '0x1',
storageHash: '0xb39609ba55cc225a26265fc5e80d51e07a4410c1725cf69dbf15a8b09ad1a0a0',
accountProof: [
'0xf8718080808080a0b356351d60bc9894cf1f1d6cb68c815f0131d50f1da83c4023a09ec855cfff91808080a086a4665abc4f7e6f3a2da6a3c112616b1954be58ac4f6ff236b5b5f9ba295e4ca043a5b2616ae3a304fe34c5402d41893c49cb75b2ecd25b8b8b53f0926c957f23808080808080',
'0xf869a03bdcfb03f3efaf0a5250648861b575109e8bb8084a0b74b0ec15d41366a4a7abb846f8440180a0b39609ba55cc225a26265fc5e80d51e07a4410c1725cf69dbf15a8b09ad1a0a0a005698751a8fe928d7049ee0af6927f3ff6e398d7d11293ea4c6786d7cfc3dbd4',
],
storageProof: [
{
key: '0x0000000000000000000000000000000000000000000000000000000000000000',
value: '0x04d2',
proof: [
'0xf8518080a036bb5f2fd6f99b186600638644e2f0396989955e201672f7e81e8c8f466ed5b9a010859880cfb38603690e8c4dfcc5595c203de6b901a503f944ef21a6120926a680808080808080808080808080',
'0xe5a0390decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563838204d2',
],
},
],
}

tape(`${method}: call with valid arguments`, async (t) => {
const blockchain = await Blockchain.create({ validateBlocks: false, validateConsensus: false })

const client = createClient({ blockchain, includeVM: true })
const manager = createManager(client)
const server = startRPC(manager.getMethods())

const service = client.services.find((s) => s.name === 'eth')
const { vm } = (service!.synchronizer as FullSynchronizer).execution

// genesis address with balance
const address = Address.fromString('0xccfd725760a68823ff1e062f4cc97e1360e8d997')

// contract inspired from https://eth.wiki/json-rpc/API#example-14
/*
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.4;
contract Storage {
uint pos0;
mapping(address => uint) pos1;
function store() public {
pos0 = 1234;
pos1[msg.sender] = 5678;
}
}
*/
const data =
'0x6080604052348015600f57600080fd5b5060bc8061001e6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063975057e714602d575b600080fd5b60336035565b005b6104d260008190555061162e600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555056fea2646970667358221220b16fe0abdbdcae31fa05c5717ebc442024b20fb637907d1a05547ea2d8ec8e5964736f6c63430007060033'

// construct block with tx
const gasLimit = 2000000
const tx = Transaction.fromTxData({ gasLimit, data }, { freeze: false })
tx.getSenderAddress = () => {
return address
}
const parent = await blockchain.getLatestHeader()
const block = Block.fromBlockData(
{
header: {
parentHash: parent.hash(),
number: 1,
gasLimit,
},
},
{ calcDifficultyFromHeader: parent }
)
block.transactions[0] = tx

// deploy contract
let ranBlock: Block | undefined = undefined
vm.once('afterBlock', (result: any) => (ranBlock = result.block))
const result = await vm.runBlock({ block, generate: true, skipBlockValidation: true })
const { createdAddress } = result.results[0]
await vm.blockchain.putBlock(ranBlock!)

// call store() method
const funcHash = '975057e7' // store()
const storeTxData = {
to: createdAddress!.toString(),
from: address.toString(),
data: `0x${funcHash}`,
gasLimit: bnToHex(new BN(530000)),
nonce: 1,
}
const storeTx = Transaction.fromTxData(storeTxData, { freeze: false })
storeTx.getSenderAddress = () => {
return address
}
const block2 = Block.fromBlockData(
{
header: {
parentHash: ranBlock!.hash(),
number: 2,
gasLimit,
},
},
{ calcDifficultyFromHeader: block.header }
)
block2.transactions[0] = storeTx

// run block
let ranBlock2: Block | undefined = undefined
vm.once('afterBlock', (result: any) => (ranBlock2 = result.block))
await vm.runBlock({ block: block2, generate: true, skipBlockValidation: true })
await vm.blockchain.putBlock(ranBlock2!)

// verify proof is accurate
const req = params(method, [createdAddress!.toString(), ['0x0'], 'latest'])
const expectRes = (res: any) => {
const msg = 'should return the correct proof'
t.deepEqual(res.body.result, expectedProof, msg)
}
await baseRequest(t, server, req, 200, expectRes)
})
4 changes: 2 additions & 2 deletions packages/vm/src/state/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { StateManager } from './interface'
export { StateManager, EIP2929StateManager, ProofStateManager } from './interface'
export { BaseStateManager } from './baseStateManager'
export { default as DefaultStateManager } from './stateManager'
export { default as DefaultStateManager, Proof } from './stateManager'
11 changes: 11 additions & 0 deletions packages/vm/src/state/interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Account, Address } from 'ethereumjs-util'
import { AccessList } from '@ethereumjs/tx'
import { Proof } from './stateManager'

/**
* Storage values of an account
Expand Down Expand Up @@ -43,3 +44,13 @@ export interface EIP2929StateManager extends StateManager {
clearWarmedAccounts(): void
generateAccessList?(addressesRemoved: Address[], addressesOnlyStorage: Address[]): AccessList
}

/**
* Note: if a StateManager supports both EIP2929StateManager and
* the ProofStateManager interface, it can be cast as:
* <EIP2929StateManager & ProofStateManager>(StateManager)
*/
export interface ProofStateManager extends StateManager {
getProof(address: Address, storageSlots: Buffer[]): Promise<Proof>
verifyProof(proof: Proof): Promise<boolean>
}
131 changes: 130 additions & 1 deletion packages/vm/src/state/stateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,34 @@ import {
KECCAK256_NULL,
rlp,
unpadBuffer,
PrefixedHexString,
bufferToHex,
bnToHex,
BN,
KECCAK256_RLP,
setLengthLeft,
} from 'ethereumjs-util'
import Common from '@ethereumjs/common'
import { StateManager, StorageDump } from './interface'
import Cache, { getCb, putCb } from './cache'
import { BaseStateManager } from './'
import { short } from '../evm/opcodes'
import { BaseStateManager } from '.'

type StorageProof = {
key: PrefixedHexString
proof: PrefixedHexString[]
value: PrefixedHexString
}

export type Proof = {
address: PrefixedHexString
balance: PrefixedHexString
codeHash: PrefixedHexString
nonce: PrefixedHexString
storageHash: PrefixedHexString
accountProof: PrefixedHexString[]
storageProof: StorageProof[]
}

/**
* Options for constructing a {@link StateManager}.
Expand Down Expand Up @@ -281,6 +303,113 @@ export default class DefaultStateManager extends BaseStateManager implements Sta
await super.revert()
}

/**
* Get an EIP-1186 proof
* @param address address to get proof of
* @param storageSlots storage slots to get proof of
*/
async getProof(address: Address, storageSlots: Buffer[] = []): Promise<Proof> {
const account = await this.getAccount(address)
const accountProof: PrefixedHexString[] = (await Trie.createProof(this._trie, address.buf)).map(
(p) => bufferToHex(p)
)
const storageProof: StorageProof[] = []
const storageTrie = await this._getStorageTrie(address)

for (const storageKey of storageSlots) {
const proof = (await Trie.createProof(storageTrie, storageKey)).map((p) => bufferToHex(p))
let value = bufferToHex(await this.getContractStorage(address, storageKey))
if (value === '0x') {
value = '0x0'
}
const proofItem: StorageProof = {
key: bufferToHex(storageKey),
value,
proof,
}
storageProof.push(proofItem)
}

const returnValue: Proof = {
address: address.toString(),
balance: bnToHex(account.balance),
codeHash: bufferToHex(account.codeHash),
nonce: bnToHex(account.nonce),
storageHash: bufferToHex(account.stateRoot),
accountProof,
storageProof,
}
return returnValue
}

/**
* Verify an EIP-1186 proof. Throws if proof is invalid, otherwise returns true.
* @param proof the proof to prove
*/
async verifyProof(proof: Proof): Promise<boolean> {
const rootHash = keccak256(toBuffer(proof.accountProof[0]))
const key = toBuffer(proof.address)
const accountProof = proof.accountProof.map((rlpString: PrefixedHexString) =>
toBuffer(rlpString)
)

// This returns the account if the proof is valid.
// Verify that it matches the reported account.
const value = await Trie.verifyProof(rootHash, key, accountProof)

if (value === null) {
// Verify that the account is empty in the proof.
const emptyBuffer = Buffer.from('')
const notEmptyErrorMsg = 'Invalid proof provided: account is not empty'
const nonce = unpadBuffer(toBuffer(proof.nonce))
if (!nonce.equals(emptyBuffer)) {
throw new Error(`${notEmptyErrorMsg} (nonce is not zero)`)
}
const balance = unpadBuffer(toBuffer(proof.balance))
if (!balance.equals(emptyBuffer)) {
throw new Error(`${notEmptyErrorMsg} (balance is not zero)`)
}
const storageHash = toBuffer(proof.storageHash)
if (!storageHash.equals(KECCAK256_RLP)) {
throw new Error(`${notEmptyErrorMsg} (storageHash does not equal KECCAK256_RLP)`)
}
const codeHash = toBuffer(proof.codeHash)
if (!codeHash.equals(KECCAK256_NULL)) {
throw new Error(`${notEmptyErrorMsg} (codeHash does not equal KECCAK256_NULL)`)
}
} else {
const account = Account.fromRlpSerializedAccount(value)
const { nonce, balance, stateRoot, codeHash } = account
const invalidErrorMsg = 'Invalid proof provided:'
if (!nonce.eq(new BN(toBuffer(proof.nonce)))) {
throw new Error(`${invalidErrorMsg} nonce does not match`)
}
if (!balance.eq(new BN(toBuffer(proof.balance)))) {
throw new Error(`${invalidErrorMsg} balance does not match`)
}
if (!stateRoot.equals(toBuffer(proof.storageHash))) {
throw new Error(`${invalidErrorMsg} storageHash does not match`)
}
if (!codeHash.equals(toBuffer(proof.codeHash))) {
throw new Error(`${invalidErrorMsg} codeHash does not match`)
}
}

const storageRoot = toBuffer(proof.storageHash)

for (const stProof of proof.storageProof) {
const storageProof = stProof.proof.map((value: PrefixedHexString) => toBuffer(value))
const storageValue = setLengthLeft(toBuffer(stProof.value), 32)
const storageKey = toBuffer(stProof.key)
const proofValue = await Trie.verifyProof(storageRoot, storageKey, storageProof)
const reportedValue = setLengthLeft(rlp.decode(proofValue as Buffer), 32)
if (!reportedValue.equals(storageValue)) {
throw new Error('Reported trie value does not match storage')
}
}
return true
}

/**
* Gets the state-root of the Merkle-Patricia trie representation
* of the state of this StateManager. Will error if there are uncommitted
Expand Down
Loading

0 comments on commit bffbc03

Please sign in to comment.