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
Original file line number Diff line number Diff line change
@@ -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<PrivateLog> {
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);
});
});
Original file line number Diff line number Diff line change
@@ -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<Tx> {
#log: Logger;

constructor(bindings?: LoggerBindings) {
this.#log = createLogger('p2p:tx_validator:contract_instance', bindings);
}

async validateTx(tx: Tx): Promise<TxValidationResult> {
const reason = await this.#hasCorrectContractInstanceAddresses(tx);
return reason ? { result: 'invalid', reason: [reason] } : { result: 'valid' };
}

async #hasCorrectContractInstanceAddresses(tx: Tx): Promise<string | undefined> {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -73,6 +74,7 @@ describe('Validator factory functions', () => {
'doubleSpendValidator',
'gasValidator',
'dataValidator',
'contractInstanceValidator',
]);
});

Expand Down Expand Up @@ -170,6 +172,7 @@ describe('Validator factory functions', () => {
MetadataTxValidator.name,
SizeTxValidator.name,
DataTxValidator.name,
ContractInstanceTxValidator.name,
TxProofValidator.name,
]);
});
Expand All @@ -187,6 +190,7 @@ describe('Validator factory functions', () => {
MetadataTxValidator.name,
SizeTxValidator.name,
DataTxValidator.name,
ContractInstanceTxValidator.name,
TxProofValidator.name,
]);
});
Expand Down Expand Up @@ -221,6 +225,7 @@ describe('Validator factory functions', () => {
BlockHeaderTxValidator.name,
DoubleSpendTxValidator.name,
DataTxValidator.name,
ContractInstanceTxValidator.name,
GasTxValidator.name,
TxProofValidator.name,
]);
Expand Down
7 changes: 7 additions & 0 deletions yarn-project/p2p/src/msg_validators/tx_validator/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -167,6 +168,10 @@ export function createFirstStageTxValidationsForGossipedTransactions(
validator: new DataTxValidator(bindings),
severity: PeerErrorSeverity.MidToleranceError,
},
contractInstanceValidator: {
validator: new ContractInstanceTxValidator(bindings),
severity: PeerErrorSeverity.MidToleranceError,
},
};
}

Expand Down Expand Up @@ -218,6 +223,7 @@ function createTxValidatorForMinimumTxIntegrityChecks(
),
new SizeTxValidator(bindings),
new DataTxValidator(bindings),
new ContractInstanceTxValidator(bindings),
new TxProofValidator(verifier, bindings),
);
}
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions yarn-project/stdlib/src/tx/validator/error_texts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Loading