Skip to content

Commit

Permalink
feat: sequencer checks list of allowed FPCs (#5310)
Browse files Browse the repository at this point in the history
The sequencer now checks the FPC being used by a tx is on its allowlist.
This can be specified either by whitelisting contract classes or
individual instances.

Fix #5000
  • Loading branch information
alexghr authored Mar 26, 2024
1 parent ef7bf79 commit adf20dc
Show file tree
Hide file tree
Showing 14 changed files with 234 additions and 62 deletions.
1 change: 0 additions & 1 deletion yarn-project/aztec-node/src/aztec-node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,6 @@ export class AztecNodeService implements AztecNode {
const publicProcessorFactory = new PublicProcessorFactory(
merkleTrees.asLatest(),
this.contractDataSource,
this.l1ToL2MessageSource,
new WASMSimulator(),
);
const processor = await publicProcessorFactory.create(prevHeader, newGlobalVariables);
Expand Down
2 changes: 2 additions & 0 deletions yarn-project/aztec/src/cli/texts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export const cliTexts = {
'transactionPollingIntervalMS:SEQ_TX_POLLING_INTERVAL_MS - number - The interval in ms to wait before polling for new transactions. Default: 1000\n' +
'acvmBinaryPath:ACVM_BINARY_PATH - string - The full path to an instance of the acvm cli application. If not provided will fallback to WASM circuit simulation\n' +
'acvmWorkingDirectory:ACVM_WORKING_DIRECTORY - string - A directory to use for temporary files used by the acvm application. If not provided WASM circuit simulation will be used\n' +
'allowedFeePaymentContractClasses:SEQ_FPC_CLASSES - string[] - Which fee payment contract classes the sequencer allows' +
'allowedFeePaymentContractInstances:SEQ_FPC_INSTANCES - string[] - Which fee payment contracts the sequencer allows.' +
contractAddresses,
prover:
'Starts a Prover with options. If started additionally to --node, the Prover will attach to that node.\n' +
Expand Down
8 changes: 7 additions & 1 deletion yarn-project/circuit-types/src/interfaces/configs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AztecAddress, EthAddress } from '@aztec/circuits.js';
import { AztecAddress, EthAddress, Fr } from '@aztec/circuits.js';

/**
* The sequencer configuration.
Expand All @@ -18,4 +18,10 @@ export interface SequencerConfig {
acvmWorkingDirectory?: string;
/** The path to the ACVM binary */
acvmBinaryPath?: string;

/** The list of permitted fee payment contract classes */
allowedFeePaymentContractClasses?: Fr[];

/** The list of permitted fee payment contract instances. Takes precedence over contract classes */
allowedFeePaymentContractInstances?: AztecAddress[];
}
5 changes: 5 additions & 0 deletions yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
PrivateFeePaymentMethod,
PublicFeePaymentMethod,
SentTx,
getContractClassFromArtifact,
} from '@aztec/aztec.js';
import { DefaultDappEntrypoint } from '@aztec/entrypoints/dapp';
import {
Expand Down Expand Up @@ -67,6 +68,10 @@ describe('e2e_dapp_subscription', () => {

const { wallets, accounts, aztecNode, deployL1ContractsValues } = e2eContext;

await aztecNode.setConfig({
allowedFeePaymentContractClasses: [getContractClassFromArtifact(FPCContract.artifact).id],
});

// this should be a SignerlessWallet but that can't call public functions directly
gasTokenContract = await GasTokenContract.at(
getCanonicalGasTokenAddress(deployL1ContractsValues.l1ContractAddresses.gasPortalAddress),
Expand Down
7 changes: 5 additions & 2 deletions yarn-project/end-to-end/src/e2e_fees.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
computeAuthWitMessageHash,
computeMessageSecretHash,
} from '@aztec/aztec.js';
import { FunctionData } from '@aztec/circuits.js';
import { FunctionData, getContractClassFromArtifact } from '@aztec/circuits.js';
import { ContractArtifact, decodeFunctionSignature } from '@aztec/foundation/abi';
import {
TokenContract as BananaCoin,
Expand All @@ -41,7 +41,7 @@ const TOKEN_SYMBOL = 'BC';
const TOKEN_DECIMALS = 18n;
const BRIDGED_FPC_GAS = 500n;

jest.setTimeout(100_000);
jest.setTimeout(1_000_000_000);

describe('e2e_fees', () => {
let aliceWallet: Wallet;
Expand All @@ -63,6 +63,9 @@ describe('e2e_fees', () => {
e2eContext = await setup(3);

const { accounts, logger, aztecNode, pxe, deployL1ContractsValues, wallets } = e2eContext;
await aztecNode.setConfig({
allowedFeePaymentContractClasses: [getContractClassFromArtifact(FPCContract.artifact).id],
});

logFunctionSignatures(BananaCoin.artifact, logger);
logFunctionSignatures(FPCContract.artifact, logger);
Expand Down
10 changes: 3 additions & 7 deletions yarn-project/sequencer-client/src/client/sequencer-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getGlobalVariableBuilder } from '../global_variable_builder/index.js';
import { getL1Publisher } from '../publisher/index.js';
import { Sequencer, SequencerConfig } from '../sequencer/index.js';
import { PublicProcessorFactory } from '../sequencer/public_processor.js';
import { TxValidatorFactory } from '../sequencer/tx_validator_factory.js';

/**
* Encapsulates the full sequencer and publisher.
Expand Down Expand Up @@ -43,12 +44,7 @@ export class SequencerClient {
const globalsBuilder = getGlobalVariableBuilder(config);
const merkleTreeDb = worldStateSynchronizer.getLatest();

const publicProcessorFactory = new PublicProcessorFactory(
merkleTreeDb,
contractDataSource,
l1ToL2MessageSource,
simulationProvider,
);
const publicProcessorFactory = new PublicProcessorFactory(merkleTreeDb, contractDataSource, simulationProvider);

const sequencer = new Sequencer(
publisher,
Expand All @@ -59,8 +55,8 @@ export class SequencerClient {
l2BlockSource,
l1ToL2MessageSource,
publicProcessorFactory,
new TxValidatorFactory(merkleTreeDb, contractDataSource, config.l1Contracts.gasPortalAddress),
config,
config.l1Contracts.gasPortalAddress,
);

await sequencer.start();
Expand Down
8 changes: 7 additions & 1 deletion yarn-project/sequencer-client/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AztecAddress } from '@aztec/circuits.js';
import { AztecAddress, Fr } from '@aztec/circuits.js';
import { L1ContractAddresses, NULL_KEY } from '@aztec/ethereum';
import { EthAddress } from '@aztec/foundation/eth-address';

Expand Down Expand Up @@ -40,6 +40,8 @@ export function getConfigEnvVars(): SequencerClientConfig {
SEQ_TX_POLLING_INTERVAL_MS,
SEQ_MAX_TX_PER_BLOCK,
SEQ_MIN_TX_PER_BLOCK,
SEQ_FPC_CLASSES,
SEQ_FPC_INSTANCES,
AVAILABILITY_ORACLE_CONTRACT_ADDRESS,
ROLLUP_CONTRACT_ADDRESS,
REGISTRY_CONTRACT_ADDRESS,
Expand Down Expand Up @@ -88,5 +90,9 @@ export function getConfigEnvVars(): SequencerClientConfig {
feeRecipient: FEE_RECIPIENT ? AztecAddress.fromString(FEE_RECIPIENT) : undefined,
acvmWorkingDirectory: ACVM_WORKING_DIRECTORY ? ACVM_WORKING_DIRECTORY : undefined,
acvmBinaryPath: ACVM_BINARY_PATH ? ACVM_BINARY_PATH : undefined,
allowedFeePaymentContractClasses: SEQ_FPC_CLASSES ? SEQ_FPC_CLASSES.split(',').map(Fr.fromString) : [],
allowedFeePaymentContractInstances: SEQ_FPC_INSTANCES
? SEQ_FPC_INSTANCES.split(',').map(AztecAddress.fromString)
: [],
};
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
FailedTx,
L1ToL2MessageSource,
ProcessedTx,
SimulationError,
Tx,
Expand Down Expand Up @@ -31,7 +30,6 @@ export class PublicProcessorFactory {
constructor(
private merkleTree: MerkleTreeOperations,
private contractDataSource: ContractDataSource,
private l1Tol2MessagesDataSource: L1ToL2MessageSource,
private simulator: SimulationProvider,
) {}

Expand All @@ -50,7 +48,7 @@ export class PublicProcessorFactory {

const publicContractsDB = new ContractsDataSourcePublicDB(this.contractDataSource);
const worldStatePublicDB = new WorldStatePublicDB(this.merkleTree);
const worldStateDB = new WorldStateDB(this.merkleTree, this.l1Tol2MessagesDataSource);
const worldStateDB = new WorldStateDB(this.merkleTree);
const publicExecutor = new PublicExecutor(worldStatePublicDB, publicContractsDB, worldStateDB, historicalHeader);
return new PublicProcessor(
this.merkleTree,
Expand Down
12 changes: 12 additions & 0 deletions yarn-project/sequencer-client/src/sequencer/sequencer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
makeEmptyProof,
} from '@aztec/circuits.js';
import { P2P, P2PClientState } from '@aztec/p2p';
import { ContractDataSource } from '@aztec/types/contracts';
import { MerkleTreeOperations, WorldStateRunningState, WorldStateSynchronizer } from '@aztec/world-state';

import { MockProxy, mock, mockFn } from 'jest-mock-extended';
Expand All @@ -29,6 +30,7 @@ import { GlobalVariableBuilder } from '../global_variable_builder/global_builder
import { L1Publisher } from '../index.js';
import { PublicProcessor, PublicProcessorFactory } from './public_processor.js';
import { Sequencer } from './sequencer.js';
import { TxValidatorFactory } from './tx_validator_factory.js';

describe('sequencer', () => {
let publisher: MockProxy<L1Publisher>;
Expand Down Expand Up @@ -86,6 +88,12 @@ describe('sequencer', () => {
getBlockNumber: () => Promise.resolve(lastBlockNumber),
});

// all txs use the same allowed FPC class
const fpcClassId = Fr.random();
const contractSource = mock<ContractDataSource>({
getContractClass: mockFn().mockResolvedValue(fpcClassId),
});

sequencer = new TestSubject(
publisher,
globalVariableBuilder,
Expand All @@ -95,6 +103,10 @@ describe('sequencer', () => {
l2BlockSource,
l1ToL2MessageSource,
publicProcessorFactory,
new TxValidatorFactory(merkleTreeOps, contractSource, EthAddress.random()),
{
allowedFeePaymentContractClasses: [fpcClassId],
},
);
});

Expand Down
27 changes: 14 additions & 13 deletions yarn-project/sequencer-client/src/sequencer/sequencer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { L1ToL2MessageSource, L2Block, L2BlockSource, MerkleTreeId, ProcessedTx, Tx } from '@aztec/circuit-types';
import { L1ToL2MessageSource, L2Block, L2BlockSource, ProcessedTx, Tx } from '@aztec/circuit-types';
import { BlockProver, PROVING_STATUS } from '@aztec/circuit-types/interfaces';
import { L2BlockBuiltStats } from '@aztec/circuit-types/stats';
import { AztecAddress, EthAddress, GlobalVariables } from '@aztec/circuits.js';
Expand All @@ -11,10 +11,10 @@ import { WorldStateStatus, WorldStateSynchronizer } from '@aztec/world-state';

import { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
import { L1Publisher } from '../publisher/l1-publisher.js';
import { WorldStatePublicDB } from '../simulator/public_executor.js';
import { SequencerConfig } from './config.js';
import { PublicProcessorFactory } from './public_processor.js';
import { TxValidator } from './tx_validator.js';
import { TxValidatorFactory } from './tx_validator_factory.js';

/**
* Sequencer client
Expand All @@ -35,6 +35,8 @@ export class Sequencer {
private _feeRecipient = AztecAddress.ZERO;
private lastPublishedBlock = 0;
private state = SequencerState.STOPPED;
private allowedFeePaymentContractClasses: Fr[] = [];
private allowedFeePaymentContractInstances: AztecAddress[] = [];

constructor(
private publisher: L1Publisher,
Expand All @@ -45,8 +47,8 @@ export class Sequencer {
private l2BlockSource: L2BlockSource,
private l1ToL2MessageSource: L1ToL2MessageSource,
private publicProcessorFactory: PublicProcessorFactory,
private txValidatorFactory: TxValidatorFactory,
config: SequencerConfig = {},
private gasPortalAddress = EthAddress.ZERO,
private log = createDebugLogger('aztec:sequencer'),
) {
this.updateConfig(config);
Expand All @@ -73,6 +75,12 @@ export class Sequencer {
if (config.feeRecipient) {
this._feeRecipient = config.feeRecipient;
}
if (config.allowedFeePaymentContractClasses) {
this.allowedFeePaymentContractClasses = config.allowedFeePaymentContractClasses;
}
if (config.allowedFeePaymentContractInstances) {
this.allowedFeePaymentContractInstances = config.allowedFeePaymentContractInstances;
}
}

/**
Expand Down Expand Up @@ -170,17 +178,10 @@ export class Sequencer {
this._feeRecipient,
);

// Filter out invalid txs
const trees = this.worldState.getLatest();
const txValidator = new TxValidator(
{
getNullifierIndex(nullifier: Fr): Promise<bigint | undefined> {
return trees.findLeafIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBuffer());
},
},
new WorldStatePublicDB(trees),
this.gasPortalAddress,
const txValidator = this.txValidatorFactory.buildTxValidator(
newGlobalVariables,
this.allowedFeePaymentContractClasses,
this.allowedFeePaymentContractInstances,
);

// TODO: It should be responsibility of the P2P layer to validate txs before passing them on here
Expand Down
67 changes: 66 additions & 1 deletion yarn-project/sequencer-client/src/sequencer/tx_validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { makeAztecAddress, makeGlobalVariables } from '@aztec/circuits.js/testin
import { makeTuple } from '@aztec/foundation/array';
import { pedersenHash } from '@aztec/foundation/crypto';
import { getCanonicalGasTokenAddress } from '@aztec/protocol-contracts/gas-token';
import { ContractDataSource } from '@aztec/types/contracts';

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

Expand All @@ -26,12 +27,18 @@ describe('TxValidator', () => {
let globalVariables: GlobalVariables;
let nullifierSource: MockProxy<NullifierSource>;
let publicStateSource: MockProxy<PublicStateSource>;
let contractDataSource: MockProxy<ContractDataSource>;
let allowedFPCClass: Fr;
let allowedFPC: AztecAddress;
let gasPortalAddress: EthAddress;
let gasTokenAddress: AztecAddress;

beforeEach(() => {
gasPortalAddress = EthAddress.random();
gasTokenAddress = getCanonicalGasTokenAddress(gasPortalAddress);
allowedFPCClass = Fr.random();
allowedFPC = makeAztecAddress(100);

nullifierSource = mock<NullifierSource>({
getNullifierIndex: mockFn().mockImplementation(() => {
return Promise.resolve(undefined);
Expand All @@ -46,8 +53,20 @@ describe('TxValidator', () => {
}
}),
});
contractDataSource = mock<ContractDataSource>({
getContract: mockFn().mockImplementation(() => {
return Promise.resolve({
contractClassId: allowedFPCClass,
});
}),
});

globalVariables = makeGlobalVariables();
validator = new TxValidator(nullifierSource, publicStateSource, gasPortalAddress, globalVariables);
validator = new TxValidator(nullifierSource, publicStateSource, contractDataSource, globalVariables, {
allowedFeePaymentContractClasses: [allowedFPCClass],
allowedFeePaymentContractInstances: [allowedFPC],
gasPortalAddress,
});
});

describe('inspects tx metadata', () => {
Expand Down Expand Up @@ -93,6 +112,52 @@ describe('TxValidator', () => {
});
});

describe('inspects how fee is paid', () => {
it('allows native gas', async () => {
const tx = nativeFeePayingTx(makeAztecAddress());
// check that the whitelist on contract address won't shadow this check
contractDataSource.getContract.mockImplementationOnce(() => {
return Promise.resolve({ contractClassId: Fr.random() } as any);
});
await expect(validator.validateTxs([tx])).resolves.toEqual([[tx], []]);
});

it('allows correct contract class', async () => {
const fpc = makeAztecAddress();
const tx = fxFeePayingTx(fpc);

contractDataSource.getContract.mockImplementationOnce(address => {
if (fpc.equals(address)) {
return Promise.resolve({ contractClassId: allowedFPCClass } as any);
} else {
return Promise.resolve({ contractClassId: Fr.random() });
}
});

await expect(validator.validateTxs([tx])).resolves.toEqual([[tx], []]);
});

it('allows correct contract', async () => {
const tx = fxFeePayingTx(allowedFPC);
// check that the whitelist on contract address works and won't get shadowed by the class whitelist
contractDataSource.getContract.mockImplementationOnce(() => {
return Promise.resolve({ contractClassId: Fr.random() } as any);
});
await expect(validator.validateTxs([tx])).resolves.toEqual([[tx], []]);
});

it('rejects incorrect contract and class', async () => {
const fpc = makeAztecAddress();
const tx = fxFeePayingTx(fpc);

contractDataSource.getContract.mockImplementationOnce(() => {
return Promise.resolve({ contractClassId: Fr.random() } as any);
});

await expect(validator.validateTxs([tx])).resolves.toEqual([[], [tx]]);
});
});

describe('inspects tx gas', () => {
it('allows native fee paying txs', async () => {
const sender = makeAztecAddress();
Expand Down
Loading

0 comments on commit adf20dc

Please sign in to comment.