diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/contract_instance_validator.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/contract_instance_validator.test.ts new file mode 100644 index 000000000000..e3f01949139a --- /dev/null +++ b/yarn-project/p2p/src/msg_validators/tx_validator/contract_instance_validator.test.ts @@ -0,0 +1,121 @@ +import { PRIVATE_LOG_SIZE_IN_FIELDS } from '@aztec/constants'; +import { padArrayEnd } from '@aztec/foundation/collection'; +import { Fr } from '@aztec/foundation/curves/bn254'; +import { CONTRACT_INSTANCE_PUBLISHED_EVENT_TAG } from '@aztec/protocol-contracts'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { computeContractAddressFromInstance } from '@aztec/stdlib/contract'; +import { PublicKeys } from '@aztec/stdlib/keys'; +import { PrivateLog } from '@aztec/stdlib/logs'; +import { mockTxForRollup } from '@aztec/stdlib/testing'; +import { + TX_ERROR_INCORRECT_CONTRACT_ADDRESS, + TX_ERROR_MALFORMED_CONTRACT_INSTANCE_LOG, + type Tx, +} from '@aztec/stdlib/tx'; + +import { ContractInstanceTxValidator } from './contract_instance_validator.js'; + +describe('ContractInstanceTxValidator', () => { + let validator: ContractInstanceTxValidator; + + beforeEach(() => { + validator = new ContractInstanceTxValidator(); + }); + + const expectValid = async (tx: Tx) => { + await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); + }; + + const expectInvalid = async (tx: Tx, reason: string) => { + await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'invalid', reason: [reason] }); + }; + + /** + * Builds a PrivateLog encoding a ContractInstancePublishedEvent. + * Layout: [tag, address, version, salt, contractClassId, initializationHash, ...publicKeys(8 fields), deployer] + */ + async function buildContractInstanceLog(opts?: { address?: AztecAddress }): Promise { + const salt = Fr.random(); + const contractClassId = Fr.random(); + const initializationHash = Fr.random(); + const publicKeys = await PublicKeys.random(); + const deployer = await AztecAddress.random(); + + const instance = { + version: 1 as const, + salt, + currentContractClassId: contractClassId, + originalContractClassId: contractClassId, + initializationHash, + publicKeys, + deployer, + }; + + const correctAddress = await computeContractAddressFromInstance(instance); + const address = opts?.address ?? correctAddress; + + // Serialize the event into fields matching the format expected by ContractInstancePublishedEvent.fromLog. + // fromLog reads from a buffer: [tag(32 bytes) | address(32) | version(32) | salt(32) | classId(32) | initHash(32) | publicKeys(4*64=256 bytes) | deployer(32)] + // PublicKeys serializes as 4 Points, each Point is 2 Fr (x, y) = 64 bytes. Total: 8 Fr fields. + const publicKeysBuffer = publicKeys.toBuffer(); + const publicKeysFields: Fr[] = []; + for (let i = 0; i < publicKeysBuffer.length; i += 32) { + publicKeysFields.push(Fr.fromBuffer(publicKeysBuffer.subarray(i, i + 32))); + } + + const emittedFields: Fr[] = [ + CONTRACT_INSTANCE_PUBLISHED_EVENT_TAG, + address.toField(), + new Fr(1), // version + salt, + contractClassId, + initializationHash, + ...publicKeysFields, + deployer.toField(), + ]; + const emittedLength = emittedFields.length; + + const fields = padArrayEnd(emittedFields, Fr.ZERO, PRIVATE_LOG_SIZE_IN_FIELDS); + return new PrivateLog(fields as any, emittedLength); + } + + function injectPrivateLog(tx: Tx, log: PrivateLog) { + // For a rollup-only tx, private logs live in forRollup.end.privateLogs + const privateLogs = tx.data.forRollup!.end.privateLogs; + const emptyIdx = privateLogs.findIndex(l => l.isEmpty()); + if (emptyIdx >= 0) { + privateLogs[emptyIdx] = log; + } else { + throw new Error('No empty private log slot available in mock tx'); + } + } + + it('allows transactions with no contract instance logs', async () => { + const tx = await mockTxForRollup(1); + await expectValid(tx); + }); + + it('allows transactions with correct contract instance addresses', async () => { + const tx = await mockTxForRollup(2); + const log = await buildContractInstanceLog(); + injectPrivateLog(tx, log); + await expectValid(tx); + }); + + it('rejects transactions with incorrect contract instance addresses', async () => { + const tx = await mockTxForRollup(3); + const wrongAddress = await AztecAddress.random(); + const log = await buildContractInstanceLog({ address: wrongAddress }); + injectPrivateLog(tx, log); + await expectInvalid(tx, TX_ERROR_INCORRECT_CONTRACT_ADDRESS); + }); + + it('rejects transactions with malformed contract instance logs', async () => { + const tx = await mockTxForRollup(4); + // Create a log that has the right tag but garbage data + const fields = padArrayEnd([CONTRACT_INSTANCE_PUBLISHED_EVENT_TAG], Fr.ZERO, PRIVATE_LOG_SIZE_IN_FIELDS); + const malformedLog = new PrivateLog(fields as any, 1); + injectPrivateLog(tx, malformedLog); + await expectInvalid(tx, TX_ERROR_MALFORMED_CONTRACT_INSTANCE_LOG); + }); +}); diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/contract_instance_validator.ts b/yarn-project/p2p/src/msg_validators/tx_validator/contract_instance_validator.ts new file mode 100644 index 000000000000..ee8d39825066 --- /dev/null +++ b/yarn-project/p2p/src/msg_validators/tx_validator/contract_instance_validator.ts @@ -0,0 +1,56 @@ +import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log'; +import { ContractInstancePublishedEvent } from '@aztec/protocol-contracts/instance-registry'; +import { computeContractAddressFromInstance } from '@aztec/stdlib/contract'; +import { + TX_ERROR_INCORRECT_CONTRACT_ADDRESS, + TX_ERROR_MALFORMED_CONTRACT_INSTANCE_LOG, + type Tx, + type TxValidationResult, + type TxValidator, +} from '@aztec/stdlib/tx'; + +/** Validates that contract instance deployment logs contain correct addresses. */ +export class ContractInstanceTxValidator implements TxValidator { + #log: Logger; + + constructor(bindings?: LoggerBindings) { + this.#log = createLogger('p2p:tx_validator:contract_instance', bindings); + } + + async validateTx(tx: Tx): Promise { + const reason = await this.#hasCorrectContractInstanceAddresses(tx); + return reason ? { result: 'invalid', reason: [reason] } : { result: 'valid' }; + } + + async #hasCorrectContractInstanceAddresses(tx: Tx): Promise { + const privateLogs = tx.data.getNonEmptyPrivateLogs(); + for (const log of privateLogs) { + if (!ContractInstancePublishedEvent.isContractInstancePublishedEvent(log)) { + continue; + } + + let event; + try { + event = ContractInstancePublishedEvent.fromLog(log); + } catch (e) { + this.#log.warn(`Rejecting tx ${tx.getTxHash()}: failed to parse contract instance event: ${e}`); + return TX_ERROR_MALFORMED_CONTRACT_INSTANCE_LOG; + } + + try { + const instance = event.toContractInstance(); + const computedAddress = await computeContractAddressFromInstance(instance); + if (!computedAddress.equals(instance.address)) { + this.#log.warn( + `Rejecting tx ${tx.getTxHash()}: contract instance address mismatch. Claimed ${instance.address}, computed ${computedAddress}`, + ); + return TX_ERROR_INCORRECT_CONTRACT_ADDRESS; + } + } catch (e) { + this.#log.warn(`Rejecting tx ${tx.getTxHash()}: failed to compute contract instance address: ${e}`); + return TX_ERROR_MALFORMED_CONTRACT_INSTANCE_LOG; + } + } + return undefined; + } +} diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts index 92ede7a4afaa..6dc67ca9294a 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts @@ -14,6 +14,7 @@ import { type MockProxy, mock } from 'jest-mock-extended'; import { AggregateTxValidator } from './aggregate_tx_validator.js'; import { BlockHeaderTxValidator } from './block_header_validator.js'; +import { ContractInstanceTxValidator } from './contract_instance_validator.js'; import { DataTxValidator } from './data_validator.js'; import { DoubleSpendTxValidator } from './double_spend_validator.js'; import { @@ -73,6 +74,7 @@ describe('Validator factory functions', () => { 'doubleSpendValidator', 'gasValidator', 'dataValidator', + 'contractInstanceValidator', ]); }); @@ -170,6 +172,7 @@ describe('Validator factory functions', () => { MetadataTxValidator.name, SizeTxValidator.name, DataTxValidator.name, + ContractInstanceTxValidator.name, TxProofValidator.name, ]); }); @@ -187,6 +190,7 @@ describe('Validator factory functions', () => { MetadataTxValidator.name, SizeTxValidator.name, DataTxValidator.name, + ContractInstanceTxValidator.name, TxProofValidator.name, ]); }); @@ -221,6 +225,7 @@ describe('Validator factory functions', () => { BlockHeaderTxValidator.name, DoubleSpendTxValidator.name, DataTxValidator.name, + ContractInstanceTxValidator.name, GasTxValidator.name, TxProofValidator.name, ]); diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts index 849f105b46be..4d4df944a34f 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts @@ -53,6 +53,7 @@ import type { TxMetaData } from '../../mem_pools/tx_pool_v2/tx_metadata.js'; import { AggregateTxValidator } from './aggregate_tx_validator.js'; import { ArchiveCache } from './archive_cache.js'; import { type ArchiveSource, BlockHeaderTxValidator } from './block_header_validator.js'; +import { ContractInstanceTxValidator } from './contract_instance_validator.js'; import { DataTxValidator } from './data_validator.js'; import { DoubleSpendTxValidator, type NullifierSource } from './double_spend_validator.js'; import { GasLimitsValidator, GasTxValidator } from './gas_validator.js'; @@ -167,6 +168,10 @@ export function createFirstStageTxValidationsForGossipedTransactions( validator: new DataTxValidator(bindings), severity: PeerErrorSeverity.MidToleranceError, }, + contractInstanceValidator: { + validator: new ContractInstanceTxValidator(bindings), + severity: PeerErrorSeverity.MidToleranceError, + }, }; } @@ -218,6 +223,7 @@ function createTxValidatorForMinimumTxIntegrityChecks( ), new SizeTxValidator(bindings), new DataTxValidator(bindings), + new ContractInstanceTxValidator(bindings), new TxProofValidator(verifier, bindings), ); } @@ -321,6 +327,7 @@ export function createTxValidatorForAcceptingTxsOverRPC( new BlockHeaderTxValidator(new ArchiveCache(db), bindings), new DoubleSpendTxValidator(new NullifierCache(db), bindings), new DataTxValidator(bindings), + new ContractInstanceTxValidator(bindings), ]; if (!skipFeeEnforcement) { diff --git a/yarn-project/stdlib/src/tx/validator/error_texts.ts b/yarn-project/stdlib/src/tx/validator/error_texts.ts index 6a8326f032a8..57599c12d9d4 100644 --- a/yarn-project/stdlib/src/tx/validator/error_texts.ts +++ b/yarn-project/stdlib/src/tx/validator/error_texts.ts @@ -41,5 +41,9 @@ export const TX_ERROR_SIZE_ABOVE_LIMIT = 'Transaction size above size limit'; // Block header export const TX_ERROR_BLOCK_HEADER = 'Block header not found'; +// Contract instance +export const TX_ERROR_INCORRECT_CONTRACT_ADDRESS = 'Incorrect contract instance deployment address'; +export const TX_ERROR_MALFORMED_CONTRACT_INSTANCE_LOG = 'Failed to parse contract instance deployment log'; + // General export const TX_ERROR_DURING_VALIDATION = 'Unexpected error during validation';