Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Rollback public state changes on failure #3393

Merged
merged 4 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
12 changes: 12 additions & 0 deletions yarn-project/acir-simulator/src/public/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ export interface PublicStateDB {
* @returns Nothing.
*/
storageWrite(contract: AztecAddress, slot: Fr, newValue: Fr): Promise<void>;

/**
* Commit the pending changes to the DB.
* @returns Nothing.
*/
commit(): Promise<void>;

/**
* Rollback the pending changes.
* @returns Nothing.
*/
rollback(): Promise<void>;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ import {
makeSelector,
} from '@aztec/circuits.js/factories';
import { padArrayEnd } from '@aztec/foundation/collection';
import { ExtendedContractData, FunctionCall, FunctionL2Logs, SiblingPath, Tx, TxL2Logs, mockTx } from '@aztec/types';
import { ExtendedContractData, FunctionCall, FunctionL2Logs, SiblingPath, SimulationError, Tx, TxL2Logs, mockTx } from '@aztec/types';
import { MerkleTreeOperations, TreeInfo } from '@aztec/world-state';

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

import { PublicProver } from '../prover/index.js';
import { PublicKernelCircuitSimulator } from '../simulator/index.js';
import { ContractsDataSourcePublicDB } from '../simulator/public_executor.js';
import { ContractsDataSourcePublicDB, WorldStatePublicDB } from '../simulator/public_executor.js';
import { WasmPublicKernelCircuitSimulator } from '../simulator/public_kernel.js';
import { PublicProcessor } from './public_processor.js';

Expand All @@ -43,6 +43,7 @@ describe('public_processor', () => {
let publicExecutor: MockProxy<PublicExecutor>;
let publicProver: MockProxy<PublicProver>;
let publicContractsDB: MockProxy<ContractsDataSourcePublicDB>;
let publicWorldStateDB: MockProxy<WorldStatePublicDB>;

let proof: Proof;
let root: Buffer;
Expand All @@ -54,6 +55,7 @@ describe('public_processor', () => {
publicExecutor = mock<PublicExecutor>();
publicProver = mock<PublicProver>();
publicContractsDB = mock<ContractsDataSourcePublicDB>();
publicWorldStateDB = mock<WorldStatePublicDB>();

proof = makeEmptyProof();
root = Buffer.alloc(32, 5);
Expand All @@ -76,6 +78,7 @@ describe('public_processor', () => {
GlobalVariables.empty(),
HistoricBlockData.empty(),
publicContractsDB,
publicWorldStateDB,
);
});

Expand Down Expand Up @@ -110,6 +113,8 @@ describe('public_processor', () => {

expect(processed).toEqual([]);
expect(failed[0].tx).toEqual(tx);
expect(publicWorldStateDB.commit).toHaveBeenCalledTimes(0);
expect(publicWorldStateDB.rollback).toHaveBeenCalledTimes(1);
});
});

Expand All @@ -128,6 +133,7 @@ describe('public_processor', () => {
GlobalVariables.empty(),
HistoricBlockData.empty(),
publicContractsDB,
publicWorldStateDB
);
});

Expand Down Expand Up @@ -165,6 +171,8 @@ describe('public_processor', () => {
expect(processed).toEqual([await expectedTxByHash(tx)]);
expect(failed).toHaveLength(0);
expect(publicExecutor.simulate).toHaveBeenCalledTimes(2);
expect(publicWorldStateDB.commit).toHaveBeenCalledTimes(1);
expect(publicWorldStateDB.rollback).toHaveBeenCalledTimes(0);
});

it('runs a tx with an enqueued public call with nested execution', async function () {
Expand Down Expand Up @@ -201,6 +209,45 @@ describe('public_processor', () => {
expect(processed).toEqual([await expectedTxByHash(tx)]);
expect(failed).toHaveLength(0);
expect(publicExecutor.simulate).toHaveBeenCalledTimes(1);
expect(publicWorldStateDB.commit).toHaveBeenCalledTimes(1);
expect(publicWorldStateDB.rollback).toHaveBeenCalledTimes(0);
});

it('rolls back db updates on failed public execution', async function () {
const callRequest: PublicCallRequest = makePublicCallRequest(0x100);
const callStackItem = callRequest.toPublicCallStackItem();
const callStackHash = computeCallStackItemHash(callStackItem);

const kernelOutput = makePrivateKernelPublicInputsFinal(0x10);
kernelOutput.end.publicCallStack = padArrayEnd([callStackHash], Fr.ZERO, MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX);
kernelOutput.end.privateCallStack = padArrayEnd([], Fr.ZERO, MAX_PRIVATE_CALL_STACK_LENGTH_PER_TX);

const tx = new Tx(
kernelOutput,
proof,
TxL2Logs.random(2, 3),
TxL2Logs.random(3, 2),
[callRequest],
[ExtendedContractData.random()],
);

const publicExecutionResult = makePublicExecutionResultFromRequest(callRequest);
publicExecutionResult.nestedExecutions = [
makePublicExecutionResult(publicExecutionResult.execution.contractAddress, {
to: makeAztecAddress(30),
functionData: new FunctionData(makeSelector(5), false, false, false),
args: new Array(ARGS_LENGTH).fill(Fr.ZERO),
}),
];
publicExecutor.simulate.mockRejectedValueOnce(new SimulationError('Simulation Failed', []));

const [processed, failed] = await processor.process([tx]);

expect(failed).toHaveLength(1);
expect(processed).toHaveLength(0);
expect(publicExecutor.simulate).toHaveBeenCalledTimes(1);
expect(publicWorldStateDB.rollback).toHaveBeenCalledTimes(1);
expect(publicWorldStateDB.commit).toHaveBeenCalledTimes(0);
});
});
});
Expand Down
21 changes: 18 additions & 3 deletions yarn-project/sequencer-client/src/sequencer/public_processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
PublicExecution,
PublicExecutionResult,
PublicExecutor,
PublicStateDB,
collectPublicDataReads,
collectPublicDataUpdateRequests,
isPublicExecutionResult,
Expand Down Expand Up @@ -43,14 +44,14 @@ import { Tuple, mapTuple, to2Fields } from '@aztec/foundation/serialize';
import { ContractDataSource, FunctionL2Logs, L1ToL2MessageSource, MerkleTreeId, Tx } from '@aztec/types';
import { MerkleTreeOperations } from '@aztec/world-state';

import { getVerificationKeys } from '../index.js';
import { EmptyPublicProver } from '../prover/empty.js';
import { PublicProver } from '../prover/index.js';
import { PublicKernelCircuitSimulator } from '../simulator/index.js';
import { ContractsDataSourcePublicDB, getPublicExecutor } from '../simulator/public_executor.js';
import { ContractsDataSourcePublicDB, WorldStateDB, WorldStatePublicDB } from '../simulator/public_executor.js';
import { WasmPublicKernelCircuitSimulator } from '../simulator/public_kernel.js';
import { FailedTx, ProcessedTx, makeEmptyProcessedTx, makeProcessedTx } from './processed_tx.js';
import { getHistoricBlockData } from './utils.js';
import { getVerificationKeys } from '../index.js';

/**
* Creates new instances of PublicProcessor given the provided merkle tree db and contract data source.
Expand All @@ -75,14 +76,23 @@ export class PublicProcessorFactory {
): Promise<PublicProcessor> {
const blockData = await getHistoricBlockData(this.merkleTree, prevGlobalVariables);
const publicContractsDB = new ContractsDataSourcePublicDB(this.contractDataSource);
const worldStatePublicDB = new WorldStatePublicDB(this.merkleTree);
const worldStateDB = new WorldStateDB(this.merkleTree, this.l1Tol2MessagesDataSource);
const publicExecutor = new PublicExecutor(
worldStatePublicDB,
publicContractsDB,
worldStateDB,
blockData,
);
return new PublicProcessor(
this.merkleTree,
getPublicExecutor(this.merkleTree, publicContractsDB, this.l1Tol2MessagesDataSource, blockData),
publicExecutor,
new WasmPublicKernelCircuitSimulator(),
new EmptyPublicProver(),
globalVariables,
blockData,
publicContractsDB,
worldStatePublicDB,
);
}
}
Expand All @@ -100,6 +110,7 @@ export class PublicProcessor {
protected globalVariables: GlobalVariables,
protected blockData: HistoricBlockData,
protected publicContractsDB: ContractsDataSourcePublicDB,
protected publicStateDB: PublicStateDB,

private log = createDebugLogger('aztec:sequencer:public-processor'),
) {}
Expand All @@ -121,6 +132,8 @@ export class PublicProcessor {
// add new contracts to the contracts db so that their functions may be found and called
await this.publicContractsDB.addNewContracts(tx);
result.push(await this.processTx(tx));
// commit the state updates from this transaction
await this.publicStateDB.commit();
} catch (err) {
this.log.warn(`Error processing tx ${await tx.getTxHash()}: ${err}`);
failed.push({
Expand All @@ -129,6 +142,8 @@ export class PublicProcessor {
});
// remove contracts on failure
await this.publicContractsDB.removeNewContracts(tx);
// rollback any state updates from this failed transaction
await this.publicStateDB.rollback();
}
}

Expand Down
60 changes: 32 additions & 28 deletions yarn-project/sequencer-client/src/simulator/public_executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,13 @@ import {
CommitmentsDB,
MessageLoadOracleInputs,
PublicContractsDB,
PublicExecutor,
PublicStateDB,
} from '@aztec/acir-simulator';
import { AztecAddress, EthAddress, Fr, FunctionSelector, HistoricBlockData } from '@aztec/circuits.js';
import { AztecAddress, EthAddress, Fr, FunctionSelector } from '@aztec/circuits.js';
import { computePublicDataTreeIndex } from '@aztec/circuits.js/abis';
import { ContractDataSource, ExtendedContractData, L1ToL2MessageSource, MerkleTreeId, Tx } from '@aztec/types';
import { MerkleTreeOperations } from '@aztec/world-state';

/**
* Returns a new PublicExecutor simulator backed by the supplied merkle tree db and contract data source.
* @param merkleTree - A merkle tree database.
* @param contractDataSource - A contract data source.
* @returns A new instance of a PublicExecutor.
*/
export function getPublicExecutor(
merkleTree: MerkleTreeOperations,
publicContractsDB: PublicContractsDB,
l1toL2MessageSource: L1ToL2MessageSource,
blockData: HistoricBlockData,
) {
return new PublicExecutor(
new WorldStatePublicDB(merkleTree),
publicContractsDB,
new WorldStateDB(merkleTree, l1toL2MessageSource),
blockData,
);
}

/**
* Implements the PublicContractsDB using a ContractDataSource.
* Progressively records contracts in transaction as they are processed in a block.
Expand Down Expand Up @@ -95,8 +74,9 @@ export class ContractsDataSourcePublicDB implements PublicContractsDB {
/**
* Implements the PublicStateDB using a world-state database.
*/
class WorldStatePublicDB implements PublicStateDB {
private writeCache: Map<bigint, Fr> = new Map();
export class WorldStatePublicDB implements PublicStateDB {
private commitedWriteCache: Map<bigint, Fr> = new Map();
private uncommitedWriteCache: Map<bigint, Fr> = new Map();

constructor(private db: MerkleTreeOperations) {}

Expand All @@ -108,9 +88,13 @@ class WorldStatePublicDB implements PublicStateDB {
*/
public async storageRead(contract: AztecAddress, slot: Fr): Promise<Fr> {
const index = computePublicDataTreeIndex(contract, slot).value;
const cached = this.writeCache.get(index);
if (cached !== undefined) {
return cached;
const uncommited = this.uncommitedWriteCache.get(index);
if (uncommited !== undefined) {
return uncommited;
}
const commited = this.commitedWriteCache.get(index);
if (commited !== undefined) {
return commited;
}
const value = await this.db.getLeafValue(MerkleTreeId.PUBLIC_DATA_TREE, index);
return value ? Fr.fromBuffer(value) : Fr.ZERO;
Expand All @@ -124,7 +108,27 @@ class WorldStatePublicDB implements PublicStateDB {
*/
public storageWrite(contract: AztecAddress, slot: Fr, newValue: Fr): Promise<void> {
const index = computePublicDataTreeIndex(contract, slot).value;
this.writeCache.set(index, newValue);
this.uncommitedWriteCache.set(index, newValue);
return Promise.resolve();
}

/**
* Commit the pending changes to the DB.
* @returns Nothing.
*/
commit(): Promise<void> {
for (const [k, v] of this.uncommitedWriteCache) {
this.commitedWriteCache.set(k, v);
}
return this.rollback();
}

/**
* Rollback the pending changes.
* @returns Nothing.
*/
rollback(): Promise<void> {
this.uncommitedWriteCache = new Map<bigint,Fr>();
return Promise.resolve();
}
}
Expand Down
Loading