Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

trie: Better error checking for invalid proofs #1373

Merged
merged 30 commits into from
Aug 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a88e064
trie: Better error checking for invalid proofs
acolytec3 Jul 21, 2021
9f0aa20
lint fixes
acolytec3 Jul 22, 2021
fcc5f78
Merge remote-tracking branch 'origin/master' into fix-verify-proof
acolytec3 Jul 22, 2021
f654196
Merge remote-tracking branch 'origin/master' into fix-verify-proof
acolytec3 Jul 23, 2021
b9c8e1e
trie: Better error checking for invalid proofs
acolytec3 Jul 21, 2021
07ea272
lint fixes
acolytec3 Jul 22, 2021
15e34bd
add Trie.get param `throwWhenNotFound` for failed proofs
ryanio Jul 28, 2021
a80b894
add changelog entry
ryanio Jul 28, 2021
f389ca7
Merge branch 'fix-verify-proof' of https://github.com/ethereumjs/ethe…
acolytec3 Jul 28, 2021
fa34c22
trie: update var names and typedocs
acolytec3 Jul 28, 2021
d1548a8
update param name, improve typedocs
ryanio Jul 28, 2021
0bedc77
Merge branch 'master' into fix-verify-proof
ryanio Jul 28, 2021
7f86f5a
add test case for invalid exclusion proof
ryanio Jul 28, 2021
fdf172f
remove duplicate proof reverse() test (added in proof.spec.ts:L57)
ryanio Jul 28, 2021
70e1959
reimplement error 'Missing node in DB'
ryanio Jul 30, 2021
c4cf00c
Merge branch 'master' into fix-verify-proof
ryanio Jul 30, 2021
90ae09a
return false if 'Missing node in DB' throws in checkRoot
ryanio Jul 30, 2021
3b5a7f1
readme updates for proofs
acolytec3 Jul 31, 2021
d481e7a
trie: Update verifyProof error message
acolytec3 Jul 31, 2021
9e6795f
Merge branch 'master' into fix-verify-proof
acolytec3 Aug 2, 2021
7e7cf73
touch ups
ryanio Aug 2, 2021
a6d29b1
Merge branch 'master' into fix-verify-proof
acolytec3 Aug 3, 2021
10fba45
Merge branch 'master' into fix-verify-proof
holgerd77 Aug 11, 2021
10a42af
Merge branch 'master' into fix-verify-proof
ryanio Aug 13, 2021
ce8eed1
Apply suggestions from code review
ryanio Aug 13, 2021
4f6f127
Merge branch 'master' into fix-verify-proof
ryanio Aug 13, 2021
e225c69
Merge branch 'fix-verify-proof' of https://github.com/ethereumjs/ethe…
ryanio Aug 13, 2021
823b48d
re-add return
ryanio Aug 13, 2021
5dc687e
add dev note on "missing note" error
acolytec3 Aug 16, 2021
bab0d84
Merge branch 'master' into fix-verify-proof
holgerd77 Aug 17, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/trie/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)

**Bug Fixes**

- Better error checking for invalid proofs, PR [#1373](https://github.com/ethereumjs/ethereumjs-monorepo/pull/1373)

**Maintenance**

- Remove use of deprecated setRoot, PR [#1376](https://github.com/ethereumjs/ethereumjs-monorepo/pull/1376)
Expand Down
49 changes: 48 additions & 1 deletion packages/trie/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The only backing store supported is LevelDB through the `levelup` module.

There are 3 variants of the tree implemented in this library, namely: `BaseTrie`, `CheckpointTrie` and `SecureTrie`. `CheckpointTrie` adds checkpointing functionality to the `BaseTrie` with the methods `checkpoint`, `commit` and `revert`. `SecureTrie` extends `CheckpointTrie` and is the most suitable variant for Ethereum applications. It stores values under the `keccak256` hash of their keys.

By default trie nodes are not deleted from the underlying DB to not corrupt older trie states (as of `v4.2.0`). If you are only interested in the latest state of a trie you can switch to a delete behavior (e.g. if you want to save disk space) by using the `deleteFromDB` constructor option (see related release notes in the changelog for more details).
By default, trie nodes are not deleted from the underlying DB to not corrupt older trie states (as of `v4.2.0`). If you are only interested in the latest state of a trie, you can switch to a delete behavior (e.g. if you want to save disk space) by using the `deleteFromDB` constructor option (see related release notes in the changelog for more details).

## Initialization and Basic Usage

Expand All @@ -42,6 +42,12 @@ test()

## Merkle Proofs

The `createProof` and `verifyProof` functions allow you to verify that a certain value does or does not exist within a Merkle-Patricia trie with a given root.

### Proof of existence

The below code demonstrates how to construct and then verify a proof that proves that the key `test` that corresponds to the value `one` does exist in the given trie, so a proof of existence.

```typescript
const trie = new Trie()

Expand All @@ -55,6 +61,47 @@ async function test() {
test()
```

### Proof of non-existence

The below code demonstrates how to construct and then verify a proof that proves that the key `test3` does not exist in the given trie, so a proof of non-existence.

```typescript
const trie = new Trie()

async function test() {
await trie.put(Buffer.from('test'), Buffer.from('one'))
await trie.put(Buffer.from('test2'), Buffer.from('two'))
const proof = await Trie.createProof(trie, Buffer.from('test3'))
const value = await Trie.verifyProof(trie.root, Buffer.from('test3'), proof)
console.log(value.toString()) // null
}

test()
```

### Invalid proofs

Note, if `verifyProof` detects an invalid proof, it throws an error. While contrived, the below example demonstrates the error condition that would result if a prover tampers with the data in a merkle proof.

```typescript
const trie = new Trie()

async function test() {
await trie.put(Buffer.from('test'), Buffer.from('one'))
await trie.put(Buffer.from('test2'), Buffer.from('two'))
const proof = await Trie.createProof(trie, Buffer.from('test2'))
proof[1].reverse()
try {
const value = await Trie.verifyProof(trie.root, Buffer.from('test2'), proof)
console.log(value.toString()) // results in error
} catch (err) {
console.log(err) // Missing node in DB
}
}

test()
```

## Read stream on Geth DB

```typescript
Expand Down
1 change: 1 addition & 0 deletions packages/trie/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"lint": "../../config/cli/lint.sh",
"lint:fix": "../../config/cli/lint-fix.sh",
"tsc": "../../config/cli/ts-compile.sh",
"tape": "tape -r ts-node/register",
holgerd77 marked this conversation as resolved.
Show resolved Hide resolved
"test": "npm run test:node && npm run test:browser",
"test:browser": "karma start karma.conf.js",
"test:node": "tape -r ts-node/register test/*.ts"
Expand Down
51 changes: 40 additions & 11 deletions packages/trie/src/baseTrie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,16 @@ export class Trie {
* Checks if a given root exists.
*/
async checkRoot(root: Buffer): Promise<boolean> {
const value = await this._lookupNode(root)
return !!value
try {
const value = await this._lookupNode(root)
return value !== null
} catch (error) {
if (error.message == 'Missing node in DB') {
return false
} else {
throw error
}
}
}

/**
Expand All @@ -116,10 +124,11 @@ export class Trie {
/**
* Gets a value given a `key`
* @param key - the key to search for
* @param throwIfMissing - if true, throws if any nodes are missing. Used for verifying proofs. (default: false)
* @returns A Promise that resolves to `Buffer` if a value was found or `null` if no value was found.
*/
async get(key: Buffer): Promise<Buffer | null> {
const { node, remaining } = await this.findPath(key)
async get(key: Buffer, throwIfMissing = false): Promise<Buffer | null> {
const { node, remaining } = await this.findPath(key, throwIfMissing)
let value = null
if (node && remaining.length === 0) {
value = node.value
Expand Down Expand Up @@ -172,16 +181,17 @@ export class Trie {
* Tries to find a path to the node for the given key.
* It returns a `stack` of nodes to the closest node.
* @param key - the search key
* @param throwIfMissing - if true, throws if any nodes are missing. Used for verifying proofs. (default: false)
*/
async findPath(key: Buffer): Promise<Path> {
async findPath(key: Buffer, throwIfMissing = false): Promise<Path> {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
return new Promise(async (resolve, reject) => {
const stack: TrieNode[] = []
const targetKey = bufferToNibbles(key)

const onFound: FoundNodeFunction = async (nodeRef, node, keyProgress, walkController) => {
if (node === null) {
return
return reject(new Error('Path not found'))
}
const keyRemainder = targetKey.slice(matchingNibbleLength(keyProgress, targetKey))
stack.push(node)
Expand Down Expand Up @@ -223,7 +233,15 @@ export class Trie {
}

// walk trie and process nodes
await this.walkTrie(this.root, onFound)
try {
await this.walkTrie(this.root, onFound)
} catch (error) {
if (error.message == 'Missing node in DB' && !throwIfMissing) {
// pass
} else {
reject(error)
}
}

// Resolve if _walkTrie finishes without finding any nodes
resolve({ node: null, remaining: [], stack })
Expand Down Expand Up @@ -270,9 +288,11 @@ export class Trie {
let value = null
let foundNode = null
value = await this.db.get(node as Buffer)

if (value) {
foundNode = decodeNode(value)
} else {
// Dev note: this error message text is used for error checking in `checkRoot`, `verifyProof`, and `findPath`
throw new Error('Missing node in DB')
holgerd77 marked this conversation as resolved.
Show resolved Hide resolved
}
return foundNode
}
Expand Down Expand Up @@ -678,7 +698,7 @@ export class Trie {
* @param key
* @param proof
* @throws If proof is found to be invalid.
* @returns The value from the key.
* @returns The value from the key, or null if valid proof of non-existence.
*/
static async verifyProof(rootHash: Buffer, key: Buffer, proof: Proof): Promise<Buffer | null> {
let proofTrie = new Trie(null, rootHash)
Expand All @@ -687,7 +707,16 @@ export class Trie {
} catch (e) {
throw new Error('Invalid proof nodes given')
}
return proofTrie.get(key)
try {
const value = await proofTrie.get(key, true)
return value
} catch (err) {
if (err.message == 'Missing node in DB') {
throw new Error('Invalid proof provided')
} else {
throw err
}
}
}

/**
Expand Down
19 changes: 16 additions & 3 deletions packages/trie/src/util/walkController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class WalkController {
readonly taskExecutor: PrioritizedTaskExecutor
readonly trie: BaseTrie
private resolve: Function
private reject: Function

/**
* Creates a new WalkController
Expand All @@ -23,6 +24,7 @@ export class WalkController {
this.taskExecutor = new PrioritizedTaskExecutor(poolSize)
this.trie = trie
this.resolve = () => {}
this.reject = () => {}
}

/**
Expand All @@ -44,9 +46,15 @@ export class WalkController {

private async startWalk(root: Buffer): Promise<void> {
// eslint-disable-next-line no-async-promise-executor
return await new Promise(async (resolve) => {
return await new Promise(async (resolve, reject) => {
this.resolve = resolve
const node = await this.trie._lookupNode(root)
this.reject = reject
let node
try {
node = await this.trie._lookupNode(root)
} catch (error) {
return this.reject(error)
}
this.processNode(root, node, [])
})
}
Expand Down Expand Up @@ -88,7 +96,12 @@ export class WalkController {
this.taskExecutor.executeOrQueue(
priority ?? key.length,
async (taskFinishedCallback: Function) => {
const childNode = await this.trie._lookupNode(nodeRef)
let childNode
try {
childNode = await this.trie._lookupNode(nodeRef)
} catch (error) {
return this.reject(error)
}
taskFinishedCallback() // this marks the current task as finished. If there are any tasks left in the queue, this will immediately execute the first task.
this.processNode(nodeRef as Buffer, childNode as TrieNode, key)
}
Expand Down
36 changes: 33 additions & 3 deletions packages/trie/test/proof.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,39 @@ tape('simple merkle proofs generation and verification', function (tester) {

// to fail our proof we can request a proof for one key
proof = await CheckpointTrie.createProof(trie, Buffer.from('another'))
// and use that proof on another key
const result = await CheckpointTrie.verifyProof(trie.root, Buffer.from('key1aa'), proof)
t.equal(result, null)
// and try to use that proof on another key
try {
await CheckpointTrie.verifyProof(trie.root, Buffer.from('key1aa'), proof)
t.fail('expected error: Invalid proof provided')
} catch (e) {
t.equal(e.message, 'Invalid proof provided')
}

// we can also corrupt a valid proof
proof = await CheckpointTrie.createProof(trie, Buffer.from('key2bb'))
proof[0].reverse()
try {
await CheckpointTrie.verifyProof(trie.root, Buffer.from('key2bb'), proof)
t.fail('expected error: Invalid proof provided')
} catch (e) {
t.equal(e.message, 'Invalid proof provided')
}

// test an invalid exclusion proof by creating
// a valid exclusion proof then making it non-null
myKey = Buffer.from('anyrandomkey')
proof = await CheckpointTrie.createProof(trie, myKey)
val = await CheckpointTrie.verifyProof(trie.root, myKey, proof)
t.equal(val, null, 'Expected value to be null')
// now make the key non-null so the exclusion proof becomes invalid
await trie.put(myKey, Buffer.from('thisisavalue'))
try {
await CheckpointTrie.verifyProof(trie.root, myKey, proof)
t.fail('expected error: Invalid proof provided')
} catch (e) {
t.equal(e.message, 'Invalid proof provided')
}

t.end()
})

Expand Down