@aztec/p2p 3.0.0-nightly.20251026 → 3.0.0-nightly.20251030-2
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/mem_pools/attestation_pool/attestation_pool.d.ts +15 -0
- package/dest/mem_pools/attestation_pool/attestation_pool.d.ts.map +1 -1
- package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts.map +1 -1
- package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.js +32 -0
- package/dest/mem_pools/attestation_pool/kv_attestation_pool.d.ts +2 -0
- package/dest/mem_pools/attestation_pool/kv_attestation_pool.d.ts.map +1 -1
- package/dest/mem_pools/attestation_pool/kv_attestation_pool.js +16 -0
- package/dest/mem_pools/attestation_pool/memory_attestation_pool.d.ts +2 -0
- package/dest/mem_pools/attestation_pool/memory_attestation_pool.d.ts.map +1 -1
- package/dest/mem_pools/attestation_pool/memory_attestation_pool.js +22 -0
- package/dest/mem_pools/tx_pool/aztec_kv_tx_pool.d.ts +1 -0
- package/dest/mem_pools/tx_pool/aztec_kv_tx_pool.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool/aztec_kv_tx_pool.js +8 -2
- package/dest/mem_pools/tx_pool/memory_tx_pool.d.ts +1 -0
- package/dest/mem_pools/tx_pool/memory_tx_pool.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool/memory_tx_pool.js +6 -0
- package/dest/mem_pools/tx_pool/tx_pool.d.ts +6 -0
- package/dest/mem_pools/tx_pool/tx_pool.d.ts.map +1 -1
- package/dest/services/encoding.d.ts +24 -3
- package/dest/services/encoding.d.ts.map +1 -1
- package/dest/services/encoding.js +73 -5
- package/dest/services/libp2p/libp2p_service.d.ts +15 -8
- package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
- package/dest/services/libp2p/libp2p_service.js +78 -26
- package/dest/services/reqresp/reqresp.js +2 -2
- package/dest/testbench/p2p_client_testbench_worker.js +4 -1
- package/dest/testbench/testbench.js +2 -2
- package/package.json +14 -14
- package/src/mem_pools/attestation_pool/attestation_pool.ts +17 -0
- package/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts +37 -0
- package/src/mem_pools/attestation_pool/kv_attestation_pool.ts +21 -0
- package/src/mem_pools/attestation_pool/memory_attestation_pool.ts +28 -0
- package/src/mem_pools/tx_pool/aztec_kv_tx_pool.ts +7 -2
- package/src/mem_pools/tx_pool/memory_tx_pool.ts +5 -0
- package/src/mem_pools/tx_pool/tx_pool.ts +7 -0
- package/src/services/encoding.ts +80 -5
- package/src/services/libp2p/libp2p_service.ts +75 -24
- package/src/services/reqresp/reqresp.ts +2 -2
- package/src/testbench/p2p_client_testbench_worker.ts +3 -0
- package/src/testbench/testbench.ts +2 -2
|
@@ -430,7 +430,11 @@ import { P2PInstrumentation } from './instrumentation.js';
|
|
|
430
430
|
const result = await this.node.services.pubsub.publish(topic, p2pMessage.toMessageData());
|
|
431
431
|
return result.recipients.length;
|
|
432
432
|
}
|
|
433
|
-
|
|
433
|
+
/**
|
|
434
|
+
* Checks if this message has already been seen, based on its msgId computed from hashing the message data.
|
|
435
|
+
* Note that we do not rely on the seenCache from gossipsub since we want to keep a longer history of seen
|
|
436
|
+
* messages to avoid tx echoes across the network.
|
|
437
|
+
*/ preValidateReceivedMessage(msg, msgId, source) {
|
|
434
438
|
let topicType;
|
|
435
439
|
switch(msg.topic){
|
|
436
440
|
case this.topicStrings[TopicType.tx]:
|
|
@@ -484,32 +488,52 @@ import { P2PInstrumentation } from './instrumentation.js';
|
|
|
484
488
|
}
|
|
485
489
|
async validateReceivedMessage(validationFunc, msgId, source, topicType) {
|
|
486
490
|
let resultAndObj = {
|
|
487
|
-
result:
|
|
488
|
-
obj: undefined
|
|
491
|
+
result: TopicValidatorResult.Reject
|
|
489
492
|
};
|
|
490
493
|
const timer = new Timer();
|
|
491
494
|
try {
|
|
492
495
|
resultAndObj = await validationFunc();
|
|
493
496
|
} catch (err) {
|
|
494
|
-
this.logger.error(`Error deserializing and validating message
|
|
497
|
+
this.logger.error(`Error deserializing and validating gossipsub message`, err, {
|
|
498
|
+
msgId,
|
|
499
|
+
source: source.toString(),
|
|
500
|
+
topicType
|
|
501
|
+
});
|
|
495
502
|
}
|
|
496
|
-
if (resultAndObj.result) {
|
|
503
|
+
if (resultAndObj.result === TopicValidatorResult.Accept) {
|
|
497
504
|
this.instrumentation.recordMessageValidation(topicType, timer);
|
|
498
505
|
}
|
|
499
|
-
this.node.services.pubsub.reportMessageValidationResult(msgId, source.toString(), resultAndObj.result
|
|
506
|
+
this.node.services.pubsub.reportMessageValidationResult(msgId, source.toString(), resultAndObj.result);
|
|
500
507
|
return resultAndObj;
|
|
501
508
|
}
|
|
502
509
|
async handleGossipedTx(payloadData, msgId, source) {
|
|
503
510
|
const validationFunc = async ()=>{
|
|
504
511
|
const tx = Tx.fromBuffer(payloadData);
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
512
|
+
const isValid = await this.validatePropagatedTx(tx, source);
|
|
513
|
+
const exists = isValid && await this.mempools.txPool.hasTx(tx.getTxHash());
|
|
514
|
+
this.logger.trace(`Validate propagated tx`, {
|
|
515
|
+
isValid,
|
|
516
|
+
exists,
|
|
517
|
+
[Attributes.P2P_ID]: source.toString()
|
|
518
|
+
});
|
|
519
|
+
if (!isValid) {
|
|
520
|
+
return {
|
|
521
|
+
result: TopicValidatorResult.Reject
|
|
522
|
+
};
|
|
523
|
+
} else if (exists) {
|
|
524
|
+
return {
|
|
525
|
+
result: TopicValidatorResult.Ignore,
|
|
526
|
+
obj: tx
|
|
527
|
+
};
|
|
528
|
+
} else {
|
|
529
|
+
return {
|
|
530
|
+
result: TopicValidatorResult.Accept,
|
|
531
|
+
obj: tx
|
|
532
|
+
};
|
|
533
|
+
}
|
|
510
534
|
};
|
|
511
535
|
const { result, obj: tx } = await this.validateReceivedMessage(validationFunc, msgId, source, TopicType.tx);
|
|
512
|
-
if (
|
|
536
|
+
if (result !== TopicValidatorResult.Accept || !tx) {
|
|
513
537
|
return;
|
|
514
538
|
}
|
|
515
539
|
const txHash = tx.getTxHash();
|
|
@@ -519,7 +543,7 @@ import { P2PInstrumentation } from './instrumentation.js';
|
|
|
519
543
|
txHash: txHashString
|
|
520
544
|
});
|
|
521
545
|
if (this.config.dropTransactions && randomInt(1000) < this.config.dropTransactionsProbability * 1000) {
|
|
522
|
-
this.logger.
|
|
546
|
+
this.logger.warn(`Intentionally dropping tx ${txHashString} (probability rule)`);
|
|
523
547
|
return;
|
|
524
548
|
}
|
|
525
549
|
await this.mempools.txPool.addTxs([
|
|
@@ -534,18 +558,32 @@ import { P2PInstrumentation } from './instrumentation.js';
|
|
|
534
558
|
*/ async processAttestationFromPeer(payloadData, msgId, source) {
|
|
535
559
|
const validationFunc = async ()=>{
|
|
536
560
|
const attestation = BlockAttestation.fromBuffer(payloadData);
|
|
537
|
-
const
|
|
538
|
-
this.
|
|
561
|
+
const isValid = await this.validateAttestation(source, attestation);
|
|
562
|
+
const exists = isValid && await this.mempools.attestationPool.hasAttestation(attestation);
|
|
563
|
+
this.logger.trace(`Validate propagated block attestation`, {
|
|
564
|
+
isValid,
|
|
565
|
+
exists,
|
|
539
566
|
[Attributes.SLOT_NUMBER]: attestation.payload.header.slotNumber.toString(),
|
|
540
567
|
[Attributes.P2P_ID]: source.toString()
|
|
541
568
|
});
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
569
|
+
if (!isValid) {
|
|
570
|
+
return {
|
|
571
|
+
result: TopicValidatorResult.Reject
|
|
572
|
+
};
|
|
573
|
+
} else if (exists) {
|
|
574
|
+
return {
|
|
575
|
+
result: TopicValidatorResult.Ignore,
|
|
576
|
+
obj: attestation
|
|
577
|
+
};
|
|
578
|
+
} else {
|
|
579
|
+
return {
|
|
580
|
+
result: TopicValidatorResult.Accept,
|
|
581
|
+
obj: attestation
|
|
582
|
+
};
|
|
583
|
+
}
|
|
546
584
|
};
|
|
547
585
|
const { result, obj: attestation } = await this.validateReceivedMessage(validationFunc, msgId, source, TopicType.block_attestation);
|
|
548
|
-
if (
|
|
586
|
+
if (result !== TopicValidatorResult.Accept || !attestation) {
|
|
549
587
|
return;
|
|
550
588
|
}
|
|
551
589
|
this.logger.debug(`Received attestation for slot ${attestation.slotNumber.toNumber()} from external peer ${source.toString()}`, {
|
|
@@ -561,15 +599,29 @@ import { P2PInstrumentation } from './instrumentation.js';
|
|
|
561
599
|
async processBlockFromPeer(payloadData, msgId, source) {
|
|
562
600
|
const validationFunc = async ()=>{
|
|
563
601
|
const block = BlockProposal.fromBuffer(payloadData);
|
|
564
|
-
const
|
|
565
|
-
this.
|
|
602
|
+
const isValid = await this.validateBlockProposal(source, block);
|
|
603
|
+
const exists = isValid && await this.mempools.attestationPool.hasBlockProposal(block);
|
|
604
|
+
this.logger.trace(`Validate propagated block proposal`, {
|
|
605
|
+
isValid,
|
|
606
|
+
exists,
|
|
566
607
|
[Attributes.SLOT_NUMBER]: block.payload.header.slotNumber.toString(),
|
|
567
608
|
[Attributes.P2P_ID]: source.toString()
|
|
568
609
|
});
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
610
|
+
if (!isValid) {
|
|
611
|
+
return {
|
|
612
|
+
result: TopicValidatorResult.Reject
|
|
613
|
+
};
|
|
614
|
+
} else if (exists) {
|
|
615
|
+
return {
|
|
616
|
+
result: TopicValidatorResult.Ignore,
|
|
617
|
+
obj: block
|
|
618
|
+
};
|
|
619
|
+
} else {
|
|
620
|
+
return {
|
|
621
|
+
result: TopicValidatorResult.Accept,
|
|
622
|
+
obj: block
|
|
623
|
+
};
|
|
624
|
+
}
|
|
573
625
|
};
|
|
574
626
|
const { result, obj: block } = await this.validateReceivedMessage(validationFunc, msgId, source, TopicType.block_proposal);
|
|
575
627
|
if (!result || !block) {
|
|
@@ -362,7 +362,7 @@ import { ReqRespStatus, ReqRespStatusError, parseStatusChunk, prettyPrintReqResp
|
|
|
362
362
|
}
|
|
363
363
|
}
|
|
364
364
|
const messageData = Buffer.concat(chunks);
|
|
365
|
-
const message = this.snappyTransform.
|
|
365
|
+
const message = this.snappyTransform.inboundTransformData(messageData);
|
|
366
366
|
return {
|
|
367
367
|
status: status ?? ReqRespStatus.UNKNOWN,
|
|
368
368
|
data: message
|
|
@@ -462,7 +462,7 @@ import { ReqRespStatus, ReqRespStatusError, parseStatusChunk, prettyPrintReqResp
|
|
|
462
462
|
}
|
|
463
463
|
stream.metadata.written = true; // Mark the stream as written to;
|
|
464
464
|
yield SUCCESS;
|
|
465
|
-
yield snappy.
|
|
465
|
+
yield snappy.outboundTransformData(response);
|
|
466
466
|
}
|
|
467
467
|
}, stream.sink);
|
|
468
468
|
}
|
|
@@ -34,6 +34,7 @@ function mockTxPool() {
|
|
|
34
34
|
getTxStatus: ()=>Promise.resolve(TxStatus.PENDING),
|
|
35
35
|
getTxsByHash: ()=>Promise.resolve([]),
|
|
36
36
|
hasTxs: ()=>Promise.resolve([]),
|
|
37
|
+
hasTx: ()=>Promise.resolve(false),
|
|
37
38
|
updateConfig: ()=>{},
|
|
38
39
|
markTxsAsNonEvictable: ()=>Promise.resolve(),
|
|
39
40
|
cleanupDeletedMinedTxs: ()=>Promise.resolve(0)
|
|
@@ -51,7 +52,9 @@ function mockAttestationPool() {
|
|
|
51
52
|
getAttestationsForSlot: ()=>Promise.resolve([]),
|
|
52
53
|
getAttestationsForSlotAndProposal: ()=>Promise.resolve([]),
|
|
53
54
|
addBlockProposal: ()=>Promise.resolve(),
|
|
54
|
-
getBlockProposal: ()=>Promise.resolve(undefined)
|
|
55
|
+
getBlockProposal: ()=>Promise.resolve(undefined),
|
|
56
|
+
hasBlockProposal: ()=>Promise.resolve(false),
|
|
57
|
+
hasAttestation: ()=>Promise.resolve(false)
|
|
55
58
|
};
|
|
56
59
|
}
|
|
57
60
|
function mockEpochCache() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createLogger } from '@aztec/foundation/log';
|
|
2
2
|
import { sleep } from '@aztec/foundation/sleep';
|
|
3
|
-
import {
|
|
3
|
+
import { ChonkProof } from '@aztec/stdlib/proofs';
|
|
4
4
|
import { mockTx } from '@aztec/stdlib/testing';
|
|
5
5
|
import assert from 'assert';
|
|
6
6
|
import path from 'path';
|
|
@@ -34,7 +34,7 @@ async function main() {
|
|
|
34
34
|
logger.info('Workers Ready');
|
|
35
35
|
// Send tx from client 0
|
|
36
36
|
const tx = await mockTx(1, {
|
|
37
|
-
|
|
37
|
+
chonkProof: ChonkProof.random()
|
|
38
38
|
});
|
|
39
39
|
workerClientManager.processes[0].send({
|
|
40
40
|
type: 'SEND_TX',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/p2p",
|
|
3
|
-
"version": "3.0.0-nightly.
|
|
3
|
+
"version": "3.0.0-nightly.20251030-2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dest/index.js",
|
|
@@ -67,17 +67,17 @@
|
|
|
67
67
|
]
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@aztec/constants": "3.0.0-nightly.
|
|
71
|
-
"@aztec/epoch-cache": "3.0.0-nightly.
|
|
72
|
-
"@aztec/ethereum": "3.0.0-nightly.
|
|
73
|
-
"@aztec/foundation": "3.0.0-nightly.
|
|
74
|
-
"@aztec/kv-store": "3.0.0-nightly.
|
|
75
|
-
"@aztec/noir-contracts.js": "3.0.0-nightly.
|
|
76
|
-
"@aztec/noir-protocol-circuits-types": "3.0.0-nightly.
|
|
77
|
-
"@aztec/protocol-contracts": "3.0.0-nightly.
|
|
78
|
-
"@aztec/simulator": "3.0.0-nightly.
|
|
79
|
-
"@aztec/stdlib": "3.0.0-nightly.
|
|
80
|
-
"@aztec/telemetry-client": "3.0.0-nightly.
|
|
70
|
+
"@aztec/constants": "3.0.0-nightly.20251030-2",
|
|
71
|
+
"@aztec/epoch-cache": "3.0.0-nightly.20251030-2",
|
|
72
|
+
"@aztec/ethereum": "3.0.0-nightly.20251030-2",
|
|
73
|
+
"@aztec/foundation": "3.0.0-nightly.20251030-2",
|
|
74
|
+
"@aztec/kv-store": "3.0.0-nightly.20251030-2",
|
|
75
|
+
"@aztec/noir-contracts.js": "3.0.0-nightly.20251030-2",
|
|
76
|
+
"@aztec/noir-protocol-circuits-types": "3.0.0-nightly.20251030-2",
|
|
77
|
+
"@aztec/protocol-contracts": "3.0.0-nightly.20251030-2",
|
|
78
|
+
"@aztec/simulator": "3.0.0-nightly.20251030-2",
|
|
79
|
+
"@aztec/stdlib": "3.0.0-nightly.20251030-2",
|
|
80
|
+
"@aztec/telemetry-client": "3.0.0-nightly.20251030-2",
|
|
81
81
|
"@chainsafe/libp2p-gossipsub": "13.0.0",
|
|
82
82
|
"@chainsafe/libp2p-noise": "^15.0.0",
|
|
83
83
|
"@chainsafe/libp2p-yamux": "^6.0.2",
|
|
@@ -104,8 +104,8 @@
|
|
|
104
104
|
"xxhash-wasm": "^1.1.0"
|
|
105
105
|
},
|
|
106
106
|
"devDependencies": {
|
|
107
|
-
"@aztec/archiver": "3.0.0-nightly.
|
|
108
|
-
"@aztec/world-state": "3.0.0-nightly.
|
|
107
|
+
"@aztec/archiver": "3.0.0-nightly.20251030-2",
|
|
108
|
+
"@aztec/world-state": "3.0.0-nightly.20251030-2",
|
|
109
109
|
"@jest/globals": "^30.0.0",
|
|
110
110
|
"@types/jest": "^30.0.0",
|
|
111
111
|
"@types/node": "^22.15.17",
|
|
@@ -21,6 +21,15 @@ export interface AttestationPool {
|
|
|
21
21
|
*/
|
|
22
22
|
getBlockProposal(id: string): Promise<BlockProposal | undefined>;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Check if a block proposal exists in the pool
|
|
26
|
+
*
|
|
27
|
+
* @param idOrProposal - The ID of the block proposal or the block proposal itself to check. The ID is proposal.payload.archive
|
|
28
|
+
*
|
|
29
|
+
* @return True if the block proposal exists, false otherwise.
|
|
30
|
+
*/
|
|
31
|
+
hasBlockProposal(idOrProposal: string | BlockProposal): Promise<boolean>;
|
|
32
|
+
|
|
24
33
|
/**
|
|
25
34
|
* AddAttestations
|
|
26
35
|
*
|
|
@@ -84,6 +93,14 @@ export interface AttestationPool {
|
|
|
84
93
|
*/
|
|
85
94
|
getAttestationsForSlotAndProposal(slot: bigint, proposalId: string): Promise<BlockAttestation[]>;
|
|
86
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Check if a specific attestation exists in the pool
|
|
98
|
+
*
|
|
99
|
+
* @param attestation - The attestation to check
|
|
100
|
+
* @return True if the attestation exists, false otherwise
|
|
101
|
+
*/
|
|
102
|
+
hasAttestation(attestation: BlockAttestation): Promise<boolean>;
|
|
103
|
+
|
|
87
104
|
/** Returns whether the pool is empty. */
|
|
88
105
|
isEmpty(): Promise<boolean>;
|
|
89
106
|
}
|
|
@@ -72,6 +72,11 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
|
|
|
72
72
|
expect(retrievedAttestations.length).toBe(attestations.length);
|
|
73
73
|
compareAttestations(retrievedAttestations, attestations);
|
|
74
74
|
|
|
75
|
+
// Check hasAttestation for added attestations
|
|
76
|
+
for (const attestation of attestations) {
|
|
77
|
+
expect(await ap.hasAttestation(attestation)).toBe(true);
|
|
78
|
+
}
|
|
79
|
+
|
|
75
80
|
const retrievedAttestationsForSlot = await ap.getAttestationsForSlot(BigInt(slotNumber));
|
|
76
81
|
expect(retrievedAttestationsForSlot.length).toBe(attestations.length);
|
|
77
82
|
compareAttestations(retrievedAttestationsForSlot, attestations);
|
|
@@ -85,6 +90,7 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
|
|
|
85
90
|
);
|
|
86
91
|
expect(retrievedAttestationsAfterAdd.length).toBe(attestations.length + 1);
|
|
87
92
|
compareAttestations(retrievedAttestationsAfterAdd, [...attestations, newAttestation]);
|
|
93
|
+
expect(await ap.hasAttestation(newAttestation)).toBe(true);
|
|
88
94
|
const retrievedAttestationsForSlotAfterAdd = await ap.getAttestationsForSlot(BigInt(slotNumber));
|
|
89
95
|
expect(retrievedAttestationsForSlotAfterAdd.length).toBe(attestations.length + 1);
|
|
90
96
|
compareAttestations(retrievedAttestationsForSlotAfterAdd, [...attestations, newAttestation]);
|
|
@@ -97,6 +103,11 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
|
|
|
97
103
|
archive.toString(),
|
|
98
104
|
);
|
|
99
105
|
expect(retreivedAttestationsAfterDelete.length).toBe(0);
|
|
106
|
+
// Check hasAttestation after deletion
|
|
107
|
+
for (const attestation of attestations) {
|
|
108
|
+
expect(await ap.hasAttestation(attestation)).toBe(false);
|
|
109
|
+
}
|
|
110
|
+
expect(await ap.hasAttestation(newAttestation)).toBe(false);
|
|
100
111
|
});
|
|
101
112
|
|
|
102
113
|
it('should handle duplicate proposals in a slot', async () => {
|
|
@@ -170,10 +181,20 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
|
|
|
170
181
|
expect(retreivedAttestations.length).toBe(NUMBER_OF_SIGNERS_PER_TEST);
|
|
171
182
|
compareAttestations(retreivedAttestations, attestations);
|
|
172
183
|
|
|
184
|
+
// Check hasAttestation before deletion
|
|
185
|
+
for (const attestation of attestations) {
|
|
186
|
+
expect(await ap.hasAttestation(attestation)).toBe(true);
|
|
187
|
+
}
|
|
188
|
+
|
|
173
189
|
await ap.deleteAttestations(attestations);
|
|
174
190
|
|
|
175
191
|
const gottenAfterDelete = await ap.getAttestationsForSlotAndProposal(BigInt(slotNumber), proposalId);
|
|
176
192
|
expect(gottenAfterDelete.length).toBe(0);
|
|
193
|
+
|
|
194
|
+
// Check hasAttestation after deletion
|
|
195
|
+
for (const attestation of attestations) {
|
|
196
|
+
expect(await ap.hasAttestation(attestation)).toBe(false);
|
|
197
|
+
}
|
|
177
198
|
});
|
|
178
199
|
|
|
179
200
|
it('should blanket delete attestations per slot', async () => {
|
|
@@ -265,12 +286,19 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
|
|
|
265
286
|
|
|
266
287
|
expect(retrievedProposal).toBeDefined();
|
|
267
288
|
expect(retrievedProposal!).toEqual(proposal);
|
|
289
|
+
|
|
290
|
+
// Check hasBlockProposal with both id and object
|
|
291
|
+
expect(await ap.hasBlockProposal(proposalId)).toBe(true);
|
|
292
|
+
expect(await ap.hasBlockProposal(proposal)).toBe(true);
|
|
268
293
|
});
|
|
269
294
|
|
|
270
295
|
it('should return undefined for non-existent block proposal', async () => {
|
|
271
296
|
const nonExistentId = Fr.random().toString();
|
|
272
297
|
const retrievedProposal = await ap.getBlockProposal(nonExistentId);
|
|
273
298
|
expect(retrievedProposal).toBeUndefined();
|
|
299
|
+
|
|
300
|
+
// Check hasBlockProposal returns false for non-existent proposal
|
|
301
|
+
expect(await ap.hasBlockProposal(nonExistentId)).toBe(false);
|
|
274
302
|
});
|
|
275
303
|
|
|
276
304
|
it('should update block proposal if added twice with same id', async () => {
|
|
@@ -323,6 +351,7 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
|
|
|
323
351
|
// Verify proposal exists
|
|
324
352
|
let retrievedProposal = await ap.getBlockProposal(proposalId);
|
|
325
353
|
expect(retrievedProposal).toBeDefined();
|
|
354
|
+
expect(await ap.hasBlockProposal(proposalId)).toBe(true);
|
|
326
355
|
|
|
327
356
|
// Delete attestations for slot and proposal
|
|
328
357
|
await ap.deleteAttestationsForSlotAndProposal(BigInt(slotNumber), proposalId);
|
|
@@ -330,6 +359,7 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
|
|
|
330
359
|
// Proposal should be deleted
|
|
331
360
|
retrievedProposal = await ap.getBlockProposal(proposalId);
|
|
332
361
|
expect(retrievedProposal).toBeUndefined();
|
|
362
|
+
expect(await ap.hasBlockProposal(proposalId)).toBe(false);
|
|
333
363
|
});
|
|
334
364
|
|
|
335
365
|
it('should delete block proposal when deleting attestations for slot', async () => {
|
|
@@ -344,6 +374,7 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
|
|
|
344
374
|
// Verify proposal exists
|
|
345
375
|
let retrievedProposal = await ap.getBlockProposal(proposalId);
|
|
346
376
|
expect(retrievedProposal).toBeDefined();
|
|
377
|
+
expect(await ap.hasBlockProposal(proposal)).toBe(true);
|
|
347
378
|
|
|
348
379
|
// Delete attestations for slot
|
|
349
380
|
await ap.deleteAttestationsForSlot(BigInt(slotNumber));
|
|
@@ -351,6 +382,7 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
|
|
|
351
382
|
// Proposal should be deleted
|
|
352
383
|
retrievedProposal = await ap.getBlockProposal(proposalId);
|
|
353
384
|
expect(retrievedProposal).toBeUndefined();
|
|
385
|
+
expect(await ap.hasBlockProposal(proposal)).toBe(false);
|
|
354
386
|
});
|
|
355
387
|
|
|
356
388
|
it('should be able to fetch both block proposal and attestations', async () => {
|
|
@@ -372,8 +404,13 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
|
|
|
372
404
|
|
|
373
405
|
expect(retrievedProposal).toBeDefined();
|
|
374
406
|
expect(retrievedProposal).toEqual(proposal);
|
|
407
|
+
expect(await ap.hasBlockProposal(proposalId)).toBe(true);
|
|
375
408
|
|
|
376
409
|
compareAttestations(retrievedAttestations, attestations);
|
|
410
|
+
// Check hasAttestation for all attestations
|
|
411
|
+
for (const attestation of attestations) {
|
|
412
|
+
expect(await ap.hasAttestation(attestation)).toBe(true);
|
|
413
|
+
}
|
|
377
414
|
});
|
|
378
415
|
});
|
|
379
416
|
}
|
|
@@ -213,6 +213,22 @@ export class KvAttestationPool implements AttestationPool {
|
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
public async hasAttestation(attestation: BlockAttestation): Promise<boolean> {
|
|
217
|
+
const slotNumber = attestation.payload.header.slotNumber;
|
|
218
|
+
const proposalId = attestation.archive;
|
|
219
|
+
const sender = attestation.getSender();
|
|
220
|
+
|
|
221
|
+
// Attestations with invalid signatures are never in the pool
|
|
222
|
+
if (!sender) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const address = sender.toString();
|
|
227
|
+
const key = this.getAttestationKey(slotNumber, proposalId, address);
|
|
228
|
+
|
|
229
|
+
return await this.attestations.hasAsync(key);
|
|
230
|
+
}
|
|
231
|
+
|
|
216
232
|
public async getBlockProposal(id: string): Promise<BlockProposal | undefined> {
|
|
217
233
|
const buffer = await this.proposals.getAsync(id);
|
|
218
234
|
try {
|
|
@@ -226,6 +242,11 @@ export class KvAttestationPool implements AttestationPool {
|
|
|
226
242
|
return Promise.resolve(undefined);
|
|
227
243
|
}
|
|
228
244
|
|
|
245
|
+
public async hasBlockProposal(idOrProposal: string | BlockProposal): Promise<boolean> {
|
|
246
|
+
const id = typeof idOrProposal === 'string' ? idOrProposal : idOrProposal.payload.archive.toString();
|
|
247
|
+
return await this.proposals.hasAsync(id);
|
|
248
|
+
}
|
|
249
|
+
|
|
229
250
|
public async addBlockProposal(blockProposal: BlockProposal): Promise<void> {
|
|
230
251
|
await this.store.transactionAsync(async () => {
|
|
231
252
|
await this.proposalsForSlot.set(blockProposal.slotNumber.toString(), blockProposal.archive.toString());
|
|
@@ -173,6 +173,29 @@ export class InMemoryAttestationPool implements AttestationPool {
|
|
|
173
173
|
return Promise.resolve();
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
public hasAttestation(attestation: BlockAttestation): Promise<boolean> {
|
|
177
|
+
const slotNumber = attestation.payload.header.slotNumber;
|
|
178
|
+
const proposalId = attestation.archive.toString();
|
|
179
|
+
const sender = attestation.getSender();
|
|
180
|
+
|
|
181
|
+
// Attestations with invalid signatures are never in the pool
|
|
182
|
+
if (!sender) {
|
|
183
|
+
return Promise.resolve(false);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const slotAttestationMap = this.attestations.get(slotNumber.toBigInt());
|
|
187
|
+
if (!slotAttestationMap) {
|
|
188
|
+
return Promise.resolve(false);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const proposalAttestationMap = slotAttestationMap.get(proposalId);
|
|
192
|
+
if (!proposalAttestationMap) {
|
|
193
|
+
return Promise.resolve(false);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return Promise.resolve(proposalAttestationMap.has(sender.toString()));
|
|
197
|
+
}
|
|
198
|
+
|
|
176
199
|
public addBlockProposal(blockProposal: BlockProposal): Promise<void> {
|
|
177
200
|
// We initialize slot-proposal mapping if it does not exist
|
|
178
201
|
// This is important to ensure we can delete this proposal if there were not attestations for it
|
|
@@ -186,6 +209,11 @@ export class InMemoryAttestationPool implements AttestationPool {
|
|
|
186
209
|
public getBlockProposal(id: string): Promise<BlockProposal | undefined> {
|
|
187
210
|
return Promise.resolve(this.proposals.get(id));
|
|
188
211
|
}
|
|
212
|
+
|
|
213
|
+
public hasBlockProposal(idOrProposal: string | BlockProposal): Promise<boolean> {
|
|
214
|
+
const id = typeof idOrProposal === 'string' ? idOrProposal : idOrProposal.payload.archive.toString();
|
|
215
|
+
return Promise.resolve(this.proposals.has(id));
|
|
216
|
+
}
|
|
189
217
|
}
|
|
190
218
|
|
|
191
219
|
/**
|
|
@@ -6,7 +6,7 @@ import type { AztecAsyncKVStore, AztecAsyncMap, AztecAsyncMultiMap, AztecAsyncSi
|
|
|
6
6
|
import { ProtocolContractAddress } from '@aztec/protocol-contracts';
|
|
7
7
|
import { GasFees } from '@aztec/stdlib/gas';
|
|
8
8
|
import type { MerkleTreeReadOperations, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
9
|
-
import {
|
|
9
|
+
import { ChonkProof } from '@aztec/stdlib/proofs';
|
|
10
10
|
import type { TxAddedToPoolStats } from '@aztec/stdlib/stats';
|
|
11
11
|
import { DatabasePublicStateSource } from '@aztec/stdlib/trees';
|
|
12
12
|
import { BlockHeader, Tx, TxHash } from '@aztec/stdlib/tx';
|
|
@@ -273,6 +273,11 @@ export class AztecKVTxPool extends (EventEmitter as new () => TypedEventEmitter<
|
|
|
273
273
|
return await Promise.all(txHashes.map(txHash => this.#txs.hasAsync(txHash.toString())));
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
async hasTx(txHash: TxHash): Promise<boolean> {
|
|
277
|
+
const result = await this.hasTxs([txHash]);
|
|
278
|
+
return result[0];
|
|
279
|
+
}
|
|
280
|
+
|
|
276
281
|
/**
|
|
277
282
|
* Checks if an archived tx exists and returns it.
|
|
278
283
|
* @param txHash - The tx hash.
|
|
@@ -546,7 +551,7 @@ export class AztecKVTxPool extends (EventEmitter as new () => TypedEventEmitter<
|
|
|
546
551
|
const archivedTx: Tx = new Tx(
|
|
547
552
|
tx.txHash,
|
|
548
553
|
tx.data,
|
|
549
|
-
|
|
554
|
+
ChonkProof.empty(),
|
|
550
555
|
tx.contractClassLogFields,
|
|
551
556
|
tx.publicFunctionCalldata,
|
|
552
557
|
);
|
|
@@ -151,6 +151,11 @@ export class InMemoryTxPool extends (EventEmitter as new () => TypedEventEmitter
|
|
|
151
151
|
return Promise.resolve(txHashes.map(txHash => this.txs.has(txHash.toBigInt())));
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
async hasTx(txHash: TxHash): Promise<boolean> {
|
|
155
|
+
const result = await this.hasTxs([txHash]);
|
|
156
|
+
return result[0];
|
|
157
|
+
}
|
|
158
|
+
|
|
154
159
|
public getArchivedTxByHash(): Promise<Tx | undefined> {
|
|
155
160
|
return Promise.resolve(undefined);
|
|
156
161
|
}
|
|
@@ -43,6 +43,13 @@ export interface TxPool extends TypedEventEmitter<TxPoolEvents> {
|
|
|
43
43
|
*/
|
|
44
44
|
hasTxs(txHashes: TxHash[]): Promise<boolean[]>;
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Checks if a transaction exists in the pool
|
|
48
|
+
* @param txHash - The hash of the transaction to check for
|
|
49
|
+
* @returns True if the transaction exists, false otherwise
|
|
50
|
+
*/
|
|
51
|
+
hasTx(txHash: TxHash): Promise<boolean>;
|
|
52
|
+
|
|
46
53
|
/**
|
|
47
54
|
* Checks if an archived transaction exists in the pool and returns it.
|
|
48
55
|
* @param txHash - The hash of the transaction, used as an ID.
|
package/src/services/encoding.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Taken from lodestar: https://github.com/ChainSafe/lodestar
|
|
2
2
|
import { sha256 } from '@aztec/foundation/crypto';
|
|
3
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
4
|
+
import { TopicType, getTopicFromString } from '@aztec/stdlib/p2p';
|
|
3
5
|
|
|
4
6
|
import type { RPC } from '@chainsafe/libp2p-gossipsub/message';
|
|
5
7
|
import type { DataTransform } from '@chainsafe/libp2p-gossipsub/types';
|
|
@@ -49,31 +51,104 @@ export function getMsgIdFn(message: Message) {
|
|
|
49
51
|
return sha256(Buffer.concat(vec)).subarray(0, 20);
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
const DefaultMaxSizesKb: Record<TopicType, number> = {
|
|
55
|
+
// Tx effects should not exceed 128kb, so 512kb for the full tx obj should be sufficient
|
|
56
|
+
[TopicType.tx]: 512,
|
|
57
|
+
// An attestation has roughly 30 fields, which is 1kb, so 5x is plenty
|
|
58
|
+
[TopicType.block_attestation]: 5,
|
|
59
|
+
// Proposals may carry some tx objects, so we allow a larger size capped at 10mb
|
|
60
|
+
// Note this may not be enough for carrying all tx objects in a block
|
|
61
|
+
[TopicType.block_proposal]: 1024 * 10,
|
|
62
|
+
};
|
|
63
|
+
|
|
52
64
|
/**
|
|
53
65
|
* Snappy transform for libp2p gossipsub
|
|
54
66
|
*/
|
|
55
67
|
export class SnappyTransform implements DataTransform {
|
|
68
|
+
constructor(
|
|
69
|
+
private maxSizesKb: Record<TopicType, number> = DefaultMaxSizesKb,
|
|
70
|
+
private defaultMaxSizeKb: number = 10 * 1024,
|
|
71
|
+
private logger = createLogger('p2p:snappy-transform'),
|
|
72
|
+
) {}
|
|
73
|
+
|
|
56
74
|
// Topic string included to satisfy DataTransform interface
|
|
57
|
-
inboundTransform(
|
|
58
|
-
|
|
75
|
+
inboundTransform(topicStr: string, data: Uint8Array): Uint8Array {
|
|
76
|
+
const topic = getTopicFromString(topicStr);
|
|
77
|
+
return this.inboundTransformData(Buffer.from(data), topic);
|
|
59
78
|
}
|
|
60
79
|
|
|
61
|
-
public
|
|
80
|
+
public inboundTransformData(data: Buffer, topic?: TopicType): Buffer {
|
|
62
81
|
if (data.length === 0) {
|
|
63
82
|
return data;
|
|
64
83
|
}
|
|
84
|
+
const maxSizeKb = this.maxSizesKb[topic!] ?? this.defaultMaxSizeKb;
|
|
85
|
+
const { decompressedSize } = readSnappyPreamble(data);
|
|
86
|
+
if (decompressedSize > maxSizeKb * 1024) {
|
|
87
|
+
this.logger.warn(`Decompressed size ${decompressedSize} exceeds maximum allowed size of ${maxSizeKb}kb`);
|
|
88
|
+
throw new Error(`Decompressed size ${decompressedSize} exceeds maximum allowed size of ${maxSizeKb}kb`);
|
|
89
|
+
}
|
|
90
|
+
|
|
65
91
|
return Buffer.from(uncompressSync(data, { asBuffer: true }));
|
|
66
92
|
}
|
|
67
93
|
|
|
68
94
|
// Topic string included to satisfy DataTransform interface
|
|
69
95
|
outboundTransform(_topicStr: string, data: Uint8Array): Uint8Array {
|
|
70
|
-
return this.
|
|
96
|
+
return this.outboundTransformData(Buffer.from(data));
|
|
71
97
|
}
|
|
72
98
|
|
|
73
|
-
public
|
|
99
|
+
public outboundTransformData(data: Buffer): Buffer {
|
|
74
100
|
if (data.length === 0) {
|
|
75
101
|
return data;
|
|
76
102
|
}
|
|
77
103
|
return Buffer.from(compressSync(data));
|
|
78
104
|
}
|
|
79
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Reads the Snappy preamble from compressed data and returns the expected decompressed size.
|
|
109
|
+
*
|
|
110
|
+
* The Snappy format starts with a little-endian varint encoding the uncompressed length.
|
|
111
|
+
* Varints consist of a series of bytes where:
|
|
112
|
+
* - Lower 7 bits contain data
|
|
113
|
+
* - Upper bit (0x80) is set if more bytes follow
|
|
114
|
+
*
|
|
115
|
+
* @param data - The compressed data starting with the Snappy preamble
|
|
116
|
+
* @returns Object containing the decompressed size and the number of bytes read from the preamble
|
|
117
|
+
* @throws Error if the data is too short or the varint is invalid
|
|
118
|
+
*/
|
|
119
|
+
export function readSnappyPreamble(data: Uint8Array): { decompressedSize: number; bytesRead: number } {
|
|
120
|
+
if (data.length === 0) {
|
|
121
|
+
throw new Error('Cannot read preamble from empty data');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let result = 0;
|
|
125
|
+
let shift = 0;
|
|
126
|
+
let bytesRead = 0;
|
|
127
|
+
|
|
128
|
+
// Maximum varint length for 32-bit value is 5 bytes
|
|
129
|
+
// (7 bits per byte, so 5 bytes = 35 bits, enough for 2^32 - 1)
|
|
130
|
+
const maxBytes = 5;
|
|
131
|
+
|
|
132
|
+
for (let i = 0; i < Math.min(data.length, maxBytes); i++) {
|
|
133
|
+
const byte = data[i];
|
|
134
|
+
bytesRead++;
|
|
135
|
+
|
|
136
|
+
// Extract lower 7 bits and add to result with appropriate shift
|
|
137
|
+
// Use >>> 0 to convert to unsigned 32-bit integer to avoid sign issues
|
|
138
|
+
result = (result | ((byte & 0x7f) << shift)) >>> 0;
|
|
139
|
+
|
|
140
|
+
// If upper bit is not set, we're done
|
|
141
|
+
if ((byte & 0x80) === 0) {
|
|
142
|
+
return { decompressedSize: result, bytesRead };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
shift += 7;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// If we get here, either we ran out of data or the varint is too long
|
|
149
|
+
if (bytesRead >= maxBytes) {
|
|
150
|
+
throw new Error('Varint is too long (max 5 bytes for 32-bit value)');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
throw new Error('Incomplete varint: data ended before varint termination');
|
|
154
|
+
}
|