@aztec/validator-client 0.0.1-commit.6d63667d → 0.0.1-commit.7cf39cb55
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 +21 -18
- 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 +23 -4
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +118 -14
- package/package.json +19 -19
- package/src/config.ts +4 -0
- package/src/validator.ts +163 -22
package/README.md
CHANGED
|
@@ -77,6 +77,8 @@ These rules must always hold:
|
|
|
77
77
|
2. **Global variables match within checkpoint**: All blocks within the same checkpoint must have identical global variables (except `blockNumber`), which includes the slot number
|
|
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
|
+
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
|
|
80
82
|
|
|
81
83
|
## Validation Flow
|
|
82
84
|
|
|
@@ -87,15 +89,14 @@ When a `BlockProposal` is received via P2P, the `BlockProposalHandler` performs:
|
|
|
87
89
|
```
|
|
88
90
|
1. Verify proposer signature
|
|
89
91
|
2. Check proposal is from current/next slot proposer (via BlockProposalValidator)
|
|
90
|
-
3.
|
|
91
|
-
4.
|
|
92
|
-
5.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
9. Compare re-execution result with proposal
|
|
92
|
+
3. Detect duplicate proposals (same slot + indexWithinCheckpoint, different archive) slashing proposer on equivocation
|
|
93
|
+
4. Find parent block by archive root (wait/retry if not synced)
|
|
94
|
+
5. Compute checkpoint number from parent
|
|
95
|
+
6. If indexWithinCheckpoint > 0, then validate global variables match parent (chainId, version, slotNumber, timestamp, coinbase, feeRecipient, gasFees)
|
|
96
|
+
7. Verify inHash matches computed from L1-to-L2 messages
|
|
97
|
+
8. Collect transactions from pool/network/proposal
|
|
98
|
+
9. Re-execute transactions (if enabled)
|
|
99
|
+
10. Compare re-execution result with proposal
|
|
99
100
|
```
|
|
100
101
|
|
|
101
102
|
### Checkpoint Proposal Validation
|
|
@@ -155,15 +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
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
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 |
|
|
167
170
|
|
|
168
171
|
### High Availability (HA) Keystore
|
|
169
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
|
/**
|
|
@@ -84,6 +93,16 @@ export declare class ValidatorClient extends ValidatorClient_base implements Val
|
|
|
84
93
|
private extractCheckpointConstants;
|
|
85
94
|
private uploadBlobsForCheckpoint;
|
|
86
95
|
private slashInvalidBlock;
|
|
96
|
+
/**
|
|
97
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
98
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
99
|
+
*/
|
|
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;
|
|
87
106
|
createBlockProposal(blockHeader: BlockHeader, indexWithinCheckpoint: IndexWithinCheckpoint, inHash: Fr, archive: Fr, txs: Tx[], proposerAddress: EthAddress | undefined, options?: BlockProposalOptions): Promise<BlockProposal>;
|
|
88
107
|
createCheckpointProposal(checkpointHeader: CheckpointHeader, archive: Fr, lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined, proposerAddress: EthAddress | undefined, options?: CheckpointProposalOptions): Promise<CheckpointProposal>;
|
|
89
108
|
broadcastBlockProposal(proposal: BlockProposal): Promise<void>;
|
|
@@ -93,4 +112,4 @@ export declare class ValidatorClient extends ValidatorClient_base implements Val
|
|
|
93
112
|
private handleAuthRequest;
|
|
94
113
|
}
|
|
95
114
|
export {};
|
|
96
|
-
//# 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":"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,EAAmD,GAAG,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAE/F,OAAO,EAAoC,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,+BAA+B,EAAW,WAAW,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEhH,OAAO,KAAK,EACV,qCAAqC,EACrC,WAAW,EACX,SAAS,EACT,yBAAyB,EACzB,sBAAsB,EACvB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,KAAK,mBAAmB,EAAiC,MAAM,yBAAyB,CAAC;AAClG,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,kBAAkB,EAClB,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC/B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAA6B,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAEnF,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,MAAM,EAAsB,MAAM,yBAAyB,CAAC;AAEhG,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAGjF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAEhD,OAAO,EAAE,oBAAoB,EAA6C,MAAM,6BAA6B,CAAC;AAC9G,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AAG1E,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;;AAc1E;;GAEG;AACH,qBAAa,eAAgB,SAAQ,oBAA2C,YAAW,SAAS,EAAE,OAAO;IAwBzG,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,oBAAoB;IAC5B,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,kBAAkB;IAC1B,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,mBAAmB;IAC3B,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,YAAY;IAjCtB,SAAgB,MAAM,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,GAAG,CAAS;IAGpB,OAAO,CAAC,qBAAqB,CAAS;IAEtC,wFAAwF;IACxF,OAAO,CAAC,iBAAiB,CAAC,CAAgB;IAE1C,sDAAsD;IACtD,OAAO,CAAC,sBAAsB,CAAC,CAAqB;IAEpD,OAAO,CAAC,+BAA+B,CAA0B;IACjE,OAAO,CAAC,oBAAoB,CAAiB;IAE7C,OAAO,CAAC,wBAAwB,CAA0B;IAE1D,mFAAmF;IACnF,OAAO,CAAC,oBAAoB,CAAC,CAAyB;IAEtD,SAAS,aACC,QAAQ,EAAE,yBAAyB,EACnC,UAAU,EAAE,UAAU,EACtB,SAAS,EAAE,GAAG,EACd,oBAAoB,EAAE,oBAAoB,EAC1C,WAAW,EAAE,aAAa,EAC1B,kBAAkB,EAAE,0BAA0B,EAC9C,UAAU,EAAE,sBAAsB,EAClC,mBAAmB,EAAE,mBAAmB,EACxC,MAAM,EAAE,yBAAyB,EACjC,UAAU,EAAE,mBAAmB,EAC/B,YAAY,GAAE,YAAiC,EACvD,SAAS,GAAE,eAAsC,EACjD,GAAG,SAA4B,EAiBhC;IAED,OAAc,6BAA6B,CAAC,eAAe,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,MAAM,QAuB5F;YAEa,0BAA0B;IA2BxC,OAAa,GAAG,CACd,MAAM,EAAE,yBAAyB,EACjC,kBAAkB,EAAE,0BAA0B,EAC9C,UAAU,EAAE,sBAAsB,EAClC,UAAU,EAAE,UAAU,EACtB,SAAS,EAAE,GAAG,EACd,WAAW,EAAE,aAAa,GAAG,WAAW,EACxC,mBAAmB,EAAE,mBAAmB,EACxC,UAAU,EAAE,WAAW,EACvB,eAAe,EAAE,eAAe,EAChC,UAAU,EAAE,mBAAmB,EAC/B,YAAY,GAAE,YAAiC,EAC/C,SAAS,GAAE,eAAsC,4BA+ClD;IAEM,qBAAqB,iBAI3B;IAEM,uBAAuB,yBAE7B;IAEM,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,mBAAmB,EAAE,OAAO,EAAE,cAAc,sBAEzF;IAEM,sBAAsB,CAAC,QAAQ,EAAE,UAAU,GAAG,UAAU,CAE9D;IAEM,0BAA0B,CAAC,QAAQ,EAAE,UAAU,GAAG,YAAY,CAEpE;IAEM,SAAS,IAAI,yBAAyB,CAE5C;IAEM,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,yBAAyB,CAAC,QAE7D;IAEY,KAAK,kBAmBjB;IAEY,IAAI,kBAGhB;IAED,0CAA0C;IAC7B,gBAAgB,kBAkC5B;IAED;;;;OAIG;IACG,qBAAqB,CAAC,QAAQ,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAkG7F;IAED;;;;;OAKG;IACG,0BAA0B,CAC9B,QAAQ,EAAE,sBAAsB,EAChC,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,qBAAqB,EAAE,GAAG,SAAS,CAAC,CAmG9C;IAED;;;OAGG;IACH,OAAO,CAAC,kBAAkB;YAiBZ,wCAAwC;YAsBxC,0BAA0B;IAgIxC;;OAEG;IACH,OAAO,CAAC,0BAA0B;YAepB,wBAAwB;IA0BtC,OAAO,CAAC,iBAAiB;IA2BzB;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAoB/B;;;OAGG;IACH,OAAO,CAAC,0BAA0B;IAkB5B,mBAAmB,CACvB,WAAW,EAAE,WAAW,EACxB,qBAAqB,EAAE,qBAAqB,EAC5C,MAAM,EAAE,EAAE,EACV,OAAO,EAAE,EAAE,EACX,GAAG,EAAE,EAAE,EAAE,EACT,eAAe,EAAE,UAAU,GAAG,SAAS,EACvC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,aAAa,CAAC,CAgCxB;IAEK,wBAAwB,CAC5B,gBAAgB,EAAE,gBAAgB,EAClC,OAAO,EAAE,EAAE,EACX,aAAa,EAAE,qCAAqC,GAAG,SAAS,EAChE,eAAe,EAAE,UAAU,GAAG,SAAS,EACvC,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,kBAAkB,CAAC,CAwB7B;IAEK,sBAAsB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnE;IAEK,0BAA0B,CAC9B,sBAAsB,EAAE,+BAA+B,EACvD,QAAQ,EAAE,UAAU,EACpB,IAAI,EAAE,UAAU,EAChB,WAAW,EAAE,WAAW,GAAG,gBAAgB,GAC1C,OAAO,CAAC,SAAS,CAAC,CAEpB;IAEK,sBAAsB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAiB3F;IAEK,mBAAmB,CACvB,QAAQ,EAAE,kBAAkB,EAC5B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,IAAI,GACb,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAiElC;YAEa,iBAAiB;CAwBhC"}
|
package/dest/validator.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
2
|
-
import { BlockNumber } from '@aztec/foundation/branded-types';
|
|
2
|
+
import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
3
3
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
4
4
|
import { createLogger } from '@aztec/foundation/log';
|
|
5
5
|
import { retryUntil } from '@aztec/foundation/retry';
|
|
@@ -8,7 +8,7 @@ import { sleep } from '@aztec/foundation/sleep';
|
|
|
8
8
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
9
9
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
10
10
|
import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
|
|
11
|
-
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
11
|
+
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
12
12
|
import { accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
13
13
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
14
14
|
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
@@ -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
|
|
@@ -183,6 +184,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
183
184
|
// and processed separately via the block handler above.
|
|
184
185
|
const checkpointHandler = (checkpoint, proposalSender)=>this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
185
186
|
this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
|
|
187
|
+
// Duplicate proposal handler - triggers slashing for equivocation
|
|
188
|
+
this.p2pClient.registerDuplicateProposalCallback((info)=>{
|
|
189
|
+
this.handleDuplicateProposal(info);
|
|
190
|
+
});
|
|
191
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
192
|
+
this.p2pClient.registerDuplicateAttestationCallback((info)=>{
|
|
193
|
+
this.handleDuplicateAttestation(info);
|
|
194
|
+
});
|
|
186
195
|
const myAddresses = this.getValidatorAddresses();
|
|
187
196
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
188
197
|
await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
|
|
@@ -203,6 +212,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
203
212
|
this.log.warn(`Received block proposal with invalid signature for slot ${slotNumber}`);
|
|
204
213
|
return false;
|
|
205
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
|
+
}
|
|
206
223
|
// Check if we're in the committee (for metrics purposes)
|
|
207
224
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
208
225
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -274,6 +291,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
274
291
|
this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
|
|
275
292
|
return undefined;
|
|
276
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
|
+
}
|
|
277
302
|
// Check that I have any address in current committee before attesting
|
|
278
303
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
279
304
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -337,11 +362,32 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
337
362
|
});
|
|
338
363
|
return undefined;
|
|
339
364
|
}
|
|
340
|
-
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;
|
|
341
381
|
}
|
|
342
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
|
+
}
|
|
343
387
|
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
344
|
-
|
|
388
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
389
|
+
this.lastAttestedProposal = proposal;
|
|
390
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
345
391
|
return attestations;
|
|
346
392
|
}
|
|
347
393
|
/**
|
|
@@ -349,7 +395,10 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
349
395
|
* @returns Validation result with isValid flag and reason if invalid.
|
|
350
396
|
*/ async validateCheckpointProposal(proposal, proposalInfo) {
|
|
351
397
|
const slot = proposal.slotNumber;
|
|
352
|
-
|
|
398
|
+
// Timeout block syncing at the start of the next slot
|
|
399
|
+
const config = this.checkpointsBuilder.getConfig();
|
|
400
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
401
|
+
const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
|
|
353
402
|
// Wait for last block to sync by archive
|
|
354
403
|
let lastBlockHeader;
|
|
355
404
|
try {
|
|
@@ -524,23 +573,75 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
524
573
|
}
|
|
525
574
|
]);
|
|
526
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
578
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
579
|
+
*/ handleDuplicateProposal(info) {
|
|
580
|
+
const { slot, proposer, type } = info;
|
|
581
|
+
this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
|
|
582
|
+
proposer: proposer.toString(),
|
|
583
|
+
slot,
|
|
584
|
+
type
|
|
585
|
+
});
|
|
586
|
+
// Emit slash event
|
|
587
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
588
|
+
{
|
|
589
|
+
validator: proposer,
|
|
590
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
591
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
592
|
+
epochOrSlot: BigInt(slot)
|
|
593
|
+
}
|
|
594
|
+
]);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
598
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
599
|
+
*/ handleDuplicateAttestation(info) {
|
|
600
|
+
const { slot, attester } = info;
|
|
601
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
602
|
+
attester: attester.toString(),
|
|
603
|
+
slot
|
|
604
|
+
});
|
|
605
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
606
|
+
{
|
|
607
|
+
validator: attester,
|
|
608
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
609
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
610
|
+
epochOrSlot: BigInt(slot)
|
|
611
|
+
}
|
|
612
|
+
]);
|
|
613
|
+
}
|
|
527
614
|
async createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
|
|
528
|
-
//
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
615
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
616
|
+
if (this.lastProposedBlock) {
|
|
617
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
618
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
619
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
620
|
+
if (newSlot < lastSlot || newSlot === lastSlot && indexWithinCheckpoint <= lastIndex) {
|
|
621
|
+
throw new Error(`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` + `already proposed block for slot ${lastSlot} index ${lastIndex}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
533
624
|
this.log.info(`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`);
|
|
534
625
|
const newProposal = await this.validationService.createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, {
|
|
535
626
|
...options,
|
|
536
627
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
|
|
537
628
|
});
|
|
538
|
-
this.
|
|
629
|
+
this.lastProposedBlock = newProposal;
|
|
539
630
|
return newProposal;
|
|
540
631
|
}
|
|
541
632
|
async createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options = {}) {
|
|
633
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
634
|
+
if (this.lastProposedCheckpoint) {
|
|
635
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
636
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
637
|
+
if (newSlot <= lastSlot) {
|
|
638
|
+
throw new Error(`Cannot create checkpoint proposal for slot ${newSlot}: ` + `already proposed checkpoint for slot ${lastSlot}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
542
641
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
543
|
-
|
|
642
|
+
const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options);
|
|
643
|
+
this.lastProposedCheckpoint = newProposal;
|
|
644
|
+
return newProposal;
|
|
544
645
|
}
|
|
545
646
|
async broadcastBlockProposal(proposal) {
|
|
546
647
|
await this.p2pClient.broadcastProposal(proposal);
|
|
@@ -555,6 +656,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
555
656
|
inCommittee
|
|
556
657
|
});
|
|
557
658
|
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
659
|
+
if (!attestations) {
|
|
660
|
+
return [];
|
|
661
|
+
}
|
|
558
662
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
559
663
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
560
664
|
// 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.7cf39cb55",
|
|
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.7cf39cb55",
|
|
68
|
+
"@aztec/blob-lib": "0.0.1-commit.7cf39cb55",
|
|
69
|
+
"@aztec/constants": "0.0.1-commit.7cf39cb55",
|
|
70
|
+
"@aztec/epoch-cache": "0.0.1-commit.7cf39cb55",
|
|
71
|
+
"@aztec/ethereum": "0.0.1-commit.7cf39cb55",
|
|
72
|
+
"@aztec/foundation": "0.0.1-commit.7cf39cb55",
|
|
73
|
+
"@aztec/node-keystore": "0.0.1-commit.7cf39cb55",
|
|
74
|
+
"@aztec/noir-protocol-circuits-types": "0.0.1-commit.7cf39cb55",
|
|
75
|
+
"@aztec/p2p": "0.0.1-commit.7cf39cb55",
|
|
76
|
+
"@aztec/protocol-contracts": "0.0.1-commit.7cf39cb55",
|
|
77
|
+
"@aztec/prover-client": "0.0.1-commit.7cf39cb55",
|
|
78
|
+
"@aztec/simulator": "0.0.1-commit.7cf39cb55",
|
|
79
|
+
"@aztec/slasher": "0.0.1-commit.7cf39cb55",
|
|
80
|
+
"@aztec/stdlib": "0.0.1-commit.7cf39cb55",
|
|
81
|
+
"@aztec/telemetry-client": "0.0.1-commit.7cf39cb55",
|
|
82
|
+
"@aztec/validator-ha-signer": "0.0.1-commit.7cf39cb55",
|
|
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.7cf39cb55",
|
|
90
|
+
"@aztec/world-state": "0.0.1-commit.7cf39cb55",
|
|
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,12 +18,12 @@ 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 { 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';
|
|
25
25
|
import type { CommitteeAttestationsAndSigners, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
26
|
-
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
26
|
+
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
27
27
|
import type {
|
|
28
28
|
CreateCheckpointProposalLastBlockData,
|
|
29
29
|
ITxProvider,
|
|
@@ -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,
|
|
@@ -309,6 +315,16 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
309
315
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
310
316
|
this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
|
|
311
317
|
|
|
318
|
+
// Duplicate proposal handler - triggers slashing for equivocation
|
|
319
|
+
this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
|
|
320
|
+
this.handleDuplicateProposal(info);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
324
|
+
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
325
|
+
this.handleDuplicateAttestation(info);
|
|
326
|
+
});
|
|
327
|
+
|
|
312
328
|
const myAddresses = this.getValidatorAddresses();
|
|
313
329
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
314
330
|
|
|
@@ -336,6 +352,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
336
352
|
return false;
|
|
337
353
|
}
|
|
338
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
|
+
|
|
339
364
|
// Check if we're in the committee (for metrics purposes)
|
|
340
365
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
341
366
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -437,6 +462,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
437
462
|
return undefined;
|
|
438
463
|
}
|
|
439
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
|
+
|
|
440
474
|
// Check that I have any address in current committee before attesting
|
|
441
475
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
442
476
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -510,15 +544,45 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
510
544
|
return undefined;
|
|
511
545
|
}
|
|
512
546
|
|
|
513
|
-
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;
|
|
514
569
|
}
|
|
515
570
|
|
|
516
571
|
private async createCheckpointAttestationsFromProposal(
|
|
517
572
|
proposal: CheckpointProposalCore,
|
|
518
573
|
attestors: EthAddress[] = [],
|
|
519
|
-
): 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
|
+
|
|
520
580
|
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
521
|
-
|
|
581
|
+
|
|
582
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
583
|
+
this.lastAttestedProposal = proposal;
|
|
584
|
+
|
|
585
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
522
586
|
return attestations;
|
|
523
587
|
}
|
|
524
588
|
|
|
@@ -531,7 +595,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
531
595
|
proposalInfo: LogData,
|
|
532
596
|
): Promise<{ isValid: true } | { isValid: false; reason: string }> {
|
|
533
597
|
const slot = proposal.slotNumber;
|
|
534
|
-
|
|
598
|
+
|
|
599
|
+
// Timeout block syncing at the start of the next slot
|
|
600
|
+
const config = this.checkpointsBuilder.getConfig();
|
|
601
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
602
|
+
const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
|
|
535
603
|
|
|
536
604
|
// Wait for last block to sync by archive
|
|
537
605
|
let lastBlockHeader: BlockHeader | undefined;
|
|
@@ -721,6 +789,52 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
721
789
|
]);
|
|
722
790
|
}
|
|
723
791
|
|
|
792
|
+
/**
|
|
793
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
794
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
795
|
+
*/
|
|
796
|
+
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
797
|
+
const { slot, proposer, type } = info;
|
|
798
|
+
|
|
799
|
+
this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
|
|
800
|
+
proposer: proposer.toString(),
|
|
801
|
+
slot,
|
|
802
|
+
type,
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// Emit slash event
|
|
806
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
807
|
+
{
|
|
808
|
+
validator: proposer,
|
|
809
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
810
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
811
|
+
epochOrSlot: BigInt(slot),
|
|
812
|
+
},
|
|
813
|
+
]);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
818
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
819
|
+
*/
|
|
820
|
+
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
821
|
+
const { slot, attester } = info;
|
|
822
|
+
|
|
823
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
824
|
+
attester: attester.toString(),
|
|
825
|
+
slot,
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
829
|
+
{
|
|
830
|
+
validator: attester,
|
|
831
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
832
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
833
|
+
epochOrSlot: BigInt(slot),
|
|
834
|
+
},
|
|
835
|
+
]);
|
|
836
|
+
}
|
|
837
|
+
|
|
724
838
|
async createBlockProposal(
|
|
725
839
|
blockHeader: BlockHeader,
|
|
726
840
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
@@ -730,11 +844,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
730
844
|
proposerAddress: EthAddress | undefined,
|
|
731
845
|
options: BlockProposalOptions = {},
|
|
732
846
|
): Promise<BlockProposal> {
|
|
733
|
-
//
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
847
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
848
|
+
if (this.lastProposedBlock) {
|
|
849
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
850
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
851
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
852
|
+
|
|
853
|
+
if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
|
|
854
|
+
throw new Error(
|
|
855
|
+
`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
|
|
856
|
+
`already proposed block for slot ${lastSlot} index ${lastIndex}`,
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
738
860
|
|
|
739
861
|
this.log.info(
|
|
740
862
|
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
@@ -751,7 +873,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
751
873
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
752
874
|
},
|
|
753
875
|
);
|
|
754
|
-
this.
|
|
876
|
+
this.lastProposedBlock = newProposal;
|
|
755
877
|
return newProposal;
|
|
756
878
|
}
|
|
757
879
|
|
|
@@ -762,14 +884,29 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
762
884
|
proposerAddress: EthAddress | undefined,
|
|
763
885
|
options: CheckpointProposalOptions = {},
|
|
764
886
|
): Promise<CheckpointProposal> {
|
|
887
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
888
|
+
if (this.lastProposedCheckpoint) {
|
|
889
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
890
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
891
|
+
|
|
892
|
+
if (newSlot <= lastSlot) {
|
|
893
|
+
throw new Error(
|
|
894
|
+
`Cannot create checkpoint proposal for slot ${newSlot}: ` +
|
|
895
|
+
`already proposed checkpoint for slot ${lastSlot}`,
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
765
900
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
766
|
-
|
|
901
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
767
902
|
checkpointHeader,
|
|
768
903
|
archive,
|
|
769
904
|
lastBlockInfo,
|
|
770
905
|
proposerAddress,
|
|
771
906
|
options,
|
|
772
907
|
);
|
|
908
|
+
this.lastProposedCheckpoint = newProposal;
|
|
909
|
+
return newProposal;
|
|
773
910
|
}
|
|
774
911
|
|
|
775
912
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
@@ -791,6 +928,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
791
928
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
792
929
|
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
793
930
|
|
|
931
|
+
if (!attestations) {
|
|
932
|
+
return [];
|
|
933
|
+
}
|
|
934
|
+
|
|
794
935
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
795
936
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
796
937
|
// due to inactivity for missed attestations.
|