@aztec/p2p 0.0.1-commit.b2a5d0dd1 → 0.0.1-commit.b3d3157a
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/bootstrap/bootstrap.d.ts +1 -1
- package/dest/bootstrap/bootstrap.d.ts.map +1 -1
- package/dest/bootstrap/bootstrap.js +9 -1
- package/dest/client/factory.d.ts +3 -3
- package/dest/client/factory.d.ts.map +1 -1
- package/dest/client/factory.js +12 -9
- package/dest/client/interface.d.ts +6 -4
- package/dest/client/interface.d.ts.map +1 -1
- package/dest/client/p2p_client.d.ts +11 -8
- package/dest/client/p2p_client.d.ts.map +1 -1
- package/dest/client/p2p_client.js +53 -17
- package/dest/config.d.ts +43 -17
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +48 -8
- package/dest/errors/reqresp.error.d.ts +1 -20
- package/dest/errors/reqresp.error.d.ts.map +1 -1
- package/dest/errors/reqresp.error.js +0 -21
- package/dest/mem_pools/attestation_pool/attestation_pool.d.ts +93 -55
- package/dest/mem_pools/attestation_pool/attestation_pool.d.ts.map +1 -1
- package/dest/mem_pools/attestation_pool/attestation_pool.js +258 -195
- package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts +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 +175 -59
- package/dest/mem_pools/attestation_pool/mocks.d.ts +1 -1
- package/dest/mem_pools/attestation_pool/mocks.d.ts.map +1 -1
- package/dest/mem_pools/attestation_pool/mocks.js +6 -4
- package/dest/mem_pools/instrumentation.d.ts +1 -1
- package/dest/mem_pools/instrumentation.d.ts.map +1 -1
- package/dest/mem_pools/instrumentation.js +17 -1
- package/dest/mem_pools/tx_pool_v2/archive/tx_archive.d.ts +1 -5
- package/dest/mem_pools/tx_pool_v2/archive/tx_archive.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool_v2/archive/tx_archive.js +1 -7
- package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +1 -1
- package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
- package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +73 -83
- package/dest/msg_validators/attestation_validator/attestation_validator.d.ts +5 -2
- package/dest/msg_validators/attestation_validator/attestation_validator.d.ts.map +1 -1
- package/dest/msg_validators/attestation_validator/attestation_validator.js +18 -2
- package/dest/msg_validators/attestation_validator/fisherman_attestation_validator.d.ts +5 -3
- package/dest/msg_validators/attestation_validator/fisherman_attestation_validator.d.ts.map +1 -1
- package/dest/msg_validators/attestation_validator/fisherman_attestation_validator.js +3 -4
- package/dest/msg_validators/clock_tolerance.d.ts +1 -1
- package/dest/msg_validators/clock_tolerance.d.ts.map +1 -1
- package/dest/msg_validators/clock_tolerance.js +13 -11
- package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts +5 -2
- package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts.map +1 -1
- package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts +5 -2
- package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts.map +1 -1
- package/dest/msg_validators/proposal_validator/proposal_validator.d.ts +8 -2
- package/dest/msg_validators/proposal_validator/proposal_validator.d.ts.map +1 -1
- package/dest/msg_validators/proposal_validator/proposal_validator.js +28 -2
- package/dest/msg_validators/tx_validator/aggregate_tx_validator.d.ts +1 -1
- package/dest/msg_validators/tx_validator/aggregate_tx_validator.d.ts.map +1 -1
- package/dest/msg_validators/tx_validator/aggregate_tx_validator.js +8 -15
- package/dest/msg_validators/tx_validator/allowed_public_setup.js +3 -3
- package/dest/msg_validators/tx_validator/cached_tx_validator.d.ts +15 -0
- package/dest/msg_validators/tx_validator/cached_tx_validator.d.ts.map +1 -0
- package/dest/msg_validators/tx_validator/cached_tx_validator.js +19 -0
- package/dest/msg_validators/tx_validator/data_validator.d.ts +2 -1
- package/dest/msg_validators/tx_validator/data_validator.d.ts.map +1 -1
- package/dest/msg_validators/tx_validator/data_validator.js +1 -0
- package/dest/msg_validators/tx_validator/factory.d.ts +5 -4
- package/dest/msg_validators/tx_validator/factory.d.ts.map +1 -1
- package/dest/msg_validators/tx_validator/factory.js +11 -7
- package/dest/msg_validators/tx_validator/index.d.ts +3 -1
- package/dest/msg_validators/tx_validator/index.d.ts.map +1 -1
- package/dest/msg_validators/tx_validator/index.js +2 -0
- package/dest/msg_validators/tx_validator/tx_proof_validator.d.ts +2 -1
- package/dest/msg_validators/tx_validator/tx_proof_validator.d.ts.map +1 -1
- package/dest/msg_validators/tx_validator/tx_proof_validator.js +2 -0
- package/dest/msg_validators/tx_validator/tx_validation_cache.d.ts +48 -0
- package/dest/msg_validators/tx_validator/tx_validation_cache.d.ts.map +1 -0
- package/dest/msg_validators/tx_validator/tx_validation_cache.js +68 -0
- package/dest/services/discv5/discV5_service.d.ts +2 -1
- package/dest/services/discv5/discV5_service.d.ts.map +1 -1
- package/dest/services/discv5/discV5_service.js +35 -8
- package/dest/services/dummy_service.d.ts +10 -17
- package/dest/services/dummy_service.d.ts.map +1 -1
- package/dest/services/dummy_service.js +9 -18
- package/dest/services/libp2p/libp2p_service.d.ts +19 -21
- package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
- package/dest/services/libp2p/libp2p_service.js +147 -148
- package/dest/services/peer-manager/peer_manager.d.ts +1 -1
- package/dest/services/peer-manager/peer_manager.d.ts.map +1 -1
- package/dest/services/peer-manager/peer_manager.js +1 -0
- package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts +1 -8
- package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts.map +1 -1
- package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.js +35 -44
- package/dest/services/reqresp/batch-tx-requester/interface.d.ts +8 -5
- package/dest/services/reqresp/batch-tx-requester/interface.d.ts.map +1 -1
- package/dest/services/reqresp/batch-tx-requester/tx_validator.d.ts +5 -14
- package/dest/services/reqresp/batch-tx-requester/tx_validator.d.ts.map +1 -1
- package/dest/services/reqresp/batch-tx-requester/tx_validator.js +6 -20
- package/dest/services/reqresp/interface.d.ts +3 -10
- package/dest/services/reqresp/interface.d.ts.map +1 -1
- package/dest/services/reqresp/interface.js +0 -9
- package/dest/services/reqresp/protocols/block_txs/bitvector.d.ts +4 -3
- package/dest/services/reqresp/protocols/block_txs/bitvector.d.ts.map +1 -1
- package/dest/services/reqresp/protocols/block_txs/bitvector.js +3 -2
- package/dest/services/reqresp/protocols/block_txs/block_txs_handler.d.ts +1 -1
- package/dest/services/reqresp/protocols/block_txs/block_txs_handler.d.ts.map +1 -1
- package/dest/services/reqresp/protocols/block_txs/block_txs_handler.js +31 -28
- package/dest/services/reqresp/protocols/block_txs/block_txs_reqresp.d.ts +12 -7
- package/dest/services/reqresp/protocols/block_txs/block_txs_reqresp.d.ts.map +1 -1
- package/dest/services/reqresp/protocols/block_txs/block_txs_reqresp.js +42 -24
- package/dest/services/reqresp/reqresp.d.ts +5 -29
- package/dest/services/reqresp/reqresp.d.ts.map +1 -1
- package/dest/services/reqresp/reqresp.js +16 -206
- package/dest/services/service.d.ts +7 -13
- package/dest/services/service.d.ts.map +1 -1
- package/dest/services/tx_collection/config.d.ts +2 -23
- package/dest/services/tx_collection/config.d.ts.map +1 -1
- package/dest/services/tx_collection/config.js +2 -55
- package/dest/services/tx_collection/file_store_tx_collection.d.ts +12 -28
- package/dest/services/tx_collection/file_store_tx_collection.d.ts.map +1 -1
- package/dest/services/tx_collection/file_store_tx_collection.js +43 -83
- package/dest/services/tx_collection/index.d.ts +2 -3
- package/dest/services/tx_collection/index.d.ts.map +1 -1
- package/dest/services/tx_collection/index.js +0 -1
- package/dest/services/tx_collection/instrumentation.d.ts +1 -1
- package/dest/services/tx_collection/instrumentation.d.ts.map +1 -1
- package/dest/services/tx_collection/instrumentation.js +0 -2
- package/dest/services/tx_collection/tx_collection.d.ts +35 -51
- package/dest/services/tx_collection/tx_collection.d.ts.map +1 -1
- package/dest/services/tx_collection/tx_collection.js +275 -119
- package/dest/services/tx_collection/tx_collection_sink.d.ts +1 -1
- package/dest/services/tx_collection/tx_collection_sink.js +2 -2
- package/dest/services/tx_file_store/tx_file_store.d.ts +1 -3
- package/dest/services/tx_file_store/tx_file_store.d.ts.map +1 -1
- package/dest/services/tx_file_store/tx_file_store.js +4 -14
- package/dest/services/tx_provider.d.ts +3 -1
- package/dest/services/tx_provider.d.ts.map +1 -1
- package/dest/services/tx_provider.js +3 -0
- package/dest/test-helpers/make-test-p2p-clients.d.ts +1 -1
- package/dest/test-helpers/make-test-p2p-clients.d.ts.map +1 -1
- package/dest/test-helpers/make-test-p2p-clients.js +1 -1
- package/dest/test-helpers/mock-pubsub.d.ts +13 -7
- package/dest/test-helpers/mock-pubsub.d.ts.map +1 -1
- package/dest/test-helpers/mock-pubsub.js +12 -37
- package/dest/test-helpers/reqresp-nodes.d.ts +4 -5
- package/dest/test-helpers/reqresp-nodes.d.ts.map +1 -1
- package/dest/test-helpers/reqresp-nodes.js +11 -15
- package/dest/test-helpers/test_tx_provider.d.ts +3 -1
- package/dest/test-helpers/test_tx_provider.d.ts.map +1 -1
- package/dest/test-helpers/test_tx_provider.js +3 -0
- package/dest/test-helpers/testbench-utils.d.ts +12 -14
- package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
- package/dest/test-helpers/testbench-utils.js +19 -14
- package/dest/testbench/p2p_client_testbench_worker.d.ts +3 -5
- package/dest/testbench/p2p_client_testbench_worker.d.ts.map +1 -1
- package/dest/testbench/p2p_client_testbench_worker.js +13 -24
- package/dest/testbench/worker_client_manager.d.ts +3 -6
- package/dest/testbench/worker_client_manager.d.ts.map +1 -1
- package/dest/testbench/worker_client_manager.js +2 -8
- package/dest/util.d.ts +11 -6
- package/dest/util.d.ts.map +1 -1
- package/dest/util.js +35 -14
- package/dest/versioning.d.ts +3 -6
- package/dest/versioning.d.ts.map +1 -1
- package/dest/versioning.js +3 -24
- package/package.json +15 -14
- package/src/bootstrap/bootstrap.ts +9 -1
- package/src/client/factory.ts +20 -5
- package/src/client/interface.ts +6 -10
- package/src/client/p2p_client.ts +61 -36
- package/src/client/test/{tx_proposal_collector/README.md → p2p_client.batch_tx_requester.bench.README.md} +23 -53
- package/src/config.ts +94 -6
- package/src/errors/reqresp.error.ts +0 -25
- package/src/mem_pools/attestation_pool/attestation_pool.ts +309 -238
- package/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts +198 -62
- package/src/mem_pools/attestation_pool/mocks.ts +13 -8
- package/src/mem_pools/instrumentation.ts +5 -1
- package/src/mem_pools/tx_pool_v2/archive/tx_archive.ts +1 -9
- package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +73 -90
- package/src/msg_validators/attestation_validator/attestation_validator.ts +21 -1
- package/src/msg_validators/attestation_validator/fisherman_attestation_validator.ts +11 -7
- package/src/msg_validators/clock_tolerance.ts +13 -12
- package/src/msg_validators/proposal_validator/block_proposal_validator.ts +9 -2
- package/src/msg_validators/proposal_validator/checkpoint_proposal_validator.ts +14 -2
- package/src/msg_validators/proposal_validator/proposal_validator.ts +39 -2
- package/src/msg_validators/tx_validator/aggregate_tx_validator.ts +3 -12
- package/src/msg_validators/tx_validator/allowed_public_setup.ts +3 -3
- package/src/msg_validators/tx_validator/cached_tx_validator.ts +31 -0
- package/src/msg_validators/tx_validator/data_validator.ts +2 -0
- package/src/msg_validators/tx_validator/factory.ts +15 -6
- package/src/msg_validators/tx_validator/index.ts +2 -0
- package/src/msg_validators/tx_validator/tx_proof_validator.ts +2 -0
- package/src/msg_validators/tx_validator/tx_validation_cache.ts +101 -0
- package/src/services/discv5/discV5_service.ts +38 -5
- package/src/services/dummy_service.ts +12 -43
- package/src/services/libp2p/libp2p_service.ts +169 -167
- package/src/services/peer-manager/peer_manager.ts +1 -0
- package/src/services/reqresp/README.md +0 -14
- package/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts +39 -52
- package/src/services/reqresp/batch-tx-requester/interface.ts +11 -4
- package/src/services/reqresp/batch-tx-requester/tx_validator.ts +12 -25
- package/src/services/reqresp/interface.ts +2 -38
- package/src/services/reqresp/protocols/block_txs/bitvector.ts +3 -2
- package/src/services/reqresp/protocols/block_txs/block_txs_handler.ts +34 -30
- package/src/services/reqresp/protocols/block_txs/block_txs_reqresp.ts +47 -20
- package/src/services/reqresp/reqresp.ts +11 -251
- package/src/services/service.ts +9 -30
- package/src/services/tx_collection/config.ts +3 -80
- package/src/services/tx_collection/file_store_tx_collection.ts +54 -103
- package/src/services/tx_collection/index.ts +1 -6
- package/src/services/tx_collection/instrumentation.ts +1 -7
- package/src/services/tx_collection/tx_collection.ts +330 -173
- package/src/services/tx_collection/tx_collection_sink.ts +2 -2
- package/src/services/tx_file_store/tx_file_store.ts +5 -17
- package/src/services/tx_provider.ts +5 -0
- package/src/test-helpers/make-test-p2p-clients.ts +1 -0
- package/src/test-helpers/mock-pubsub.ts +17 -60
- package/src/test-helpers/reqresp-nodes.ts +8 -20
- package/src/test-helpers/test_tx_provider.ts +5 -0
- package/src/test-helpers/testbench-utils.ts +23 -27
- package/src/testbench/p2p_client_testbench_worker.ts +16 -42
- package/src/testbench/worker_client_manager.ts +4 -19
- package/src/util.ts +32 -17
- package/src/versioning.ts +3 -33
- package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker.d.ts +0 -2
- package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker.d.ts.map +0 -1
- package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker.js +0 -318
- package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker_protocol.d.ts +0 -73
- package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker_protocol.d.ts.map +0 -1
- package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker_protocol.js +0 -8
- package/dest/services/reqresp/connection-sampler/batch_connection_sampler.d.ts +0 -64
- package/dest/services/reqresp/connection-sampler/batch_connection_sampler.d.ts.map +0 -1
- package/dest/services/reqresp/connection-sampler/batch_connection_sampler.js +0 -151
- package/dest/services/tx_collection/fast_tx_collection.d.ts +0 -51
- package/dest/services/tx_collection/fast_tx_collection.d.ts.map +0 -1
- package/dest/services/tx_collection/fast_tx_collection.js +0 -311
- package/dest/services/tx_collection/proposal_tx_collector.d.ts +0 -48
- package/dest/services/tx_collection/proposal_tx_collector.d.ts.map +0 -1
- package/dest/services/tx_collection/proposal_tx_collector.js +0 -50
- package/dest/services/tx_collection/slow_tx_collection.d.ts +0 -57
- package/dest/services/tx_collection/slow_tx_collection.d.ts.map +0 -1
- package/dest/services/tx_collection/slow_tx_collection.js +0 -211
- package/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts +0 -357
- package/src/client/test/tx_proposal_collector/proposal_tx_collector_worker_protocol.ts +0 -43
- package/src/services/reqresp/connection-sampler/batch_connection_sampler.ts +0 -161
- package/src/services/tx_collection/fast_tx_collection.ts +0 -361
- package/src/services/tx_collection/proposal_tx_collector.ts +0 -108
- package/src/services/tx_collection/slow_tx_collection.ts +0 -266
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
1
|
+
import type { BlockProposalHash, CheckpointProposalHash, SlotNumber } from '@aztec/foundation/branded-types';
|
|
3
2
|
import { toArray } from '@aztec/foundation/iterable';
|
|
4
3
|
import { createLogger } from '@aztec/foundation/log';
|
|
5
4
|
import type { AztecAsyncKVStore, AztecAsyncMap, AztecAsyncMultiMap } from '@aztec/kv-store';
|
|
@@ -15,17 +14,22 @@ import { PoolInstrumentation, PoolName, type PoolStatsCallback } from '../instru
|
|
|
15
14
|
|
|
16
15
|
/** Result of trying to add an item (proposal or attestation) to the pool */
|
|
17
16
|
export type TryAddResult = {
|
|
18
|
-
/** Whether the item was
|
|
17
|
+
/** Whether the item was accepted into pool state. False when it already existed, was invalid, or hit a cap. */
|
|
19
18
|
added: boolean;
|
|
20
|
-
/** Whether the exact
|
|
19
|
+
/** Whether the exact signed payload (matched by payload hash) already existed in the pool. */
|
|
21
20
|
alreadyExists: boolean;
|
|
22
|
-
/**
|
|
23
|
-
* - tryAddBlockProposal:
|
|
24
|
-
* - tryAddCheckpointProposal:
|
|
25
|
-
* - tryAddCheckpointAttestation:
|
|
21
|
+
/** Number of distinct signed-payload hashes seen for the position. Meaning varies by method:
|
|
22
|
+
* - tryAddBlockProposal: distinct payload hashes at (slot, indexWithinCheckpoint)
|
|
23
|
+
* - tryAddCheckpointProposal: distinct payload hashes at slot
|
|
24
|
+
* - tryAddCheckpointAttestation: distinct payload hashes by this signer for this slot */
|
|
26
25
|
count: number;
|
|
27
26
|
};
|
|
28
27
|
|
|
28
|
+
export type ProposalsForSlot = {
|
|
29
|
+
blockProposals: BlockProposal[];
|
|
30
|
+
checkpointProposals: CheckpointProposalCore[];
|
|
31
|
+
};
|
|
32
|
+
|
|
29
33
|
export const MAX_CHECKPOINT_PROPOSALS_PER_SLOT = 2;
|
|
30
34
|
export const MAX_BLOCK_PROPOSALS_PER_POSITION = 2;
|
|
31
35
|
/** Maximum attestations a single signer can make per slot before being rejected. */
|
|
@@ -35,7 +39,8 @@ export const MAX_ATTESTATIONS_PER_SLOT_AND_SIGNER = 2;
|
|
|
35
39
|
export type AttestationPoolApi = Pick<
|
|
36
40
|
AttestationPool,
|
|
37
41
|
| 'tryAddBlockProposal'
|
|
38
|
-
| '
|
|
42
|
+
| 'getBlockProposalByArchive'
|
|
43
|
+
| 'getProposalsForSlot'
|
|
39
44
|
| 'tryAddCheckpointProposal'
|
|
40
45
|
| 'getCheckpointProposal'
|
|
41
46
|
| 'addOwnCheckpointAttestations'
|
|
@@ -52,31 +57,45 @@ export type AttestationPoolApi = Pick<
|
|
|
52
57
|
*
|
|
53
58
|
* Attestations and proposals observed via the p2p network are stored for requests
|
|
54
59
|
* from the validator to produce a block, or to serve to other peers.
|
|
60
|
+
*
|
|
61
|
+
* Equivocation detection: distinct *signed payload hashes* arriving at the same
|
|
62
|
+
* position are tracked in the matching index multimap so the equivocation count
|
|
63
|
+
* reaches 2 even when archive collides on `feeAssetPriceModifier` variants.
|
|
64
|
+
* Proposal bytes are retained per accepted payload hash, up to the same equivocation
|
|
65
|
+
* caps, for slashing watchers that need signed P2P proposals.
|
|
55
66
|
*/
|
|
56
67
|
export class AttestationPool {
|
|
57
68
|
private metrics: PoolInstrumentation<CheckpointAttestation>;
|
|
58
69
|
|
|
59
|
-
// Checkpoint attestations from
|
|
60
|
-
//
|
|
61
|
-
|
|
70
|
+
// Checkpoint attestations from `${paddedSlot}-${signer}` to serialized CheckpointAttestation.
|
|
71
|
+
// Stores the first attestation seen per (slot, signer); subsequent distinct payload
|
|
72
|
+
// hashes from the same signer are tracked only in `attestationHashesPerSlotAndSigner`
|
|
73
|
+
// for equivocation detection.
|
|
74
|
+
private attestationPerSlotAndSigner: AztecAsyncMap<string, Buffer>;
|
|
62
75
|
|
|
63
|
-
//
|
|
64
|
-
|
|
76
|
+
// Distinct payload hashes seen per (slot, signer) for tracking attestation equivocations.
|
|
77
|
+
// Key: `${paddedSlot}-${signerAddress}`, Value: CheckpointProposalHash (`0x`-prefixed hex)
|
|
78
|
+
private attestationHashesPerSlotAndSigner: AztecAsyncMultiMap<string, CheckpointProposalHash>;
|
|
65
79
|
|
|
66
|
-
// Checkpoint proposals
|
|
67
|
-
//
|
|
68
|
-
private
|
|
80
|
+
// Checkpoint proposals from `${paddedSlot}-${payloadHash}` to serialized CheckpointProposalCore.
|
|
81
|
+
// Stores every accepted distinct payload up to MAX_CHECKPOINT_PROPOSALS_PER_SLOT.
|
|
82
|
+
private checkpointProposalsPerSlotAndHash: AztecAsyncMap<string, Buffer>;
|
|
69
83
|
|
|
70
|
-
//
|
|
71
|
-
|
|
84
|
+
// Distinct payload hashes seen per slot. Hash collision = duplicate.
|
|
85
|
+
// Hash count reaching 2 = equivocation.
|
|
86
|
+
// Key: slot number, Value: CheckpointProposalHash (`0x`-prefixed hex)
|
|
87
|
+
private checkpointProposalHashesPerSlot: AztecAsyncMultiMap<number, CheckpointProposalHash>;
|
|
72
88
|
|
|
73
|
-
// Block proposals
|
|
74
|
-
//
|
|
75
|
-
private
|
|
89
|
+
// Block proposals from `${paddedSlot}-${paddedIndex}-${payloadHash}` to serialized BlockProposal.
|
|
90
|
+
// Stores every accepted distinct payload up to MAX_BLOCK_PROPOSALS_PER_POSITION.
|
|
91
|
+
private blockProposalsPerSlotIndexAndHash: AztecAsyncMap<string, Buffer>;
|
|
76
92
|
|
|
77
|
-
//
|
|
78
|
-
// Key:
|
|
79
|
-
private
|
|
93
|
+
// Distinct payload hashes seen per (slot, indexWithinCheckpoint).
|
|
94
|
+
// Key: slot * (1 << INDEX_BITS) + indexWithinCheckpoint, Value: BlockProposalHash (`0x`-prefixed hex)
|
|
95
|
+
private blockProposalHashesPerSlotAndIndex: AztecAsyncMultiMap<number, BlockProposalHash>;
|
|
96
|
+
|
|
97
|
+
// Secondary index from archive root to all retained block proposal keys.
|
|
98
|
+
private blockProposalKeysPerArchive: AztecAsyncMultiMap<string, string>;
|
|
80
99
|
|
|
81
100
|
constructor(
|
|
82
101
|
private store: AztecAsyncKVStore,
|
|
@@ -84,75 +103,93 @@ export class AttestationPool {
|
|
|
84
103
|
private log = createLogger('aztec:attestation_pool'),
|
|
85
104
|
) {
|
|
86
105
|
// Initialize block proposal storage
|
|
87
|
-
this.
|
|
88
|
-
this.
|
|
106
|
+
this.blockProposalsPerSlotIndexAndHash = store.openMap('block_proposals_by_slot_index_and_hash');
|
|
107
|
+
this.blockProposalHashesPerSlotAndIndex = store.openMultiMap('block_proposals_for_slot_and_index');
|
|
108
|
+
this.blockProposalKeysPerArchive = store.openMultiMap('block_proposals_by_archive');
|
|
89
109
|
|
|
90
110
|
// Initialize checkpoint attestations storage
|
|
91
|
-
this.
|
|
92
|
-
this.
|
|
111
|
+
this.attestationPerSlotAndSigner = store.openMap('checkpoint_attestations');
|
|
112
|
+
this.attestationHashesPerSlotAndSigner = store.openMultiMap('checkpoint_attestations_per_slot_and_signer');
|
|
93
113
|
|
|
94
114
|
// Initialize checkpoint proposal storage
|
|
95
|
-
this.
|
|
96
|
-
this.
|
|
115
|
+
this.checkpointProposalsPerSlotAndHash = store.openMap('checkpoint_proposals_by_slot_and_hash');
|
|
116
|
+
this.checkpointProposalHashesPerSlot = store.openMultiMap('checkpoint_proposals_for_slot');
|
|
97
117
|
|
|
98
118
|
this.metrics = new PoolInstrumentation(telemetry, PoolName.ATTESTATION_POOL, this.poolStats);
|
|
99
119
|
}
|
|
100
120
|
|
|
101
121
|
private poolStats: PoolStatsCallback = async () => {
|
|
102
122
|
return {
|
|
103
|
-
itemCount: await this.
|
|
123
|
+
itemCount: await this.attestationPerSlotAndSigner.sizeAsync(),
|
|
104
124
|
};
|
|
105
125
|
};
|
|
106
126
|
|
|
107
127
|
/** Returns whether the pool is empty. */
|
|
108
128
|
public async isEmpty(): Promise<boolean> {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return
|
|
129
|
+
const [attestationCount, blockProposalCount, checkpointProposalCount] = await Promise.all([
|
|
130
|
+
this.attestationPerSlotAndSigner.sizeAsync(),
|
|
131
|
+
this.blockProposalsPerSlotIndexAndHash.sizeAsync(),
|
|
132
|
+
this.checkpointProposalsPerSlotAndHash.sizeAsync(),
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
return attestationCount === 0 && blockProposalCount === 0 && checkpointProposalCount === 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Number of bits reserved for indexWithinCheckpoint in position keys. */
|
|
139
|
+
private static readonly INDEX_BITS = 10;
|
|
140
|
+
/** Maximum indexWithinCheckpoint value (2^10 - 1 = 1023). */
|
|
141
|
+
private static readonly MAX_INDEX = (1 << AttestationPool.INDEX_BITS) - 1;
|
|
142
|
+
/** Decimal digits used to left-pad slot numbers in string keys.
|
|
143
|
+
* 10 digits ≈ 3500 years at 36 s/slot, leaving ample headroom. */
|
|
144
|
+
private static readonly SLOT_PAD_DIGITS = 10;
|
|
145
|
+
|
|
146
|
+
/** Fixed-width decimal slot string for use in composite string keys. */
|
|
147
|
+
private slotPaddedKey(slot: SlotNumber | number): string {
|
|
148
|
+
return slot.toString().padStart(AttestationPool.SLOT_PAD_DIGITS, '0');
|
|
116
149
|
}
|
|
117
150
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
? proposalId
|
|
123
|
-
: Buffer.isBuffer(proposalId)
|
|
124
|
-
? Fr.fromBuffer(proposalId).toString()
|
|
125
|
-
: proposalId.toString();
|
|
151
|
+
/** Fixed-width decimal index string for use in composite string keys. */
|
|
152
|
+
private indexPaddedKey(indexWithinCheckpoint: number): string {
|
|
153
|
+
return indexWithinCheckpoint.toString().padStart(4, '0');
|
|
154
|
+
}
|
|
126
155
|
|
|
127
|
-
|
|
156
|
+
/** Key for retained block proposals. */
|
|
157
|
+
private getBlockProposalKey(
|
|
158
|
+
slot: SlotNumber | number,
|
|
159
|
+
indexWithinCheckpoint: number,
|
|
160
|
+
payloadHash: BlockProposalHash,
|
|
161
|
+
): string {
|
|
162
|
+
return `${this.slotPaddedKey(slot)}-${this.indexPaddedKey(indexWithinCheckpoint)}-${payloadHash}`;
|
|
128
163
|
}
|
|
129
164
|
|
|
130
|
-
|
|
131
|
-
|
|
165
|
+
/** Range bounds for all retained block proposals in a slot. */
|
|
166
|
+
private getBlockProposalKeyRangeForSlot(slot: SlotNumber): { start: string; end: string } {
|
|
167
|
+
return { start: `${this.slotPaddedKey(slot)}-`, end: `${this.slotPaddedKey(slot + 1)}-` };
|
|
132
168
|
}
|
|
133
169
|
|
|
134
|
-
/**
|
|
135
|
-
private
|
|
136
|
-
|
|
137
|
-
return { start: `${slotStr}-`, end: `${slotStr}-Z` }; // 'Z' sorts after any hex character
|
|
170
|
+
/** Key for retained checkpoint proposals. */
|
|
171
|
+
private getCheckpointProposalKey(slot: SlotNumber | number, payloadHash: CheckpointProposalHash): string {
|
|
172
|
+
return `${this.slotPaddedKey(slot)}-${payloadHash}`;
|
|
138
173
|
}
|
|
139
174
|
|
|
140
|
-
/**
|
|
141
|
-
private
|
|
142
|
-
|
|
143
|
-
return { start: `${proposalKey}-`, end: `${proposalKey}-Z` };
|
|
175
|
+
/** Range bounds for all retained checkpoint proposals in a slot. */
|
|
176
|
+
private getCheckpointProposalKeyRangeForSlot(slot: SlotNumber): { start: string; end: string } {
|
|
177
|
+
return { start: `${this.slotPaddedKey(slot)}-`, end: `${this.slotPaddedKey(slot + 1)}-` };
|
|
144
178
|
}
|
|
145
179
|
|
|
146
|
-
/**
|
|
180
|
+
/** Key for the per-(slot, signer) attestation main store and equivocation index. */
|
|
147
181
|
private getSlotSignerKey(slot: SlotNumber, signerAddress: string): string {
|
|
148
|
-
|
|
149
|
-
return `${slotStr}-${signerAddress}`;
|
|
182
|
+
return `${this.slotPaddedKey(slot)}-${signerAddress}`;
|
|
150
183
|
}
|
|
151
184
|
|
|
152
|
-
/**
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Returns range bounds for querying all attestations for a given slot.
|
|
187
|
+
* Fixed-width padding ensures the slot prefix sorts cleanly, so using the next
|
|
188
|
+
* slot's prefix as the upper bound captures exactly the current slot's entries.
|
|
189
|
+
*/
|
|
190
|
+
private getAttestationKeyRangeForSlot(slot: SlotNumber): { start: string; end: string } {
|
|
191
|
+
return { start: `${this.slotPaddedKey(slot)}-`, end: `${this.slotPaddedKey(slot + 1)}-` };
|
|
192
|
+
}
|
|
156
193
|
|
|
157
194
|
/** Creates a position key for block proposals: slot * 1024 + indexWithinCheckpoint.
|
|
158
195
|
* Uses multiplication instead of bit-shift to avoid 32-bit signed integer overflow
|
|
@@ -166,48 +203,59 @@ export class AttestationPool {
|
|
|
166
203
|
return slot * (1 << AttestationPool.INDEX_BITS) + indexWithinCheckpoint;
|
|
167
204
|
}
|
|
168
205
|
|
|
206
|
+
/** Returns true if the multimap already contains the given value for the given key. */
|
|
207
|
+
private async multimapHasValue<TKey extends number | string, TValue extends string>(
|
|
208
|
+
map: AztecAsyncMultiMap<TKey, TValue>,
|
|
209
|
+
key: TKey,
|
|
210
|
+
value: TValue,
|
|
211
|
+
): Promise<boolean> {
|
|
212
|
+
const values = await toArray(map.getValuesAsync(key));
|
|
213
|
+
return values.includes(value);
|
|
214
|
+
}
|
|
215
|
+
|
|
169
216
|
/**
|
|
170
217
|
* Attempts to add a block proposal to the pool.
|
|
171
218
|
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* -
|
|
175
|
-
*
|
|
219
|
+
* - Detects duplicates by signed-payload hash (not archive); a re-broadcast of the
|
|
220
|
+
* exact same signed payload returns `alreadyExists: true`.
|
|
221
|
+
* - Distinct payload hashes at the same `(slot, indexWithinCheckpoint)` are tracked
|
|
222
|
+
* in the equivocation index and retained up to the cap.
|
|
176
223
|
*
|
|
177
224
|
* @param blockProposal - The block proposal to add
|
|
178
225
|
* @returns Result indicating whether the proposal was added and duplicate detection info
|
|
179
226
|
*/
|
|
180
227
|
public async tryAddBlockProposal(blockProposal: BlockProposal): Promise<TryAddResult> {
|
|
181
228
|
return await this.store.transactionAsync(async () => {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (
|
|
187
|
-
const count = await this.
|
|
188
|
-
blockProposal.slotNumber,
|
|
189
|
-
blockProposal.indexWithinCheckpoint,
|
|
190
|
-
);
|
|
229
|
+
const positionKey = this.getBlockPositionKey(blockProposal.slotNumber, blockProposal.indexWithinCheckpoint);
|
|
230
|
+
const payloadHash = blockProposal.getPayloadHash();
|
|
231
|
+
|
|
232
|
+
// Hash already tracked => exact same signed payload was already received.
|
|
233
|
+
if (await this.multimapHasValue(this.blockProposalHashesPerSlotAndIndex, positionKey, payloadHash)) {
|
|
234
|
+
const count = await this.blockProposalHashesPerSlotAndIndex.getValueCountAsync(positionKey);
|
|
191
235
|
return { added: false, alreadyExists: true, count };
|
|
192
236
|
}
|
|
193
237
|
|
|
194
|
-
//
|
|
195
|
-
const count = await this.
|
|
196
|
-
blockProposal.slotNumber,
|
|
197
|
-
blockProposal.indexWithinCheckpoint,
|
|
198
|
-
);
|
|
199
|
-
|
|
238
|
+
// Cap reached for this position (no more new payload hashes accepted).
|
|
239
|
+
const count = await this.blockProposalHashesPerSlotAndIndex.getValueCountAsync(positionKey);
|
|
200
240
|
if (count >= MAX_BLOCK_PROPOSALS_PER_POSITION) {
|
|
201
241
|
return { added: false, alreadyExists: false, count };
|
|
202
242
|
}
|
|
203
243
|
|
|
204
|
-
//
|
|
205
|
-
await this.
|
|
244
|
+
// Track the new payload hash for equivocation detection.
|
|
245
|
+
await this.blockProposalHashesPerSlotAndIndex.set(positionKey, payloadHash);
|
|
246
|
+
const proposalKey = this.getBlockProposalKey(
|
|
247
|
+
blockProposal.slotNumber,
|
|
248
|
+
blockProposal.indexWithinCheckpoint,
|
|
249
|
+
payloadHash,
|
|
250
|
+
);
|
|
251
|
+
await this.blockProposalsPerSlotIndexAndHash.set(proposalKey, blockProposal.withoutSignedTxs().toBuffer());
|
|
252
|
+
await this.blockProposalKeysPerArchive.set(blockProposal.archive.toString(), proposalKey);
|
|
206
253
|
|
|
207
254
|
this.log.debug(
|
|
208
255
|
`Added block proposal for slot ${blockProposal.slotNumber} and index ${blockProposal.indexWithinCheckpoint}`,
|
|
209
256
|
{
|
|
210
|
-
|
|
257
|
+
archive: blockProposal.archive.toString(),
|
|
258
|
+
payloadHash,
|
|
211
259
|
slotNumber: blockProposal.slotNumber,
|
|
212
260
|
indexWithinCheckpoint: blockProposal.indexWithinCheckpoint,
|
|
213
261
|
},
|
|
@@ -217,60 +265,77 @@ export class AttestationPool {
|
|
|
217
265
|
});
|
|
218
266
|
}
|
|
219
267
|
|
|
220
|
-
/** Gets the count of block proposals for a given position (slot, indexWithinCheckpoint). */
|
|
221
|
-
private getBlockProposalCountForPosition(
|
|
222
|
-
slot: SlotNumber,
|
|
223
|
-
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
224
|
-
): Promise<number> {
|
|
225
|
-
const positionKey = this.getBlockPositionKey(slot, indexWithinCheckpoint);
|
|
226
|
-
return this.blockProposalsForSlotAndIndex.getValueCountAsync(positionKey);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** Internal method - must be called within a transaction. */
|
|
230
|
-
private async addBlockProposal(blockProposal: BlockProposal): Promise<void> {
|
|
231
|
-
const proposalId = blockProposal.archive.toString();
|
|
232
|
-
// Strip signedTxs before storing to avoid persisting full tx data
|
|
233
|
-
await this.blockProposals.set(proposalId, blockProposal.withoutSignedTxs().toBuffer());
|
|
234
|
-
|
|
235
|
-
// Index by slot and position for duplicate detection
|
|
236
|
-
const positionKey = this.getBlockPositionKey(blockProposal.slotNumber, blockProposal.indexWithinCheckpoint);
|
|
237
|
-
await this.blockProposalsForSlotAndIndex.set(positionKey, proposalId);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
268
|
/**
|
|
241
|
-
* Get block proposal by
|
|
269
|
+
* Get block proposal by archive root.
|
|
242
270
|
*
|
|
243
|
-
*
|
|
271
|
+
* Resolves the archive root through the archive index and returns the first
|
|
272
|
+
* retained proposal for that archive. This lookup is used by block-txs req/resp,
|
|
273
|
+
* where any retained proposal for the requested archive gives the tx hash list.
|
|
244
274
|
*
|
|
245
|
-
* @
|
|
275
|
+
* @param archiveRoot - The archive root to look up
|
|
276
|
+
* @return The block proposal if it exists and its archive matches, otherwise undefined.
|
|
246
277
|
*/
|
|
247
|
-
public async
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
if (buffer
|
|
251
|
-
|
|
278
|
+
public async getBlockProposalByArchive(archiveRoot: string): Promise<BlockProposal | undefined> {
|
|
279
|
+
for await (const proposalKey of this.blockProposalKeysPerArchive.getValuesAsync(archiveRoot)) {
|
|
280
|
+
const buffer = await this.blockProposalsPerSlotIndexAndHash.getAsync(proposalKey);
|
|
281
|
+
if (!buffer || buffer.length === 0) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const proposal = BlockProposal.fromBuffer(buffer);
|
|
286
|
+
if (proposal.archive.toString() === archiveRoot) {
|
|
287
|
+
return proposal;
|
|
288
|
+
}
|
|
289
|
+
} catch {
|
|
290
|
+
continue;
|
|
252
291
|
}
|
|
253
|
-
} catch {
|
|
254
|
-
return undefined;
|
|
255
292
|
}
|
|
256
|
-
|
|
257
293
|
return undefined;
|
|
258
294
|
}
|
|
259
295
|
|
|
296
|
+
/** Returns retained signed proposals for a slot. */
|
|
297
|
+
public async getProposalsForSlot(slot: SlotNumber): Promise<ProposalsForSlot> {
|
|
298
|
+
const blockProposals: BlockProposal[] = [];
|
|
299
|
+
const checkpointProposals: CheckpointProposalCore[] = [];
|
|
300
|
+
|
|
301
|
+
for await (const [_, buffer] of this.blockProposalsPerSlotIndexAndHash.entriesAsync(
|
|
302
|
+
this.getBlockProposalKeyRangeForSlot(slot),
|
|
303
|
+
)) {
|
|
304
|
+
try {
|
|
305
|
+
blockProposals.push(BlockProposal.fromBuffer(buffer));
|
|
306
|
+
} catch {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for await (const [_, buffer] of this.checkpointProposalsPerSlotAndHash.entriesAsync(
|
|
312
|
+
this.getCheckpointProposalKeyRangeForSlot(slot),
|
|
313
|
+
)) {
|
|
314
|
+
try {
|
|
315
|
+
checkpointProposals.push(CheckpointProposal.fromBuffer(buffer));
|
|
316
|
+
} catch {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return { blockProposals, checkpointProposals };
|
|
322
|
+
}
|
|
323
|
+
|
|
260
324
|
/** Checks if any block proposals exist for a given slot (at index 0). */
|
|
261
325
|
public async hasBlockProposalsForSlot(slot: SlotNumber): Promise<boolean> {
|
|
262
326
|
const positionKey = this.getBlockPositionKey(slot, 0);
|
|
263
|
-
const count = await this.
|
|
327
|
+
const count = await this.blockProposalHashesPerSlotAndIndex.getValueCountAsync(positionKey);
|
|
264
328
|
return count > 0;
|
|
265
329
|
}
|
|
266
330
|
|
|
267
331
|
/**
|
|
268
332
|
* Attempts to add a checkpoint proposal to the pool.
|
|
269
333
|
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* -
|
|
273
|
-
*
|
|
334
|
+
* - Detects duplicates by signed-payload hash (not archive); a re-broadcast of the
|
|
335
|
+
* exact same signed payload returns `alreadyExists: true`.
|
|
336
|
+
* - Distinct payload hashes at the same slot are tracked in the equivocation index.
|
|
337
|
+
* Distinct payload bytes are retained up to the same cap so slashing watchers
|
|
338
|
+
* can recover signed proposals.
|
|
274
339
|
*
|
|
275
340
|
* Note: This method only handles the CheckpointProposalCore. If the original
|
|
276
341
|
* CheckpointProposal contains a lastBlock, the caller should extract it via
|
|
@@ -280,62 +345,59 @@ export class AttestationPool {
|
|
|
280
345
|
* @returns Result indicating whether the proposal was added and duplicate detection info
|
|
281
346
|
*/
|
|
282
347
|
public async tryAddCheckpointProposal(proposal: CheckpointProposalCore): Promise<TryAddResult> {
|
|
283
|
-
|
|
284
|
-
const
|
|
348
|
+
return await this.store.transactionAsync(async () => {
|
|
349
|
+
const slot = proposal.slotNumber;
|
|
350
|
+
const payloadHash = proposal.getPayloadHash();
|
|
285
351
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (alreadyExists) {
|
|
289
|
-
const count = await this.checkpointProposalsForSlot.getValueCountAsync(proposal.slotNumber);
|
|
352
|
+
if (await this.multimapHasValue(this.checkpointProposalHashesPerSlot, slot, payloadHash)) {
|
|
353
|
+
const count = await this.checkpointProposalHashesPerSlot.getValueCountAsync(slot);
|
|
290
354
|
return { added: false, alreadyExists: true, count };
|
|
291
355
|
}
|
|
292
356
|
|
|
293
|
-
|
|
294
|
-
const count = await this.checkpointProposalsForSlot.getValueCountAsync(proposal.slotNumber);
|
|
357
|
+
const count = await this.checkpointProposalHashesPerSlot.getValueCountAsync(slot);
|
|
295
358
|
if (count >= MAX_CHECKPOINT_PROPOSALS_PER_SLOT) {
|
|
296
359
|
return { added: false, alreadyExists: false, count };
|
|
297
360
|
}
|
|
298
361
|
|
|
299
|
-
//
|
|
300
|
-
await this.
|
|
362
|
+
// Track the new payload hash for equivocation detection.
|
|
363
|
+
await this.checkpointProposalHashesPerSlot.set(slot, payloadHash);
|
|
364
|
+
await this.checkpointProposalsPerSlotAndHash.set(
|
|
365
|
+
this.getCheckpointProposalKey(slot, payloadHash),
|
|
366
|
+
proposal.toBuffer(),
|
|
367
|
+
);
|
|
301
368
|
|
|
302
|
-
this.log.debug(`Added checkpoint proposal for slot ${
|
|
303
|
-
|
|
304
|
-
|
|
369
|
+
this.log.debug(`Added checkpoint proposal for slot ${slot}`, {
|
|
370
|
+
archive: proposal.archive.toString(),
|
|
371
|
+
payloadHash,
|
|
372
|
+
slotNumber: slot,
|
|
305
373
|
});
|
|
306
374
|
|
|
307
375
|
return { added: true, alreadyExists: false, count: count + 1 };
|
|
308
376
|
});
|
|
309
|
-
|
|
310
|
-
return result;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/** Internal method - must be called within a transaction. */
|
|
314
|
-
private async addCheckpointProposal(proposal: CheckpointProposalCore): Promise<void> {
|
|
315
|
-
const slotKey = proposal.slotNumber;
|
|
316
|
-
const proposalId = proposal.archive.toString();
|
|
317
|
-
|
|
318
|
-
await this.checkpointProposalsForSlot.set(slotKey, proposalId);
|
|
319
|
-
await this.checkpointProposals.set(proposalId, proposal.toBuffer());
|
|
320
377
|
}
|
|
321
378
|
|
|
322
379
|
/**
|
|
323
|
-
* Get checkpoint proposal
|
|
380
|
+
* Get a retained checkpoint proposal stored for the given slot.
|
|
381
|
+
* If multiple proposals were retained for an equivocation, returns the lowest
|
|
382
|
+
* payload hash deterministically.
|
|
324
383
|
*
|
|
325
384
|
* Returns a CheckpointProposalCore (without lastBlock info) since the lastBlock
|
|
326
385
|
* is extracted and stored separately as a BlockProposal when added.
|
|
327
386
|
*
|
|
328
|
-
* @param
|
|
329
|
-
* @return The checkpoint proposal core if
|
|
387
|
+
* @param slot - The slot to look up
|
|
388
|
+
* @return The checkpoint proposal core if one is stored, otherwise undefined.
|
|
330
389
|
*/
|
|
331
|
-
public async getCheckpointProposal(
|
|
332
|
-
const buffer
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
390
|
+
public async getCheckpointProposal(slot: SlotNumber): Promise<CheckpointProposalCore | undefined> {
|
|
391
|
+
for await (const [_, buffer] of this.checkpointProposalsPerSlotAndHash.entriesAsync(
|
|
392
|
+
this.getCheckpointProposalKeyRangeForSlot(slot),
|
|
393
|
+
)) {
|
|
394
|
+
try {
|
|
395
|
+
if (buffer && buffer.length > 0) {
|
|
396
|
+
return CheckpointProposal.fromBuffer(buffer);
|
|
397
|
+
}
|
|
398
|
+
} catch {
|
|
399
|
+
continue;
|
|
336
400
|
}
|
|
337
|
-
} catch {
|
|
338
|
-
return undefined;
|
|
339
401
|
}
|
|
340
402
|
|
|
341
403
|
return undefined;
|
|
@@ -343,13 +405,13 @@ export class AttestationPool {
|
|
|
343
405
|
|
|
344
406
|
/**
|
|
345
407
|
* Adds own checkpoint attestations to the pool.
|
|
346
|
-
* Skips
|
|
408
|
+
* Skips per-signer cap and equivocation tracking; the caller is trusted.
|
|
409
|
+
* Each (slot, signer) gets a single stored attestation; later additions overwrite.
|
|
347
410
|
*/
|
|
348
411
|
public async addOwnCheckpointAttestations(attestations: CheckpointAttestation[]): Promise<void> {
|
|
349
412
|
await this.store.transactionAsync(async () => {
|
|
350
413
|
for (const attestation of attestations) {
|
|
351
414
|
const slotNumber = attestation.payload.header.slotNumber;
|
|
352
|
-
const proposalId = attestation.archive.toString();
|
|
353
415
|
const sender = attestation.getSender();
|
|
354
416
|
|
|
355
417
|
// Skip attestations with invalid signatures
|
|
@@ -357,22 +419,30 @@ export class AttestationPool {
|
|
|
357
419
|
this.log.warn(`Skipping own checkpoint attestation with invalid signature for slot ${slotNumber}`, {
|
|
358
420
|
signature: attestation.signature.toString(),
|
|
359
421
|
slotNumber,
|
|
360
|
-
|
|
422
|
+
archive: attestation.archive.toString(),
|
|
361
423
|
});
|
|
362
424
|
continue;
|
|
363
425
|
}
|
|
364
426
|
|
|
365
427
|
const address = sender.toString();
|
|
366
|
-
const ownKey = this.
|
|
428
|
+
const ownKey = this.getSlotSignerKey(slotNumber, address);
|
|
429
|
+
const payloadHash = attestation.getPayloadHash();
|
|
367
430
|
|
|
368
|
-
await this.
|
|
431
|
+
await this.attestationPerSlotAndSigner.set(ownKey, attestation.toBuffer());
|
|
369
432
|
this.metrics.trackMempoolItemAdded(ownKey);
|
|
370
433
|
|
|
434
|
+
// Track our own payload hash so that an equivocating attestation from another
|
|
435
|
+
// peer at the same (slot, signer) is detected as a duplicate.
|
|
436
|
+
if (!(await this.multimapHasValue(this.attestationHashesPerSlotAndSigner, ownKey, payloadHash))) {
|
|
437
|
+
await this.attestationHashesPerSlotAndSigner.set(ownKey, payloadHash);
|
|
438
|
+
}
|
|
439
|
+
|
|
371
440
|
this.log.debug(`Added own checkpoint attestation for slot ${slotNumber} from ${address}`, {
|
|
372
441
|
signature: attestation.signature.toString(),
|
|
373
442
|
slotNumber,
|
|
374
443
|
address,
|
|
375
|
-
|
|
444
|
+
archive: attestation.archive.toString(),
|
|
445
|
+
payloadHash,
|
|
376
446
|
});
|
|
377
447
|
}
|
|
378
448
|
});
|
|
@@ -381,6 +451,10 @@ export class AttestationPool {
|
|
|
381
451
|
/**
|
|
382
452
|
* Get all checkpoint attestations for a given slot.
|
|
383
453
|
*
|
|
454
|
+
* Returns one attestation per (slot, signer) — the first seen for each signer.
|
|
455
|
+
* Later equivocating attestations from the same signer are tracked in the index
|
|
456
|
+
* but their bytes are not retained.
|
|
457
|
+
*
|
|
384
458
|
* @param slot - The slot to query
|
|
385
459
|
* @return CheckpointAttestations
|
|
386
460
|
*/
|
|
@@ -388,7 +462,7 @@ export class AttestationPool {
|
|
|
388
462
|
const range = this.getAttestationKeyRangeForSlot(slot);
|
|
389
463
|
const attestations: CheckpointAttestation[] = [];
|
|
390
464
|
|
|
391
|
-
for await (const [_, buf] of this.
|
|
465
|
+
for await (const [_, buf] of this.attestationPerSlotAndSigner.entriesAsync(range)) {
|
|
392
466
|
attestations.push(CheckpointAttestation.fromBuffer(buf));
|
|
393
467
|
}
|
|
394
468
|
|
|
@@ -396,24 +470,19 @@ export class AttestationPool {
|
|
|
396
470
|
}
|
|
397
471
|
|
|
398
472
|
/**
|
|
399
|
-
* Get checkpoint attestations for slot
|
|
473
|
+
* Get checkpoint attestations for a slot whose signed payload matches the given
|
|
474
|
+
* proposal payload hash.
|
|
400
475
|
*
|
|
401
476
|
* @param slot - The slot to query
|
|
402
|
-
* @param
|
|
403
|
-
* @return CheckpointAttestations
|
|
477
|
+
* @param proposalPayloadHash - Hex-encoded keccak256 of the target proposal's signed payload
|
|
478
|
+
* @return CheckpointAttestations whose `getPayloadHash()` matches `proposalPayloadHash`
|
|
404
479
|
*/
|
|
405
480
|
public async getCheckpointAttestationsForSlotAndProposal(
|
|
406
481
|
slot: SlotNumber,
|
|
407
|
-
|
|
482
|
+
proposalPayloadHash: CheckpointProposalHash,
|
|
408
483
|
): Promise<CheckpointAttestation[]> {
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
for await (const [_, buf] of this.checkpointAttestations.entriesAsync(range)) {
|
|
413
|
-
attestations.push(CheckpointAttestation.fromBuffer(buf));
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return attestations;
|
|
484
|
+
const all = await this.getCheckpointAttestationsForSlot(slot);
|
|
485
|
+
return all.filter(att => att.getPayloadHash() === proposalPayloadHash);
|
|
417
486
|
}
|
|
418
487
|
|
|
419
488
|
/**
|
|
@@ -427,43 +496,51 @@ export class AttestationPool {
|
|
|
427
496
|
let numberOfBlockProposals = 0;
|
|
428
497
|
|
|
429
498
|
await this.store.transactionAsync(async () => {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
499
|
+
const oldestSlotPadded = this.slotPaddedKey(oldestSlot);
|
|
500
|
+
|
|
501
|
+
// Delete checkpoint attestations whose key < `${oldestSlotPadded}-`. Fixed-width
|
|
502
|
+
// decimal padding means the slot prefix sorts strictly before any key at that slot.
|
|
503
|
+
for await (const key of this.attestationPerSlotAndSigner.keysAsync({ end: `${oldestSlotPadded}-` })) {
|
|
504
|
+
await this.attestationPerSlotAndSigner.delete(key);
|
|
435
505
|
this.metrics.trackMempoolItemRemoved(key);
|
|
436
506
|
numberOfAttestations++;
|
|
437
507
|
}
|
|
438
508
|
|
|
439
|
-
// Clean up per-signer-per-slot index
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const slotSignerEndKey = new Fr(oldestSlot).toString();
|
|
443
|
-
for await (const key of this.checkpointAttestationsPerSlotAndSigner.keysAsync({ end: slotSignerEndKey })) {
|
|
444
|
-
await this.checkpointAttestationsPerSlotAndSigner.delete(key);
|
|
509
|
+
// Clean up per-signer-per-slot index using the same end bound.
|
|
510
|
+
for await (const key of this.attestationHashesPerSlotAndSigner.keysAsync({ end: `${oldestSlotPadded}-` })) {
|
|
511
|
+
await this.attestationHashesPerSlotAndSigner.delete(key);
|
|
445
512
|
}
|
|
446
513
|
|
|
447
|
-
// Delete checkpoint proposals for slots < oldestSlot
|
|
448
|
-
for await (const slot of this.
|
|
449
|
-
|
|
450
|
-
for (const proposalId of proposalIds) {
|
|
451
|
-
await this.checkpointProposals.delete(proposalId);
|
|
452
|
-
numberOfCheckpointProposals++;
|
|
453
|
-
}
|
|
454
|
-
await this.checkpointProposalsForSlot.delete(slot);
|
|
514
|
+
// Delete checkpoint proposals for slots < oldestSlot.
|
|
515
|
+
for await (const slot of this.checkpointProposalHashesPerSlot.keysAsync({ end: oldestSlot })) {
|
|
516
|
+
await this.checkpointProposalHashesPerSlot.delete(slot);
|
|
455
517
|
}
|
|
456
518
|
|
|
457
|
-
|
|
458
|
-
|
|
519
|
+
for await (const key of this.checkpointProposalsPerSlotAndHash.keysAsync({
|
|
520
|
+
end: `${oldestSlotPadded}-`,
|
|
521
|
+
})) {
|
|
522
|
+
await this.checkpointProposalsPerSlotAndHash.delete(key);
|
|
523
|
+
numberOfCheckpointProposals++;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Delete block proposals for slots < oldestSlot, using blockProposalHashesPerSlotAndIndex as index.
|
|
527
|
+
// Key format: slot * (1 << INDEX_BITS) + indexWithinCheckpoint
|
|
459
528
|
const blockPositionEndKey = oldestSlot * (1 << AttestationPool.INDEX_BITS);
|
|
460
|
-
for await (const positionKey of this.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
529
|
+
for await (const positionKey of this.blockProposalHashesPerSlotAndIndex.keysAsync({ end: blockPositionEndKey })) {
|
|
530
|
+
await this.blockProposalHashesPerSlotAndIndex.delete(positionKey);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
for await (const [key, buffer] of this.blockProposalsPerSlotIndexAndHash.entriesAsync({
|
|
534
|
+
end: `${oldestSlotPadded}-`,
|
|
535
|
+
})) {
|
|
536
|
+
try {
|
|
537
|
+
const proposal = BlockProposal.fromBuffer(buffer);
|
|
538
|
+
await this.blockProposalKeysPerArchive.deleteValue(proposal.archive.toString(), key);
|
|
539
|
+
} catch {
|
|
540
|
+
// ignore decode errors when cleaning up
|
|
465
541
|
}
|
|
466
|
-
await this.
|
|
542
|
+
await this.blockProposalsPerSlotIndexAndHash.delete(key);
|
|
543
|
+
numberOfBlockProposals++;
|
|
467
544
|
}
|
|
468
545
|
});
|
|
469
546
|
|
|
@@ -478,18 +555,19 @@ export class AttestationPool {
|
|
|
478
555
|
/**
|
|
479
556
|
* Attempts to add a checkpoint attestation to the pool.
|
|
480
557
|
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
* -
|
|
484
|
-
*
|
|
558
|
+
* - Detects duplicates by signed-payload hash (not archive); a re-broadcast of the
|
|
559
|
+
* exact same signed payload from the same signer returns `alreadyExists: true`.
|
|
560
|
+
* - Distinct payload hashes from the same (slot, signer) are tracked in the
|
|
561
|
+
* equivocation index. The first one's bytes are stored; later distinct hashes
|
|
562
|
+
* bump `count` so libp2p can fire its duplicate callback.
|
|
485
563
|
*
|
|
486
564
|
* @param attestation - The checkpoint attestation to add
|
|
487
|
-
* @returns Result indicating whether the attestation was added, existence info,
|
|
488
|
-
*
|
|
565
|
+
* @returns Result indicating whether the attestation was added, existence info,
|
|
566
|
+
* and number of distinct payload hashes by this signer for this slot
|
|
567
|
+
* (for equivocation detection).
|
|
489
568
|
*/
|
|
490
569
|
public async tryAddCheckpointAttestation(attestation: CheckpointAttestation): Promise<TryAddResult> {
|
|
491
570
|
const slotNumber = attestation.payload.header.slotNumber;
|
|
492
|
-
const proposalId = attestation.archive.toString();
|
|
493
571
|
const sender = attestation.getSender();
|
|
494
572
|
|
|
495
573
|
if (!sender) {
|
|
@@ -497,28 +575,23 @@ export class AttestationPool {
|
|
|
497
575
|
}
|
|
498
576
|
|
|
499
577
|
const signerAddress = sender.toString();
|
|
578
|
+
const slotSignerKey = this.getSlotSignerKey(slotNumber, signerAddress);
|
|
579
|
+
const payloadHash = attestation.getPayloadHash();
|
|
500
580
|
|
|
501
581
|
return await this.store.transactionAsync(async () => {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
// Get count of attestations by this signer for this slot (for duplicate detection)
|
|
506
|
-
const signerAttestationCount = await this.getSignerAttestationCountForSlot(slotNumber, signerAddress);
|
|
507
|
-
|
|
508
|
-
if (alreadyExists) {
|
|
509
|
-
return {
|
|
510
|
-
added: false,
|
|
511
|
-
alreadyExists: true,
|
|
512
|
-
count: signerAttestationCount,
|
|
513
|
-
};
|
|
582
|
+
if (await this.multimapHasValue(this.attestationHashesPerSlotAndSigner, slotSignerKey, payloadHash)) {
|
|
583
|
+
const count = await this.attestationHashesPerSlotAndSigner.getValueCountAsync(slotSignerKey);
|
|
584
|
+
return { added: false, alreadyExists: true, count };
|
|
514
585
|
}
|
|
515
586
|
|
|
516
|
-
|
|
587
|
+
const signerAttestationCount = await this.attestationHashesPerSlotAndSigner.getValueCountAsync(slotSignerKey);
|
|
588
|
+
|
|
517
589
|
if (signerAttestationCount >= MAX_ATTESTATIONS_PER_SLOT_AND_SIGNER) {
|
|
518
590
|
this.log.debug(`Rejecting attestation: signer ${signerAddress} exceeded per-slot cap for slot ${slotNumber}`, {
|
|
519
591
|
slotNumber,
|
|
520
592
|
signerAddress,
|
|
521
|
-
|
|
593
|
+
archive: attestation.archive.toString(),
|
|
594
|
+
payloadHash,
|
|
522
595
|
signerAttestationCount,
|
|
523
596
|
});
|
|
524
597
|
return {
|
|
@@ -528,22 +601,26 @@ export class AttestationPool {
|
|
|
528
601
|
};
|
|
529
602
|
}
|
|
530
603
|
|
|
531
|
-
//
|
|
532
|
-
await this.
|
|
533
|
-
this.metrics.trackMempoolItemAdded(key);
|
|
604
|
+
// Track the new payload hash for equivocation detection.
|
|
605
|
+
await this.attestationHashesPerSlotAndSigner.set(slotSignerKey, payloadHash);
|
|
534
606
|
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
await this.
|
|
607
|
+
// Only the first distinct payload at (slot, signer) is stored; later
|
|
608
|
+
// equivocations are detected via the multimap but their bytes are not retained.
|
|
609
|
+
const alreadyHasStored = await this.attestationPerSlotAndSigner.hasAsync(slotSignerKey);
|
|
610
|
+
if (!alreadyHasStored) {
|
|
611
|
+
await this.attestationPerSlotAndSigner.set(slotSignerKey, attestation.toBuffer());
|
|
612
|
+
this.metrics.trackMempoolItemAdded(slotSignerKey);
|
|
613
|
+
}
|
|
538
614
|
|
|
539
615
|
this.log.debug(`Added checkpoint attestation for slot ${slotNumber} from ${signerAddress}`, {
|
|
540
616
|
signature: attestation.signature.toString(),
|
|
541
617
|
slotNumber,
|
|
542
618
|
address: signerAddress,
|
|
543
|
-
|
|
619
|
+
archive: attestation.archive.toString(),
|
|
620
|
+
payloadHash,
|
|
621
|
+
stored: !alreadyHasStored,
|
|
544
622
|
});
|
|
545
623
|
|
|
546
|
-
// Return the new count
|
|
547
624
|
return {
|
|
548
625
|
added: true,
|
|
549
626
|
alreadyExists: false,
|
|
@@ -551,12 +628,6 @@ export class AttestationPool {
|
|
|
551
628
|
};
|
|
552
629
|
});
|
|
553
630
|
}
|
|
554
|
-
|
|
555
|
-
/** Gets the count of attestations by a specific signer for a given slot. */
|
|
556
|
-
private async getSignerAttestationCountForSlot(slot: SlotNumber, signerAddress: string): Promise<number> {
|
|
557
|
-
const slotSignerKey = this.getSlotSignerKey(slot, signerAddress);
|
|
558
|
-
return await this.checkpointAttestationsPerSlotAndSigner.getValueCountAsync(slotSignerKey);
|
|
559
|
-
}
|
|
560
631
|
}
|
|
561
632
|
|
|
562
633
|
/** Creates an AttestationPool backed by a temporary store for testing. */
|