diff --git a/yarn-project/node-keystore/src/keystore_manager.test.ts b/yarn-project/node-keystore/src/keystore_manager.test.ts index 6ac491678b8c..b2861a964a8f 100644 --- a/yarn-project/node-keystore/src/keystore_manager.test.ts +++ b/yarn-project/node-keystore/src/keystore_manager.test.ts @@ -1442,7 +1442,7 @@ describe('KeystoreManager', () => { expect(validateAccessSpy).toHaveBeenCalledWith(testUrl, [publisherAddress.toString()]); }); - it('should handle validation errors', async () => { + it('should handle validation errors after retries are exhausted', async () => { const testUrl = 'http://test-signer:9000'; const address = EthAddress.random(); @@ -1456,11 +1456,52 @@ describe('KeystoreManager', () => { ], }; + const manager = new KeystoreManager(keystore); + using validateAccessSpy = jest.spyOn(RemoteSigner, 'validateAccess'); - validateAccessSpy.mockRejectedValueOnce(new Error('Connection refused')); + validateAccessSpy.mockRejectedValue(new Error('Connection refused')); + + jest.useFakeTimers(); + + const promise = manager.validateSigners().catch(err => err); + await jest.advanceTimersByTimeAsync(32_000); + const error = await promise; + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toBe('Connection refused'); + + jest.useRealTimers(); + }); + + it('should retry and succeed when validateAccess fails transiently', async () => { + const testUrl = 'http://test-signer:9000'; + const address = EthAddress.random(); + + const keystore: KeyStore = { + schemaVersion: 1, + validators: [ + { + attester: { address, remoteSignerUrl: testUrl }, + feeRecipient: await AztecAddress.random(), + }, + ], + }; const manager = new KeystoreManager(keystore); - await expect(manager.validateSigners()).rejects.toThrow('Connection refused'); + + using validateAccessSpy = jest.spyOn(RemoteSigner, 'validateAccess'); + validateAccessSpy + .mockRejectedValueOnce(new Error('ECONNREFUSED')) + .mockRejectedValueOnce(new Error('ECONNREFUSED')) + .mockResolvedValueOnce(undefined); + + jest.useFakeTimers(); + + const promise = manager.validateSigners(); + await jest.advanceTimersByTimeAsync(4_000); + await expect(promise).resolves.not.toThrow(); + expect(validateAccessSpy).toHaveBeenCalledTimes(3); + + jest.useRealTimers(); }); it('should skip validation for mnemonic and JSON V3 configs', async () => { diff --git a/yarn-project/node-keystore/src/keystore_manager.ts b/yarn-project/node-keystore/src/keystore_manager.ts index f734d7c33ceb..23391fe6bbc1 100644 --- a/yarn-project/node-keystore/src/keystore_manager.ts +++ b/yarn-project/node-keystore/src/keystore_manager.ts @@ -7,6 +7,7 @@ import type { EthSigner } from '@aztec/ethereum/eth-signer'; import { Buffer32 } from '@aztec/foundation/buffer'; import { EthAddress } from '@aztec/foundation/eth-address'; import type { Signature } from '@aztec/foundation/eth-signature'; +import { makeBackoff, retry } from '@aztec/foundation/retry'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { Wallet } from '@ethersproject/wallet'; @@ -61,7 +62,7 @@ export class KeystoreManager { /** * Validates all remote signers in the keystore are accessible and have the required addresses. - * Should be called after construction if validation is needed. + * Retries each web3signer URL with backoff to tolerate transient unavailability at boot time. */ async validateSigners(): Promise { // Collect all remote signers with their addresses grouped by URL @@ -127,12 +128,18 @@ export class KeystoreManager { collectRemoteSigners(this.keystore.prover.publisher, this.keystore.remoteSigner); } - // Validate each remote signer URL with all its addresses - for (const [url, addresses] of remoteSignersByUrl.entries()) { - if (addresses.size > 0) { - await RemoteSigner.validateAccess(url, Array.from(addresses)); - } - } + // Validate each remote signer URL with all its addresses, retrying on transient failures + await Promise.all( + Array.from(remoteSignersByUrl.entries()) + .filter(([, addresses]) => addresses.size > 0) + .map(([url, addresses]) => + retry( + () => RemoteSigner.validateAccess(url, Array.from(addresses)), + `Validating web3signer at ${url}`, + makeBackoff([1, 2, 4, 8, 16]), + ), + ), + ); } /**