diff --git a/l1-contracts/package.json b/l1-contracts/package.json index 2156f8abf3a..1969ca98520 100644 --- a/l1-contracts/package.json +++ b/l1-contracts/package.json @@ -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": { diff --git a/yarn-project/aztec-cli/package.json b/yarn-project/aztec-cli/package.json index 6f575328879..8e82eac0e2a 100644 --- a/yarn-project/aztec-cli/package.json +++ b/yarn-project/aztec-cli/package.json @@ -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" }, @@ -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" diff --git a/yarn-project/aztec-cli/src/client.test.ts b/yarn-project/aztec-cli/src/client.test.ts new file mode 100644 index 00000000000..d4e6de3fdb9 --- /dev/null +++ b/yarn-project/aztec-cli/src/client.test.ts @@ -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; + + beforeEach(() => { + rpc = mock(); + }); + + it('checks versions match', async () => { + rpc.getNodeInfo.mockResolvedValue({ client: 'rpc@0.1.0-alpha47' } as NodeInfo); + await checkServerVersion(rpc, '0.1.0-alpha47'); + }); + + it('reports mismatch on older rpc version', async () => { + rpc.getNodeInfo.mockResolvedValue({ client: 'rpc@0.1.0-alpha47' } 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: 'rpc@0.1.0-alpha48' } as NodeInfo); + await expect(checkServerVersion(rpc, '0.1.0-alpha47')).rejects.toThrowError( + /is newer than the expected by this CLI/, + ); + }); + }); +}); diff --git a/yarn-project/aztec-cli/src/client.ts b/yarn-project/aztec-cli/src/client.ts index 1eb6ac3ccb3..2878119a3d8 100644 --- a/yarn-project/aztec-cli/src/client.ts +++ b/yarn-project/aztec-cli/src/client.ts @@ -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]; @@ -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}).`, + ); + } + } +} diff --git a/yarn-project/aztec-cli/src/index.ts b/yarn-project/aztec-cli/src/index.ts index 1399aba3f3c..ae3cdc24cc8 100644 --- a/yarn-project/aztec-cli/src/index.ts +++ b/yarn-project/aztec-cli/src/index.ts @@ -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, @@ -127,7 +127,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { ) .option('-u, --rpc-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 privateKey = options.privateKey ? new PrivateKey(Buffer.from(stripLeadingHex(options.privateKey), 'hex')) : PrivateKey.random(); @@ -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); @@ -189,7 +189,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .requiredOption('-ca, --contract-address
', 'An Aztec address to check if contract has been deployed to.') .option('-u, --rpc-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`); @@ -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 ', '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) { @@ -219,7 +219,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .option('-b, --include-bytecode ', "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) @@ -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}`); @@ -273,7 +273,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .requiredOption('-pa, --partial-address ', '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); @@ -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 ', '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.'); @@ -305,7 +305,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .argument('
', 'The Aztec address to get account for') .option('-u, --rpc-url ', '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); @@ -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 ', '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.'); @@ -339,7 +339,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .argument('
', 'The Aztec address to get recipient for') .option('-u, --rpc-url ', '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); @@ -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, @@ -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'); @@ -463,7 +463,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .description('Gets the current Aztec L2 block number.') .option('-u, --rpcUrl ', '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`); }); diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts b/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts index acb4f54ad19..73800bc8f41 100644 --- a/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts +++ b/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts @@ -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'; @@ -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, @@ -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}`; } /** @@ -276,6 +280,7 @@ export class AztecRPCServer implements AztecRPC { version, chainId, rollupAddress, + client: this.clientInfo, }; } diff --git a/yarn-project/aztec-rpc/src/config/index.ts b/yarn-project/aztec-rpc/src/config/index.ts index 5f19540ed4e..c44f505decd 100644 --- a/yarn-project/aztec-rpc/src/config/index.ts +++ b/yarn-project/aztec-rpc/src/config/index.ts @@ -1,3 +1,7 @@ +import { readFileSync } from 'fs'; +import { dirname, resolve } from 'path'; +import { fileURLToPath } from 'url'; + /** * Configuration settings for the RPC Server. */ @@ -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 }; +} diff --git a/yarn-project/aztec-sandbox/package.json b/yarn-project/aztec-sandbox/package.json index 059da6ad785..e1554d4cc79 100644 --- a/yarn-project/aztec-sandbox/package.json +++ b/yarn-project/aztec-sandbox/package.json @@ -1,6 +1,6 @@ { "name": "@aztec/aztec-sandbox", - "version": "0.0.0", + "version": "0.1.0", "type": "module", "exports": { ".": "./dest/index.js", diff --git a/yarn-project/aztec-sandbox/src/index.ts b/yarn-project/aztec-sandbox/src/index.ts index 6b90b3bcd63..fb79a0b14d9 100644 --- a/yarn-project/aztec-sandbox/src/index.ts +++ b/yarn-project/aztec-sandbox/src/index.ts @@ -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'; @@ -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!)); diff --git a/yarn-project/aztec.js/src/contract/contract.test.ts b/yarn-project/aztec.js/src/contract/contract.test.ts index c29107f6baa..898f60b0dfa 100644 --- a/yarn-project/aztec.js/src/contract/contract.test.ts +++ b/yarn-project/aztec.js/src/contract/contract.test.ts @@ -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', diff --git a/yarn-project/noir-compiler/package.json b/yarn-project/noir-compiler/package.json index c24df172a5a..c5d9365430f 100644 --- a/yarn-project/noir-compiler/package.json +++ b/yarn-project/noir-compiler/package.json @@ -1,6 +1,6 @@ { "name": "@aztec/noir-compiler", - "version": "0.0.0", + "version": "0.1.0", "type": "module", "exports": { ".": "./dest/index.js", diff --git a/yarn-project/prover-client/package.json b/yarn-project/prover-client/package.json index 6772f6dfe5c..19f2aec28b8 100644 --- a/yarn-project/prover-client/package.json +++ b/yarn-project/prover-client/package.json @@ -1,6 +1,6 @@ { "name": "@aztec/prover-client", - "version": "0.0.0", + "version": "0.1.0", "type": "module", "exports": "./dest/index.js", "typedocOptions": { diff --git a/yarn-project/rollup-provider/package.json b/yarn-project/rollup-provider/package.json index 5c41382358c..604418c7338 100644 --- a/yarn-project/rollup-provider/package.json +++ b/yarn-project/rollup-provider/package.json @@ -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", diff --git a/yarn-project/types/src/interfaces/aztec_rpc.ts b/yarn-project/types/src/interfaces/aztec_rpc.ts index 8878021954a..3c4ce90c0e8 100644 --- a/yarn-project/types/src/interfaces/aztec_rpc.ts +++ b/yarn-project/types/src/interfaces/aztec_rpc.ts @@ -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. */ diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 01ffd54c2dd..50e77824359 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -322,6 +322,8 @@ __metadata: "@types/node": ^18.7.23 commander: ^9.0.0 jest: ^29.5.0 + jest-mock-extended: ^3.0.5 + semver: ^7.5.4 ts-jest: ^29.1.0 ts-node: ^10.9.1 tslib: ^2.4.0 @@ -8065,6 +8067,18 @@ __metadata: languageName: node linkType: hard +"jest-mock-extended@npm:^3.0.5": + version: 3.0.5 + resolution: "jest-mock-extended@npm:3.0.5" + dependencies: + ts-essentials: ^7.0.3 + peerDependencies: + jest: ^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0 + typescript: ^3.0.0 || ^4.0.0 || ^5.0.0 + checksum: 440c52f743af588493c2cd02fa7e4e42177748ac3f7ae720f414bd58a4a72fad4271878457bf8796b62abcf9cf32cde4dc5151caad0805037bd965cc9ef07ca8 + languageName: node + linkType: hard + "jest-mock@npm:^29.6.2": version: 29.6.2 resolution: "jest-mock@npm:29.6.2"