Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions yarn-project/node-keystore/src/keystore_manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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 () => {
Expand Down
21 changes: 14 additions & 7 deletions yarn-project/node-keystore/src/keystore_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<void> {
// Collect all remote signers with their addresses grouped by URL
Expand Down Expand Up @@ -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]),
),
),
);
}

/**
Expand Down
Loading