Skip to content

Commit dacef9f

Browse files
authored
feat(p2p): attestation pool persistence (#10667)
1 parent 2cad3e5 commit dacef9f

File tree

12 files changed

+645
-224
lines changed

12 files changed

+645
-224
lines changed

yarn-project/kv-store/src/interfaces/map.ts

+21
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ export interface AztecMap<K extends Key, V> extends AztecBaseMap<K, V> {
6262
* @param range - The range of keys to iterate over
6363
*/
6464
keys(range?: Range<K>): IterableIterator<K>;
65+
66+
/**
67+
* Clears the map.
68+
*/
69+
clear(): Promise<void>;
70+
}
71+
72+
export interface AztecMapWithSize<K extends Key, V> extends AztecMap<K, V> {
73+
/**
74+
* Gets the size of the map.
75+
* @returns The size of the map
76+
*/
77+
size(): number;
6578
}
6679

6780
/**
@@ -82,6 +95,14 @@ export interface AztecMultiMap<K extends Key, V> extends AztecMap<K, V> {
8295
deleteValue(key: K, val: V): Promise<void>;
8396
}
8497

98+
export interface AztecMultiMapWithSize<K extends Key, V> extends AztecMultiMap<K, V> {
99+
/**
100+
* Gets the size of the map.
101+
* @returns The size of the map
102+
*/
103+
size(): number;
104+
}
105+
85106
/**
86107
* A map backed by a persistent store.
87108
*/

yarn-project/kv-store/src/interfaces/store.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { type AztecArray, type AztecAsyncArray } from './array.js';
22
import { type Key } from './common.js';
33
import { type AztecAsyncCounter, type AztecCounter } from './counter.js';
4-
import { type AztecAsyncMap, type AztecAsyncMultiMap, type AztecMap, type AztecMultiMap } from './map.js';
4+
import {
5+
type AztecAsyncMap,
6+
type AztecAsyncMultiMap,
7+
type AztecMap,
8+
type AztecMapWithSize,
9+
type AztecMultiMap,
10+
type AztecMultiMapWithSize,
11+
} from './map.js';
512
import { type AztecAsyncSet, type AztecSet } from './set.js';
613
import { type AztecAsyncSingleton, type AztecSingleton } from './singleton.js';
714

@@ -29,6 +36,20 @@ export interface AztecKVStore {
2936
*/
3037
openMultiMap<K extends Key, V>(name: string): AztecMultiMap<K, V>;
3138

39+
/**
40+
* Creates a new multi-map with size.
41+
* @param name - The name of the multi-map
42+
* @returns The multi-map
43+
*/
44+
openMultiMapWithSize<K extends Key, V>(name: string): AztecMultiMapWithSize<K, V>;
45+
46+
/**
47+
* Creates a new map with size.
48+
* @param name - The name of the map
49+
* @returns The map
50+
*/
51+
openMapWithSize<K extends Key, V>(name: string): AztecMapWithSize<K, V>;
52+
3253
/**
3354
* Creates a new array.
3455
* @param name - The name of the array

yarn-project/kv-store/src/lmdb/map.test.ts

+53
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { expect } from 'chai';
2+
3+
import { type AztecMapWithSize, type AztecMultiMapWithSize } from '../interfaces/map.js';
14
import { describeAztecMap } from '../interfaces/map_test_suite.js';
25
import { openTmpStore } from './index.js';
36

@@ -6,3 +9,53 @@ describe('LMDBMap', () => {
69

710
describeAztecMap('Async AztecMap', () => Promise.resolve(openTmpStore(true)), true);
811
});
12+
13+
describe('AztecMultiMapWithSize', () => {
14+
let map: AztecMultiMapWithSize<string, string>;
15+
let map2: AztecMultiMapWithSize<string, string>;
16+
17+
beforeEach(() => {
18+
const store = openTmpStore(true);
19+
map = store.openMultiMapWithSize('test');
20+
map2 = store.openMultiMapWithSize('test2');
21+
});
22+
23+
it('should be able to delete values', async () => {
24+
await map.set('foo', 'bar');
25+
await map.set('foo', 'baz');
26+
27+
await map2.set('foo', 'bar');
28+
await map2.set('foo', 'baz');
29+
30+
expect(map.size()).to.equal(2);
31+
expect(map2.size()).to.equal(2);
32+
33+
await map.deleteValue('foo', 'bar');
34+
35+
expect(map.size()).to.equal(1);
36+
expect(map.get('foo')).to.equal('baz');
37+
38+
expect(map2.size()).to.equal(2);
39+
});
40+
});
41+
42+
describe('AztecMapWithSize', () => {
43+
let map: AztecMapWithSize<string, string>;
44+
45+
beforeEach(() => {
46+
const store = openTmpStore(true);
47+
map = store.openMapWithSize('test');
48+
});
49+
50+
it('should be able to delete values', async () => {
51+
await map.set('foo', 'bar');
52+
await map.set('fizz', 'buzz');
53+
54+
expect(map.size()).to.equal(2);
55+
56+
await map.delete('foo');
57+
58+
expect(map.size()).to.equal(1);
59+
expect(map.get('fizz')).to.equal('buzz');
60+
});
61+
});

yarn-project/kv-store/src/lmdb/map.ts

+97-22
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type Database, type RangeOptions } from 'lmdb';
22

33
import { type Key, type Range } from '../interfaces/common.js';
4-
import { type AztecAsyncMultiMap, type AztecMultiMap } from '../interfaces/map.js';
4+
import { type AztecAsyncMultiMap, type AztecMapWithSize, type AztecMultiMap } from '../interfaces/map.js';
55

66
/** The slot where a key-value entry would be stored */
77
type MapValueSlot<K extends Key | Buffer> = ['map', string, 'slot', K];
@@ -13,8 +13,8 @@ export class LmdbAztecMap<K extends Key, V> implements AztecMultiMap<K, V>, Azte
1313
protected db: Database<[K, V], MapValueSlot<K>>;
1414
protected name: string;
1515

16-
#startSentinel: MapValueSlot<Buffer>;
17-
#endSentinel: MapValueSlot<Buffer>;
16+
protected startSentinel: MapValueSlot<Buffer>;
17+
protected endSentinel: MapValueSlot<Buffer>;
1818

1919
constructor(rootDb: Database, mapName: string) {
2020
this.name = mapName;
@@ -23,24 +23,24 @@ export class LmdbAztecMap<K extends Key, V> implements AztecMultiMap<K, V>, Azte
2323
// sentinels are used to define the start and end of the map
2424
// with LMDB's key encoding, no _primitive value_ can be "less than" an empty buffer or greater than Byte 255
2525
// these will be used later to answer range queries
26-
this.#startSentinel = ['map', this.name, 'slot', Buffer.from([])];
27-
this.#endSentinel = ['map', this.name, 'slot', Buffer.from([255])];
26+
this.startSentinel = ['map', this.name, 'slot', Buffer.from([])];
27+
this.endSentinel = ['map', this.name, 'slot', Buffer.from([255])];
2828
}
2929

3030
close(): Promise<void> {
3131
return this.db.close();
3232
}
3333

3434
get(key: K): V | undefined {
35-
return this.db.get(this.#slot(key))?.[1];
35+
return this.db.get(this.slot(key))?.[1];
3636
}
3737

3838
getAsync(key: K): Promise<V | undefined> {
3939
return Promise.resolve(this.get(key));
4040
}
4141

4242
*getValues(key: K): IterableIterator<V> {
43-
const values = this.db.getValues(this.#slot(key));
43+
const values = this.db.getValues(this.slot(key));
4444
for (const value of values) {
4545
yield value?.[1];
4646
}
@@ -53,38 +53,38 @@ export class LmdbAztecMap<K extends Key, V> implements AztecMultiMap<K, V>, Azte
5353
}
5454

5555
has(key: K): boolean {
56-
return this.db.doesExist(this.#slot(key));
56+
return this.db.doesExist(this.slot(key));
5757
}
5858

5959
hasAsync(key: K): Promise<boolean> {
6060
return Promise.resolve(this.has(key));
6161
}
6262

6363
async set(key: K, val: V): Promise<void> {
64-
await this.db.put(this.#slot(key), [key, val]);
64+
await this.db.put(this.slot(key), [key, val]);
6565
}
6666

6767
swap(key: K, fn: (val: V | undefined) => V): Promise<void> {
6868
return this.db.childTransaction(() => {
69-
const slot = this.#slot(key);
69+
const slot = this.slot(key);
7070
const entry = this.db.get(slot);
7171
void this.db.put(slot, [key, fn(entry?.[1])]);
7272
});
7373
}
7474

7575
setIfNotExists(key: K, val: V): Promise<boolean> {
76-
const slot = this.#slot(key);
76+
const slot = this.slot(key);
7777
return this.db.ifNoExists(slot, () => {
7878
void this.db.put(slot, [key, val]);
7979
});
8080
}
8181

8282
async delete(key: K): Promise<void> {
83-
await this.db.remove(this.#slot(key));
83+
await this.db.remove(this.slot(key));
8484
}
8585

8686
async deleteValue(key: K, val: V): Promise<void> {
87-
await this.db.remove(this.#slot(key), [key, val]);
87+
await this.db.remove(this.slot(key), [key, val]);
8888
}
8989

9090
*entries(range: Range<K> = {}): IterableIterator<[K, V]> {
@@ -93,19 +93,19 @@ export class LmdbAztecMap<K extends Key, V> implements AztecMultiMap<K, V>, Azte
9393
// in that case, we need to swap the start and end sentinels
9494
const start = reverse
9595
? range.end
96-
? this.#slot(range.end)
97-
: this.#endSentinel
96+
? this.slot(range.end)
97+
: this.endSentinel
9898
: range.start
99-
? this.#slot(range.start)
100-
: this.#startSentinel;
99+
? this.slot(range.start)
100+
: this.startSentinel;
101101

102102
const end = reverse
103103
? range.start
104-
? this.#slot(range.start)
105-
: this.#startSentinel
104+
? this.slot(range.start)
105+
: this.startSentinel
106106
: range.end
107-
? this.#slot(range.end)
108-
: this.#endSentinel;
107+
? this.slot(range.end)
108+
: this.endSentinel;
109109

110110
const lmdbRange: RangeOptions = {
111111
start,
@@ -153,7 +153,82 @@ export class LmdbAztecMap<K extends Key, V> implements AztecMultiMap<K, V>, Azte
153153
}
154154
}
155155

156-
#slot(key: K): MapValueSlot<K> {
156+
protected slot(key: K): MapValueSlot<K> {
157157
return ['map', this.name, 'slot', key];
158158
}
159+
160+
async clear(): Promise<void> {
161+
const lmdbRange: RangeOptions = {
162+
start: this.startSentinel,
163+
end: this.endSentinel,
164+
};
165+
166+
const iterator = this.db.getRange(lmdbRange);
167+
168+
for (const { key } of iterator) {
169+
await this.db.remove(key);
170+
}
171+
}
172+
}
173+
174+
export class LmdbAztecMapWithSize<K extends Key, V>
175+
extends LmdbAztecMap<K, V>
176+
implements AztecMapWithSize<K, V>, AztecAsyncMultiMap<K, V>
177+
{
178+
#sizeCache?: number;
179+
180+
constructor(rootDb: Database, mapName: string) {
181+
super(rootDb, mapName);
182+
}
183+
184+
override async set(key: K, val: V): Promise<void> {
185+
await this.db.childTransaction(() => {
186+
const exists = this.db.doesExist(this.slot(key));
187+
this.db.putSync(this.slot(key), [key, val], {
188+
appendDup: true,
189+
});
190+
if (!exists) {
191+
this.#sizeCache = undefined; // Invalidate cache
192+
}
193+
});
194+
}
195+
196+
override async delete(key: K): Promise<void> {
197+
await this.db.childTransaction(async () => {
198+
const exists = this.db.doesExist(this.slot(key));
199+
if (exists) {
200+
await this.db.remove(this.slot(key));
201+
this.#sizeCache = undefined; // Invalidate cache
202+
}
203+
});
204+
}
205+
206+
override async deleteValue(key: K, val: V): Promise<void> {
207+
await this.db.childTransaction(async () => {
208+
const exists = this.db.doesExist(this.slot(key));
209+
if (exists) {
210+
await this.db.remove(this.slot(key), [key, val]);
211+
this.#sizeCache = undefined; // Invalidate cache
212+
}
213+
});
214+
}
215+
216+
/**
217+
* Gets the size of the map by counting entries.
218+
* @returns The number of entries in the map
219+
*/
220+
size(): number {
221+
if (this.#sizeCache === undefined) {
222+
this.#sizeCache = this.db.getCount({
223+
start: this.startSentinel,
224+
end: this.endSentinel,
225+
});
226+
}
227+
return this.#sizeCache;
228+
}
229+
230+
// Reset cache on clear/drop operations
231+
clearCache() {
232+
this.#sizeCache = undefined;
233+
}
159234
}

yarn-project/kv-store/src/lmdb/store.ts

+26-2
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,20 @@ import { join } from 'path';
99
import { type AztecArray, type AztecAsyncArray } from '../interfaces/array.js';
1010
import { type Key } from '../interfaces/common.js';
1111
import { type AztecAsyncCounter, type AztecCounter } from '../interfaces/counter.js';
12-
import { type AztecAsyncMap, type AztecAsyncMultiMap, type AztecMap, type AztecMultiMap } from '../interfaces/map.js';
12+
import {
13+
type AztecAsyncMap,
14+
type AztecAsyncMultiMap,
15+
type AztecMap,
16+
type AztecMapWithSize,
17+
type AztecMultiMap,
18+
type AztecMultiMapWithSize,
19+
} from '../interfaces/map.js';
1320
import { type AztecAsyncSet, type AztecSet } from '../interfaces/set.js';
1421
import { type AztecAsyncSingleton, type AztecSingleton } from '../interfaces/singleton.js';
1522
import { type AztecAsyncKVStore, type AztecKVStore } from '../interfaces/store.js';
1623
import { LmdbAztecArray } from './array.js';
1724
import { LmdbAztecCounter } from './counter.js';
18-
import { LmdbAztecMap } from './map.js';
25+
import { LmdbAztecMap, LmdbAztecMapWithSize } from './map.js';
1926
import { LmdbAztecSet } from './set.js';
2027
import { LmdbAztecSingleton } from './singleton.js';
2128

@@ -118,6 +125,23 @@ export class AztecLmdbStore implements AztecKVStore, AztecAsyncKVStore {
118125
openCounter<K extends Key>(name: string): AztecCounter<K> & AztecAsyncCounter<K> {
119126
return new LmdbAztecCounter(this.#data, name);
120127
}
128+
/**
129+
* Creates a new AztecMultiMapWithSize in the store. A multi-map with size stores multiple values for a single key automatically.
130+
* @param name - Name of the map
131+
* @returns A new AztecMultiMapWithSize
132+
*/
133+
openMultiMapWithSize<K extends Key, V>(name: string): AztecMultiMapWithSize<K, V> {
134+
return new LmdbAztecMapWithSize(this.#multiMapData, name);
135+
}
136+
137+
/**
138+
* Creates a new AztecMapWithSize in the store.
139+
* @param name - Name of the map
140+
* @returns A new AztecMapWithSize
141+
*/
142+
openMapWithSize<K extends Key, V>(name: string): AztecMapWithSize<K, V> {
143+
return new LmdbAztecMapWithSize(this.#data, name);
144+
}
121145

122146
/**
123147
* Creates a new AztecArray in the store.

0 commit comments

Comments
 (0)