@aztec/validator-client 0.0.1-commit.f2ce05ee → 0.0.1-commit.f8ca9b2f3
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/README.md +12 -10
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +4 -0
- package/dest/validator.d.ts +18 -4
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +87 -10
- package/package.json +19 -19
- package/src/config.ts +4 -0
- package/src/validator.ts +127 -19
package/README.md
CHANGED
|
@@ -78,6 +78,7 @@ These rules must always hold:
|
|
|
78
78
|
3. **inHash is constant**: All blocks in a checkpoint share the same L1-to-L2 messages hash
|
|
79
79
|
4. **Sequential indexWithinCheckpoint**: Block N must have `indexWithinCheckpoint = parent.indexWithinCheckpoint + 1`
|
|
80
80
|
5. **One proposer per slot**: Each slot has exactly one designated proposer. Sending multiple proposals for the same position (slot, indexWithinCheckpoint) with different content is equivocation and slashable
|
|
81
|
+
6. **One attestation per slot**: Validators should only attest to one checkpoint per slot. Attesting to different proposals (different archives) for the same slot is equivocation and slashable
|
|
81
82
|
|
|
82
83
|
## Validation Flow
|
|
83
84
|
|
|
@@ -155,16 +156,17 @@ Time | Proposer | Validator
|
|
|
155
156
|
|
|
156
157
|
## Configuration
|
|
157
158
|
|
|
158
|
-
| Flag | Purpose
|
|
159
|
-
| ------------------------------------- |
|
|
160
|
-
| `validatorReexecute` | Re-execute transactions to verify proposals
|
|
161
|
-
| `fishermanMode` | Validate proposals but don't broadcast attestations (monitoring only)
|
|
162
|
-
| `alwaysReexecuteBlockProposals` | Force re-execution even when not in committee
|
|
163
|
-
| `slashBroadcastedInvalidBlockPenalty` | Penalty amount for invalid proposals (0 = disabled)
|
|
164
|
-
| `slashDuplicateProposalPenalty` | Penalty amount for duplicate proposals (0 = disabled)
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
159
|
+
| Flag | Purpose |
|
|
160
|
+
| ------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
161
|
+
| `validatorReexecute` | Re-execute transactions to verify proposals |
|
|
162
|
+
| `fishermanMode` | Validate proposals but don't broadcast attestations (monitoring only) |
|
|
163
|
+
| `alwaysReexecuteBlockProposals` | Force re-execution even when not in committee |
|
|
164
|
+
| `slashBroadcastedInvalidBlockPenalty` | Penalty amount for invalid proposals (0 = disabled) |
|
|
165
|
+
| `slashDuplicateProposalPenalty` | Penalty amount for duplicate proposals (0 = disabled) |
|
|
166
|
+
| `slashDuplicateAttestationPenalty` | Penalty amount for duplicate attestations (0 = disabled) |
|
|
167
|
+
| `validatorReexecuteDeadlineMs` | Time reserved at end of slot for propagation/publishing |
|
|
168
|
+
| `attestationPollingIntervalMs` | How often to poll for attestations when collecting |
|
|
169
|
+
| `disabledValidators` | Validator addresses to exclude from duties |
|
|
168
170
|
|
|
169
171
|
### High Availability (HA) Keystore
|
|
170
172
|
|
package/dest/config.d.ts
CHANGED
|
@@ -8,4 +8,4 @@ export declare const validatorClientConfigMappings: ConfigMappingsType<Validator
|
|
|
8
8
|
* @returns The validator configuration.
|
|
9
9
|
*/
|
|
10
10
|
export declare function getProverEnvVars(): ValidatorClientConfig;
|
|
11
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
11
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlnLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvY29uZmlnLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFDTCxLQUFLLGtCQUFrQixFQUt4QixNQUFNLDBCQUEwQixDQUFDO0FBRWxDLE9BQU8sS0FBSyxFQUFFLHFCQUFxQixFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFHN0UsWUFBWSxFQUFFLHFCQUFxQixFQUFFLENBQUM7QUFFdEMsZUFBTyxNQUFNLDZCQUE2QixFQUFFLGtCQUFrQixDQUFDLHFCQUFxQixDQW1FbkYsQ0FBQztBQUVGOzs7O0dBSUc7QUFDSCx3QkFBZ0IsZ0JBQWdCLElBQUkscUJBQXFCLENBRXhEIn0=
|
package/dest/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,kBAAkB,EAKxB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAG7E,YAAY,EAAE,qBAAqB,EAAE,CAAC;AAEtC,eAAO,MAAM,6BAA6B,EAAE,kBAAkB,CAAC,qBAAqB,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,kBAAkB,EAKxB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAG7E,YAAY,EAAE,qBAAqB,EAAE,CAAC;AAEtC,eAAO,MAAM,6BAA6B,EAAE,kBAAkB,CAAC,qBAAqB,CAmEnF,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,qBAAqB,CAExD"}
|
package/dest/config.js
CHANGED
|
@@ -53,6 +53,10 @@ export const validatorClientConfigMappings = {
|
|
|
53
53
|
description: 'Skip pushing re-executed blocks to archiver (default: false)',
|
|
54
54
|
defaultValue: false
|
|
55
55
|
},
|
|
56
|
+
attestToEquivocatedProposals: {
|
|
57
|
+
description: 'Agree to attest to equivocated checkpoint proposals (for testing purposes only)',
|
|
58
|
+
...booleanConfigHelper(false)
|
|
59
|
+
},
|
|
56
60
|
...validatorHASignerConfigMappings
|
|
57
61
|
};
|
|
58
62
|
/**
|
package/dest/validator.d.ts
CHANGED
|
@@ -13,8 +13,7 @@ import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
|
13
13
|
import type { CommitteeAttestationsAndSigners, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
14
14
|
import type { CreateCheckpointProposalLastBlockData, ITxProvider, Validator, ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
15
15
|
import { type L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
16
|
-
import type
|
|
17
|
-
import { CheckpointProposal } from '@aztec/stdlib/p2p';
|
|
16
|
+
import { type BlockProposal, type BlockProposalOptions, type CheckpointAttestation, CheckpointProposal, type CheckpointProposalCore, type CheckpointProposalOptions } from '@aztec/stdlib/p2p';
|
|
18
17
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
19
18
|
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
|
|
20
19
|
import { type TelemetryClient, type Tracer } from '@aztec/telemetry-client';
|
|
@@ -44,10 +43,15 @@ export declare class ValidatorClient extends ValidatorClient_base implements Val
|
|
|
44
43
|
private metrics;
|
|
45
44
|
private log;
|
|
46
45
|
private hasRegisteredHandlers;
|
|
47
|
-
|
|
46
|
+
/** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
|
|
47
|
+
private lastProposedBlock?;
|
|
48
|
+
/** Tracks the last checkpoint proposal we created. */
|
|
49
|
+
private lastProposedCheckpoint?;
|
|
48
50
|
private lastEpochForCommitteeUpdateLoop;
|
|
49
51
|
private epochCacheUpdateLoop;
|
|
50
52
|
private proposersOfInvalidBlocks;
|
|
53
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
54
|
+
private lastAttestedProposal?;
|
|
51
55
|
protected constructor(keyStore: ExtendedValidatorKeyStore, epochCache: EpochCache, p2pClient: P2P, blockProposalHandler: BlockProposalHandler, blockSource: L2BlockSource, checkpointsBuilder: FullNodeCheckpointsBuilder, worldState: WorldStateSynchronizer, l1ToL2MessageSource: L1ToL2MessageSource, config: ValidatorClientFullConfig, blobClient: BlobClientInterface, dateProvider?: DateProvider, telemetry?: TelemetryClient, log?: Logger);
|
|
52
56
|
static validateKeyStoreConfiguration(keyStoreManager: KeystoreManager, logger?: Logger): void;
|
|
53
57
|
private handleEpochCommitteeUpdate;
|
|
@@ -76,6 +80,11 @@ export declare class ValidatorClient extends ValidatorClient_base implements Val
|
|
|
76
80
|
* @returns Checkpoint attestations if valid, undefined otherwise
|
|
77
81
|
*/
|
|
78
82
|
attestToCheckpointProposal(proposal: CheckpointProposalCore, _proposalSender: PeerId): Promise<CheckpointAttestation[] | undefined>;
|
|
83
|
+
/**
|
|
84
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
85
|
+
* @returns true if we should attest, false if we should skip
|
|
86
|
+
*/
|
|
87
|
+
private shouldAttestToSlot;
|
|
79
88
|
private createCheckpointAttestationsFromProposal;
|
|
80
89
|
private validateCheckpointProposal;
|
|
81
90
|
/**
|
|
@@ -89,6 +98,11 @@ export declare class ValidatorClient extends ValidatorClient_base implements Val
|
|
|
89
98
|
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
90
99
|
*/
|
|
91
100
|
private handleDuplicateProposal;
|
|
101
|
+
/**
|
|
102
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
103
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
104
|
+
*/
|
|
105
|
+
private handleDuplicateAttestation;
|
|
92
106
|
createBlockProposal(blockHeader: BlockHeader, indexWithinCheckpoint: IndexWithinCheckpoint, inHash: Fr, archive: Fr, txs: Tx[], proposerAddress: EthAddress | undefined, options?: BlockProposalOptions): Promise<BlockProposal>;
|
|
93
107
|
createCheckpointProposal(checkpointHeader: CheckpointHeader, archive: Fr, lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined, proposerAddress: EthAddress | undefined, options?: CheckpointProposalOptions): Promise<CheckpointProposal>;
|
|
94
108
|
broadcastBlockProposal(proposal: BlockProposal): Promise<void>;
|
|
@@ -98,4 +112,4 @@ export declare class ValidatorClient extends ValidatorClient_base implements Val
|
|
|
98
112
|
private handleAuthRequest;
|
|
99
113
|
}
|
|
100
114
|
export {};
|
|
101
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
115
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdG9yLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvdmFsaWRhdG9yLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFFckUsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDckQsT0FBTyxFQUNMLFdBQVcsRUFDWCxnQkFBZ0IsRUFFaEIscUJBQXFCLEVBQ3JCLFVBQVUsRUFDWCxNQUFNLGlDQUFpQyxDQUFDO0FBQ3pDLE9BQU8sRUFBRSxFQUFFLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUVwRCxPQUFPLEtBQUssRUFBRSxVQUFVLEVBQUUsTUFBTSwrQkFBK0IsQ0FBQztBQUNoRSxPQUFPLEtBQUssRUFBRSxTQUFTLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUNqRSxPQUFPLEVBQWdCLEtBQUssTUFBTSxFQUFnQixNQUFNLHVCQUF1QixDQUFDO0FBSWhGLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUN2RCxPQUFPLEtBQUssRUFBRSxlQUFlLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUM1RCxPQUFPLEtBQUssRUFBbUQsR0FBRyxFQUFFLE1BQU0sRUFBRSxNQUFNLFlBQVksQ0FBQztBQUUvRixPQUFPLEVBQW9DLEtBQUssT0FBTyxFQUFFLEtBQUssY0FBYyxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDckcsT0FBTyxLQUFLLEVBQUUsWUFBWSxFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUFDaEUsT0FBTyxLQUFLLEVBQUUsK0JBQStCLEVBQVcsV0FBVyxFQUFFLGFBQWEsRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBRWhILE9BQU8sS0FBSyxFQUNWLHFDQUFxQyxFQUNyQyxXQUFXLEVBQ1gsU0FBUyxFQUNULHlCQUF5QixFQUN6QixzQkFBc0IsRUFDdkIsTUFBTSxpQ0FBaUMsQ0FB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
|
package/dest/validator.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../src/validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EACL,WAAW,EACX,gBAAgB,EAEhB,qBAAqB,EACrB,UAAU,EACX,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,EAAE,EAAE,MAAM,gCAAgC,CAAC;AAEpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAgB,KAAK,MAAM,EAAgB,MAAM,uBAAuB,CAAC;AAIhF,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../src/validator.ts"],"names":[],"mappings":"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"}
|
package/dest/validator.js
CHANGED
|
@@ -48,11 +48,12 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
48
48
|
log;
|
|
49
49
|
// Whether it has already registered handlers on the p2p client
|
|
50
50
|
hasRegisteredHandlers;
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
/** Tracks the last block proposal we created, to detect duplicate proposal attempts. */ lastProposedBlock;
|
|
52
|
+
/** Tracks the last checkpoint proposal we created. */ lastProposedCheckpoint;
|
|
53
53
|
lastEpochForCommitteeUpdateLoop;
|
|
54
54
|
epochCacheUpdateLoop;
|
|
55
55
|
proposersOfInvalidBlocks;
|
|
56
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */ lastAttestedProposal;
|
|
56
57
|
constructor(keyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
|
|
57
58
|
super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockProposalHandler = blockProposalHandler, this.blockSource = blockSource, this.checkpointsBuilder = checkpointsBuilder, this.worldState = worldState, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.blobClient = blobClient, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.proposersOfInvalidBlocks = new Set();
|
|
58
59
|
// Create child logger with fisherman prefix if in fisherman mode
|
|
@@ -187,6 +188,10 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
187
188
|
this.p2pClient.registerDuplicateProposalCallback((info)=>{
|
|
188
189
|
this.handleDuplicateProposal(info);
|
|
189
190
|
});
|
|
191
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
192
|
+
this.p2pClient.registerDuplicateAttestationCallback((info)=>{
|
|
193
|
+
this.handleDuplicateAttestation(info);
|
|
194
|
+
});
|
|
190
195
|
const myAddresses = this.getValidatorAddresses();
|
|
191
196
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
192
197
|
await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
|
|
@@ -207,6 +212,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
207
212
|
this.log.warn(`Received block proposal with invalid signature for slot ${slotNumber}`);
|
|
208
213
|
return false;
|
|
209
214
|
}
|
|
215
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
216
|
+
if (this.getValidatorAddresses().some((addr)=>addr.equals(proposer))) {
|
|
217
|
+
this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
218
|
+
proposer: proposer.toString(),
|
|
219
|
+
slotNumber
|
|
220
|
+
});
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
210
223
|
// Check if we're in the committee (for metrics purposes)
|
|
211
224
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
212
225
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -278,6 +291,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
278
291
|
this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
|
|
279
292
|
return undefined;
|
|
280
293
|
}
|
|
294
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
295
|
+
if (this.getValidatorAddresses().some((addr)=>addr.equals(proposer))) {
|
|
296
|
+
this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
297
|
+
proposer: proposer.toString(),
|
|
298
|
+
slotNumber
|
|
299
|
+
});
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
281
302
|
// Check that I have any address in current committee before attesting
|
|
282
303
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
283
304
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -341,10 +362,31 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
341
362
|
});
|
|
342
363
|
return undefined;
|
|
343
364
|
}
|
|
344
|
-
return this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
365
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
369
|
+
* @returns true if we should attest, false if we should skip
|
|
370
|
+
*/ shouldAttestToSlot(slotNumber) {
|
|
371
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
372
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
// Check if incoming slot is strictly greater than last attested
|
|
376
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
377
|
+
this.log.warn(`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`);
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
return true;
|
|
345
381
|
}
|
|
346
382
|
async createCheckpointAttestationsFromProposal(proposal, attestors = []) {
|
|
383
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
384
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
347
387
|
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
388
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
389
|
+
this.lastAttestedProposal = proposal;
|
|
348
390
|
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
349
391
|
return attestations;
|
|
350
392
|
}
|
|
@@ -548,23 +590,55 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
548
590
|
}
|
|
549
591
|
]);
|
|
550
592
|
}
|
|
593
|
+
/**
|
|
594
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
595
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
596
|
+
*/ handleDuplicateAttestation(info) {
|
|
597
|
+
const { slot, attester } = info;
|
|
598
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
599
|
+
attester: attester.toString(),
|
|
600
|
+
slot
|
|
601
|
+
});
|
|
602
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
603
|
+
{
|
|
604
|
+
validator: attester,
|
|
605
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
606
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
607
|
+
epochOrSlot: BigInt(slot)
|
|
608
|
+
}
|
|
609
|
+
]);
|
|
610
|
+
}
|
|
551
611
|
async createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
612
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
613
|
+
if (this.lastProposedBlock) {
|
|
614
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
615
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
616
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
617
|
+
if (newSlot < lastSlot || newSlot === lastSlot && indexWithinCheckpoint <= lastIndex) {
|
|
618
|
+
throw new Error(`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` + `already proposed block for slot ${lastSlot} index ${lastIndex}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
557
621
|
this.log.info(`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`);
|
|
558
622
|
const newProposal = await this.validationService.createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, {
|
|
559
623
|
...options,
|
|
560
624
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
|
|
561
625
|
});
|
|
562
|
-
this.
|
|
626
|
+
this.lastProposedBlock = newProposal;
|
|
563
627
|
return newProposal;
|
|
564
628
|
}
|
|
565
629
|
async createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options = {}) {
|
|
630
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
631
|
+
if (this.lastProposedCheckpoint) {
|
|
632
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
633
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
634
|
+
if (newSlot <= lastSlot) {
|
|
635
|
+
throw new Error(`Cannot create checkpoint proposal for slot ${newSlot}: ` + `already proposed checkpoint for slot ${lastSlot}`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
566
638
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
567
|
-
|
|
639
|
+
const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options);
|
|
640
|
+
this.lastProposedCheckpoint = newProposal;
|
|
641
|
+
return newProposal;
|
|
568
642
|
}
|
|
569
643
|
async broadcastBlockProposal(proposal) {
|
|
570
644
|
await this.p2pClient.broadcastProposal(proposal);
|
|
@@ -579,6 +653,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
579
653
|
inCommittee
|
|
580
654
|
});
|
|
581
655
|
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
656
|
+
if (!attestations) {
|
|
657
|
+
return [];
|
|
658
|
+
}
|
|
582
659
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
583
660
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
584
661
|
// due to inactivity for missed attestations.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/validator-client",
|
|
3
|
-
"version": "0.0.1-commit.
|
|
3
|
+
"version": "0.0.1-commit.f8ca9b2f3",
|
|
4
4
|
"main": "dest/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -64,30 +64,30 @@
|
|
|
64
64
|
]
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@aztec/blob-client": "0.0.1-commit.
|
|
68
|
-
"@aztec/blob-lib": "0.0.1-commit.
|
|
69
|
-
"@aztec/constants": "0.0.1-commit.
|
|
70
|
-
"@aztec/epoch-cache": "0.0.1-commit.
|
|
71
|
-
"@aztec/ethereum": "0.0.1-commit.
|
|
72
|
-
"@aztec/foundation": "0.0.1-commit.
|
|
73
|
-
"@aztec/node-keystore": "0.0.1-commit.
|
|
74
|
-
"@aztec/noir-protocol-circuits-types": "0.0.1-commit.
|
|
75
|
-
"@aztec/p2p": "0.0.1-commit.
|
|
76
|
-
"@aztec/protocol-contracts": "0.0.1-commit.
|
|
77
|
-
"@aztec/prover-client": "0.0.1-commit.
|
|
78
|
-
"@aztec/simulator": "0.0.1-commit.
|
|
79
|
-
"@aztec/slasher": "0.0.1-commit.
|
|
80
|
-
"@aztec/stdlib": "0.0.1-commit.
|
|
81
|
-
"@aztec/telemetry-client": "0.0.1-commit.
|
|
82
|
-
"@aztec/validator-ha-signer": "0.0.1-commit.
|
|
67
|
+
"@aztec/blob-client": "0.0.1-commit.f8ca9b2f3",
|
|
68
|
+
"@aztec/blob-lib": "0.0.1-commit.f8ca9b2f3",
|
|
69
|
+
"@aztec/constants": "0.0.1-commit.f8ca9b2f3",
|
|
70
|
+
"@aztec/epoch-cache": "0.0.1-commit.f8ca9b2f3",
|
|
71
|
+
"@aztec/ethereum": "0.0.1-commit.f8ca9b2f3",
|
|
72
|
+
"@aztec/foundation": "0.0.1-commit.f8ca9b2f3",
|
|
73
|
+
"@aztec/node-keystore": "0.0.1-commit.f8ca9b2f3",
|
|
74
|
+
"@aztec/noir-protocol-circuits-types": "0.0.1-commit.f8ca9b2f3",
|
|
75
|
+
"@aztec/p2p": "0.0.1-commit.f8ca9b2f3",
|
|
76
|
+
"@aztec/protocol-contracts": "0.0.1-commit.f8ca9b2f3",
|
|
77
|
+
"@aztec/prover-client": "0.0.1-commit.f8ca9b2f3",
|
|
78
|
+
"@aztec/simulator": "0.0.1-commit.f8ca9b2f3",
|
|
79
|
+
"@aztec/slasher": "0.0.1-commit.f8ca9b2f3",
|
|
80
|
+
"@aztec/stdlib": "0.0.1-commit.f8ca9b2f3",
|
|
81
|
+
"@aztec/telemetry-client": "0.0.1-commit.f8ca9b2f3",
|
|
82
|
+
"@aztec/validator-ha-signer": "0.0.1-commit.f8ca9b2f3",
|
|
83
83
|
"koa": "^2.16.1",
|
|
84
84
|
"koa-router": "^13.1.1",
|
|
85
85
|
"tslib": "^2.4.0",
|
|
86
86
|
"viem": "npm:@aztec/viem@2.38.2"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
-
"@aztec/archiver": "0.0.1-commit.
|
|
90
|
-
"@aztec/world-state": "0.0.1-commit.
|
|
89
|
+
"@aztec/archiver": "0.0.1-commit.f8ca9b2f3",
|
|
90
|
+
"@aztec/world-state": "0.0.1-commit.f8ca9b2f3",
|
|
91
91
|
"@electric-sql/pglite": "^0.3.14",
|
|
92
92
|
"@jest/globals": "^30.0.0",
|
|
93
93
|
"@types/jest": "^30.0.0",
|
package/src/config.ts
CHANGED
|
@@ -73,6 +73,10 @@ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientCo
|
|
|
73
73
|
description: 'Skip pushing re-executed blocks to archiver (default: false)',
|
|
74
74
|
defaultValue: false,
|
|
75
75
|
},
|
|
76
|
+
attestToEquivocatedProposals: {
|
|
77
|
+
description: 'Agree to attest to equivocated checkpoint proposals (for testing purposes only)',
|
|
78
|
+
...booleanConfigHelper(false),
|
|
79
|
+
},
|
|
76
80
|
...validatorHASignerConfigMappings,
|
|
77
81
|
};
|
|
78
82
|
|
package/src/validator.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
|
18
18
|
import { sleep } from '@aztec/foundation/sleep';
|
|
19
19
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
20
20
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
21
|
-
import type { DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
21
|
+
import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
22
22
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
23
23
|
import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
|
|
24
24
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
@@ -32,14 +32,14 @@ import type {
|
|
|
32
32
|
WorldStateSynchronizer,
|
|
33
33
|
} from '@aztec/stdlib/interfaces/server';
|
|
34
34
|
import { type L1ToL2MessageSource, accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
35
|
-
import
|
|
36
|
-
BlockProposal,
|
|
37
|
-
BlockProposalOptions,
|
|
38
|
-
CheckpointAttestation,
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
import {
|
|
36
|
+
type BlockProposal,
|
|
37
|
+
type BlockProposalOptions,
|
|
38
|
+
type CheckpointAttestation,
|
|
39
|
+
CheckpointProposal,
|
|
40
|
+
type CheckpointProposalCore,
|
|
41
|
+
type CheckpointProposalOptions,
|
|
41
42
|
} from '@aztec/stdlib/p2p';
|
|
42
|
-
import { CheckpointProposal } from '@aztec/stdlib/p2p';
|
|
43
43
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
44
44
|
import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
|
|
45
45
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
@@ -80,14 +80,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
80
80
|
// Whether it has already registered handlers on the p2p client
|
|
81
81
|
private hasRegisteredHandlers = false;
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
private
|
|
83
|
+
/** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
|
|
84
|
+
private lastProposedBlock?: BlockProposal;
|
|
85
|
+
|
|
86
|
+
/** Tracks the last checkpoint proposal we created. */
|
|
87
|
+
private lastProposedCheckpoint?: CheckpointProposal;
|
|
85
88
|
|
|
86
89
|
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
87
90
|
private epochCacheUpdateLoop: RunningPromise;
|
|
88
91
|
|
|
89
92
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
90
93
|
|
|
94
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
95
|
+
private lastAttestedProposal?: CheckpointProposalCore;
|
|
96
|
+
|
|
91
97
|
protected constructor(
|
|
92
98
|
private keyStore: ExtendedValidatorKeyStore,
|
|
93
99
|
private epochCache: EpochCache,
|
|
@@ -314,6 +320,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
314
320
|
this.handleDuplicateProposal(info);
|
|
315
321
|
});
|
|
316
322
|
|
|
323
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
324
|
+
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
325
|
+
this.handleDuplicateAttestation(info);
|
|
326
|
+
});
|
|
327
|
+
|
|
317
328
|
const myAddresses = this.getValidatorAddresses();
|
|
318
329
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
319
330
|
|
|
@@ -341,6 +352,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
341
352
|
return false;
|
|
342
353
|
}
|
|
343
354
|
|
|
355
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
356
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
357
|
+
this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
358
|
+
proposer: proposer.toString(),
|
|
359
|
+
slotNumber,
|
|
360
|
+
});
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
344
364
|
// Check if we're in the committee (for metrics purposes)
|
|
345
365
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
346
366
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -442,6 +462,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
442
462
|
return undefined;
|
|
443
463
|
}
|
|
444
464
|
|
|
465
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
466
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
467
|
+
this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
468
|
+
proposer: proposer.toString(),
|
|
469
|
+
slotNumber,
|
|
470
|
+
});
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
|
|
445
474
|
// Check that I have any address in current committee before attesting
|
|
446
475
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
447
476
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -515,14 +544,44 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
515
544
|
return undefined;
|
|
516
545
|
}
|
|
517
546
|
|
|
518
|
-
return this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
547
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
552
|
+
* @returns true if we should attest, false if we should skip
|
|
553
|
+
*/
|
|
554
|
+
private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
|
|
555
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
556
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Check if incoming slot is strictly greater than last attested
|
|
561
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
562
|
+
this.log.warn(
|
|
563
|
+
`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
|
|
564
|
+
);
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return true;
|
|
519
569
|
}
|
|
520
570
|
|
|
521
571
|
private async createCheckpointAttestationsFromProposal(
|
|
522
572
|
proposal: CheckpointProposalCore,
|
|
523
573
|
attestors: EthAddress[] = [],
|
|
524
|
-
): Promise<CheckpointAttestation[]> {
|
|
574
|
+
): Promise<CheckpointAttestation[] | undefined> {
|
|
575
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
576
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
577
|
+
return undefined;
|
|
578
|
+
}
|
|
579
|
+
|
|
525
580
|
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
581
|
+
|
|
582
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
583
|
+
this.lastAttestedProposal = proposal;
|
|
584
|
+
|
|
526
585
|
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
527
586
|
return attestations;
|
|
528
587
|
}
|
|
@@ -750,6 +809,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
750
809
|
]);
|
|
751
810
|
}
|
|
752
811
|
|
|
812
|
+
/**
|
|
813
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
814
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
815
|
+
*/
|
|
816
|
+
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
817
|
+
const { slot, attester } = info;
|
|
818
|
+
|
|
819
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
820
|
+
attester: attester.toString(),
|
|
821
|
+
slot,
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
825
|
+
{
|
|
826
|
+
validator: attester,
|
|
827
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
828
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
829
|
+
epochOrSlot: BigInt(slot),
|
|
830
|
+
},
|
|
831
|
+
]);
|
|
832
|
+
}
|
|
833
|
+
|
|
753
834
|
async createBlockProposal(
|
|
754
835
|
blockHeader: BlockHeader,
|
|
755
836
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
@@ -759,11 +840,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
759
840
|
proposerAddress: EthAddress | undefined,
|
|
760
841
|
options: BlockProposalOptions = {},
|
|
761
842
|
): Promise<BlockProposal> {
|
|
762
|
-
//
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
843
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
844
|
+
if (this.lastProposedBlock) {
|
|
845
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
846
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
847
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
848
|
+
|
|
849
|
+
if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
|
|
850
|
+
throw new Error(
|
|
851
|
+
`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
|
|
852
|
+
`already proposed block for slot ${lastSlot} index ${lastIndex}`,
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
767
856
|
|
|
768
857
|
this.log.info(
|
|
769
858
|
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
@@ -780,7 +869,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
780
869
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
781
870
|
},
|
|
782
871
|
);
|
|
783
|
-
this.
|
|
872
|
+
this.lastProposedBlock = newProposal;
|
|
784
873
|
return newProposal;
|
|
785
874
|
}
|
|
786
875
|
|
|
@@ -791,14 +880,29 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
791
880
|
proposerAddress: EthAddress | undefined,
|
|
792
881
|
options: CheckpointProposalOptions = {},
|
|
793
882
|
): Promise<CheckpointProposal> {
|
|
883
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
884
|
+
if (this.lastProposedCheckpoint) {
|
|
885
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
886
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
887
|
+
|
|
888
|
+
if (newSlot <= lastSlot) {
|
|
889
|
+
throw new Error(
|
|
890
|
+
`Cannot create checkpoint proposal for slot ${newSlot}: ` +
|
|
891
|
+
`already proposed checkpoint for slot ${lastSlot}`,
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
794
896
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
795
|
-
|
|
897
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
796
898
|
checkpointHeader,
|
|
797
899
|
archive,
|
|
798
900
|
lastBlockInfo,
|
|
799
901
|
proposerAddress,
|
|
800
902
|
options,
|
|
801
903
|
);
|
|
904
|
+
this.lastProposedCheckpoint = newProposal;
|
|
905
|
+
return newProposal;
|
|
802
906
|
}
|
|
803
907
|
|
|
804
908
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
@@ -820,6 +924,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
820
924
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
821
925
|
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
822
926
|
|
|
927
|
+
if (!attestations) {
|
|
928
|
+
return [];
|
|
929
|
+
}
|
|
930
|
+
|
|
823
931
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
824
932
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
825
933
|
// due to inactivity for missed attestations.
|