@aztec/pxe 0.65.2 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/bin/index.js +5 -6
- package/dest/config/index.d.ts +0 -9
- package/dest/config/index.d.ts.map +1 -1
- package/dest/config/index.js +1 -17
- package/dest/config/package_info.d.ts +5 -0
- package/dest/config/package_info.d.ts.map +1 -0
- package/dest/config/package_info.js +4 -0
- package/dest/contract_data_oracle/index.js +2 -2
- package/dest/database/incoming_note_dao.d.ts +1 -1
- package/dest/database/incoming_note_dao.d.ts.map +1 -1
- package/dest/database/incoming_note_dao.js +2 -2
- package/dest/database/kv_pxe_database.d.ts +10 -13
- package/dest/database/kv_pxe_database.d.ts.map +1 -1
- package/dest/database/kv_pxe_database.js +153 -230
- package/dest/database/outgoing_note_dao.js +2 -2
- package/dest/database/pxe_database.d.ts +7 -25
- package/dest/database/pxe_database.d.ts.map +1 -1
- package/dest/database/pxe_database_test_suite.d.ts.map +1 -1
- package/dest/database/pxe_database_test_suite.js +18 -59
- package/dest/index.d.ts +2 -0
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +3 -1
- package/dest/kernel_oracle/index.d.ts.map +1 -1
- package/dest/kernel_oracle/index.js +3 -3
- package/dest/kernel_prover/hints/build_private_kernel_reset_private_inputs.d.ts +1 -1
- package/dest/kernel_prover/hints/build_private_kernel_reset_private_inputs.d.ts.map +1 -1
- package/dest/kernel_prover/hints/build_private_kernel_reset_private_inputs.js +13 -15
- package/dest/kernel_prover/index.d.ts +1 -0
- package/dest/kernel_prover/index.d.ts.map +1 -1
- package/dest/kernel_prover/index.js +2 -1
- package/dest/kernel_prover/kernel_prover.d.ts +1 -0
- package/dest/kernel_prover/kernel_prover.d.ts.map +1 -1
- package/dest/kernel_prover/kernel_prover.js +38 -4
- package/dest/kernel_prover/test/test_circuit_prover.d.ts.map +1 -1
- package/dest/kernel_prover/test/test_circuit_prover.js +4 -4
- package/dest/note_decryption_utils/add_public_values_to_payload.js +2 -2
- package/dest/note_decryption_utils/brute_force_note_info.d.ts +3 -3
- package/dest/note_decryption_utils/brute_force_note_info.d.ts.map +1 -1
- package/dest/note_decryption_utils/brute_force_note_info.js +8 -8
- package/dest/note_decryption_utils/produce_note_daos.d.ts +3 -6
- package/dest/note_decryption_utils/produce_note_daos.d.ts.map +1 -1
- package/dest/note_decryption_utils/produce_note_daos.js +5 -19
- package/dest/note_decryption_utils/produce_note_daos_for_key.d.ts +1 -1
- package/dest/note_decryption_utils/produce_note_daos_for_key.d.ts.map +1 -1
- package/dest/pxe_service/error_enriching.d.ts +3 -3
- package/dest/pxe_service/error_enriching.d.ts.map +1 -1
- package/dest/pxe_service/error_enriching.js +10 -10
- package/dest/pxe_service/index.d.ts +0 -1
- package/dest/pxe_service/index.d.ts.map +1 -1
- package/dest/pxe_service/index.js +1 -2
- package/dest/pxe_service/pxe_service.d.ts +4 -16
- package/dest/pxe_service/pxe_service.d.ts.map +1 -1
- package/dest/pxe_service/pxe_service.js +86 -101
- package/dest/pxe_service/test/pxe_test_suite.d.ts.map +1 -1
- package/dest/pxe_service/test/pxe_test_suite.js +1 -3
- package/dest/simulator/index.d.ts +1 -1
- package/dest/simulator/index.d.ts.map +1 -1
- package/dest/simulator/index.js +2 -2
- package/dest/simulator_oracle/index.d.ts +7 -6
- package/dest/simulator_oracle/index.d.ts.map +1 -1
- package/dest/simulator_oracle/index.js +65 -44
- package/dest/synchronizer/synchronizer.d.ts +10 -40
- package/dest/synchronizer/synchronizer.d.ts.map +1 -1
- package/dest/synchronizer/synchronizer.js +35 -69
- package/dest/{pxe_service → utils}/create_pxe_service.d.ts +1 -1
- package/dest/utils/create_pxe_service.d.ts.map +1 -0
- package/dest/utils/create_pxe_service.js +49 -0
- package/package.json +31 -19
- package/src/bin/index.ts +4 -5
- package/src/config/index.ts +0 -21
- package/src/config/package_info.ts +3 -0
- package/src/contract_data_oracle/index.ts +1 -1
- package/src/database/incoming_note_dao.ts +2 -2
- package/src/database/kv_pxe_database.ts +212 -309
- package/src/database/outgoing_note_dao.ts +1 -1
- package/src/database/pxe_database.ts +7 -28
- package/src/database/pxe_database_test_suite.ts +20 -75
- package/src/index.ts +2 -0
- package/src/kernel_oracle/index.ts +2 -2
- package/src/kernel_prover/hints/build_private_kernel_reset_private_inputs.ts +14 -16
- package/src/kernel_prover/index.ts +2 -0
- package/src/kernel_prover/kernel_prover.ts +61 -2
- package/src/kernel_prover/test/test_circuit_prover.ts +5 -3
- package/src/note_decryption_utils/add_public_values_to_payload.ts +1 -1
- package/src/note_decryption_utils/brute_force_note_info.ts +9 -9
- package/src/note_decryption_utils/produce_note_daos.ts +5 -48
- package/src/note_decryption_utils/produce_note_daos_for_key.ts +1 -1
- package/src/pxe_service/error_enriching.ts +14 -12
- package/src/pxe_service/index.ts +0 -1
- package/src/pxe_service/pxe_service.ts +137 -160
- package/src/pxe_service/test/pxe_test_suite.ts +0 -3
- package/src/simulator/index.ts +1 -1
- package/src/simulator_oracle/index.ts +71 -75
- package/src/synchronizer/synchronizer.ts +37 -77
- package/src/{pxe_service → utils}/create_pxe_service.ts +10 -10
- package/dest/pxe_service/create_pxe_service.d.ts.map +0 -1
- package/dest/pxe_service/create_pxe_service.js +0 -49
|
@@ -14,28 +14,28 @@ import {
|
|
|
14
14
|
} from '@aztec/circuit-types';
|
|
15
15
|
import {
|
|
16
16
|
type AztecAddress,
|
|
17
|
+
type BlockHeader,
|
|
17
18
|
type CompleteAddress,
|
|
18
19
|
type ContractInstance,
|
|
19
20
|
Fr,
|
|
20
21
|
type FunctionSelector,
|
|
21
|
-
type Header,
|
|
22
22
|
IndexedTaggingSecret,
|
|
23
23
|
type KeyValidationRequest,
|
|
24
24
|
type L1_TO_L2_MSG_TREE_HEIGHT,
|
|
25
|
+
PrivateLog,
|
|
25
26
|
computeAddressSecret,
|
|
26
27
|
computeTaggingSecret,
|
|
27
28
|
} from '@aztec/circuits.js';
|
|
28
29
|
import { type FunctionArtifact, getFunctionArtifact } from '@aztec/foundation/abi';
|
|
29
30
|
import { poseidon2Hash } from '@aztec/foundation/crypto';
|
|
30
|
-
import {
|
|
31
|
-
import { createDebugLogger } from '@aztec/foundation/log';
|
|
31
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
32
32
|
import { type KeyStore } from '@aztec/key-store';
|
|
33
|
-
import {
|
|
33
|
+
import { MessageLoadOracleInputs } from '@aztec/simulator/acvm';
|
|
34
|
+
import { type AcirSimulator, type DBOracle } from '@aztec/simulator/client';
|
|
34
35
|
|
|
35
36
|
import { type ContractDataOracle } from '../contract_data_oracle/index.js';
|
|
36
37
|
import { type IncomingNoteDao } from '../database/incoming_note_dao.js';
|
|
37
38
|
import { type PxeDatabase } from '../database/index.js';
|
|
38
|
-
import { type OutgoingNoteDao } from '../database/outgoing_note_dao.js';
|
|
39
39
|
import { produceNoteDaos } from '../note_decryption_utils/produce_note_daos.js';
|
|
40
40
|
import { getAcirSimulator } from '../simulator/index.js';
|
|
41
41
|
|
|
@@ -48,7 +48,7 @@ export class SimulatorOracle implements DBOracle {
|
|
|
48
48
|
private db: PxeDatabase,
|
|
49
49
|
private keyStore: KeyStore,
|
|
50
50
|
private aztecNode: AztecNode,
|
|
51
|
-
private log =
|
|
51
|
+
private log = createLogger('pxe:simulator_oracle'),
|
|
52
52
|
) {}
|
|
53
53
|
|
|
54
54
|
getKeyValidationRequest(pkMHash: Fr, contractAddress: AztecAddress): Promise<KeyValidationRequest> {
|
|
@@ -228,10 +228,10 @@ export class SimulatorOracle implements DBOracle {
|
|
|
228
228
|
* Retrieve the databases view of the Block Header object.
|
|
229
229
|
* This structure is fed into the circuits simulator and is used to prove against certain historical roots.
|
|
230
230
|
*
|
|
231
|
-
* @returns A Promise that resolves to a
|
|
231
|
+
* @returns A Promise that resolves to a BlockHeader object.
|
|
232
232
|
*/
|
|
233
|
-
|
|
234
|
-
return
|
|
233
|
+
getBlockHeader(): Promise<BlockHeader> {
|
|
234
|
+
return this.db.getBlockHeader();
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
/**
|
|
@@ -252,7 +252,7 @@ export class SimulatorOracle implements DBOracle {
|
|
|
252
252
|
* finally the index specified tag. We will then query the node with this tag for each address in the address book.
|
|
253
253
|
* @returns The full list of the users contact addresses.
|
|
254
254
|
*/
|
|
255
|
-
public getContacts(): AztecAddress[] {
|
|
255
|
+
public getContacts(): Promise<AztecAddress[]> {
|
|
256
256
|
return this.db.getContactAddresses();
|
|
257
257
|
}
|
|
258
258
|
|
|
@@ -290,9 +290,13 @@ export class SimulatorOracle implements DBOracle {
|
|
|
290
290
|
): Promise<void> {
|
|
291
291
|
const secret = await this.#calculateTaggingSecret(contractAddress, sender, recipient);
|
|
292
292
|
const contractName = await this.contractDataOracle.getDebugContractName(contractAddress);
|
|
293
|
-
this.log.
|
|
294
|
-
|
|
295
|
-
|
|
293
|
+
this.log.debug(`Incrementing app tagging secret at ${contractName}(${contractAddress})`, {
|
|
294
|
+
secret,
|
|
295
|
+
sender,
|
|
296
|
+
recipient,
|
|
297
|
+
contractName,
|
|
298
|
+
contractAddress,
|
|
299
|
+
});
|
|
296
300
|
|
|
297
301
|
const [index] = await this.db.getTaggingSecretsIndexesAsSender([secret]);
|
|
298
302
|
await this.db.setTaggingSecretsIndexesAsSender([new IndexedTaggingSecret(secret, index + 1)]);
|
|
@@ -324,7 +328,7 @@ export class SimulatorOracle implements DBOracle {
|
|
|
324
328
|
const recipientIvsk = await this.keyStore.getMasterIncomingViewingSecretKey(recipient);
|
|
325
329
|
|
|
326
330
|
// We implicitly add all PXE accounts as contacts, this helps us decrypt tags on notes that we send to ourselves (recipient = us, sender = us)
|
|
327
|
-
const contacts = [...this.db.getContactAddresses(), ...(await this.keyStore.getAccounts())].filter(
|
|
331
|
+
const contacts = [...(await this.db.getContactAddresses()), ...(await this.keyStore.getAccounts())].filter(
|
|
328
332
|
(address, index, self) => index === self.findIndex(otherAddress => otherAddress.equals(address)),
|
|
329
333
|
);
|
|
330
334
|
const appTaggingSecrets = contacts.map(contact => {
|
|
@@ -373,7 +377,7 @@ export class SimulatorOracle implements DBOracle {
|
|
|
373
377
|
do {
|
|
374
378
|
const currentTags = [...new Array(INDEX_OFFSET)].map((_, i) => {
|
|
375
379
|
const indexedAppTaggingSecret = new IndexedTaggingSecret(appTaggingSecret, currentIndex + i);
|
|
376
|
-
return indexedAppTaggingSecret.
|
|
380
|
+
return indexedAppTaggingSecret.computeSiloedTag(recipient, contractAddress);
|
|
377
381
|
});
|
|
378
382
|
previousEmptyBack = currentEmptyBack;
|
|
379
383
|
|
|
@@ -394,13 +398,17 @@ export class SimulatorOracle implements DBOracle {
|
|
|
394
398
|
await this.db.setTaggingSecretsIndexesAsSender([new IndexedTaggingSecret(appTaggingSecret, newIndex)]);
|
|
395
399
|
|
|
396
400
|
const contractName = await this.contractDataOracle.getDebugContractName(contractAddress);
|
|
397
|
-
this.log.debug(
|
|
398
|
-
|
|
399
|
-
|
|
401
|
+
this.log.debug(`Syncing logs for sender ${sender} at contract ${contractName}(${contractAddress})`, {
|
|
402
|
+
sender,
|
|
403
|
+
secret: appTaggingSecret,
|
|
404
|
+
index: currentIndex,
|
|
405
|
+
contractName,
|
|
406
|
+
contractAddress,
|
|
407
|
+
});
|
|
400
408
|
}
|
|
401
409
|
|
|
402
410
|
/**
|
|
403
|
-
* Synchronizes the logs tagged with scoped addresses and all the senders in the
|
|
411
|
+
* Synchronizes the logs tagged with scoped addresses and all the senders in the address book.
|
|
404
412
|
* Returns the unsynched logs and updates the indexes of the secrets used to tag them until there are no more logs to sync.
|
|
405
413
|
* @param contractAddress - The address of the contract that the logs are tagged for
|
|
406
414
|
* @param recipient - The address of the recipient
|
|
@@ -426,7 +434,7 @@ export class SimulatorOracle implements DBOracle {
|
|
|
426
434
|
const appTaggingSecrets = await this.#getAppTaggingSecretsForContacts(contractAddress, recipient);
|
|
427
435
|
|
|
428
436
|
// 1.1 Set up a sliding window with an offset. Chances are the sender might have messed up
|
|
429
|
-
// and
|
|
437
|
+
// and inadvertently incremented their index without use getting any logs (for example, in case
|
|
430
438
|
// of a revert). If we stopped looking for logs the first time
|
|
431
439
|
// we receive 0 for a tag, we might never receive anything from that sender again.
|
|
432
440
|
// Also there's a possibility that we have advanced our index, but the sender has reused it, so
|
|
@@ -466,30 +474,40 @@ export class SimulatorOracle implements DBOracle {
|
|
|
466
474
|
|
|
467
475
|
while (currentTagggingSecrets.length > 0) {
|
|
468
476
|
// 2. Compute tags using the secrets, recipient and index. Obtain logs for each tag (#9380)
|
|
469
|
-
const currentTags = currentTagggingSecrets.map(taggingSecret =>
|
|
477
|
+
const currentTags = currentTagggingSecrets.map(taggingSecret =>
|
|
478
|
+
taggingSecret.computeSiloedTag(recipient, contractAddress),
|
|
479
|
+
);
|
|
470
480
|
const logsByTags = await this.aztecNode.getLogsByTags(currentTags);
|
|
471
481
|
const newTaggingSecrets: IndexedTaggingSecret[] = [];
|
|
472
482
|
logsByTags.forEach((logsByTag, logIndex) => {
|
|
473
483
|
const { secret: currentSecret, index: currentIndex } = currentTagggingSecrets[logIndex];
|
|
474
484
|
const currentSecretAsStr = currentSecret.toString();
|
|
475
|
-
this.log.debug(
|
|
476
|
-
|
|
477
|
-
|
|
485
|
+
this.log.debug(`Syncing logs for recipient ${recipient} at contract ${contractName}(${contractAddress})`, {
|
|
486
|
+
recipient,
|
|
487
|
+
secret: currentSecret,
|
|
488
|
+
index: currentIndex,
|
|
489
|
+
contractName,
|
|
490
|
+
contractAddress,
|
|
491
|
+
});
|
|
478
492
|
// 3.1. Append logs to the list and increment the index for the tags that have logs (#9380)
|
|
479
493
|
if (logsByTag.length > 0) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
494
|
+
const newIndex = currentIndex + 1;
|
|
495
|
+
this.log.debug(
|
|
496
|
+
`Found ${logsByTag.length} logs as recipient ${recipient}. Incrementing index to ${newIndex} at contract ${contractName}(${contractAddress})`,
|
|
497
|
+
{
|
|
498
|
+
recipient,
|
|
499
|
+
secret: currentSecret,
|
|
500
|
+
newIndex,
|
|
501
|
+
contractName,
|
|
502
|
+
contractAddress,
|
|
503
|
+
},
|
|
486
504
|
);
|
|
487
505
|
logs.push(...logsByTag);
|
|
488
506
|
|
|
489
507
|
if (currentIndex >= initialSecretIndexes[currentSecretAsStr]) {
|
|
490
508
|
// 3.2. Increment the index for the tags that have logs, provided they're higher than the one
|
|
491
509
|
// we have stored in the db (#9380)
|
|
492
|
-
secretsToIncrement[currentSecretAsStr] =
|
|
510
|
+
secretsToIncrement[currentSecretAsStr] = newIndex;
|
|
493
511
|
// 3.3. Slide the window forwards if we have found logs beyond the initial index
|
|
494
512
|
maxIndexesToCheck[currentSecretAsStr] = currentIndex + INDEX_OFFSET;
|
|
495
513
|
}
|
|
@@ -503,7 +521,7 @@ export class SimulatorOracle implements DBOracle {
|
|
|
503
521
|
});
|
|
504
522
|
await this.db.setTaggingSecretsIndexesAsRecipient(
|
|
505
523
|
Object.keys(secretsToIncrement).map(
|
|
506
|
-
secret => new IndexedTaggingSecret(Fr.
|
|
524
|
+
secret => new IndexedTaggingSecret(Fr.fromHexString(secret), secretsToIncrement[secret]),
|
|
507
525
|
),
|
|
508
526
|
);
|
|
509
527
|
currentTagggingSecrets = newTaggingSecrets;
|
|
@@ -535,36 +553,21 @@ export class SimulatorOracle implements DBOracle {
|
|
|
535
553
|
recipientCompleteAddress.publicKeys.masterIncomingViewingPublicKey,
|
|
536
554
|
);
|
|
537
555
|
const addressSecret = computeAddressSecret(recipientCompleteAddress.getPreaddress(), ivskM);
|
|
538
|
-
|
|
539
|
-
recipientCompleteAddress.publicKeys.masterOutgoingViewingPublicKey,
|
|
540
|
-
);
|
|
556
|
+
|
|
541
557
|
// Since we could have notes with the same index for different txs, we need
|
|
542
558
|
// to keep track of them scoping by txHash
|
|
543
559
|
const excludedIndices: Map<string, Set<number>> = new Map();
|
|
544
560
|
const incomingNotes: IncomingNoteDao[] = [];
|
|
545
|
-
const outgoingNotes: OutgoingNoteDao[] = [];
|
|
546
561
|
|
|
547
562
|
const txEffectsCache = new Map<string, InBlock<TxEffect> | undefined>();
|
|
548
563
|
|
|
549
564
|
for (const scopedLog of scopedLogs) {
|
|
550
|
-
const incomingNotePayload =
|
|
551
|
-
scopedLog.logData,
|
|
552
|
-
addressSecret
|
|
553
|
-
scopedLog.isFromPublic,
|
|
554
|
-
);
|
|
555
|
-
const outgoingNotePayload = L1NotePayload.decryptAsOutgoing(scopedLog.logData, ovskM, scopedLog.isFromPublic);
|
|
556
|
-
|
|
557
|
-
if (incomingNotePayload || outgoingNotePayload) {
|
|
558
|
-
if (incomingNotePayload && outgoingNotePayload && !incomingNotePayload.equals(outgoingNotePayload)) {
|
|
559
|
-
this.log.warn(
|
|
560
|
-
`Incoming and outgoing note payloads do not match. Incoming: ${tryJsonStringify(
|
|
561
|
-
incomingNotePayload,
|
|
562
|
-
)}, Outgoing: ${tryJsonStringify(outgoingNotePayload)}`,
|
|
563
|
-
);
|
|
564
|
-
continue;
|
|
565
|
-
}
|
|
565
|
+
const incomingNotePayload = scopedLog.isFromPublic
|
|
566
|
+
? L1NotePayload.decryptAsIncomingFromPublic(scopedLog.logData, addressSecret)
|
|
567
|
+
: L1NotePayload.decryptAsIncoming(PrivateLog.fromBuffer(scopedLog.logData), addressSecret);
|
|
566
568
|
|
|
567
|
-
|
|
569
|
+
if (incomingNotePayload) {
|
|
570
|
+
const payload = incomingNotePayload;
|
|
568
571
|
|
|
569
572
|
const txEffect =
|
|
570
573
|
txEffectsCache.get(scopedLog.txHash.toString()) ?? (await this.aztecNode.getTxEffect(scopedLog.txHash));
|
|
@@ -579,14 +582,13 @@ export class SimulatorOracle implements DBOracle {
|
|
|
579
582
|
if (!excludedIndices.has(scopedLog.txHash.toString())) {
|
|
580
583
|
excludedIndices.set(scopedLog.txHash.toString(), new Set());
|
|
581
584
|
}
|
|
582
|
-
const { incomingNote
|
|
585
|
+
const { incomingNote } = await produceNoteDaos(
|
|
583
586
|
// I don't like this at all, but we need a simulator to run `computeNoteHashAndOptionallyANullifier`. This generates
|
|
584
587
|
// a chicken-and-egg problem due to this oracle requiring a simulator, which in turn requires this oracle. Furthermore, since jest doesn't allow
|
|
585
588
|
// mocking ESM exports, we have to pollute the method even more by providing a simulator parameter so tests can inject a fake one.
|
|
586
589
|
simulator ?? getAcirSimulator(this.db, this.aztecNode, this.keyStore, this.contractDataOracle),
|
|
587
590
|
this.db,
|
|
588
591
|
incomingNotePayload ? recipient.toAddressPoint() : undefined,
|
|
589
|
-
outgoingNotePayload ? recipientCompleteAddress.publicKeys.masterOutgoingViewingPublicKey : undefined,
|
|
590
592
|
payload!,
|
|
591
593
|
txEffect.data.txHash,
|
|
592
594
|
txEffect.l2BlockNumber,
|
|
@@ -600,12 +602,9 @@ export class SimulatorOracle implements DBOracle {
|
|
|
600
602
|
if (incomingNote) {
|
|
601
603
|
incomingNotes.push(incomingNote);
|
|
602
604
|
}
|
|
603
|
-
if (outgoingNote) {
|
|
604
|
-
outgoingNotes.push(outgoingNote);
|
|
605
|
-
}
|
|
606
605
|
}
|
|
607
606
|
}
|
|
608
|
-
return { incomingNotes
|
|
607
|
+
return { incomingNotes };
|
|
609
608
|
}
|
|
610
609
|
|
|
611
610
|
/**
|
|
@@ -618,18 +617,15 @@ export class SimulatorOracle implements DBOracle {
|
|
|
618
617
|
recipient: AztecAddress,
|
|
619
618
|
simulator?: AcirSimulator,
|
|
620
619
|
): Promise<void> {
|
|
621
|
-
const { incomingNotes
|
|
622
|
-
if (incomingNotes.length
|
|
623
|
-
await this.db.addNotes(incomingNotes,
|
|
620
|
+
const { incomingNotes } = await this.#decryptTaggedLogs(logs, recipient, simulator);
|
|
621
|
+
if (incomingNotes.length) {
|
|
622
|
+
await this.db.addNotes(incomingNotes, recipient);
|
|
624
623
|
incomingNotes.forEach(noteDao => {
|
|
625
|
-
this.log.verbose(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
);
|
|
630
|
-
});
|
|
631
|
-
outgoingNotes.forEach(noteDao => {
|
|
632
|
-
this.log.verbose(`Added outgoing note for contract ${noteDao.contractAddress} at slot ${noteDao.storageSlot}`);
|
|
624
|
+
this.log.verbose(`Added incoming note for contract ${noteDao.contractAddress} at slot ${noteDao.storageSlot}`, {
|
|
625
|
+
contract: noteDao.contractAddress,
|
|
626
|
+
slot: noteDao.storageSlot,
|
|
627
|
+
nullifier: noteDao.siloedNullifier.toString(),
|
|
628
|
+
});
|
|
633
629
|
});
|
|
634
630
|
}
|
|
635
631
|
const nullifiedNotes: IncomingNoteDao[] = [];
|
|
@@ -648,11 +644,11 @@ export class SimulatorOracle implements DBOracle {
|
|
|
648
644
|
|
|
649
645
|
await this.db.removeNullifiedNotes(foundNullifiers, recipient.toAddressPoint());
|
|
650
646
|
nullifiedNotes.forEach(noteDao => {
|
|
651
|
-
this.log.verbose(
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
);
|
|
647
|
+
this.log.verbose(`Removed note for contract ${noteDao.contractAddress} at slot ${noteDao.storageSlot}`, {
|
|
648
|
+
contract: noteDao.contractAddress,
|
|
649
|
+
slot: noteDao.storageSlot,
|
|
650
|
+
nullifier: noteDao.siloedNullifier.toString(),
|
|
651
|
+
});
|
|
656
652
|
});
|
|
657
653
|
}
|
|
658
654
|
}
|
|
@@ -5,39 +5,36 @@ import {
|
|
|
5
5
|
type L2BlockStreamEventHandler,
|
|
6
6
|
} from '@aztec/circuit-types';
|
|
7
7
|
import { INITIAL_L2_BLOCK_NUM } from '@aztec/circuits.js';
|
|
8
|
-
import { type
|
|
8
|
+
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
9
9
|
import { type L2TipsStore } from '@aztec/kv-store/stores';
|
|
10
10
|
|
|
11
11
|
import { type PXEConfig } from '../config/index.js';
|
|
12
12
|
import { type PxeDatabase } from '../database/index.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* The Synchronizer class manages the synchronization
|
|
16
|
-
*
|
|
17
|
-
* It provides methods to
|
|
18
|
-
* details, and fetch transactions by hash.
|
|
19
|
-
* in sync with the blockchain while handling retries and errors gracefully.
|
|
15
|
+
* The Synchronizer class manages the synchronization with the aztec node, allowing PXE to retrieve the
|
|
16
|
+
* latest block header and handle reorgs.
|
|
17
|
+
* It provides methods to trigger a sync and get the block number we are syncec to
|
|
18
|
+
* details, and fetch transactions by hash.
|
|
20
19
|
*/
|
|
21
20
|
export class Synchronizer implements L2BlockStreamEventHandler {
|
|
22
|
-
private running = false;
|
|
23
21
|
private initialSyncBlockNumber = INITIAL_L2_BLOCK_NUM - 1;
|
|
24
|
-
private log:
|
|
22
|
+
private log: Logger;
|
|
25
23
|
protected readonly blockStream: L2BlockStream;
|
|
26
24
|
|
|
27
25
|
constructor(
|
|
28
26
|
private node: AztecNode,
|
|
29
27
|
private db: PxeDatabase,
|
|
30
28
|
private l2TipsStore: L2TipsStore,
|
|
31
|
-
config: Partial<Pick<PXEConfig, '
|
|
29
|
+
config: Partial<Pick<PXEConfig, 'l2StartingBlock'>> = {},
|
|
32
30
|
logSuffix?: string,
|
|
33
31
|
) {
|
|
34
|
-
this.log =
|
|
32
|
+
this.log = createLogger(logSuffix ? `pxe:synchronizer:${logSuffix}` : 'pxe:synchronizer');
|
|
35
33
|
this.blockStream = this.createBlockStream(config);
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
protected createBlockStream(config: Partial<Pick<PXEConfig, '
|
|
39
|
-
return new L2BlockStream(this.node, this.l2TipsStore, this, {
|
|
40
|
-
pollIntervalMS: config.l2BlockPollingIntervalMS,
|
|
36
|
+
protected createBlockStream(config: Partial<Pick<PXEConfig, 'l2StartingBlock'>>) {
|
|
37
|
+
return new L2BlockStream(this.node, this.l2TipsStore, this, createLogger('pxe:block_stream'), {
|
|
41
38
|
startingBlock: config.l2StartingBlock,
|
|
42
39
|
});
|
|
43
40
|
}
|
|
@@ -47,12 +44,18 @@ export class Synchronizer implements L2BlockStreamEventHandler {
|
|
|
47
44
|
await this.l2TipsStore.handleBlockStreamEvent(event);
|
|
48
45
|
|
|
49
46
|
switch (event.type) {
|
|
50
|
-
case 'blocks-added':
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
case 'blocks-added': {
|
|
48
|
+
const lastBlock = event.blocks.at(-1)!;
|
|
49
|
+
this.log.verbose(`Updated pxe last block to ${lastBlock.number}`, {
|
|
50
|
+
blockHash: lastBlock.hash(),
|
|
51
|
+
archive: lastBlock.archive.root.toString(),
|
|
52
|
+
header: lastBlock.header.toInspect(),
|
|
53
|
+
});
|
|
54
|
+
await this.db.setHeader(lastBlock.header);
|
|
53
55
|
break;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
}
|
|
57
|
+
case 'chain-pruned': {
|
|
58
|
+
this.log.warn(`Pruning data after block ${event.blockNumber} due to reorg`);
|
|
56
59
|
// We first unnullify and then remove so that unnullified notes that were created after the block number end up deleted.
|
|
57
60
|
await this.db.unnullifyNotesAfter(event.blockNumber);
|
|
58
61
|
await this.db.removeNotesAfter(event.blockNumber);
|
|
@@ -62,73 +65,30 @@ export class Synchronizer implements L2BlockStreamEventHandler {
|
|
|
62
65
|
// Update the header to the last block.
|
|
63
66
|
await this.db.setHeader(await this.node.getBlockHeader(event.blockNumber));
|
|
64
67
|
break;
|
|
68
|
+
}
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* available, it retries after a specified interval.
|
|
72
|
-
*
|
|
73
|
-
* @param limit - The maximum number of encrypted, unencrypted logs and blocks to fetch in each iteration.
|
|
74
|
-
* @param retryInterval - The time interval (in ms) to wait before retrying if no data is available.
|
|
73
|
+
* Syncs PXE and the node by dowloading the metadata of the latest blocks, allowing simulations to use
|
|
74
|
+
* recent data (e.g. notes), and handling any reorgs that might have occurred.
|
|
75
75
|
*/
|
|
76
|
-
public async
|
|
77
|
-
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
this.running = true;
|
|
81
|
-
|
|
82
|
-
// REFACTOR: We should know the header of the genesis block without having to request it from the node.
|
|
83
|
-
await this.db.setHeader(await this.node.getBlockHeader(0));
|
|
76
|
+
public async sync() {
|
|
77
|
+
let currentHeader;
|
|
84
78
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
* will no longer process blocks or encrypted logs and must be restarted using the start method.
|
|
95
|
-
*
|
|
96
|
-
* @returns A promise that resolves when the synchronizer has successfully stopped.
|
|
97
|
-
*/
|
|
98
|
-
public async stop() {
|
|
99
|
-
this.running = false;
|
|
100
|
-
await this.blockStream.stop();
|
|
101
|
-
this.log.info('Stopped');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Triggers a single run. */
|
|
105
|
-
public async trigger() {
|
|
79
|
+
try {
|
|
80
|
+
currentHeader = await this.db.getBlockHeader();
|
|
81
|
+
} catch (e) {
|
|
82
|
+
this.log.debug('Header is not set, requesting from the node');
|
|
83
|
+
}
|
|
84
|
+
if (!currentHeader) {
|
|
85
|
+
// REFACTOR: We should know the header of the genesis block without having to request it from the node.
|
|
86
|
+
await this.db.setHeader(await this.node.getBlockHeader(0));
|
|
87
|
+
}
|
|
106
88
|
await this.blockStream.sync();
|
|
107
89
|
}
|
|
108
90
|
|
|
109
|
-
|
|
110
|
-
return this.db.getBlockNumber() ?? this.initialSyncBlockNumber;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Checks whether all the blocks were processed (tree roots updated, txs updated with block info, etc.).
|
|
115
|
-
* @returns True if there are no outstanding blocks to be synched.
|
|
116
|
-
* @remarks This indicates that blocks and transactions are synched even if notes are not.
|
|
117
|
-
* @remarks Compares local block number with the block number from aztec node.
|
|
118
|
-
*/
|
|
119
|
-
public async isGlobalStateSynchronized() {
|
|
120
|
-
const latest = await this.node.getBlockNumber();
|
|
121
|
-
return latest <= this.getSynchedBlockNumber();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Returns the latest block that has been synchronized by the synchronizer and each account.
|
|
126
|
-
* @returns The latest block synchronized for blocks, and the latest block synched for notes for each public key being tracked.
|
|
127
|
-
*/
|
|
128
|
-
public getSyncStatus() {
|
|
129
|
-
const lastBlockNumber = this.getSynchedBlockNumber();
|
|
130
|
-
return {
|
|
131
|
-
blocks: lastBlockNumber,
|
|
132
|
-
};
|
|
91
|
+
public async getSynchedBlockNumber() {
|
|
92
|
+
return (await this.db.getBlockNumber()) ?? this.initialSyncBlockNumber;
|
|
133
93
|
}
|
|
134
94
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { BBNativePrivateKernelProver } from '@aztec/bb-prover';
|
|
2
2
|
import { type AztecNode, type PrivateKernelProver } from '@aztec/circuit-types';
|
|
3
3
|
import { randomBytes } from '@aztec/foundation/crypto';
|
|
4
|
-
import {
|
|
4
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
5
5
|
import { KeyStore } from '@aztec/key-store';
|
|
6
|
+
import { createStore } from '@aztec/kv-store/lmdb';
|
|
6
7
|
import { L2TipsStore } from '@aztec/kv-store/stores';
|
|
7
|
-
import { createStore } from '@aztec/kv-store/utils';
|
|
8
8
|
|
|
9
9
|
import { type PXEServiceConfig } from '../config/index.js';
|
|
10
10
|
import { KVPxeDatabase } from '../database/kv_pxe_database.js';
|
|
11
11
|
import { TestPrivateKernelProver } from '../kernel_prover/test/test_circuit_prover.js';
|
|
12
|
-
import { PXEService } from '
|
|
12
|
+
import { PXEService } from '../pxe_service/pxe_service.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Create and start an PXEService instance with the given AztecNode.
|
|
@@ -38,18 +38,18 @@ export async function createPXEService(
|
|
|
38
38
|
} as PXEServiceConfig;
|
|
39
39
|
|
|
40
40
|
const keyStore = new KeyStore(
|
|
41
|
-
await createStore('pxe_key_store', configWithContracts,
|
|
41
|
+
await createStore('pxe_key_store', configWithContracts, createLogger('pxe:keystore:lmdb')),
|
|
42
42
|
);
|
|
43
43
|
|
|
44
|
-
const store = await createStore('pxe_data', configWithContracts,
|
|
44
|
+
const store = await createStore('pxe_data', configWithContracts, createLogger('pxe:data:lmdb'));
|
|
45
45
|
|
|
46
|
-
const db =
|
|
46
|
+
const db = await KVPxeDatabase.create(store);
|
|
47
47
|
const tips = new L2TipsStore(store, 'pxe');
|
|
48
48
|
|
|
49
49
|
const prover = proofCreator ?? (await createProver(config, logSuffix));
|
|
50
|
-
const
|
|
51
|
-
await
|
|
52
|
-
return
|
|
50
|
+
const pxe = new PXEService(keyStore, aztecNode, db, tips, prover, config, logSuffix);
|
|
51
|
+
await pxe.init();
|
|
52
|
+
return pxe;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
function createProver(config: PXEServiceConfig, logSuffix?: string) {
|
|
@@ -62,6 +62,6 @@ function createProver(config: PXEServiceConfig, logSuffix?: string) {
|
|
|
62
62
|
throw new Error(`Prover must be configured with binary path and working directory`);
|
|
63
63
|
}
|
|
64
64
|
const bbConfig = config as Required<Pick<PXEServiceConfig, 'bbBinaryPath' | 'bbWorkingDirectory'>> & PXEServiceConfig;
|
|
65
|
-
const log =
|
|
65
|
+
const log = createLogger('pxe:bb-native-prover' + (logSuffix ? `:${logSuffix}` : ''));
|
|
66
66
|
return BBNativePrivateKernelProver.new({ bbSkipCleanup: false, ...bbConfig }, log);
|
|
67
67
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"create_pxe_service.d.ts","sourceRoot":"","sources":["../../src/pxe_service/create_pxe_service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAOhF,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,gBAAgB,EACxB,YAAY,GAAE,MAAM,GAAG,OAAO,GAAG,SAAqB,EACtD,YAAY,CAAC,EAAE,mBAAmB,uBAwBnC"}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { BBNativePrivateKernelProver } from '@aztec/bb-prover';
|
|
2
|
-
import { randomBytes } from '@aztec/foundation/crypto';
|
|
3
|
-
import { createDebugLogger } from '@aztec/foundation/log';
|
|
4
|
-
import { KeyStore } from '@aztec/key-store';
|
|
5
|
-
import { L2TipsStore } from '@aztec/kv-store/stores';
|
|
6
|
-
import { createStore } from '@aztec/kv-store/utils';
|
|
7
|
-
import { KVPxeDatabase } from '../database/kv_pxe_database.js';
|
|
8
|
-
import { TestPrivateKernelProver } from '../kernel_prover/test/test_circuit_prover.js';
|
|
9
|
-
import { PXEService } from './pxe_service.js';
|
|
10
|
-
/**
|
|
11
|
-
* Create and start an PXEService instance with the given AztecNode.
|
|
12
|
-
* If no keyStore or database is provided, it will use KeyStore and MemoryDB as default values.
|
|
13
|
-
* Returns a Promise that resolves to the started PXEService instance.
|
|
14
|
-
*
|
|
15
|
-
* @param aztecNode - The AztecNode instance to be used by the server.
|
|
16
|
-
* @param config - The PXE Service Config to use
|
|
17
|
-
* @param options - (Optional) Optional information for creating an PXEService.
|
|
18
|
-
* @param proofCreator - An optional proof creator to use in place of any other configuration
|
|
19
|
-
* @returns A Promise that resolves to the started PXEService instance.
|
|
20
|
-
*/
|
|
21
|
-
export async function createPXEService(aztecNode, config, useLogSuffix = undefined, proofCreator) {
|
|
22
|
-
const logSuffix = typeof useLogSuffix === 'boolean' ? (useLogSuffix ? randomBytes(3).toString('hex') : undefined) : useLogSuffix;
|
|
23
|
-
const l1Contracts = await aztecNode.getL1ContractAddresses();
|
|
24
|
-
const configWithContracts = {
|
|
25
|
-
...config,
|
|
26
|
-
l1Contracts,
|
|
27
|
-
};
|
|
28
|
-
const keyStore = new KeyStore(await createStore('pxe_key_store', configWithContracts, createDebugLogger('aztec:pxe:keystore:lmdb')));
|
|
29
|
-
const store = await createStore('pxe_data', configWithContracts, createDebugLogger('aztec:pxe:data:lmdb'));
|
|
30
|
-
const db = new KVPxeDatabase(store);
|
|
31
|
-
const tips = new L2TipsStore(store, 'pxe');
|
|
32
|
-
const prover = proofCreator ?? (await createProver(config, logSuffix));
|
|
33
|
-
const server = new PXEService(keyStore, aztecNode, db, tips, prover, config, logSuffix);
|
|
34
|
-
await server.start();
|
|
35
|
-
return server;
|
|
36
|
-
}
|
|
37
|
-
function createProver(config, logSuffix) {
|
|
38
|
-
if (!config.proverEnabled) {
|
|
39
|
-
return new TestPrivateKernelProver();
|
|
40
|
-
}
|
|
41
|
-
// (@PhilWindle) Temporary validation until WASM is implemented
|
|
42
|
-
if (!config.bbBinaryPath || !config.bbWorkingDirectory) {
|
|
43
|
-
throw new Error(`Prover must be configured with binary path and working directory`);
|
|
44
|
-
}
|
|
45
|
-
const bbConfig = config;
|
|
46
|
-
const log = createDebugLogger('aztec:pxe:bb-native-prover' + (logSuffix ? `:${logSuffix}` : ''));
|
|
47
|
-
return BBNativePrivateKernelProver.new({ bbSkipCleanup: false, ...bbConfig }, log);
|
|
48
|
-
}
|
|
49
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3JlYXRlX3B4ZV9zZXJ2aWNlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3B4ZV9zZXJ2aWNlL2NyZWF0ZV9weGVfc2VydmljZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsMkJBQTJCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUUvRCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDdkQsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDMUQsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQzVDLE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUNyRCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFHcEQsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLGdDQUFnQyxDQUFDO0FBQy9ELE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxNQUFNLDhDQUE4QyxDQUFDO0FBQ3ZGLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUU5Qzs7Ozs7Ozs7OztHQVVHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxnQkFBZ0IsQ0FDcEMsU0FBb0IsRUFDcEIsTUFBd0IsRUFDeEIsZUFBNkMsU0FBUyxFQUN0RCxZQUFrQztJQUVsQyxNQUFNLFNBQVMsR0FDYixPQUFPLFlBQVksS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUMsWUFBWSxDQUFDO0lBRWpILE1BQU0sV0FBVyxHQUFHLE1BQU0sU0FBUyxDQUFDLHNCQUFzQixFQUFFLENBQUM7SUFDN0QsTUFBTSxtQkFBbUIsR0FBRztRQUMxQixHQUFHLE1BQU07UUFDVCxXQUFXO0tBQ1EsQ0FBQztJQUV0QixNQUFNLFFBQVEsR0FBRyxJQUFJLFFBQVEsQ0FDM0IsTUFBTSxXQUFXLENBQUMsZUFBZSxFQUFFLG1CQUFtQixFQUFFLGlCQUFpQixDQUFDLHlCQUF5QixDQUFDLENBQUMsQ0FDdEcsQ0FBQztJQUVGLE1BQU0sS0FBSyxHQUFHLE1BQU0sV0FBVyxDQUFDLFVBQVUsRUFBRSxtQkFBbUIsRUFBRSxpQkFBaUIsQ0FBQyxxQkFBcUIsQ0FBQyxDQUFDLENBQUM7SUFFM0csTUFBTSxFQUFFLEdBQUcsSUFBSSxhQUFhLENBQUMsS0FBSyxDQUFDLENBQUM7SUFDcEMsTUFBTSxJQUFJLEdBQUcsSUFBSSxXQUFXLENBQUMsS0FBSyxFQUFFLEtBQUssQ0FBQyxDQUFDO0lBRTNDLE1BQU0sTUFBTSxHQUFHLFlBQVksSUFBSSxDQUFDLE1BQU0sWUFBWSxDQUFDLE1BQU0sRUFBRSxTQUFTLENBQUMsQ0FBQyxDQUFDO0lBQ3ZFLE1BQU0sTUFBTSxHQUFHLElBQUksVUFBVSxDQUFDLFFBQVEsRUFBRSxTQUFTLEVBQUUsRUFBRSxFQUFFLElBQUksRUFBRSxNQUFNLEVBQUUsTUFBTSxFQUFFLFNBQVMsQ0FBQyxDQUFDO0lBQ3hGLE1BQU0sTUFBTSxDQUFDLEtBQUssRUFBRSxDQUFDO0lBQ3JCLE9BQU8sTUFBTSxDQUFDO0FBQ2hCLENBQUM7QUFFRCxTQUFTLFlBQVksQ0FBQyxNQUF3QixFQUFFLFNBQWtCO0lBQ2hFLElBQUksQ0FBQyxNQUFNLENBQUMsYUFBYSxFQUFFLENBQUM7UUFDMUIsT0FBTyxJQUFJLHVCQUF1QixFQUFFLENBQUM7SUFDdkMsQ0FBQztJQUVELCtEQUErRDtJQUMvRCxJQUFJLENBQUMsTUFBTSxDQUFDLFlBQVksSUFBSSxDQUFDLE1BQU0sQ0FBQyxrQkFBa0IsRUFBRSxDQUFDO1FBQ3ZELE1BQU0sSUFBSSxLQUFLLENBQUMsa0VBQWtFLENBQUMsQ0FBQztJQUN0RixDQUFDO0lBQ0QsTUFBTSxRQUFRLEdBQUcsTUFBb0csQ0FBQztJQUN0SCxNQUFNLEdBQUcsR0FBRyxpQkFBaUIsQ0FBQyw0QkFBNEIsR0FBRyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsSUFBSSxTQUFTLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQztJQUNqRyxPQUFPLDJCQUEyQixDQUFDLEdBQUcsQ0FBQyxFQUFFLGFBQWEsRUFBRSxLQUFLLEVBQUUsR0FBRyxRQUFRLEVBQUUsRUFBRSxHQUFHLENBQUMsQ0FBQztBQUNyRixDQUFDIn0=
|