Skip to content

Commit

Permalink
feat: Check sandbox version matches CLI's (#1849)
Browse files Browse the repository at this point in the history
Checks on every CLI command that the Aztec RPC server version matches
the expected one. Checks exact matches for now, but can be extended to
semver checks. If no match, emits a warn.

```
$ yarn aztec-cli get-accounts
  cli WARN Aztec Sandbox is running version 0.1.0 which is newer than the expected by this CLI (0.0.0). Consider upgrading your CLI to a newer version.

Accounts found: 

Address: 0x2e13f0201905944184fc2c09d29fcf0cac07647be171656a275f63d99b819360, Public Key: 0x157922ba4defc5ccc36862afdf51dec2df3da3f683507ebffd59d472d6cbf0eb193a4e294e4fb994b22bd5dcb382b83b88734eef20e01a90f1498f9d60f0db8b, Partial Address: 0x044770258feb9223e966ae7bbae2b3a975d4a43e480688e7c51119ee4eb2e054
```

To support this, the `NodeInfo` struct returned from the RPC server
returns also the `client` identifier, which looks like
`[email protected]`.
  • Loading branch information
spalladino authored Aug 29, 2023
1 parent 2dae3f0 commit 7279730
Show file tree
Hide file tree
Showing 15 changed files with 165 additions and 24 deletions.
2 changes: 1 addition & 1 deletion l1-contracts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aztec/l1-contracts",
"version": "0.0.1",
"version": "0.1.0",
"license": "Apache-2.0",
"description": "Aztec contracts for the Ethereum mainnet and testnets",
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions yarn-project/aztec-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@aztec/noir-contracts": "workspace:^",
"@aztec/types": "workspace:^",
"commander": "^9.0.0",
"semver": "^7.5.4",
"tslib": "^2.4.0",
"viem": "^1.2.5"
},
Expand All @@ -52,6 +53,7 @@
"@types/jest": "^29.5.0",
"@types/node": "^18.7.23",
"jest": "^29.5.0",
"jest-mock-extended": "^3.0.5",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
Expand Down
34 changes: 34 additions & 0 deletions yarn-project/aztec-cli/src/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { AztecRPC, NodeInfo } from '@aztec/types';

import { MockProxy, mock } from 'jest-mock-extended';

import { checkServerVersion } from './client.js';

describe('client', () => {
describe('checkServerVersion', () => {
let rpc: MockProxy<AztecRPC>;

beforeEach(() => {
rpc = mock<AztecRPC>();
});

it('checks versions match', async () => {
rpc.getNodeInfo.mockResolvedValue({ client: '[email protected]' } as NodeInfo);
await checkServerVersion(rpc, '0.1.0-alpha47');
});

it('reports mismatch on older rpc version', async () => {
rpc.getNodeInfo.mockResolvedValue({ client: '[email protected]' } as NodeInfo);
await expect(checkServerVersion(rpc, '0.1.0-alpha48')).rejects.toThrowError(
/is older than the expected by this CLI/,
);
});

it('reports mismatch on newer rpc version', async () => {
rpc.getNodeInfo.mockResolvedValue({ client: '[email protected]' } as NodeInfo);
await expect(checkServerVersion(rpc, '0.1.0-alpha47')).rejects.toThrowError(
/is newer than the expected by this CLI/,
);
});
});
});
66 changes: 65 additions & 1 deletion yarn-project/aztec-cli/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { createAztecRpcClient } from '@aztec/aztec.js';
import { AztecRPC, createAztecRpcClient } from '@aztec/aztec.js';
import { makeFetch } from '@aztec/foundation/json-rpc/client';
import { DebugLogger } from '@aztec/foundation/log';

import { readFileSync } from 'fs';
import { dirname, resolve } from 'path';
import { gtr, ltr, satisfies, valid } from 'semver';
import { fileURLToPath } from 'url';

const retries = [1, 1, 2];

Expand All @@ -12,3 +18,61 @@ export function createClient(rpcUrl: string) {
const fetch = makeFetch(retries, true);
return createAztecRpcClient(rpcUrl, fetch);
}

/**
* Creates an Aztec RPC client with a given set of retries on non-server errors.
* Checks that the RPC server matches the expected version, and warns if not.
* @param rpcUrl - URL of the RPC server.
* @param logger - Debug logger to warn version incompatibilities.
* @returns An RPC client.
*/
export async function createCompatibleClient(rpcUrl: string, logger: DebugLogger) {
const client = createClient(rpcUrl);
const packageJsonPath = resolve(dirname(fileURLToPath(import.meta.url)), '../package.json');
const packageJsonContents = JSON.parse(readFileSync(packageJsonPath).toString());
const expectedVersionRange = packageJsonContents.version; // During sandbox, we'll expect exact matches

try {
await checkServerVersion(client, expectedVersionRange, logger);
} catch (err) {
if (err instanceof VersionMismatchError) {
logger.warn(err.message);
} else {
throw err;
}
}

return client;
}

/** Mismatch between server and client versions. */
class VersionMismatchError extends Error {}

/**
* Checks that the RPC server version matches the expected one by this CLI. Throws if not.
* @param rpc - RPC server connection.
* @param expectedVersionRange - Expected version by CLI.
*/
export async function checkServerVersion(rpc: AztecRPC, expectedVersionRange: string, logger?: DebugLogger) {
const serverName = 'Aztec Sandbox';
const { client } = await rpc.getNodeInfo();
const version = client.split('@')[1];
logger?.debug(`Comparing server version ${version} against CLI expected ${expectedVersionRange}`);
if (!version || !valid(version)) {
throw new VersionMismatchError(`Missing or invalid version identifier for ${serverName} (${version ?? 'empty'}).`);
} else if (!satisfies(version, expectedVersionRange)) {
if (gtr(version, expectedVersionRange)) {
throw new VersionMismatchError(
`${serverName} is running version ${version} which is newer than the expected by this CLI (${expectedVersionRange}). Consider upgrading your CLI to a newer version.`,
);
} else if (ltr(version, expectedVersionRange)) {
throw new VersionMismatchError(
`${serverName} is running version ${version} which is older than the expected by this CLI (${expectedVersionRange}). Consider upgrading your ${serverName} to a newer version.`,
);
} else {
throw new VersionMismatchError(
`${serverName} is running version ${version} which does not match the expected by this CLI (${expectedVersionRange}).`,
);
}
}
}
30 changes: 15 additions & 15 deletions yarn-project/aztec-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
import { mnemonicToAccount } from 'viem/accounts';

import { createClient } from './client.js';
import { createCompatibleClient } from './client.js';
import { encodeArgs, parseStructString } from './encoding.js';
import {
deployAztecContracts,
Expand Down Expand Up @@ -127,7 +127,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
)
.option('-u, --rpc-url <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
.action(async options => {
const client = createClient(options.rpcUrl);
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const privateKey = options.privateKey
? new PrivateKey(Buffer.from(stripLeadingHex(options.privateKey), 'hex'))
: PrivateKey.random();
Expand Down Expand Up @@ -161,7 +161,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
const contractAbi = await getContractAbi(abiPath, log);
const constructorAbi = contractAbi.functions.find(({ name }) => name === 'constructor');

const client = createClient(options.rpcUrl);
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const publicKey = options.publicKey ? Point.fromString(options.publicKey) : undefined;
const salt = options.salt ? Fr.fromBuffer(Buffer.from(stripLeadingHex(options.salt), 'hex')) : undefined;
const deployer = new ContractDeployer(contractAbi, client, publicKey);
Expand Down Expand Up @@ -189,7 +189,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
.requiredOption('-ca, --contract-address <address>', 'An Aztec address to check if contract has been deployed to.')
.option('-u, --rpc-url <url>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
.action(async options => {
const client = createClient(options.rpcUrl);
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const address = AztecAddress.fromString(options.contractAddress);
const isDeployed = await isContractDeployed(client, address);
if (isDeployed) log(`\nContract found at ${address.toString()}\n`);
Expand All @@ -202,7 +202,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
.description('Gets the receipt for the specified transaction hash.')
.option('-u, --rpc-url <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
.action(async (_txHash, options) => {
const client = createClient(options.rpcUrl);
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const txHash = TxHash.fromString(_txHash);
const receipt = await client.getTxReceipt(txHash);
if (!receipt) {
Expand All @@ -219,7 +219,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
.option('-u, --rpc-url <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
.option('-b, --include-bytecode <boolean>', "Include the contract's public function bytecode, if any.", false)
.action(async (contractAddress, options) => {
const client = createClient(options.rpcUrl);
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const address = AztecAddress.fromString(contractAddress);
const contractDataWithOrWithoutBytecode = options.includeBytecode
? await client.getContractDataAndBytecode(address)
Expand Down Expand Up @@ -255,7 +255,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
const fromBlock = from ? parseInt(from) : 1;
const limitCount = limit ? parseInt(limit) : 100;

const client = createClient(options.rpcUrl);
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const logs = await client.getUnencryptedLogs(fromBlock, limitCount);
if (!logs.length) {
log(`No logs found in blocks ${fromBlock} to ${fromBlock + limitCount}`);
Expand All @@ -273,7 +273,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
.requiredOption('-pa, --partial-address <partialAddress', 'The partially computed address of the account contract.')
.option('-u, --rpc-url <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
.action(async options => {
const client = createClient(options.rpcUrl);
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const address = AztecAddress.fromString(options.address);
const publicKey = Point.fromString(options.publicKey);
const partialAddress = Fr.fromString(options.partialAddress);
Expand All @@ -287,7 +287,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
.description('Gets all the Aztec accounts stored in the Aztec RPC.')
.option('-u, --rpc-url <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
.action(async (options: any) => {
const client = createClient(options.rpcUrl);
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const accounts = await client.getAccounts();
if (!accounts.length) {
log('No accounts found.');
Expand All @@ -305,7 +305,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
.argument('<address>', 'The Aztec address to get account for')
.option('-u, --rpc-url <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
.action(async (_address, options) => {
const client = createClient(options.rpcUrl);
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const address = AztecAddress.fromString(_address);
const account = await client.getAccount(address);

Expand All @@ -321,7 +321,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
.description('Gets all the recipients stored in the Aztec RPC.')
.option('-u, --rpc-url <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
.action(async (options: any) => {
const client = createClient(options.rpcUrl);
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const recipients = await client.getRecipients();
if (!recipients.length) {
log('No recipients found.');
Expand All @@ -339,7 +339,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
.argument('<address>', 'The Aztec address to get recipient for')
.option('-u, --rpc-url <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
.action(async (_address, options) => {
const client = createClient(options.rpcUrl);
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const address = AztecAddress.fromString(_address);
const recipient = await client.getRecipient(address);

Expand Down Expand Up @@ -381,7 +381,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {

const privateKey = new PrivateKey(Buffer.from(stripLeadingHex(options.privateKey), 'hex'));

const client = createClient(options.rpcUrl);
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const wallet = await getAccountWallets(
client,
SchnorrAccountContractAbi,
Expand Down Expand Up @@ -428,7 +428,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
`Invalid number of args passed. Expected ${fnAbi.parameters.length}; Received: ${options.args.length}`,
);
}
const client = createClient(options.rpcUrl);
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const from = await getTxSender(client, options.from);
const result = await client.viewTx(functionName, functionArgs, contractAddress, from);
log('\nView result: ', JsonStringify(result, true), '\n');
Expand Down Expand Up @@ -463,7 +463,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
.description('Gets the current Aztec L2 block number.')
.option('-u, --rpcUrl <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
.action(async (options: any) => {
const client = createClient(options.rpcUrl);
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const num = await client.getBlockNumber();
log(`${num}\n`);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {
toContractDao,
} from '@aztec/types';

import { RpcServerConfig } from '../config/index.js';
import { RpcServerConfig, getPackageInfo } from '../config/index.js';
import { ContractDataOracle } from '../contract_data_oracle/index.js';
import { Database } from '../database/index.js';
import { KernelOracle } from '../kernel_oracle/index.js';
Expand All @@ -56,6 +56,7 @@ import { Synchroniser } from '../synchroniser/index.js';
export class AztecRPCServer implements AztecRPC {
private synchroniser: Synchroniser;
private log: DebugLogger;
private clientInfo: string;

constructor(
private keyStore: KeyStore,
Expand All @@ -66,6 +67,9 @@ export class AztecRPCServer implements AztecRPC {
) {
this.log = createDebugLogger(logSuffix ? `aztec:rpc_server_${logSuffix}` : `aztec:rpc_server`);
this.synchroniser = new Synchroniser(node, db, logSuffix);

const { version, name } = getPackageInfo();
this.clientInfo = `${name.split('/')[name.split('/').length - 1]}@${version}`;
}

/**
Expand Down Expand Up @@ -276,6 +280,7 @@ export class AztecRPCServer implements AztecRPC {
version,
chainId,
rollupAddress,
client: this.clientInfo,
};
}

Expand Down
13 changes: 13 additions & 0 deletions yarn-project/aztec-rpc/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { readFileSync } from 'fs';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';

/**
* Configuration settings for the RPC Server.
*/
Expand All @@ -18,3 +22,12 @@ export function getConfigEnvVars(): RpcServerConfig {
l2BlockPollingIntervalMS: RPC_SERVER_BLOCK_POLLING_INTERVAL_MS ? +RPC_SERVER_BLOCK_POLLING_INTERVAL_MS : 1000,
};
}

/**
* Returns package name and version.
*/
export function getPackageInfo() {
const packageJsonPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json');
const { version, name } = JSON.parse(readFileSync(packageJsonPath).toString());
return { version, name };
}
2 changes: 1 addition & 1 deletion yarn-project/aztec-sandbox/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aztec/aztec-sandbox",
"version": "0.0.0",
"version": "0.1.0",
"type": "module",
"exports": {
".": "./dest/index.js",
Expand Down
7 changes: 6 additions & 1 deletion yarn-project/aztec-sandbox/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { deployL1Contracts } from '@aztec/ethereum';
import { createDebugLogger } from '@aztec/foundation/log';
import { retryUntil } from '@aztec/foundation/retry';

import { readFileSync } from 'fs';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
import { HDAccount, createPublicClient, http as httpViemTransport } from 'viem';
import { mnemonicToAccount } from 'viem/accounts';
import { foundry } from 'viem/chains';
Expand Down Expand Up @@ -61,8 +64,10 @@ async function main() {
const rpcConfig = getRpcConfigEnvVars();
const hdAccount = mnemonicToAccount(MNEMONIC);
const privKey = hdAccount.getHdKey().privateKey;
const packageJsonPath = resolve(dirname(fileURLToPath(import.meta.url)), '../package.json');
const version: string = JSON.parse(readFileSync(packageJsonPath).toString()).version;

logger.info('Setting up Aztec Sandbox, please stand by...');
logger.info(`Setting up Aztec Sandbox v${version}, please stand by...`);
logger.info('Deploying rollup contracts to L1...');
const deployedL1Contracts = await waitThenDeploy(aztecNodeConfig.rpcUrl, hdAccount);
aztecNodeConfig.publisherPrivateKey = new PrivateKey(Buffer.from(privKey!));
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/aztec.js/src/contract/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('Contract Class', () => {
const mockTxHash = { type: 'TxHash' } as any as TxHash;
const mockTxReceipt = { type: 'TxReceipt' } as any as TxReceipt;
const mockViewResultValue = 1;
const mockNodeInfo: NodeInfo = { version: 1, chainId: 2, rollupAddress: EthAddress.random() };
const mockNodeInfo: NodeInfo = { version: 1, chainId: 2, rollupAddress: EthAddress.random(), client: '' };

const defaultAbi: ContractAbi = {
name: 'FooContract',
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/noir-compiler/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aztec/noir-compiler",
"version": "0.0.0",
"version": "0.1.0",
"type": "module",
"exports": {
".": "./dest/index.js",
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/prover-client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aztec/prover-client",
"version": "0.0.0",
"version": "0.1.0",
"type": "module",
"exports": "./dest/index.js",
"typedocOptions": {
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/rollup-provider/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aztec/rollup-provider",
"version": "0.0.0",
"version": "0.1.0",
"main": "dest/index.js",
"type": "module",
"exports": "./dest/index.js",
Expand Down
4 changes: 4 additions & 0 deletions yarn-project/types/src/interfaces/aztec_rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export type NodeInfo = {
* The rollup contract address
*/
rollupAddress: EthAddress;
/**
* Identifier of the client software.
*/
client: string;
};

/** Provides up to which block has been synced by different components. */
Expand Down
Loading

0 comments on commit 7279730

Please sign in to comment.