Skip to content

Commit 5ac5bc7

Browse files
authored
Merge pull request #1035 from ethereumjs/checkpoint-trie-cache
Trie: cache CheckpointTrie in memory
2 parents 83e591a + 827a184 commit 5ac5bc7

File tree

7 files changed

+244
-175
lines changed

7 files changed

+244
-175
lines changed

packages/trie/src/checkpointDb.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { LevelUp } from 'levelup'
2+
import { DB, BatchDBOp, ENCODING_OPTS } from './db'
3+
4+
export type Checkpoint = {
5+
// We cannot use a Buffer => Buffer map directly. If you create two Buffers with the same internal value,
6+
// then when setting a value on the Map, it actually creates two indices.
7+
keyValueMap: Map<string, Buffer | null>
8+
root: Buffer
9+
}
10+
11+
/**
12+
* DB is a thin wrapper around the underlying levelup db,
13+
* which validates inputs and sets encoding type.
14+
*/
15+
export class CheckpointDB extends DB {
16+
public checkpoints: Checkpoint[]
17+
18+
/**
19+
* Initialize a DB instance. If `leveldb` is not provided, DB
20+
* defaults to an [in-memory store](https://github.com/Level/memdown).
21+
* @param leveldb - An abstract-leveldown compliant store
22+
*/
23+
constructor(leveldb?: LevelUp) {
24+
super(leveldb)
25+
// Roots of trie at the moment of checkpoint
26+
this.checkpoints = []
27+
}
28+
29+
/**
30+
* Is the DB during a checkpoint phase?
31+
*/
32+
get isCheckpoint() {
33+
return this.checkpoints.length > 0
34+
}
35+
36+
/**
37+
* Adds a new checkpoint to the stack
38+
* @param root
39+
*/
40+
checkpoint(root: Buffer) {
41+
this.checkpoints.push({ keyValueMap: new Map<string, Buffer>(), root })
42+
}
43+
44+
/**
45+
* Commits the latest checkpoint
46+
*/
47+
async commit() {
48+
const { keyValueMap } = this.checkpoints.pop()!
49+
if (!this.isCheckpoint) {
50+
// This was the final checkpoint, we should now commit and flush everything to disk
51+
const batchOp: BatchDBOp[] = []
52+
keyValueMap.forEach(function (value, key) {
53+
if (value === null) {
54+
batchOp.push({
55+
type: 'del',
56+
key: Buffer.from(key, 'binary'),
57+
})
58+
} else {
59+
batchOp.push({
60+
type: 'put',
61+
key: Buffer.from(key, 'binary'),
62+
value,
63+
})
64+
}
65+
})
66+
await this.batch(batchOp)
67+
} else {
68+
// dump everything into the current (higher level) cache
69+
const currentKeyValueMap = this.checkpoints[this.checkpoints.length - 1].keyValueMap
70+
keyValueMap.forEach((value, key) => currentKeyValueMap.set(key, value))
71+
}
72+
}
73+
74+
/**
75+
* Reverts the latest checkpoint
76+
*/
77+
async revert() {
78+
const { root } = this.checkpoints.pop()!
79+
return root
80+
}
81+
82+
/**
83+
* Retrieves a raw value from leveldb.
84+
* @param key
85+
* @returns A Promise that resolves to `Buffer` if a value is found or `null` if no value is found.
86+
*/
87+
async get(key: Buffer): Promise<Buffer | null> {
88+
// Lookup the value in our cache. We return the latest checkpointed value (which should be the value on disk)
89+
for (let index = this.checkpoints.length - 1; index >= 0; index--) {
90+
const value = this.checkpoints[index].keyValueMap.get(key.toString('binary'))
91+
if (value !== undefined) {
92+
return value
93+
}
94+
}
95+
// Nothing has been found in cache, look up from disk
96+
97+
const value = await super.get(key)
98+
if (this.isCheckpoint) {
99+
// Since we are a checkpoint, put this value in cache, so future `get` calls will not look the key up again from disk.
100+
this.checkpoints[this.checkpoints.length - 1].keyValueMap.set(key.toString('binary'), value)
101+
}
102+
103+
return value
104+
}
105+
106+
/**
107+
* Writes a value directly to leveldb.
108+
* @param key The key as a `Buffer`
109+
* @param value The value to be stored
110+
*/
111+
async put(key: Buffer, val: Buffer): Promise<void> {
112+
if (this.isCheckpoint) {
113+
// put value in cache
114+
this.checkpoints[this.checkpoints.length - 1].keyValueMap.set(key.toString('binary'), val)
115+
} else {
116+
await super.put(key, val)
117+
}
118+
}
119+
120+
/**
121+
* Removes a raw value in the underlying leveldb.
122+
* @param keys
123+
*/
124+
async del(key: Buffer): Promise<void> {
125+
if (this.isCheckpoint) {
126+
// delete the value in the current cache
127+
this.checkpoints[this.checkpoints.length - 1].keyValueMap.set(key.toString('binary'), null)
128+
} else {
129+
// delete the value on disk
130+
await this._leveldb.del(key, ENCODING_OPTS)
131+
}
132+
}
133+
134+
/**
135+
* Performs a batch operation on db.
136+
* @param opStack A stack of levelup operations
137+
*/
138+
async batch(opStack: BatchDBOp[]): Promise<void> {
139+
if (this.isCheckpoint) {
140+
for (const op of opStack) {
141+
if (op.type === 'put') {
142+
await this.put(op.key, op.value)
143+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
144+
} else if (op.type === 'del') {
145+
await this.del(op.key)
146+
}
147+
}
148+
} else {
149+
await super.batch(opStack)
150+
}
151+
}
152+
}

packages/trie/src/checkpointTrie.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import { Trie as BaseTrie } from './baseTrie'
2+
import { CheckpointDB } from './checkpointDb'
23

34
/**
45
* Adds checkpointing to the {@link BaseTrie}
56
*/
67
export class CheckpointTrie extends BaseTrie {
8+
db: CheckpointDB
9+
710
constructor(...args: any) {
811
super(...args)
12+
this.db = new CheckpointDB(...args)
913
}
1014

1115
/**
1216
* Is the trie during a checkpoint phase?
1317
*/
1418
get isCheckpoint() {
15-
return this.db.checkpoints.length > 0
19+
return this.db.isCheckpoint
1620
}
1721

1822
/**
@@ -34,7 +38,7 @@ export class CheckpointTrie extends BaseTrie {
3438
}
3539

3640
await this.lock.wait()
37-
this.db.commit()
41+
await this.db.commit()
3842
this.lock.signal()
3943
}
4044

@@ -61,7 +65,7 @@ export class CheckpointTrie extends BaseTrie {
6165
const db = this.db.copy()
6266
const trie = new CheckpointTrie(db._leveldb, this.root)
6367
if (includeCheckpoints && this.isCheckpoint) {
64-
trie.db.checkpoints = this.db.checkpoints.slice()
68+
trie.db.checkpoints = [...this.db.checkpoints]
6569
}
6670
return trie
6771
}

packages/trie/src/db.ts

Lines changed: 1 addition & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@ const level = require('level-mem')
33

44
export const ENCODING_OPTS = { keyEncoding: 'binary', valueEncoding: 'binary' }
55

6-
export type Checkpoint = {
7-
root: Buffer
8-
revertOps: BatchDBOp[]
9-
}
10-
116
export type BatchDBOp = PutBatch | DelBatch
127
export interface PutBatch {
138
type: 'put'
@@ -24,8 +19,6 @@ export interface DelBatch {
2419
* which validates inputs and sets encoding type.
2520
*/
2621
export class DB {
27-
public checkpoints: Checkpoint[]
28-
2922
_leveldb: LevelUp
3023

3124
/**
@@ -34,53 +27,9 @@ export class DB {
3427
* @param leveldb - An abstract-leveldown compliant store
3528
*/
3629
constructor(leveldb?: LevelUp) {
37-
// Roots of trie at the moment of checkpoint
38-
this.checkpoints = []
39-
4030
this._leveldb = leveldb || level()
4131
}
4232

43-
/**
44-
* Is the DB during a checkpoint phase?
45-
*/
46-
get isCheckpoint() {
47-
return this.checkpoints.length > 0
48-
}
49-
50-
/**
51-
* Adds a new checkpoint to the stack
52-
* @param root
53-
*/
54-
checkpoint(root: Buffer) {
55-
this.checkpoints.push({ root, revertOps: [] })
56-
}
57-
58-
/**
59-
* Commits the latest checkpoint
60-
*/
61-
commit() {
62-
const { root, revertOps } = this.checkpoints.pop()!
63-
// On nested checkpoints put the revertOps on the parent
64-
// stack in case there is a revert
65-
if (this.isCheckpoint) {
66-
this.checkpoints[this.checkpoints.length - 1].revertOps.concat(revertOps)
67-
}
68-
return root
69-
}
70-
71-
/**
72-
* Reverts the latest checkpoint
73-
*/
74-
async revert() {
75-
const { root, revertOps } = this.checkpoints.pop()!
76-
await this.batch(revertOps.reverse())
77-
return root
78-
}
79-
80-
private addCPRevertOperation(op: BatchDBOp) {
81-
this.checkpoints[this.checkpoints.length - 1].revertOps.push(op)
82-
}
83-
8433
/**
8534
* Retrieves a raw value from leveldb.
8635
* @param key
@@ -106,75 +55,23 @@ export class DB {
10655
* @param value The value to be stored
10756
*/
10857
async put(key: Buffer, val: Buffer): Promise<void> {
109-
const revertOps: BatchDBOp[] = []
110-
// In CP mode check for an old value to be put
111-
// on the revert ops stack
112-
if (this.isCheckpoint) {
113-
const oldValue = await this.get(key)
114-
if (oldValue !== null) {
115-
revertOps.push({
116-
type: 'put',
117-
key: key,
118-
value: oldValue,
119-
})
120-
}
121-
}
12258
await this._leveldb.put(key, val, ENCODING_OPTS)
123-
// In CP mode add del to the revert ops stack
124-
if (this.isCheckpoint) {
125-
revertOps.push({
126-
type: 'del',
127-
key: key,
128-
})
129-
for (const revertOp of revertOps) {
130-
this.addCPRevertOperation(revertOp)
131-
}
132-
}
13359
}
13460

13561
/**
13662
* Removes a raw value in the underlying leveldb.
13763
* @param keys
13864
*/
13965
async del(key: Buffer): Promise<void> {
140-
const revertOps: BatchDBOp[] = []
141-
// In CP mode check for an old value to be put
142-
// on the revert ops stack
143-
if (this.isCheckpoint) {
144-
const oldValue = await this.get(key)
145-
if (oldValue !== null) {
146-
revertOps.push({
147-
type: 'put',
148-
key: key,
149-
value: oldValue,
150-
})
151-
}
152-
}
15366
await this._leveldb.del(key, ENCODING_OPTS)
154-
if (this.isCheckpoint) {
155-
for (const revertOp of revertOps) {
156-
this.addCPRevertOperation(revertOp)
157-
}
158-
}
15967
}
16068

16169
/**
16270
* Performs a batch operation on db.
16371
* @param opStack A stack of levelup operations
16472
*/
16573
async batch(opStack: BatchDBOp[]): Promise<void> {
166-
if (this.isCheckpoint) {
167-
for (const op of opStack) {
168-
if (op.type === 'put') {
169-
await this.put(op.key, op.value)
170-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
171-
} else if (op.type === 'del') {
172-
await this.del(op.key)
173-
}
174-
}
175-
} else {
176-
await this._leveldb.batch(opStack, ENCODING_OPTS)
177-
}
74+
await this._leveldb.batch(opStack, ENCODING_OPTS)
17875
}
17976

18077
/**

packages/trie/src/secure.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,11 @@ export class SecureTrie extends CheckpointTrie {
8989
* @param includeCheckpoints - If true and during a checkpoint, the copy will contain the checkpointing metadata and will use the same scratch as underlying db.
9090
*/
9191
copy(includeCheckpoints = true): SecureTrie {
92-
const trie = super.copy(includeCheckpoints)
93-
const db = trie.db.copy()
94-
return new SecureTrie(db._leveldb, this.root)
92+
const db = this.db.copy()
93+
const secureTrie = new SecureTrie(db._leveldb, this.root)
94+
if (includeCheckpoints && this.isCheckpoint) {
95+
secureTrie.db.checkpoints = [...this.db.checkpoints]
96+
}
97+
return secureTrie
9598
}
9699
}

0 commit comments

Comments
 (0)