Skip to content

Commit

Permalink
Merge branch 'master' into better-error-messages
Browse files Browse the repository at this point in the history
  • Loading branch information
jochem-brouwer committed Jan 30, 2024
2 parents 96469a3 + 5be55b2 commit d8f7544
Show file tree
Hide file tree
Showing 15 changed files with 238 additions and 85 deletions.
27 changes: 16 additions & 11 deletions packages/block/src/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,24 +606,29 @@ export class Block {
* - The transactions trie is valid
* - The uncle hash is valid
* @param onlyHeader if only passed the header, skip validating txTrie and unclesHash (default: false)
* @param verifyTxs if set to `false`, will not check for transaction validation errors (default: true)
*/
async validateData(onlyHeader: boolean = false): Promise<void> {
const txErrors = this.getTransactionsValidationErrors()
if (txErrors.length > 0) {
const msg = this._errorMsg(`invalid transactions: ${txErrors.join(' ')}`)
throw new Error(msg)
async validateData(onlyHeader: boolean = false, verifyTxs: boolean = true): Promise<void> {
if (verifyTxs) {
const txErrors = this.getTransactionsValidationErrors()
if (txErrors.length > 0) {
const msg = this._errorMsg(`invalid transactions: ${txErrors.join(' ')}`)
throw new Error(msg)
}
}

if (onlyHeader) {
return
}

for (const [index, tx] of this.transactions.entries()) {
if (!tx.isSigned()) {
const msg = this._errorMsg(
`invalid transactions: transaction at index ${index} is unsigned`
)
throw new Error(msg)
if (verifyTxs) {
for (const [index, tx] of this.transactions.entries()) {
if (!tx.isSigned()) {
const msg = this._errorMsg(
`invalid transactions: transaction at index ${index} is unsigned`
)
throw new Error(msg)
}
}
}

Expand Down
77 changes: 76 additions & 1 deletion packages/block/test/block.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { Chain, Common, Hardfork } from '@ethereumjs/common'
import { RLP } from '@ethereumjs/rlp'
import { LegacyTransaction } from '@ethereumjs/tx'
import { bytesToHex, equalsBytes, hexToBytes, toBytes, zeros } from '@ethereumjs/util'
import {
KECCAK256_RLP_ARRAY,
bytesToHex,
equalsBytes,
hexToBytes,
toBytes,
zeros,
} from '@ethereumjs/util'
import { assert, describe, it } from 'vitest'

import { blockFromRpc } from '../src/from-rpc.js'
Expand Down Expand Up @@ -238,6 +245,74 @@ describe('[Block]: block functions', () => {
}
})

it('should test data integrity', async () => {
const unsignedTx = LegacyTransaction.fromTxData({})
const txRoot = await Block.genTransactionsTrieRoot([unsignedTx])

let block = Block.fromBlockData({
transactions: [unsignedTx],
header: {
transactionsTrie: txRoot,
},
})

// Verifies that the "signed tx check" is skipped
await block.validateData(false, false)

async function checkThrowsAsync(fn: Promise<void>, errorMsg: string) {
try {
await fn
assert.fail('should throw')
} catch (e: any) {
assert.ok((e.message as string).includes(errorMsg))
}
}

const zeroRoot = zeros(32)

// Tx root
block = Block.fromBlockData({
transactions: [unsignedTx],
header: {
transactionsTrie: zeroRoot,
},
})
await checkThrowsAsync(block.validateData(false, false), 'invalid transaction trie')

// Withdrawals root
block = Block.fromBlockData(
{
header: {
withdrawalsRoot: zeroRoot,
uncleHash: KECCAK256_RLP_ARRAY,
},
},
{ common: new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Shanghai }) }
)
await checkThrowsAsync(block.validateData(false, false), 'invalid withdrawals trie')

// Uncle root
block = Block.fromBlockData(
{
header: {
uncleHash: zeroRoot,
},
},
{ common: new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Chainstart }) }
)
await checkThrowsAsync(block.validateData(false, false), 'invalid uncle hash')

// Verkle withness
const common = new Common({ chain: Chain.Mainnet, eips: [6800], hardfork: Hardfork.Cancun })
// Note: `executionWitness: undefined` will still initialize an execution witness in the block
// So, only testing for `null` here
block = Block.fromBlockData({ executionWitness: null }, { common })
await checkThrowsAsync(
block.validateData(false, false),
'Invalid block: ethereumjs stateless client needs executionWitness'
)
})

it('should test isGenesis (mainnet default)', () => {
const block = Block.fromBlockData({ header: { number: 1 } })
assert.notEqual(block.isGenesis(), true)
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/sync/beaconsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export class BeaconSynchronizer extends Synchronizer {
const latest = await this.latest(peer)
if (latest) {
const { number } = latest
if ((!best && number >= this.chain.blocks.height) || (best && best[1] < number)) {
if (!best || best[1] < number) {
best = [peer, number]
}
}
Expand Down
6 changes: 5 additions & 1 deletion packages/client/src/sync/fetcher/blockfetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ export class BlockFetcher extends BlockFetcherBase<Block[], Block> {
}
// Supply the common from the corresponding block header already set on correct fork
const block = Block.fromValuesArray(values, { common: headers[i].common })
await block.validateData()
// Only validate the data integrity
// Upon putting blocks into blockchain (for BlockFetcher), `validateData` is called again
// In ReverseBlockFetcher we do not need to validate the entire block, since CL
// expects us to sync with the requested chain tip header
await block.validateData(false, false)
blocks.push(block)
}
this.debug(
Expand Down
2 changes: 1 addition & 1 deletion packages/client/test/sim/beaconsync.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Note: All commands should be run from the `client` package directory root (so so

```bash
rm -rf ./datadir
```
```

2. Run the sim

Expand Down
4 changes: 2 additions & 2 deletions packages/client/test/testdata/geth-genesis/kaustinen2.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"berlinBlock": 0,
"londonBlock": 0,
"mergeNetsplitBlock": 0,
"shanghaiTime":0,
"pragueTime":1679652600,
"shanghaiTime": 0,
"pragueTime": 1679652600,
"terminalTotalDifficulty": 0,
"terminalTotalDifficultyPassed": true,
"proofInBlocks": true
Expand Down
9 changes: 3 additions & 6 deletions packages/statemanager/src/rpcStateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,9 @@ export class RPCStateManager implements EVMStateManagerInterface {

const proofBuf = proof.accountProof.map((proofNode: string) => toBytes(proofNode))

const trie = new Trie({ useKeyHashing: true, common: this.common })
const verified = await trie.verifyProof(
this.keccakFunction(proofBuf[0]),
address.bytes,
proofBuf
)
const verified = await Trie.verifyProof(address.bytes, proofBuf, {
useKeyHashing: true,
})
// if not verified (i.e. verifyProof returns null), account does not exist
return verified === null ? false : true
}
Expand Down
17 changes: 9 additions & 8 deletions packages/statemanager/src/stateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,7 @@ export class DefaultStateManager implements EVMStateManagerInterface {
} else {
const trie =
opts.trie ??
(await Trie.createTrieFromProof(
(await Trie.createFromProof(
proof[0].accountProof.map((e) => hexToBytes(e)),
{ useKeyHashing: true }
))
Expand Down Expand Up @@ -808,7 +808,7 @@ export class DefaultStateManager implements EVMStateManagerInterface {
const trie = this._getStorageTrie(address)
trie.root(hexToBytes(storageHash))
for (let i = 0; i < storageProof.length; i++) {
await trie.updateTrieFromProof(
await trie.updateFromProof(
storageProof[i].proof.map((e) => hexToBytes(e)),
safe
)
Expand All @@ -824,7 +824,7 @@ export class DefaultStateManager implements EVMStateManagerInterface {
async addProofData(proof: Proof | Proof[], safe: boolean = false) {
if (Array.isArray(proof)) {
for (let i = 0; i < proof.length; i++) {
await this._trie.updateTrieFromProof(
await this._trie.updateFromProof(
proof[i].accountProof.map((e) => hexToBytes(e)),
safe
)
Expand All @@ -845,15 +845,16 @@ export class DefaultStateManager implements EVMStateManagerInterface {
* @param proof the proof to prove
*/
async verifyProof(proof: Proof): Promise<boolean> {
const rootHash = this.keccakFunction(hexToBytes(proof.accountProof[0]))
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 this._proofTrie.verifyProof(rootHash, key, accountProof)
const value = await Trie.verifyProof(key, accountProof, {
useKeyHashing: true,
})

if (value === null) {
// Verify that the account is empty in the proof.
Expand Down Expand Up @@ -893,13 +894,13 @@ export class DefaultStateManager implements EVMStateManagerInterface {
}
}

const storageRoot = hexToBytes(proof.storageHash)

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 this._proofTrie.verifyProof(storageRoot, storageKey, storageProof)
const proofValue = await Trie.verifyProof(storageKey, storageProof, {
useKeyHashing: true,
})
const reportedValue = setLengthLeft(
RLP.decode(proofValue ?? new Uint8Array(0)) as Uint8Array,
32
Expand Down
4 changes: 2 additions & 2 deletions packages/trie/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ test()

When the static `Trie.create` constructor is used without any options, the `trie` object is instantiated with defaults configured to match the Ethereum production spec (i.e. keys are hashed using SHA256). It also persists the state root of the tree on each write operation, ensuring that your trie remains in the state you left it when you start your application the next time.

#### `.createTrieFromProof()`
#### `.createFromProof()`

```ts
// ./examples/staticCreateTrieFromProof.ts
Expand Down Expand Up @@ -100,7 +100,7 @@ async function main() {
main()
```

When the `Trie.createTrieFromProof` constructor is used, it instantiates a new partial trie based only on the branch of the trie contained in the provided proof.
When the `Trie.createFromProof` constructor is used, it instantiates a new partial trie based only on the branch of the trie contained in the provided proof.

### Walking a Trie

Expand Down
6 changes: 3 additions & 3 deletions packages/trie/examples/staticCreateTrieFromProof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ async function main() {
await someOtherTrie.put(k2, utf8ToBytes('valueTwo'))

const proof = await someOtherTrie.createProof(k1)
const trie = await Trie.createTrieFromProof(proof, { useKeyHashing: true })
const trie = await Trie.createFromProof(proof, { useKeyHashing: true })
const otherProof = await someOtherTrie.createProof(k2)

// To add more proofs to the trie, use `updateTrieFromProof`
await trie.updateTrieFromProof(otherProof)
// To add more proofs to the trie, use `updateFromProof`
await trie.updateFromProof(otherProof)

const value = await trie.get(k1)
console.log(bytesToUtf8(value!)) // valueOne
Expand Down
13 changes: 5 additions & 8 deletions packages/trie/src/proof/range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,12 +322,7 @@ async function verifyProof(
proof: Uint8Array[],
useKeyHashingFunction: HashKeysFunction
): Promise<{ value: Uint8Array | null; trie: Trie }> {
const proofTrie = new Trie({ root: rootHash, useKeyHashingFunction })
try {
await proofTrie.fromProof(proof)
} catch (e) {
throw new Error('Invalid proof nodes given')
}
const proofTrie = await Trie.fromProof(proof, { root: rootHash, useKeyHashingFunction })
try {
const value = await proofTrie.get(key, true)
return {
Expand Down Expand Up @@ -499,8 +494,10 @@ export async function verifyRangeProof(
)
}

const trie = new Trie({ root: rootHash, useKeyHashingFunction })
await trie.fromProof(proof)
const trie = await Trie.fromProof(proof, {
useKeyHashingFunction,
root: rootHash,
})

// Remove all nodes between two edge proofs
const empty = await unsetInternal(trie, firstKey, lastKey)
Expand Down
Loading

0 comments on commit d8f7544

Please sign in to comment.