@aztec/p2p 5.0.0-rc.1 → 5.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/bootstrap/bootstrap.d.ts +1 -1
- package/dest/bootstrap/bootstrap.d.ts.map +1 -1
- package/dest/bootstrap/bootstrap.js +20 -12
- package/dest/client/interface.d.ts +9 -2
- package/dest/client/interface.d.ts.map +1 -1
- package/dest/client/p2p_client.d.ts +3 -2
- package/dest/client/p2p_client.d.ts.map +1 -1
- package/dest/client/p2p_client.js +5 -0
- package/dest/config.js +1 -1
- package/dest/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.js +1 -1
- package/dest/mem_pools/tx_pool_v2/tx_pool_v2_impl.js +1 -1
- package/dest/msg_validators/proposal_validator/proposal_validator.d.ts +10 -6
- package/dest/msg_validators/proposal_validator/proposal_validator.d.ts.map +1 -1
- package/dest/msg_validators/proposal_validator/proposal_validator.js +17 -11
- package/dest/msg_validators/tx_validator/phases_validator.js +2 -2
- package/dest/services/discv5/discV5_service.d.ts +12 -2
- package/dest/services/discv5/discV5_service.d.ts.map +1 -1
- package/dest/services/discv5/discV5_service.js +84 -15
- package/dest/services/discv5/persisted_enr_store.d.ts +38 -0
- package/dest/services/discv5/persisted_enr_store.d.ts.map +1 -0
- package/dest/services/discv5/persisted_enr_store.js +145 -0
- package/dest/services/dummy_service.d.ts +6 -2
- package/dest/services/dummy_service.d.ts.map +1 -1
- package/dest/services/dummy_service.js +3 -0
- package/dest/services/encoding.d.ts +8 -8
- package/dest/services/encoding.d.ts.map +1 -1
- package/dest/services/encoding.js +11 -15
- package/dest/services/libp2p/libp2p_service.d.ts +10 -2
- package/dest/services/libp2p/libp2p_service.d.ts.map +1 -1
- package/dest/services/libp2p/libp2p_service.js +93 -33
- 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 +21 -12
- package/dest/services/service.d.ts +17 -1
- package/dest/services/service.d.ts.map +1 -1
- package/package.json +16 -17
- package/src/bootstrap/bootstrap.ts +15 -9
- package/src/client/interface.ts +9 -0
- package/src/client/p2p_client.ts +7 -0
- package/src/config.ts +1 -1
- package/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.ts +1 -1
- package/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +1 -1
- package/src/msg_validators/proposal_validator/proposal_validator.ts +17 -14
- package/src/msg_validators/tx_validator/phases_validator.ts +2 -2
- package/src/services/discv5/discV5_service.ts +88 -9
- package/src/services/discv5/persisted_enr_store.ts +158 -0
- package/src/services/dummy_service.ts +6 -0
- package/src/services/encoding.ts +11 -21
- package/src/services/libp2p/libp2p_service.ts +104 -35
- package/src/services/peer-manager/peer_manager.ts +18 -11
- package/src/services/service.ts +19 -0
package/src/services/encoding.ts
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
import { createLogger } from '@aztec/foundation/log';
|
|
3
3
|
import { MAX_TX_SIZE_KB, TopicType, getTopicFromString } from '@aztec/stdlib/p2p';
|
|
4
4
|
|
|
5
|
-
import type { RPC } from '@chainsafe/libp2p-gossipsub/message';
|
|
6
5
|
import type { DataTransform } from '@chainsafe/libp2p-gossipsub/types';
|
|
7
6
|
import type { Message } from '@libp2p/interface';
|
|
8
7
|
import { webcrypto } from 'node:crypto';
|
|
9
8
|
import { compressSync, uncompressSync } from 'snappy';
|
|
10
|
-
import xxhashFactory from 'xxhash-wasm';
|
|
11
9
|
|
|
12
10
|
/** Thrown when a Snappy-compressed response exceeds the allowed decompressed size. */
|
|
13
11
|
export class OversizedSnappyResponseError extends Error {
|
|
@@ -17,26 +15,9 @@ export class OversizedSnappyResponseError extends Error {
|
|
|
17
15
|
}
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
// Load WASM
|
|
21
|
-
const xxhash = await xxhashFactory();
|
|
22
|
-
|
|
23
|
-
// Use salt to prevent msgId from being mined for collisions
|
|
24
|
-
const h64Seed = BigInt(Math.floor(Math.random() * 1e9));
|
|
25
|
-
|
|
26
18
|
// Shared buffer to convert msgId to string
|
|
27
19
|
const sharedMsgIdBuf = Buffer.alloc(20);
|
|
28
20
|
|
|
29
|
-
/**
|
|
30
|
-
* The function used to generate a gossipsub message id
|
|
31
|
-
* We use the first 8 bytes of SHA256(data) for content addressing
|
|
32
|
-
*/
|
|
33
|
-
export function fastMsgIdFn(rpcMsg: RPC.Message): string {
|
|
34
|
-
if (rpcMsg.data) {
|
|
35
|
-
return xxhash.h64Raw(rpcMsg.data, h64Seed).toString(16);
|
|
36
|
-
}
|
|
37
|
-
return '0000000000000000';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
21
|
export function msgIdToStrFn(msgId: Uint8Array): string {
|
|
41
22
|
// This happens serially, no need to reallocate the buffer
|
|
42
23
|
sharedMsgIdBuf.set(msgId);
|
|
@@ -49,11 +30,20 @@ export function msgIdToStrFn(msgId: Uint8Array): string {
|
|
|
49
30
|
* Follows similarly to:
|
|
50
31
|
* https://github.com/ethereum/consensus-specs/blob/v1.1.0-alpha.7/specs/altair/p2p-interface.md#topics-and-messages
|
|
51
32
|
*
|
|
33
|
+
* The topic length is framed into the hash input (`uint32be(topicLen) || topic || data`) so the
|
|
34
|
+
* `(topic, data)` boundary is unambiguous. A raw `topic || data` concatenation is not injective —
|
|
35
|
+
* shifting bytes across the boundary (e.g. `(topic + data[0], data[1:])`) yields the same bytes and
|
|
36
|
+
* thus the same id, letting an unsubscribed-topic message pre-occupy a real message's seenCache slot
|
|
37
|
+
* and suppress it as a duplicate.
|
|
38
|
+
*
|
|
52
39
|
* @param message - The libp2p message
|
|
53
40
|
* @returns The message identifier
|
|
54
41
|
*/
|
|
55
|
-
export async function getMsgIdFn({ topic, data }: Message): Promise<Uint8Array> {
|
|
56
|
-
const
|
|
42
|
+
export async function getMsgIdFn({ topic, data }: Pick<Message, 'topic' | 'data'>): Promise<Uint8Array> {
|
|
43
|
+
const topicBytes = Buffer.from(topic);
|
|
44
|
+
const framedTopicLength = Buffer.allocUnsafe(4);
|
|
45
|
+
framedTopicLength.writeUInt32BE(topicBytes.length);
|
|
46
|
+
const buffer = Buffer.concat([framedTopicLength, topicBytes, data]);
|
|
57
47
|
const hash = await webcrypto.subtle.digest('SHA-256', buffer);
|
|
58
48
|
return Buffer.from(hash.slice(0, 20));
|
|
59
49
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EpochCacheInterface } from '@aztec/epoch-cache';
|
|
2
2
|
import { BlockNumber, type SlotNumber } from '@aztec/foundation/branded-types';
|
|
3
|
-
import { maxBy, merge } from '@aztec/foundation/collection';
|
|
3
|
+
import { compactArray, maxBy, merge } from '@aztec/foundation/collection';
|
|
4
4
|
import { type Logger, createLibp2pComponentLogger, createLogger } from '@aztec/foundation/log';
|
|
5
5
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
6
6
|
import { Timer } from '@aztec/foundation/timer';
|
|
@@ -83,7 +83,7 @@ import { type PubSubLibp2p, convertToMultiaddr } from '../../util.js';
|
|
|
83
83
|
import { getVersions } from '../../versioning.js';
|
|
84
84
|
import { AztecDatastore } from '../data_store.js';
|
|
85
85
|
import { DiscV5Service } from '../discv5/discV5_service.js';
|
|
86
|
-
import { SnappyTransform,
|
|
86
|
+
import { SnappyTransform, getMsgIdFn, msgIdToStrFn } from '../encoding.js';
|
|
87
87
|
import { APP_SPECIFIC_WEIGHT, gossipScoreThresholds } from '../gossipsub/scoring.js';
|
|
88
88
|
import { createAllTopicScoreParams } from '../gossipsub/topic_score_params.js';
|
|
89
89
|
import type { PeerManagerInterface } from '../peer-manager/interface.js';
|
|
@@ -114,6 +114,7 @@ import type {
|
|
|
114
114
|
P2PCheckpointAttestationCallback,
|
|
115
115
|
P2PCheckpointReceivedCallback,
|
|
116
116
|
P2PDuplicateAttestationCallback,
|
|
117
|
+
P2POversizedProposalCallback,
|
|
117
118
|
P2PService,
|
|
118
119
|
PeerDiscoveryService,
|
|
119
120
|
} from '../service.js';
|
|
@@ -172,6 +173,9 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
172
173
|
type: 'checkpoint' | 'block';
|
|
173
174
|
}) => void;
|
|
174
175
|
|
|
176
|
+
/** Callback invoked when an oversized block proposal is stored as slashing evidence (triggers slashing). */
|
|
177
|
+
private oversizedProposalCallback?: P2POversizedProposalCallback;
|
|
178
|
+
|
|
175
179
|
/** Callback invoked when a duplicate attestation is detected (triggers slashing). */
|
|
176
180
|
private duplicateAttestationCallback?: P2PDuplicateAttestationCallback;
|
|
177
181
|
|
|
@@ -364,6 +368,7 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
364
368
|
packageVersion,
|
|
365
369
|
telemetry,
|
|
366
370
|
createLogger(`${logger.module}:discv5_service`, logger.getBindings()),
|
|
371
|
+
peerStore,
|
|
367
372
|
);
|
|
368
373
|
|
|
369
374
|
// Seed libp2p's bootstrap discovery with private and trusted peers
|
|
@@ -378,21 +383,27 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
378
383
|
const protocolVersion = compressComponentVersions(versions);
|
|
379
384
|
|
|
380
385
|
const preferredPeersEnrs: ENR[] = config.preferredPeers.map(enr => ENR.decodeTxt(enr));
|
|
381
|
-
const directPeers = (
|
|
386
|
+
const directPeers = compactArray(
|
|
382
387
|
await Promise.all(
|
|
383
388
|
preferredPeersEnrs.map(async enr => {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
389
|
+
try {
|
|
390
|
+
const peerId = await enr.peerId();
|
|
391
|
+
const address = enr.getLocationMultiaddr('tcp');
|
|
392
|
+
if (address === undefined) {
|
|
393
|
+
throw new Error(`Direct peer ${peerId.toString()} has no TCP address, ENR: ${enr.encodeTxt()}`);
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
id: peerId,
|
|
397
|
+
addrs: [address],
|
|
398
|
+
};
|
|
399
|
+
} catch (err) {
|
|
400
|
+
// A malformed configured ENR shouldn't abort node setup — skip it and log.
|
|
401
|
+
logger.warn(`Skipping preferred peer with invalid ENR`, { err });
|
|
402
|
+
return undefined;
|
|
388
403
|
}
|
|
389
|
-
return {
|
|
390
|
-
id: peerId,
|
|
391
|
-
addrs: [address],
|
|
392
|
-
};
|
|
393
404
|
}),
|
|
394
|
-
)
|
|
395
|
-
)
|
|
405
|
+
),
|
|
406
|
+
);
|
|
396
407
|
|
|
397
408
|
const announceTcpMultiaddr = config.p2pIp ? [convertToMultiaddr(config.p2pIp, p2pPort, 'tcp')] : [];
|
|
398
409
|
|
|
@@ -408,6 +419,13 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
408
419
|
expectedBlockProposalsPerSlot: config.expectedBlockProposalsPerSlot,
|
|
409
420
|
});
|
|
410
421
|
|
|
422
|
+
// Restrict gossipsub to exactly the topics we subscribe to. Without this, an arbitrary-topic
|
|
423
|
+
// message is transformed, msg-id'd and inserted into the seenCache before the subscription check,
|
|
424
|
+
// so a crafted topic colliding on msg id can suppress a real message as a duplicate.
|
|
425
|
+
const allowedTopics = getTopicsForConfig(config.disableTransactions).map(topic =>
|
|
426
|
+
createTopicString(topic, protocolVersion),
|
|
427
|
+
);
|
|
428
|
+
|
|
411
429
|
const node = await createLibp2p({
|
|
412
430
|
start: false,
|
|
413
431
|
peerId,
|
|
@@ -492,9 +510,13 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
492
510
|
mcacheLength: config.gossipsubMcacheLength,
|
|
493
511
|
mcacheGossip: config.gossipsubMcacheGossip,
|
|
494
512
|
seenTTL: config.gossipsubSeenTTL,
|
|
513
|
+
allowedTopics,
|
|
514
|
+
// No fastMsgIdFn: the fast-path dedup cache keys on a non-cryptographic 64-bit hash of the
|
|
515
|
+
// raw data only (no topic), so a collision — accidental or engineered via a weak seed — drops
|
|
516
|
+
// a different message with no fallback to the full id. Dedup instead rests solely on the
|
|
517
|
+
// cryptographic, topic-framed msgIdFn below.
|
|
495
518
|
msgIdFn: getMsgIdFn,
|
|
496
519
|
msgIdToStrFn: msgIdToStrFn,
|
|
497
|
-
fastMsgIdFn: fastMsgIdFn,
|
|
498
520
|
dataTransform: new SnappyTransform(),
|
|
499
521
|
metricsRegister: otelMetricsAdapter,
|
|
500
522
|
metricsTopicStrToLabel: metricsTopicStrToLabels(protocolVersion),
|
|
@@ -762,6 +784,13 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
762
784
|
this.duplicateProposalCallback = callback;
|
|
763
785
|
}
|
|
764
786
|
|
|
787
|
+
/**
|
|
788
|
+
* Registers a callback to be invoked when an oversized block proposal is stored as slashing evidence.
|
|
789
|
+
*/
|
|
790
|
+
public registerOversizedProposalCallback(callback: P2POversizedProposalCallback): void {
|
|
791
|
+
this.oversizedProposalCallback = callback;
|
|
792
|
+
}
|
|
793
|
+
|
|
765
794
|
/**
|
|
766
795
|
* Registers a callback to be invoked when a duplicate attestation is detected.
|
|
767
796
|
* A validator signing attestations for different proposals at the same slot.
|
|
@@ -1243,16 +1272,17 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1243
1272
|
const {
|
|
1244
1273
|
result,
|
|
1245
1274
|
obj: block,
|
|
1246
|
-
metadata: { isEquivocated } = {},
|
|
1247
|
-
} = await this.validateReceivedMessage<BlockProposal, { isEquivocated: boolean }>(
|
|
1275
|
+
metadata: { isEquivocated, isOversized } = {},
|
|
1276
|
+
} = await this.validateReceivedMessage<BlockProposal, { isEquivocated: boolean; isOversized: boolean }>(
|
|
1248
1277
|
() => this.validateAndStoreBlockProposal(source, BlockProposal.fromBuffer(payloadData)),
|
|
1249
1278
|
msgId,
|
|
1250
1279
|
source,
|
|
1251
1280
|
TopicType.block_proposal,
|
|
1252
1281
|
);
|
|
1253
1282
|
|
|
1254
|
-
// If not accepted or
|
|
1255
|
-
|
|
1283
|
+
// If not accepted, equivocated, or oversized, return. Oversized proposals are re-broadcast as
|
|
1284
|
+
// slashing evidence but never attested or processed.
|
|
1285
|
+
if (result !== TopicValidatorResult.Accept || !block || isEquivocated || isOversized) {
|
|
1256
1286
|
return;
|
|
1257
1287
|
}
|
|
1258
1288
|
|
|
@@ -1267,7 +1297,7 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1267
1297
|
protected async validateAndStoreBlockProposal(
|
|
1268
1298
|
peerId: PeerId,
|
|
1269
1299
|
block: BlockProposal,
|
|
1270
|
-
): Promise<ReceivedMessageValidationResult<BlockProposal, { isEquivocated: boolean }>> {
|
|
1300
|
+
): Promise<ReceivedMessageValidationResult<BlockProposal, { isEquivocated: boolean; isOversized: boolean }>> {
|
|
1271
1301
|
const validationResult = await this.blockProposalValidator.validate(block);
|
|
1272
1302
|
|
|
1273
1303
|
if (validationResult.result === 'reject') {
|
|
@@ -1282,6 +1312,12 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1282
1312
|
// Try to add the proposal: this handles existence check, cap check, and adding in one call
|
|
1283
1313
|
const { added, alreadyExists, count } = await this.mempools.attestationPool.tryAddBlockProposal(block);
|
|
1284
1314
|
const isEquivocated = count !== undefined && count > 1;
|
|
1315
|
+
// An oversized proposal (index at or beyond the consensus per-checkpoint limit) is structurally valid
|
|
1316
|
+
// proposer misbehavior: it is stored and re-broadcast as slashing evidence but never processed or
|
|
1317
|
+
// attested to. No-ops when maxBlocksPerCheckpoint is unset (local/test).
|
|
1318
|
+
const isOversized =
|
|
1319
|
+
this.config.maxBlocksPerCheckpoint !== undefined &&
|
|
1320
|
+
block.indexWithinCheckpoint >= this.config.maxBlocksPerCheckpoint;
|
|
1285
1321
|
|
|
1286
1322
|
// Duplicate proposal received, no need to re-broadcast
|
|
1287
1323
|
if (alreadyExists) {
|
|
@@ -1291,7 +1327,7 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1291
1327
|
proposer: block.getSender()?.toString(),
|
|
1292
1328
|
source: peerId.toString(),
|
|
1293
1329
|
});
|
|
1294
|
-
return { result: TopicValidatorResult.Ignore, obj: block, metadata: { isEquivocated } };
|
|
1330
|
+
return { result: TopicValidatorResult.Ignore, obj: block, metadata: { isEquivocated, isOversized } };
|
|
1295
1331
|
}
|
|
1296
1332
|
|
|
1297
1333
|
// Too many blocks received for this slot and index, penalize peer and do not re-broadcast
|
|
@@ -1305,11 +1341,27 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1305
1341
|
});
|
|
1306
1342
|
return {
|
|
1307
1343
|
result: TopicValidatorResult.Reject,
|
|
1308
|
-
metadata: { isEquivocated },
|
|
1344
|
+
metadata: { isEquivocated, isOversized },
|
|
1309
1345
|
severity: PeerErrorSeverity.HighToleranceError,
|
|
1310
1346
|
};
|
|
1311
1347
|
}
|
|
1312
1348
|
|
|
1349
|
+
// The proposal was stored: if oversized, invoke the oversized callback so the proposer can be
|
|
1350
|
+
// slashed. Fired alongside (not instead of) equivocation detection below.
|
|
1351
|
+
if (isOversized) {
|
|
1352
|
+
const proposer = block.getSender();
|
|
1353
|
+
if (proposer) {
|
|
1354
|
+
this.logger.warn(`Detected oversized block proposal at slot ${block.slotNumber}`, {
|
|
1355
|
+
...block.toBlockInfo(),
|
|
1356
|
+
indexWithinCheckpoint: block.indexWithinCheckpoint,
|
|
1357
|
+
maxBlocksPerCheckpoint: this.config.maxBlocksPerCheckpoint,
|
|
1358
|
+
source: peerId.toString(),
|
|
1359
|
+
proposer: proposer.toString(),
|
|
1360
|
+
});
|
|
1361
|
+
this.oversizedProposalCallback?.({ slot: block.slotNumber, proposer });
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1313
1365
|
// If this was a duplicate proposal, do not process it, but do invoke the duplicate callback,
|
|
1314
1366
|
// and do re-broadcast it so other nodes in the network know to slash the proposer
|
|
1315
1367
|
if (isEquivocated) {
|
|
@@ -1323,11 +1375,11 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1323
1375
|
if (proposer && count === 2) {
|
|
1324
1376
|
this.duplicateProposalCallback?.({ slot: block.slotNumber, proposer, type: 'block' });
|
|
1325
1377
|
}
|
|
1326
|
-
return { result: TopicValidatorResult.Accept, obj: block, metadata: { isEquivocated } };
|
|
1378
|
+
return { result: TopicValidatorResult.Accept, obj: block, metadata: { isEquivocated, isOversized } };
|
|
1327
1379
|
}
|
|
1328
1380
|
|
|
1329
1381
|
// Otherwise, we're good to go!
|
|
1330
|
-
return { result: TopicValidatorResult.Accept, obj: block };
|
|
1382
|
+
return { result: TopicValidatorResult.Accept, obj: block, metadata: { isEquivocated: false, isOversized } };
|
|
1331
1383
|
}
|
|
1332
1384
|
|
|
1333
1385
|
// REFACTOR(palla): This method should be moved to the p2p_client or to a separate component,
|
|
@@ -1367,17 +1419,21 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1367
1419
|
const {
|
|
1368
1420
|
result,
|
|
1369
1421
|
obj: checkpoint,
|
|
1370
|
-
metadata: { isEquivocated, processBlock } = {},
|
|
1371
|
-
} = await this.validateReceivedMessage<
|
|
1422
|
+
metadata: { isEquivocated, processBlock, isOversized } = {},
|
|
1423
|
+
} = await this.validateReceivedMessage<
|
|
1424
|
+
CheckpointProposal,
|
|
1425
|
+
{ isEquivocated: boolean; processBlock: boolean; isOversized: boolean }
|
|
1426
|
+
>(
|
|
1372
1427
|
() => this.validateAndStoreCheckpointProposal(source, CheckpointProposal.fromBuffer(payloadData)),
|
|
1373
1428
|
msgId,
|
|
1374
1429
|
source,
|
|
1375
1430
|
TopicType.checkpoint_proposal,
|
|
1376
1431
|
);
|
|
1377
1432
|
|
|
1378
|
-
//
|
|
1433
|
+
// An oversized checkpoint is re-broadcast as slashing evidence but never attested or processed.
|
|
1434
|
+
// Process checkpoint proposal if valid and neither equivocated nor oversized.
|
|
1379
1435
|
const processCheckpointFn = () =>
|
|
1380
|
-
result === TopicValidatorResult.Accept && checkpoint && !isEquivocated
|
|
1436
|
+
result === TopicValidatorResult.Accept && checkpoint && !isEquivocated && !isOversized
|
|
1381
1437
|
? this.processValidCheckpointProposal(checkpoint.toCore(), source)
|
|
1382
1438
|
: Promise.resolve();
|
|
1383
1439
|
|
|
@@ -1414,7 +1470,12 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1414
1470
|
protected async validateAndStoreCheckpointProposal(
|
|
1415
1471
|
peerId: PeerId,
|
|
1416
1472
|
checkpoint: CheckpointProposal,
|
|
1417
|
-
): Promise<
|
|
1473
|
+
): Promise<
|
|
1474
|
+
ReceivedMessageValidationResult<
|
|
1475
|
+
CheckpointProposal,
|
|
1476
|
+
{ isEquivocated: boolean; processBlock: boolean; isOversized: boolean }
|
|
1477
|
+
>
|
|
1478
|
+
> {
|
|
1418
1479
|
const validationResult = await this.checkpointProposalValidator.validate(checkpoint);
|
|
1419
1480
|
|
|
1420
1481
|
if (validationResult.result === 'reject') {
|
|
@@ -1429,13 +1490,16 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1429
1490
|
// Extract and try to add the block proposal first if present
|
|
1430
1491
|
const blockProposal = checkpoint.getBlockProposal();
|
|
1431
1492
|
let processBlock = false;
|
|
1493
|
+
let isOversized = false;
|
|
1432
1494
|
if (blockProposal) {
|
|
1433
1495
|
this.logger.debug(`Validating block proposal from propagated checkpoint`, {
|
|
1434
1496
|
[Attributes.SLOT_NUMBER]: checkpoint.slotNumber.toString(),
|
|
1435
1497
|
[Attributes.P2P_ID]: peerId.toString(),
|
|
1436
1498
|
});
|
|
1437
1499
|
const blockProposalResult = await this.validateAndStoreBlockProposal(peerId, blockProposal);
|
|
1438
|
-
const { obj, metadata: { isEquivocated } = {} } = blockProposalResult;
|
|
1500
|
+
const { obj, metadata: { isEquivocated, isOversized: blockIsOversized } = {} } = blockProposalResult;
|
|
1501
|
+
isOversized = blockIsOversized ?? false;
|
|
1502
|
+
|
|
1439
1503
|
if (blockProposalResult.result === TopicValidatorResult.Reject || !obj || isEquivocated) {
|
|
1440
1504
|
this.logger.debug(`Rejecting checkpoint due to invalid last block proposal`, {
|
|
1441
1505
|
[Attributes.SLOT_NUMBER]: checkpoint.slotNumber.toString(),
|
|
@@ -1448,7 +1512,8 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1448
1512
|
severity:
|
|
1449
1513
|
'severity' in blockProposalResult ? blockProposalResult.severity : PeerErrorSeverity.MidToleranceError,
|
|
1450
1514
|
};
|
|
1451
|
-
} else if (blockProposalResult.result === TopicValidatorResult.Accept && obj && !isEquivocated) {
|
|
1515
|
+
} else if (blockProposalResult.result === TopicValidatorResult.Accept && obj && !isEquivocated && !isOversized) {
|
|
1516
|
+
// An oversized terminal block is re-broadcast as slashing evidence but never processed.
|
|
1452
1517
|
processBlock = true;
|
|
1453
1518
|
}
|
|
1454
1519
|
}
|
|
@@ -1468,11 +1533,11 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1468
1533
|
return {
|
|
1469
1534
|
result: TopicValidatorResult.Ignore,
|
|
1470
1535
|
obj: checkpoint,
|
|
1471
|
-
metadata: { isEquivocated, processBlock },
|
|
1536
|
+
metadata: { isEquivocated, processBlock, isOversized },
|
|
1472
1537
|
};
|
|
1473
1538
|
}
|
|
1474
1539
|
|
|
1475
|
-
// Too many checkpoint proposals received for this slot, penalize peer and do not re-broadcast
|
|
1540
|
+
// Too many checkpoint proposals received for this slot, penalize peer and do not re-broadcast.
|
|
1476
1541
|
// Note: We still return the checkpoint obj so the lastBlock can be processed if valid
|
|
1477
1542
|
if (!added) {
|
|
1478
1543
|
this.logger.warn(`Penalizing peer for checkpoint proposal exceeding per-slot cap`, {
|
|
@@ -1483,7 +1548,7 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1483
1548
|
return {
|
|
1484
1549
|
result: TopicValidatorResult.Reject,
|
|
1485
1550
|
obj: checkpoint,
|
|
1486
|
-
metadata: { isEquivocated, processBlock },
|
|
1551
|
+
metadata: { isEquivocated, processBlock, isOversized },
|
|
1487
1552
|
severity: PeerErrorSeverity.HighToleranceError,
|
|
1488
1553
|
};
|
|
1489
1554
|
}
|
|
@@ -1504,12 +1569,16 @@ export class LibP2PService extends WithTracer implements P2PService {
|
|
|
1504
1569
|
return {
|
|
1505
1570
|
result: TopicValidatorResult.Accept,
|
|
1506
1571
|
obj: checkpoint,
|
|
1507
|
-
metadata: { isEquivocated, processBlock },
|
|
1572
|
+
metadata: { isEquivocated, processBlock, isOversized },
|
|
1508
1573
|
};
|
|
1509
1574
|
}
|
|
1510
1575
|
|
|
1511
1576
|
// Otherwise, we're good to go!
|
|
1512
|
-
return {
|
|
1577
|
+
return {
|
|
1578
|
+
result: TopicValidatorResult.Accept,
|
|
1579
|
+
obj: checkpoint,
|
|
1580
|
+
metadata: { processBlock, isEquivocated, isOversized },
|
|
1581
|
+
};
|
|
1513
1582
|
}
|
|
1514
1583
|
|
|
1515
1584
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EpochCacheInterface } from '@aztec/epoch-cache';
|
|
2
|
+
import { compactArray } from '@aztec/foundation/collection';
|
|
2
3
|
import { makeEthSignDigest, tryRecoverAddress } from '@aztec/foundation/crypto/secp256k1-signer';
|
|
3
4
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
4
5
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
@@ -199,21 +200,27 @@ export class PeerManager implements PeerManagerInterface {
|
|
|
199
200
|
.then(peerIds => peerIds.forEach(peerId => this.preferredPeers.add(peerId.toString())))
|
|
200
201
|
.catch(e => this.logger.error('Error initializing preferred peers', e));
|
|
201
202
|
|
|
202
|
-
const directPeers = (
|
|
203
|
+
const directPeers = compactArray(
|
|
203
204
|
await Promise.all(
|
|
204
205
|
preferredPeersEnrs.map(async enr => {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
try {
|
|
207
|
+
const peerId = await enr.peerId();
|
|
208
|
+
const address = enr.getLocationMultiaddr('tcp');
|
|
209
|
+
if (address === undefined) {
|
|
210
|
+
throw new Error(`Direct peer ${peerId.toString()} has no TCP address, ENR: ${enr.encodeTxt()}`);
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
id: peerId,
|
|
214
|
+
addrs: [address],
|
|
215
|
+
};
|
|
216
|
+
} catch (err) {
|
|
217
|
+
// A malformed configured ENR shouldn't abort preferred-peer setup — skip it and log.
|
|
218
|
+
this.logger.warn(`Skipping preferred peer with invalid ENR`, { err });
|
|
219
|
+
return undefined;
|
|
209
220
|
}
|
|
210
|
-
return {
|
|
211
|
-
id: peerId,
|
|
212
|
-
addrs: [address],
|
|
213
|
-
};
|
|
214
221
|
}),
|
|
215
|
-
)
|
|
216
|
-
)
|
|
222
|
+
),
|
|
223
|
+
);
|
|
217
224
|
|
|
218
225
|
await Promise.all(
|
|
219
226
|
directPeers.map(peer => {
|
package/src/services/service.ts
CHANGED
|
@@ -58,6 +58,19 @@ export type DuplicateProposalInfo = {
|
|
|
58
58
|
*/
|
|
59
59
|
export type P2PDuplicateProposalCallback = (info: DuplicateProposalInfo) => void;
|
|
60
60
|
|
|
61
|
+
/** Minimal info passed to the oversized proposal callback. */
|
|
62
|
+
export type OversizedProposalInfo = {
|
|
63
|
+
slot: SlotNumber;
|
|
64
|
+
proposer: EthAddress;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Callback for when a block proposal whose index lands at or beyond the consensus per-checkpoint block
|
|
69
|
+
* limit is stored and re-broadcast as slashing evidence. May fire multiple times per (slot, proposer)
|
|
70
|
+
* if the proposer signed several oversized proposals; consumers are expected to dedup.
|
|
71
|
+
*/
|
|
72
|
+
export type P2POversizedProposalCallback = (info: OversizedProposalInfo) => void;
|
|
73
|
+
|
|
61
74
|
/** Minimal info passed to the duplicate attestation callback. */
|
|
62
75
|
export type DuplicateAttestationInfo = {
|
|
63
76
|
slot: SlotNumber;
|
|
@@ -108,6 +121,12 @@ export interface P2PService {
|
|
|
108
121
|
*/
|
|
109
122
|
registerDuplicateProposalCallback(callback: P2PDuplicateProposalCallback): void;
|
|
110
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Registers a callback invoked when an oversized block proposal (index at or beyond the consensus
|
|
126
|
+
* per-checkpoint block limit) is stored and re-broadcast as slashing evidence.
|
|
127
|
+
*/
|
|
128
|
+
registerOversizedProposalCallback(callback: P2POversizedProposalCallback): void;
|
|
129
|
+
|
|
111
130
|
/**
|
|
112
131
|
* Registers a callback invoked when a duplicate attestation is detected (equivocation).
|
|
113
132
|
* A validator signing attestations for different proposals at the same slot.
|