Skip to content

Commit 7e10af7

Browse files
committed
feat: ValidatorKeystore implementation for high-availability signer
Fixes [A-354](https://linear.app/aztec-labs/issue/A-354/integrate-slash-protection-in-signing-path) Implements `ExtendedValidatorKeyStore` utilizing `ValidatorHASigner`. Updated interfaces to return `null` when the signature isn't acquired & updated handling of signing for `null` results. Also some cleanup on previous HA work like consolidating config
1 parent d3be5a7 commit 7e10af7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+4481
-79
lines changed

yarn-project/aztec-node/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"@aztec/stdlib": "workspace:^",
8989
"@aztec/telemetry-client": "workspace:^",
9090
"@aztec/validator-client": "workspace:^",
91+
"@aztec/validator-ha-signer": "workspace:^",
9192
"@aztec/world-state": "workspace:^",
9293
"koa": "^2.16.1",
9394
"koa-router": "^13.1.1",

yarn-project/aztec-node/src/aztec-node/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
345345
const watchers: Watcher[] = [];
346346

347347
// Create validator client if required
348-
const validatorClient = createValidatorClient(config, {
348+
const validatorClient = await createValidatorClient(config, {
349349
p2pClient,
350350
telemetry,
351351
dateProvider,

yarn-project/aztec-node/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@
7272
{
7373
"path": "../validator-client"
7474
},
75+
{
76+
"path": "../validator-ha-signer"
77+
},
7578
{
7679
"path": "../world-state"
7780
}

yarn-project/end-to-end/src/e2e_p2p/reex.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,10 @@ describe('e2e_p2p_reex', () => {
143143
.keyStore as ValidatorKeyStore;
144144
const newProposal = new BlockProposal(
145145
proposal.payload,
146-
await signer.signMessageWithAddress(
146+
(await signer.signMessageWithAddress(
147147
proposerAddress!,
148148
getHashedSignaturePayload(proposal.payload, SignatureDomainSeparator.blockProposal),
149-
),
149+
))!,
150150
proposal.txHashes,
151151
);
152152

yarn-project/foundation/src/config/env_var.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,4 +313,14 @@ export type EnvVar =
313313
| 'FISHERMAN_MODE'
314314
| 'MAX_ALLOWED_ETH_CLIENT_DRIFT_SECONDS'
315315
| 'LEGACY_BLS_CLI'
316-
| 'DEBUG_FORCE_TX_PROOF_VERIFICATION';
316+
| 'DEBUG_FORCE_TX_PROOF_VERIFICATION'
317+
| 'VALIDATOR_HA_ENABLED'
318+
| 'VALIDATOR_HA_NODE_ID'
319+
| 'VALIDATOR_HA_POLLING_INTERVAL_MS'
320+
| 'VALIDATOR_HA_SIGNING_TIMEOUT_MS'
321+
| 'VALIDATOR_HA_DB_URL'
322+
| 'VALIDATOR_HA_RUN_MIGRATIONS'
323+
| 'VALIDATOR_HA_POOL_MAX'
324+
| 'VALIDATOR_HA_POOL_MIN'
325+
| 'VALIDATOR_HA_POOL_IDLE_TIMEOUT_MS'
326+
| 'VALIDATOR_HA_POOL_CONNECTION_TIMEOUT_MS';

yarn-project/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"txe",
6262
"test-wallet",
6363
"validator-client",
64+
"validator-ha-signer",
6465
"wallet-sdk",
6566
"world-state"
6667
],

yarn-project/sequencer-client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@aztec/stdlib": "workspace:^",
5050
"@aztec/telemetry-client": "workspace:^",
5151
"@aztec/validator-client": "workspace:^",
52+
"@aztec/validator-ha-signer": "workspace:^",
5253
"@aztec/world-state": "workspace:^",
5354
"lodash.chunk": "^4.2.0",
5455
"tslib": "^2.4.0",

yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
3333
import { type FailedTx, Tx } from '@aztec/stdlib/tx';
3434
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
3535
import type { ValidatorClient } from '@aztec/validator-client';
36+
import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
3637

3738
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
3839
import type { InvalidateBlockRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
@@ -207,21 +208,62 @@ export class CheckpointProposalJob {
207208
}
208209

209210
// TODO(palla/mbps): Wire this to the new p2p API once available, including the pendingBroadcast.block
210-
const proposal = await this.validatorClient.createCheckpointProposal(
211-
checkpoint.header,
212-
checkpoint.archive.root,
213-
pendingBroadcast?.txs ?? [],
214-
this.proposer,
215-
blockProposalOptions,
216-
);
211+
let proposal: BlockProposal;
212+
try {
213+
proposal = await this.validatorClient.createCheckpointProposal(
214+
checkpoint.header,
215+
checkpoint.archive.root,
216+
pendingBroadcast?.txs ?? [],
217+
this.proposer,
218+
blockProposalOptions,
219+
);
220+
} catch (err) {
221+
if (err instanceof DutyAlreadySignedError) {
222+
this.log.info(`Checkpoint proposal for slot ${this.slot} already signed by another HA node, yielding`, {
223+
slot: this.slot,
224+
signedByNode: err.signedByNode,
225+
});
226+
return undefined;
227+
}
228+
if (err instanceof SlashingProtectionError) {
229+
this.log.warn(`Checkpoint proposal for slot ${this.slot} blocked by slashing protection`, {
230+
slot: this.slot,
231+
existingSigningRoot: err.existingSigningRoot,
232+
attemptedSigningRoot: err.attemptedSigningRoot,
233+
});
234+
return undefined;
235+
}
236+
throw err;
237+
}
238+
217239
await this.p2pClient.broadcastProposal(proposal);
218240

219241
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.slot);
220242
const attestations = await this.waitForAttestations(proposal);
221243

222244
// Proposer must sign over the attestations before pushing them to L1
223245
const signer = this.proposer ?? this.publisher.getSenderAddress();
224-
const attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer);
246+
let attestationsSignature: Signature;
247+
try {
248+
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer);
249+
} catch (err) {
250+
if (err instanceof DutyAlreadySignedError) {
251+
this.log.info(`Attestations signature for slot ${this.slot} already signed by another HA node, yielding`, {
252+
slot: this.slot,
253+
signedByNode: err.signedByNode,
254+
});
255+
return undefined;
256+
}
257+
if (err instanceof SlashingProtectionError) {
258+
this.log.warn(`Attestations signature for slot ${this.slot} blocked by slashing protection`, {
259+
slot: this.slot,
260+
existingSigningRoot: err.existingSigningRoot,
261+
attemptedSigningRoot: err.attemptedSigningRoot,
262+
});
263+
return undefined;
264+
}
265+
throw err;
266+
}
225267

226268
// Enqueue publishing the checkpoint to L1
227269
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.slot);
@@ -331,15 +373,38 @@ export class CheckpointProposalJob {
331373
// If the block is the last one, we'll broadcast it along with the checkpoint at the end of the loop
332374
if (!this.config.fishermanMode) {
333375
// TODO(palla/mbps): Wire this to the new p2p API once available
334-
const proposal = await this.validatorClient.createBlockProposal(
335-
block.header.globalVariables.blockNumber,
336-
(await checkpointBuilder.getCheckpoint()).header,
337-
block.archive.root,
338-
usedTxs,
339-
this.proposer,
340-
blockProposalOptions,
341-
);
342-
await this.p2pClient.broadcastProposal(proposal);
376+
try {
377+
const proposal = await this.validatorClient.createBlockProposal(
378+
block.header.globalVariables.blockNumber,
379+
(await checkpointBuilder.getCheckpoint()).header,
380+
block.archive.root,
381+
usedTxs,
382+
this.proposer,
383+
blockProposalOptions,
384+
);
385+
await this.p2pClient.broadcastProposal(proposal);
386+
} catch (err) {
387+
if (err instanceof DutyAlreadySignedError) {
388+
this.log.info(`Block proposal for slot ${this.slot} already signed by another HA node, yielding`, {
389+
slot: this.slot,
390+
blockNumber,
391+
signedByNode: err.signedByNode,
392+
});
393+
// Another HA node is handling this slot, stop building
394+
break;
395+
}
396+
if (err instanceof SlashingProtectionError) {
397+
this.log.warn(`Block proposal for slot ${this.slot} blocked by slashing protection`, {
398+
slot: this.slot,
399+
blockNumber,
400+
existingSigningRoot: err.existingSigningRoot,
401+
attemptedSigningRoot: err.attemptedSigningRoot,
402+
});
403+
// Stop building to avoid further slashing issues
404+
break;
405+
}
406+
throw err;
407+
}
343408
}
344409

345410
// Wait until the next block's start time

yarn-project/sequencer-client/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@
6969
{
7070
"path": "../validator-client"
7171
},
72+
{
73+
"path": "../validator-ha-signer"
74+
},
7275
{
7376
"path": "../world-state"
7477
},

yarn-project/stdlib/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"@aztec/foundation": "workspace:^",
8585
"@aztec/l1-artifacts": "workspace:^",
8686
"@aztec/noir-noirc_abi": "portal:../../noir/packages/noirc_abi",
87+
"@aztec/validator-ha-signer": "workspace:^",
8788
"@google-cloud/storage": "^7.15.0",
8889
"axios": "^1.12.0",
8990
"json-stringify-deterministic": "1.0.12",

0 commit comments

Comments
 (0)