@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.
Files changed (243) hide show
  1. package/dest/bootstrap/bootstrap.d.ts +1 -1
  2. package/dest/bootstrap/bootstrap.d.ts.map +1 -1
  3. package/dest/bootstrap/bootstrap.js +9 -1
  4. package/dest/client/factory.d.ts +3 -3
  5. package/dest/client/factory.d.ts.map +1 -1
  6. package/dest/client/factory.js +12 -9
  7. package/dest/client/interface.d.ts +6 -4
  8. package/dest/client/interface.d.ts.map +1 -1
  9. package/dest/client/p2p_client.d.ts +11 -8
  10. package/dest/client/p2p_client.d.ts.map +1 -1
  11. package/dest/client/p2p_client.js +53 -17
  12. package/dest/config.d.ts +43 -17
  13. package/dest/config.d.ts.map +1 -1
  14. package/dest/config.js +48 -8
  15. package/dest/errors/reqresp.error.d.ts +1 -20
  16. package/dest/errors/reqresp.error.d.ts.map +1 -1
  17. package/dest/errors/reqresp.error.js +0 -21
  18. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts +93 -55
  19. package/dest/mem_pools/attestation_pool/attestation_pool.d.ts.map +1 -1
  20. package/dest/mem_pools/attestation_pool/attestation_pool.js +258 -195
  21. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts +1 -1
  22. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.d.ts.map +1 -1
  23. package/dest/mem_pools/attestation_pool/attestation_pool_test_suite.js +175 -59
  24. package/dest/mem_pools/attestation_pool/mocks.d.ts +1 -1
  25. package/dest/mem_pools/attestation_pool/mocks.d.ts.map +1 -1
  26. package/dest/mem_pools/attestation_pool/mocks.js +6 -4
  27. package/dest/mem_pools/instrumentation.d.ts +1 -1
  28. package/dest/mem_pools/instrumentation.d.ts.map +1 -1
  29. package/dest/mem_pools/instrumentation.js +17 -1
  30. package/dest/mem_pools/tx_pool_v2/archive/tx_archive.d.ts +1 -5
  31. package/dest/mem_pools/tx_pool_v2/archive/tx_archive.d.ts.map +1 -1
  32. package/dest/mem_pools/tx_pool_v2/archive/tx_archive.js +1 -7
  33. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts +1 -1
  34. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.d.ts.map +1 -1
  35. package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +73 -83
  36. package/dest/msg_validators/attestation_validator/attestation_validator.d.ts +5 -2
  37. package/dest/msg_validators/attestation_validator/attestation_validator.d.ts.map +1 -1
  38. package/dest/msg_validators/attestation_validator/attestation_validator.js +18 -2
  39. package/dest/msg_validators/attestation_validator/fisherman_attestation_validator.d.ts +5 -3
  40. package/dest/msg_validators/attestation_validator/fisherman_attestation_validator.d.ts.map +1 -1
  41. package/dest/msg_validators/attestation_validator/fisherman_attestation_validator.js +3 -4
  42. package/dest/msg_validators/clock_tolerance.d.ts +1 -1
  43. package/dest/msg_validators/clock_tolerance.d.ts.map +1 -1
  44. package/dest/msg_validators/clock_tolerance.js +13 -11
  45. package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts +5 -2
  46. package/dest/msg_validators/proposal_validator/block_proposal_validator.d.ts.map +1 -1
  47. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts +5 -2
  48. package/dest/msg_validators/proposal_validator/checkpoint_proposal_validator.d.ts.map +1 -1
  49. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts +8 -2
  50. package/dest/msg_validators/proposal_validator/proposal_validator.d.ts.map +1 -1
  51. package/dest/msg_validators/proposal_validator/proposal_validator.js +28 -2
  52. package/dest/msg_validators/tx_validator/aggregate_tx_validator.d.ts +1 -1
  53. package/dest/msg_validators/tx_validator/aggregate_tx_validator.d.ts.map +1 -1
  54. package/dest/msg_validators/tx_validator/aggregate_tx_validator.js +8 -15
  55. package/dest/msg_validators/tx_validator/allowed_public_setup.js +3 -3
  56. package/dest/msg_validators/tx_validator/cached_tx_validator.d.ts +15 -0
  57. package/dest/msg_validators/tx_validator/cached_tx_validator.d.ts.map +1 -0
  58. package/dest/msg_validators/tx_validator/cached_tx_validator.js +19 -0
  59. package/dest/msg_validators/tx_validator/data_validator.d.ts +2 -1
  60. package/dest/msg_validators/tx_validator/data_validator.d.ts.map +1 -1
  61. package/dest/msg_validators/tx_validator/data_validator.js +1 -0
  62. package/dest/msg_validators/tx_validator/factory.d.ts +5 -4
  63. package/dest/msg_validators/tx_validator/factory.d.ts.map +1 -1
  64. package/dest/msg_validators/tx_validator/factory.js +11 -7
  65. package/dest/msg_validators/tx_validator/index.d.ts +3 -1
  66. package/dest/msg_validators/tx_validator/index.d.ts.map +1 -1
  67. package/dest/msg_validators/tx_validator/index.js +2 -0
  68. package/dest/msg_validators/tx_validator/tx_proof_validator.d.ts +2 -1
  69. package/dest/msg_validators/tx_validator/tx_proof_validator.d.ts.map +1 -1
  70. package/dest/msg_validators/tx_validator/tx_proof_validator.js +2 -0
  71. package/dest/msg_validators/tx_validator/tx_validation_cache.d.ts +48 -0
  72. package/dest/msg_validators/tx_validator/tx_validation_cache.d.ts.map +1 -0
  73. package/dest/msg_validators/tx_validator/tx_validation_cache.js +68 -0
  74. package/dest/services/discv5/discV5_service.d.ts +2 -1
  75. package/dest/services/discv5/discV5_service.d.ts.map +1 -1
  76. package/dest/services/discv5/discV5_service.js +35 -8
  77. package/dest/services/dummy_service.d.ts +10 -17
  78. package/dest/services/dummy_service.d.ts.map +1 -1
  79. package/dest/services/dummy_service.js +9 -18
  80. package/dest/services/libp2p/libp2p_service.d.ts +19 -21
  81. package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
  82. package/dest/services/libp2p/libp2p_service.js +147 -148
  83. package/dest/services/peer-manager/peer_manager.d.ts +1 -1
  84. package/dest/services/peer-manager/peer_manager.d.ts.map +1 -1
  85. package/dest/services/peer-manager/peer_manager.js +1 -0
  86. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts +1 -8
  87. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.d.ts.map +1 -1
  88. package/dest/services/reqresp/batch-tx-requester/batch_tx_requester.js +35 -44
  89. package/dest/services/reqresp/batch-tx-requester/interface.d.ts +8 -5
  90. package/dest/services/reqresp/batch-tx-requester/interface.d.ts.map +1 -1
  91. package/dest/services/reqresp/batch-tx-requester/tx_validator.d.ts +5 -14
  92. package/dest/services/reqresp/batch-tx-requester/tx_validator.d.ts.map +1 -1
  93. package/dest/services/reqresp/batch-tx-requester/tx_validator.js +6 -20
  94. package/dest/services/reqresp/interface.d.ts +3 -10
  95. package/dest/services/reqresp/interface.d.ts.map +1 -1
  96. package/dest/services/reqresp/interface.js +0 -9
  97. package/dest/services/reqresp/protocols/block_txs/bitvector.d.ts +4 -3
  98. package/dest/services/reqresp/protocols/block_txs/bitvector.d.ts.map +1 -1
  99. package/dest/services/reqresp/protocols/block_txs/bitvector.js +3 -2
  100. package/dest/services/reqresp/protocols/block_txs/block_txs_handler.d.ts +1 -1
  101. package/dest/services/reqresp/protocols/block_txs/block_txs_handler.d.ts.map +1 -1
  102. package/dest/services/reqresp/protocols/block_txs/block_txs_handler.js +31 -28
  103. package/dest/services/reqresp/protocols/block_txs/block_txs_reqresp.d.ts +12 -7
  104. package/dest/services/reqresp/protocols/block_txs/block_txs_reqresp.d.ts.map +1 -1
  105. package/dest/services/reqresp/protocols/block_txs/block_txs_reqresp.js +42 -24
  106. package/dest/services/reqresp/reqresp.d.ts +5 -29
  107. package/dest/services/reqresp/reqresp.d.ts.map +1 -1
  108. package/dest/services/reqresp/reqresp.js +16 -206
  109. package/dest/services/service.d.ts +7 -13
  110. package/dest/services/service.d.ts.map +1 -1
  111. package/dest/services/tx_collection/config.d.ts +2 -23
  112. package/dest/services/tx_collection/config.d.ts.map +1 -1
  113. package/dest/services/tx_collection/config.js +2 -55
  114. package/dest/services/tx_collection/file_store_tx_collection.d.ts +12 -28
  115. package/dest/services/tx_collection/file_store_tx_collection.d.ts.map +1 -1
  116. package/dest/services/tx_collection/file_store_tx_collection.js +43 -83
  117. package/dest/services/tx_collection/index.d.ts +2 -3
  118. package/dest/services/tx_collection/index.d.ts.map +1 -1
  119. package/dest/services/tx_collection/index.js +0 -1
  120. package/dest/services/tx_collection/instrumentation.d.ts +1 -1
  121. package/dest/services/tx_collection/instrumentation.d.ts.map +1 -1
  122. package/dest/services/tx_collection/instrumentation.js +0 -2
  123. package/dest/services/tx_collection/tx_collection.d.ts +35 -51
  124. package/dest/services/tx_collection/tx_collection.d.ts.map +1 -1
  125. package/dest/services/tx_collection/tx_collection.js +275 -119
  126. package/dest/services/tx_collection/tx_collection_sink.d.ts +1 -1
  127. package/dest/services/tx_collection/tx_collection_sink.js +2 -2
  128. package/dest/services/tx_file_store/tx_file_store.d.ts +1 -3
  129. package/dest/services/tx_file_store/tx_file_store.d.ts.map +1 -1
  130. package/dest/services/tx_file_store/tx_file_store.js +4 -14
  131. package/dest/services/tx_provider.d.ts +3 -1
  132. package/dest/services/tx_provider.d.ts.map +1 -1
  133. package/dest/services/tx_provider.js +3 -0
  134. package/dest/test-helpers/make-test-p2p-clients.d.ts +1 -1
  135. package/dest/test-helpers/make-test-p2p-clients.d.ts.map +1 -1
  136. package/dest/test-helpers/make-test-p2p-clients.js +1 -1
  137. package/dest/test-helpers/mock-pubsub.d.ts +13 -7
  138. package/dest/test-helpers/mock-pubsub.d.ts.map +1 -1
  139. package/dest/test-helpers/mock-pubsub.js +12 -37
  140. package/dest/test-helpers/reqresp-nodes.d.ts +4 -5
  141. package/dest/test-helpers/reqresp-nodes.d.ts.map +1 -1
  142. package/dest/test-helpers/reqresp-nodes.js +11 -15
  143. package/dest/test-helpers/test_tx_provider.d.ts +3 -1
  144. package/dest/test-helpers/test_tx_provider.d.ts.map +1 -1
  145. package/dest/test-helpers/test_tx_provider.js +3 -0
  146. package/dest/test-helpers/testbench-utils.d.ts +12 -14
  147. package/dest/test-helpers/testbench-utils.d.ts.map +1 -1
  148. package/dest/test-helpers/testbench-utils.js +19 -14
  149. package/dest/testbench/p2p_client_testbench_worker.d.ts +3 -5
  150. package/dest/testbench/p2p_client_testbench_worker.d.ts.map +1 -1
  151. package/dest/testbench/p2p_client_testbench_worker.js +13 -24
  152. package/dest/testbench/worker_client_manager.d.ts +3 -6
  153. package/dest/testbench/worker_client_manager.d.ts.map +1 -1
  154. package/dest/testbench/worker_client_manager.js +2 -8
  155. package/dest/util.d.ts +11 -6
  156. package/dest/util.d.ts.map +1 -1
  157. package/dest/util.js +35 -14
  158. package/dest/versioning.d.ts +3 -6
  159. package/dest/versioning.d.ts.map +1 -1
  160. package/dest/versioning.js +3 -24
  161. package/package.json +15 -14
  162. package/src/bootstrap/bootstrap.ts +9 -1
  163. package/src/client/factory.ts +20 -5
  164. package/src/client/interface.ts +6 -10
  165. package/src/client/p2p_client.ts +61 -36
  166. package/src/client/test/{tx_proposal_collector/README.md → p2p_client.batch_tx_requester.bench.README.md} +23 -53
  167. package/src/config.ts +94 -6
  168. package/src/errors/reqresp.error.ts +0 -25
  169. package/src/mem_pools/attestation_pool/attestation_pool.ts +309 -238
  170. package/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts +198 -62
  171. package/src/mem_pools/attestation_pool/mocks.ts +13 -8
  172. package/src/mem_pools/instrumentation.ts +5 -1
  173. package/src/mem_pools/tx_pool_v2/archive/tx_archive.ts +1 -9
  174. package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +73 -90
  175. package/src/msg_validators/attestation_validator/attestation_validator.ts +21 -1
  176. package/src/msg_validators/attestation_validator/fisherman_attestation_validator.ts +11 -7
  177. package/src/msg_validators/clock_tolerance.ts +13 -12
  178. package/src/msg_validators/proposal_validator/block_proposal_validator.ts +9 -2
  179. package/src/msg_validators/proposal_validator/checkpoint_proposal_validator.ts +14 -2
  180. package/src/msg_validators/proposal_validator/proposal_validator.ts +39 -2
  181. package/src/msg_validators/tx_validator/aggregate_tx_validator.ts +3 -12
  182. package/src/msg_validators/tx_validator/allowed_public_setup.ts +3 -3
  183. package/src/msg_validators/tx_validator/cached_tx_validator.ts +31 -0
  184. package/src/msg_validators/tx_validator/data_validator.ts +2 -0
  185. package/src/msg_validators/tx_validator/factory.ts +15 -6
  186. package/src/msg_validators/tx_validator/index.ts +2 -0
  187. package/src/msg_validators/tx_validator/tx_proof_validator.ts +2 -0
  188. package/src/msg_validators/tx_validator/tx_validation_cache.ts +101 -0
  189. package/src/services/discv5/discV5_service.ts +38 -5
  190. package/src/services/dummy_service.ts +12 -43
  191. package/src/services/libp2p/libp2p_service.ts +169 -167
  192. package/src/services/peer-manager/peer_manager.ts +1 -0
  193. package/src/services/reqresp/README.md +0 -14
  194. package/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts +39 -52
  195. package/src/services/reqresp/batch-tx-requester/interface.ts +11 -4
  196. package/src/services/reqresp/batch-tx-requester/tx_validator.ts +12 -25
  197. package/src/services/reqresp/interface.ts +2 -38
  198. package/src/services/reqresp/protocols/block_txs/bitvector.ts +3 -2
  199. package/src/services/reqresp/protocols/block_txs/block_txs_handler.ts +34 -30
  200. package/src/services/reqresp/protocols/block_txs/block_txs_reqresp.ts +47 -20
  201. package/src/services/reqresp/reqresp.ts +11 -251
  202. package/src/services/service.ts +9 -30
  203. package/src/services/tx_collection/config.ts +3 -80
  204. package/src/services/tx_collection/file_store_tx_collection.ts +54 -103
  205. package/src/services/tx_collection/index.ts +1 -6
  206. package/src/services/tx_collection/instrumentation.ts +1 -7
  207. package/src/services/tx_collection/tx_collection.ts +330 -173
  208. package/src/services/tx_collection/tx_collection_sink.ts +2 -2
  209. package/src/services/tx_file_store/tx_file_store.ts +5 -17
  210. package/src/services/tx_provider.ts +5 -0
  211. package/src/test-helpers/make-test-p2p-clients.ts +1 -0
  212. package/src/test-helpers/mock-pubsub.ts +17 -60
  213. package/src/test-helpers/reqresp-nodes.ts +8 -20
  214. package/src/test-helpers/test_tx_provider.ts +5 -0
  215. package/src/test-helpers/testbench-utils.ts +23 -27
  216. package/src/testbench/p2p_client_testbench_worker.ts +16 -42
  217. package/src/testbench/worker_client_manager.ts +4 -19
  218. package/src/util.ts +32 -17
  219. package/src/versioning.ts +3 -33
  220. package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker.d.ts +0 -2
  221. package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker.d.ts.map +0 -1
  222. package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker.js +0 -318
  223. package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker_protocol.d.ts +0 -73
  224. package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker_protocol.d.ts.map +0 -1
  225. package/dest/client/test/tx_proposal_collector/proposal_tx_collector_worker_protocol.js +0 -8
  226. package/dest/services/reqresp/connection-sampler/batch_connection_sampler.d.ts +0 -64
  227. package/dest/services/reqresp/connection-sampler/batch_connection_sampler.d.ts.map +0 -1
  228. package/dest/services/reqresp/connection-sampler/batch_connection_sampler.js +0 -151
  229. package/dest/services/tx_collection/fast_tx_collection.d.ts +0 -51
  230. package/dest/services/tx_collection/fast_tx_collection.d.ts.map +0 -1
  231. package/dest/services/tx_collection/fast_tx_collection.js +0 -311
  232. package/dest/services/tx_collection/proposal_tx_collector.d.ts +0 -48
  233. package/dest/services/tx_collection/proposal_tx_collector.d.ts.map +0 -1
  234. package/dest/services/tx_collection/proposal_tx_collector.js +0 -50
  235. package/dest/services/tx_collection/slow_tx_collection.d.ts +0 -57
  236. package/dest/services/tx_collection/slow_tx_collection.d.ts.map +0 -1
  237. package/dest/services/tx_collection/slow_tx_collection.js +0 -211
  238. package/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts +0 -357
  239. package/src/client/test/tx_proposal_collector/proposal_tx_collector_worker_protocol.ts +0 -43
  240. package/src/services/reqresp/connection-sampler/batch_connection_sampler.ts +0 -161
  241. package/src/services/tx_collection/fast_tx_collection.ts +0 -361
  242. package/src/services/tx_collection/proposal_tx_collector.ts +0 -108
  243. package/src/services/tx_collection/slow_tx_collection.ts +0 -266
@@ -1,5 +1,4 @@
1
- import { IndexWithinCheckpoint, SlotNumber } from '@aztec/foundation/branded-types';
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 added */
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 item already existed */
19
+ /** Whether the exact signed payload (matched by payload hash) already existed in the pool. */
21
20
  alreadyExists: boolean;
22
- /** Count of items for the position. Meaning varies by method:
23
- * - tryAddBlockProposal: proposals at (slot, indexWithinCheckpoint)
24
- * - tryAddCheckpointProposal: proposals at slot
25
- * - tryAddCheckpointAttestation: attestations by this signer for this slot */
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
- | 'getBlockProposal'
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 attestation key (slot-proposalId-signer) to serialized CheckpointAttestation
60
- // Keys are lexicographically sortable allowing range queries by slot or by (slot, proposalId)
61
- private checkpointAttestations: AztecAsyncMap<string, Buffer>;
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
- // Checkpoint proposals from proposal archive to serialized CheckpointProposal
64
- private checkpointProposals: AztecAsyncMap<string, Buffer>;
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 indexed by slot for querying all proposals in a slot
67
- // Key: slot number, Value: proposal archive strings
68
- private checkpointProposalsForSlot: AztecAsyncMultiMap<number, string>;
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
- // Block proposals from proposal archive to serialized BlockProposal
71
- private blockProposals: AztecAsyncMap<string, Buffer>;
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 indexed by slot and index-within-checkpoint for duplicate detection
74
- // Key: (slot << 10) | indexWithinCheckpoint, Value: archive string
75
- private blockProposalsForSlotAndIndex: AztecAsyncMultiMap<number, string>;
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
- // Checkpoint attestations indexed by (slot, signer) for tracking attestations per (slot, signer) for duplicate detection
78
- // Key: `${Fr(slot).toString()}-${signerAddress}` string (padded for lexicographic ordering), Value: `proposalId` strings
79
- private checkpointAttestationsPerSlotAndSigner: AztecAsyncMultiMap<string, string>;
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.blockProposals = store.openMap('proposals');
88
- this.blockProposalsForSlotAndIndex = store.openMultiMap('block_proposals_for_slot_and_index');
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.checkpointAttestations = store.openMap('checkpoint_attestations');
92
- this.checkpointAttestationsPerSlotAndSigner = store.openMultiMap('checkpoint_attestations_per_slot_and_signer');
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.checkpointProposals = store.openMap('checkpoint_proposals');
96
- this.checkpointProposalsForSlot = store.openMultiMap('checkpoint_proposals_for_slot');
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.checkpointAttestations.sizeAsync(),
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
- for await (const _ of this.checkpointAttestations.entriesAsync()) {
110
- return false;
111
- }
112
- for await (const _ of this.blockProposals.entriesAsync()) {
113
- return false;
114
- }
115
- return true;
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
- private getProposalKey(slot: number | bigint | Fr | string, proposalId: Fr | string | Buffer): string {
119
- const slotStr = typeof slot === 'string' ? slot : new Fr(slot).toString();
120
- const proposalIdStr =
121
- typeof proposalId === 'string'
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
- return `${slotStr}-${proposalIdStr}`;
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
- private getAttestationKey(slot: number | bigint | Fr | string, proposalId: Fr | string, address: string): string {
131
- return `${this.getProposalKey(slot, proposalId)}-${address}`;
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
- /** Returns range bounds for querying all attestations for a given slot. */
135
- private getAttestationKeyRangeForSlot(slot: SlotNumber): { start: string; end: string } {
136
- const slotStr = new Fr(slot).toString();
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
- /** Returns range bounds for querying all attestations for a given (slot, proposalId). */
141
- private getAttestationKeyRangeForProposal(slot: SlotNumber, proposalId: string): { start: string; end: string } {
142
- const proposalKey = this.getProposalKey(slot, proposalId);
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
- /** Creates a key for the per-signer-per-slot attestation index. Uses padded slot for lexicographic ordering. */
180
+ /** Key for the per-(slot, signer) attestation main store and equivocation index. */
147
181
  private getSlotSignerKey(slot: SlotNumber, signerAddress: string): string {
148
- const slotStr = new Fr(slot).toString();
149
- return `${slotStr}-${signerAddress}`;
182
+ return `${this.slotPaddedKey(slot)}-${signerAddress}`;
150
183
  }
151
184
 
152
- /** Number of bits reserved for indexWithinCheckpoint in position keys. */
153
- private static readonly INDEX_BITS = 10;
154
- /** Maximum indexWithinCheckpoint value (2^10 - 1 = 1023). */
155
- private static readonly MAX_INDEX = (1 << AttestationPool.INDEX_BITS) - 1;
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
- * This method performs validation and addition in a single call:
173
- * - Checks if the proposal already exists (returns alreadyExists: true if so)
174
- * - Checks if the position has reached the proposal cap (returns added: false if so)
175
- * - Adds the proposal if validation passes
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 proposalId = blockProposal.archive.toString();
183
-
184
- // Check if already exists
185
- const alreadyExists = await this.blockProposals.hasAsync(proposalId);
186
- if (alreadyExists) {
187
- const count = await this.getBlockProposalCountForPosition(
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
- // Get current count for position and check cap, do not add if exceeded
195
- const count = await this.getBlockProposalCountForPosition(
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
- // Add the proposal
205
- await this.addBlockProposal(blockProposal);
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
- proposalId,
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 its ID.
269
+ * Get block proposal by archive root.
242
270
  *
243
- * @param id - The ID of the block proposal to retrieve. The ID is proposal.payload.archive
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
- * @return The block proposal if it exists, otherwise undefined.
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 getBlockProposal(id: string): Promise<BlockProposal | undefined> {
248
- const buffer = await this.blockProposals.getAsync(id);
249
- try {
250
- if (buffer && buffer.length > 0) {
251
- return BlockProposal.fromBuffer(buffer);
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.blockProposalsForSlotAndIndex.getValueCountAsync(positionKey);
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
- * This method performs validation and addition in a single call:
271
- * - Checks if the proposal already exists (returns alreadyExists: true if so)
272
- * - Checks if the slot has reached the proposal cap (returns added: false if so)
273
- * - Adds the proposal if validation passes
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
- const result = await this.store.transactionAsync(async () => {
284
- const proposalId = proposal.archive.toString();
348
+ return await this.store.transactionAsync(async () => {
349
+ const slot = proposal.slotNumber;
350
+ const payloadHash = proposal.getPayloadHash();
285
351
 
286
- // Check if already exists
287
- const alreadyExists = await this.checkpointProposals.hasAsync(proposalId);
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
- // Get current count for slot and check cap
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
- // Add the proposal if cap not exceeded
300
- await this.addCheckpointProposal(proposal);
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 ${proposal.slotNumber}`, {
303
- proposalId,
304
- slotNumber: proposal.slotNumber,
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 by its ID.
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 id - The ID of the checkpoint proposal to retrieve (proposal.archive)
329
- * @return The checkpoint proposal core if it exists, otherwise undefined.
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(id: string): Promise<CheckpointProposalCore | undefined> {
332
- const buffer = await this.checkpointProposals.getAsync(id);
333
- try {
334
- if (buffer && buffer.length > 0) {
335
- return CheckpointProposal.fromBuffer(buffer);
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 validations on number of checkpoint attestations stored for the given slot.
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
- proposalId,
422
+ archive: attestation.archive.toString(),
361
423
  });
362
424
  continue;
363
425
  }
364
426
 
365
427
  const address = sender.toString();
366
- const ownKey = this.getAttestationKey(slotNumber, proposalId, address);
428
+ const ownKey = this.getSlotSignerKey(slotNumber, address);
429
+ const payloadHash = attestation.getPayloadHash();
367
430
 
368
- await this.checkpointAttestations.set(ownKey, attestation.toBuffer());
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
- proposalId,
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.checkpointAttestations.entriesAsync(range)) {
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 and given proposal.
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 proposalId - The proposal to query
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
- proposalId: string,
482
+ proposalPayloadHash: CheckpointProposalHash,
408
483
  ): Promise<CheckpointAttestation[]> {
409
- const range = this.getAttestationKeyRangeForProposal(slot, proposalId);
410
- const attestations: CheckpointAttestation[] = [];
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
- // Delete checkpoint attestations with slot < oldestSlot
431
- // Attestation keys start with Fr(slot).toString(), so we use end bound of Fr(oldestSlot).toString()
432
- const attestationEndKey = new Fr(oldestSlot).toString();
433
- for await (const key of this.checkpointAttestations.keysAsync({ end: attestationEndKey })) {
434
- await this.checkpointAttestations.delete(key);
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. Keys are formatted as `${Fr(slot).toString()}-${signerAddress}`.
440
- // Since Fr pads to fixed-width hex, Fr(oldestSlot) is lexicographically greater than any key with
441
- // a smaller slot (even with the signer suffix), so using it as the exclusive end bound is correct.
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, using checkpointProposalsForSlot as index
448
- for await (const slot of this.checkpointProposalsForSlot.keysAsync({ end: oldestSlot })) {
449
- const proposalIds = await toArray(this.checkpointProposalsForSlot.getValuesAsync(slot));
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
- // Delete block proposals for slots < oldestSlot, using blockProposalsForSlotAndIndex as index
458
- // Key format: (slot << INDEX_BITS) | indexWithinCheckpoint
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.blockProposalsForSlotAndIndex.keysAsync({ end: blockPositionEndKey })) {
461
- const proposalIds = await toArray(this.blockProposalsForSlotAndIndex.getValuesAsync(positionKey));
462
- for (const proposalId of proposalIds) {
463
- await this.blockProposals.delete(proposalId);
464
- numberOfBlockProposals++;
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.blockProposalsForSlotAndIndex.delete(positionKey);
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
- * This method performs validation and addition in a single call:
482
- * - Checks if the attestation already exists (returns alreadyExists: true if so)
483
- * - Checks if this signer has reached the per-signer attestation cap for this slot
484
- * - Adds the attestation if validation passes
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, and count of
488
- * attestations by this signer for this slot (for equivocation detection)
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
- const key = this.getAttestationKey(slotNumber, proposalId, signerAddress);
503
- const alreadyExists = await this.checkpointAttestations.hasAsync(key);
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
- // Check if this signer has exceeded the per-signer cap for this slot
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
- proposalId,
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
- // Add the attestation
532
- await this.checkpointAttestations.set(key, attestation.toBuffer());
533
- this.metrics.trackMempoolItemAdded(key);
604
+ // Track the new payload hash for equivocation detection.
605
+ await this.attestationHashesPerSlotAndSigner.set(slotSignerKey, payloadHash);
534
606
 
535
- // Track this attestation in the per-signer-per-slot index for duplicate detection
536
- const slotSignerKey = this.getSlotSignerKey(slotNumber, signerAddress);
537
- await this.checkpointAttestationsPerSlotAndSigner.set(slotSignerKey, proposalId);
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
- proposalId,
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. */