diff --git a/build_manifest.json b/build_manifest.json index 2435ce95a01..0441fb67830 100644 --- a/build_manifest.json +++ b/build_manifest.json @@ -2,38 +2,53 @@ "circuits-wasm-linux-clang": { "buildDir": "circuits/cpp", "dockerfile": "dockerfiles/Dockerfile.wasm-linux-clang", - "rebuildPatterns": ["^circuits/"], + "rebuildPatterns": [ + "^circuits/" + ], "dependencies": [] }, "circuits-wasm-linux-clang-assert": { "buildDir": "circuits/cpp", "dockerfile": "dockerfiles/Dockerfile.wasm-linux-clang-assert", - "rebuildPatterns": ["^circuits/"], + "rebuildPatterns": [ + "^circuits/" + ], "dependencies": [] }, "circuits-x86_64-linux-clang": { "buildDir": "circuits/cpp", "dockerfile": "dockerfiles/Dockerfile.x86_64-linux-clang", - "rebuildPatterns": ["^circuits/"], + "rebuildPatterns": [ + "^circuits/" + ], "dependencies": [] }, "circuits-x86_64-linux-clang-assert": { "buildDir": "circuits/cpp", "dockerfile": "dockerfiles/Dockerfile.x86_64-linux-clang-assert", - "rebuildPatterns": ["^circuits/"], + "rebuildPatterns": [ + "^circuits/" + ], "dependencies": [] }, "circuits-x86_64-linux-gcc": { "buildDir": "circuits/cpp", "dockerfile": "dockerfiles/Dockerfile.x86_64-linux-gcc", - "rebuildPatterns": ["^circuits/"], + "rebuildPatterns": [ + "^circuits/" + ], "dependencies": [] }, "l1-contracts": { "buildDir": "l1-contracts", "dockerfile": "Dockerfile", - "rebuildPatterns": ["^l1-contracts/"], - "dependencies": ["ethereum.js", "foundation"] + "rebuildPatterns": [ + "^l1-contracts/" + ], + "dependencies": [ + "ethereum.js", + "foundation" + ] }, "yarn-project-base": { "buildDir": "yarn-project", @@ -43,13 +58,17 @@ "^yarn-project/yarn-project-base/", "^yarn-project/yarn.lock" ], - "dependencies": ["circuits-wasm-linux-clang"] + "dependencies": [ + "circuits-wasm-linux-clang" + ] }, "acir-simulator": { "buildDir": "yarn-project", "projectDir": "yarn-project/acir-simulator", "dockerfile": "acir-simulator/Dockerfile", - "rebuildPatterns": ["^yarn-project/acir-simulator/"], + "rebuildPatterns": [ + "^yarn-project/acir-simulator/" + ], "dependencies": [ "barretenberg.js", "circuits.js", @@ -62,21 +81,34 @@ "buildDir": "yarn-project", "projectDir": "yarn-project/archiver", "dockerfile": "archiver/Dockerfile", - "rebuildPatterns": ["^yarn-project/archiver/"], - "dependencies": ["ethereum.js", "foundation", "l1-contracts", "types"] + "rebuildPatterns": [ + "^yarn-project/archiver/" + ], + "dependencies": [ + "ethereum.js", + "foundation", + "l1-contracts", + "types" + ] }, "aztec-cli": { "buildDir": "yarn-project", "projectDir": "yarn-project/aztec-cli", "dockerfile": "aztec-cli/Dockerfile", - "rebuildPatterns": ["^yarn-project/aztec-cli/"], - "dependencies": ["foundation"] + "rebuildPatterns": [ + "^yarn-project/aztec-cli/" + ], + "dependencies": [ + "foundation" + ] }, "aztec-rpc": { "buildDir": "yarn-project", "projectDir": "yarn-project/aztec-rpc", "dockerfile": "aztec-rpc/Dockerfile", - "rebuildPatterns": ["^yarn-project/aztec-rpc/"], + "rebuildPatterns": [ + "^yarn-project/aztec-rpc/" + ], "dependencies": [ "acir-simulator", "aztec-node", @@ -92,28 +124,48 @@ "buildDir": "yarn-project", "projectDir": "yarn-project/aztec.js", "dockerfile": "aztec.js/Dockerfile", - "rebuildPatterns": ["^yarn-project/aztec.js/"], - "dependencies": ["aztec-rpc", "circuits.js", "foundation", "noir-contracts"] + "rebuildPatterns": [ + "^yarn-project/aztec.js/" + ], + "dependencies": [ + "aztec-rpc", + "circuits.js", + "foundation", + "noir-contracts" + ] }, "barretenberg.js": { "buildDir": "yarn-project", "projectDir": "yarn-project/barretenberg.js", "dockerfile": "barretenberg.js/Dockerfile", - "rebuildPatterns": ["^yarn-project/barretenberg.js/"], - "dependencies": ["foundation", "yarn-project-base"] + "rebuildPatterns": [ + "^yarn-project/barretenberg.js/" + ], + "dependencies": [ + "foundation", + "yarn-project-base" + ] }, "circuits.js": { "buildDir": "yarn-project", "projectDir": "yarn-project/circuits.js", "dockerfile": "circuits.js/Dockerfile", - "rebuildPatterns": ["^yarn-project/circuits.js/"], - "dependencies": ["barretenberg.js", "foundation", "yarn-project-base"] + "rebuildPatterns": [ + "^yarn-project/circuits.js/" + ], + "dependencies": [ + "barretenberg.js", + "foundation", + "yarn-project-base" + ] }, "end-to-end": { "buildDir": "yarn-project", "projectDir": "yarn-project/end-to-end", "dockerfile": "end-to-end/Dockerfile", - "rebuildPatterns": ["^yarn-project/end-to-end/"], + "rebuildPatterns": [ + "^yarn-project/end-to-end/" + ], "dependencies": [ "aztec-node", "aztec.js", @@ -127,70 +179,114 @@ "buildDir": "yarn-project", "projectDir": "yarn-project/ethereum.js", "dockerfile": "ethereum.js/Dockerfile", - "rebuildPatterns": ["^yarn-project/ethereum.js/"], - "dependencies": ["foundation"] + "rebuildPatterns": [ + "^yarn-project/ethereum.js/" + ], + "dependencies": [ + "foundation" + ] }, "foundation": { "buildDir": "yarn-project", "projectDir": "yarn-project/foundation", "dockerfile": "foundation/Dockerfile", - "rebuildPatterns": ["^yarn-project/foundation/"], + "rebuildPatterns": [ + "^yarn-project/foundation/" + ], "dependencies": [] }, "kernel-prover": { "buildDir": "yarn-project", "projectDir": "yarn-project/kernel-prover", "dockerfile": "kernel-prover/Dockerfile", - "rebuildPatterns": ["^yarn-project/kernel-prover/"], - "dependencies": ["acir-simulator", "circuits.js", "foundation"] + "rebuildPatterns": [ + "^yarn-project/kernel-prover/" + ], + "dependencies": [ + "acir-simulator", + "circuits.js", + "foundation" + ] }, "key-store": { "buildDir": "yarn-project", "projectDir": "yarn-project/key-store", "dockerfile": "key-store/Dockerfile", - "rebuildPatterns": ["^yarn-project/key-store/"], - "dependencies": ["foundation"] + "rebuildPatterns": [ + "^yarn-project/key-store/" + ], + "dependencies": [ + "foundation" + ] }, "l2-block": { "buildDir": "yarn-project", "projectDir": "yarn-project/l2-block", "dockerfile": "l2-block/Dockerfile", - "rebuildPatterns": ["^yarn-project/l2-block/"], - "dependencies": ["circuits.js", "foundation", "l1-contracts", "tx"] + "rebuildPatterns": [ + "^yarn-project/l2-block/" + ], + "dependencies": [ + "circuits.js", + "foundation", + "l1-contracts", + "tx" + ] }, "merkle-tree": { "buildDir": "yarn-project", "projectDir": "yarn-project/merkle-tree", "dockerfile": "merkle-tree/Dockerfile", - "rebuildPatterns": ["^yarn-project/merkle-tree/"], - "dependencies": ["barretenberg.js", "foundation"] + "rebuildPatterns": [ + "^yarn-project/merkle-tree/" + ], + "dependencies": [ + "barretenberg.js", + "foundation" + ] }, "noir-contracts": { "buildDir": "yarn-project", "projectDir": "yarn-project/noir-contracts", "dockerfile": "noir-contracts/Dockerfile", - "rebuildPatterns": ["^yarn-project/noir-contracts/"], - "dependencies": ["foundation"] + "rebuildPatterns": [ + "^yarn-project/noir-contracts/" + ], + "dependencies": [ + "foundation" + ] }, "p2p": { "buildDir": "yarn-project", "projectDir": "yarn-project/p2p", "dockerfile": "p2p/Dockerfile", - "rebuildPatterns": ["^yarn-project/p2p/"], - "dependencies": ["circuits.js", "foundation", "types"] + "rebuildPatterns": [ + "^yarn-project/p2p/" + ], + "dependencies": [ + "circuits.js", + "foundation", + "types" + ] }, "prover-client": { "buildDir": "yarn-project", "projectDir": "yarn-project/prover-client", "dockerfile": "prover-client/Dockerfile", - "rebuildPatterns": ["^yarn-project/prover-client/"], - "dependencies": ["foundation"] + "rebuildPatterns": [ + "^yarn-project/prover-client/" + ], + "dependencies": [ + "foundation" + ] }, "aztec-node": { "buildDir": "yarn-project", "projectDir": "yarn-project/aztec-node", "dockerfile": "aztec-node/Dockerfile", - "rebuildPatterns": ["^yarn-project/aztec-node/"], + "rebuildPatterns": [ + "^yarn-project/aztec-node/" + ], "dependencies": [ "archiver", "barretenberg.js", @@ -208,8 +304,11 @@ "buildDir": "yarn-project", "projectDir": "yarn-project/sequencer-client", "dockerfile": "sequencer-client/Dockerfile", - "rebuildPatterns": ["^yarn-project/sequencer-client/"], + "rebuildPatterns": [ + "^yarn-project/sequencer-client/" + ], "dependencies": [ + "acir-simulator", "circuits.js", "ethereum.js", "foundation", @@ -224,21 +323,36 @@ "buildDir": "yarn-project", "projectDir": "yarn-project/tx", "dockerfile": "tx/Dockerfile", - "rebuildPatterns": ["^yarn-project/tx/"], - "dependencies": ["barretenberg.js", "circuits.js", "foundation", "types"] + "rebuildPatterns": [ + "^yarn-project/tx/" + ], + "dependencies": [ + "barretenberg.js", + "circuits.js", + "foundation", + "types" + ] }, "types": { "buildDir": "yarn-project", "projectDir": "yarn-project/types", "dockerfile": "types/Dockerfile", - "rebuildPatterns": ["^yarn-project/types/"], - "dependencies": ["circuits.js", "foundation", "l1-contracts"] + "rebuildPatterns": [ + "^yarn-project/types/" + ], + "dependencies": [ + "circuits.js", + "foundation", + "l1-contracts" + ] }, "world-state": { "buildDir": "yarn-project", "projectDir": "yarn-project/world-state", "dockerfile": "world-state/Dockerfile", - "rebuildPatterns": ["^yarn-project/world-state/"], + "rebuildPatterns": [ + "^yarn-project/world-state/" + ], "dependencies": [ "barretenberg.js", "circuits.js", @@ -247,4 +361,4 @@ "types" ] } -} +} \ No newline at end of file diff --git a/yarn-project/acir-simulator/src/index.ts b/yarn-project/acir-simulator/src/index.ts index 58036d6241a..45a07a6ddae 100644 --- a/yarn-project/acir-simulator/src/index.ts +++ b/yarn-project/acir-simulator/src/index.ts @@ -3,3 +3,5 @@ export * from './execution.js'; export * from './db_oracle.js'; export * from './acvm/acvm.js'; export * from './arguments_encoder/index.js'; +export * from './public/index.js'; +export { computeSlot } from './utils.js'; diff --git a/yarn-project/acir-simulator/src/public/db.ts b/yarn-project/acir-simulator/src/public/db.ts index df81b1f84ef..f4c2baed013 100644 --- a/yarn-project/acir-simulator/src/public/db.ts +++ b/yarn-project/acir-simulator/src/public/db.ts @@ -1,6 +1,11 @@ import { AztecAddress, Fr } from '@aztec/foundation'; export interface PublicDB { + /** + * Reads a value from public storage, returning zero if none. + * @param contract - owner of the storage. + * @param slot - slot to read in the contract storage. + * @returns The current value in the storage slot. + */ storageRead(contract: AztecAddress, slot: Fr): Promise; - storageWrite(contract: AztecAddress, slot: Fr, value: Fr): Promise; } diff --git a/yarn-project/acir-simulator/src/public/execution.ts b/yarn-project/acir-simulator/src/public/execution.ts index 6330fd6d0a8..6aef082ca53 100644 --- a/yarn-project/acir-simulator/src/public/execution.ts +++ b/yarn-project/acir-simulator/src/public/execution.ts @@ -1,21 +1,11 @@ -import { CallContext, FunctionData, TxRequest } from '@aztec/circuits.js'; +import { CallContext, FunctionData, StateRead, StateTransition, TxRequest } from '@aztec/circuits.js'; import { AztecAddress, EthAddress, Fr } from '@aztec/foundation'; import { createDebugLogger } from '@aztec/foundation/log'; import { FunctionAbi } from '@aztec/noir-contracts'; import { acvm, fromACVMField, toACVMField, toACVMWitness } from '../acvm/index.js'; import { PublicDB } from './db.js'; import { select_return_flattened as selectPublicWitnessFlattened } from '@noir-lang/noir_util_wasm'; - -export interface StateRead { - storageSlot: Fr; - value: Fr; -} - -export interface StateTransition { - storageSlot: Fr; - oldValue: Fr; - newValue: Fr; -} +import { StateActionsCollector } from './state_actions.js'; export interface PublicExecutionResult { acir: Buffer; @@ -40,12 +30,12 @@ function getInitialWitness(args: Fr[], callContext: CallContext, witnessStartInd export class PublicExecution { constructor( - private db: PublicDB, - private abi: FunctionAbi, - private contractAddress: AztecAddress, - private functionData: FunctionData, - private args: Fr[], - private callContext: CallContext, + public readonly db: PublicDB, + public readonly abi: FunctionAbi, + public readonly contractAddress: AztecAddress, + public readonly functionData: FunctionData, + public readonly args: Fr[], + public readonly callContext: CallContext, private log = createDebugLogger('aztec:simulator:public-execution'), ) {} @@ -74,9 +64,7 @@ export class PublicExecution { const acir = Buffer.from(this.abi.bytecode, 'hex'); const initialWitness = getInitialWitness(this.args, this.callContext); - - const stateReads: StateRead[] = []; - const stateTransitions: StateTransition[] = []; + const stateActions = new StateActionsCollector(this.db, this.contractAddress); const notAvailable = () => Promise.reject(`Built-in not available for public execution simulation`); @@ -89,16 +77,14 @@ export class PublicExecution { callPrivateFunction: notAvailable, storageRead: async ([slot]) => { const storageSlot = fromACVMField(slot); - const value = await this.db.storageRead(this.contractAddress, storageSlot); - stateReads.push({ value, storageSlot }); + const value = await stateActions.read(storageSlot); this.log(`Oracle storage read: slot=${storageSlot.toShortString()} value=${value.toString()}`); return [toACVMField(value)]; }, storageWrite: async ([slot, value]) => { const storageSlot = fromACVMField(slot); const newValue = fromACVMField(value); - const oldValue = await this.db.storageWrite(this.contractAddress, storageSlot, newValue); - stateTransitions.push({ storageSlot, newValue, oldValue }); + await stateActions.write(storageSlot, newValue); this.log(`Oracle storage write: slot=${storageSlot.toShortString()} value=${value.toString()}`); return [toACVMField(newValue)]; }, @@ -106,6 +92,7 @@ export class PublicExecution { const returnValues = selectPublicWitnessFlattened(acir, partialWitness).map(fromACVMField); const vk = Buffer.from(this.abi.verificationKey!, 'hex'); + const [stateReads, stateTransitions] = stateActions.collect(); return { acir, diff --git a/yarn-project/acir-simulator/src/public/index.test.ts b/yarn-project/acir-simulator/src/public/index.test.ts index ec9d4258cbf..9c00f18ecb4 100644 --- a/yarn-project/acir-simulator/src/public/index.test.ts +++ b/yarn-project/acir-simulator/src/public/index.test.ts @@ -54,7 +54,6 @@ describe('ACIR public execution simulator', () => { // Mock the old value for the recipient balance to be 20 const previousBalance = new Fr(20n); - oracle.storageWrite.mockResolvedValue(previousBalance); oracle.storageRead.mockResolvedValue(previousBalance); const execution = new PublicExecution(oracle, abi, contractAddress, functionData, args, callContext); @@ -64,13 +63,11 @@ describe('ACIR public execution simulator', () => { expect(result.returnValues).toEqual([expectedBalance]); const storageSlot = computeSlot(new Fr(1n), recipient, bbWasm); - expect(oracle.storageRead).toHaveBeenCalledWith(contractAddress, storageSlot); - expect(oracle.storageWrite).toHaveBeenCalledWith(contractAddress, storageSlot, expectedBalance); - - expect(result.stateReads).toEqual([{ storageSlot: storageSlot, value: previousBalance }]); expect(result.stateTransitions).toEqual([ { storageSlot, oldValue: previousBalance, newValue: expectedBalance }, ]); + + expect(result.stateReads).toEqual([]); }); }); @@ -106,7 +103,7 @@ describe('ACIR public execution simulator', () => { const mockStore = (senderBalance: Fr, recipientBalance: Fr) => { // eslint-disable-next-line require-await - const mockStoreImplementation = async (_addr: AztecAddress, slot: Fr) => { + oracle.storageRead.mockImplementation(async (_addr: AztecAddress, slot: Fr) => { if (slot.equals(recipientStorageSlot)) { return recipientBalance; } else if (slot.equals(senderStorageSlot)) { @@ -114,10 +111,7 @@ describe('ACIR public execution simulator', () => { } else { return Fr.ZERO; } - }; - - oracle.storageRead.mockImplementation(mockStoreImplementation); - oracle.storageWrite.mockImplementation(mockStoreImplementation); + }); }; it('should run the transfer function', async () => { @@ -133,18 +127,17 @@ describe('ACIR public execution simulator', () => { expect(result.returnValues).toEqual([expectedRecipientBalance]); - expect(result.stateReads).toEqual([ - { storageSlot: recipientStorageSlot, value: recipientBalance }, - { storageSlot: senderStorageSlot, value: senderBalance }, - ]); - expect(result.stateTransitions).toEqual([ { storageSlot: senderStorageSlot, oldValue: senderBalance, newValue: expectedSenderBalance }, { storageSlot: recipientStorageSlot, oldValue: recipientBalance, newValue: expectedRecipientBalance }, ]); + + expect(result.stateReads).toEqual([]); }); - // TODO: Figure out why we're not hitting the conditional + // State reads and writes are implemented as built-ins, which at the moment Noir does not + // now whether they have side-effects or not, so they get run even when their code path + // is not picked by a conditional. Once that's fixed, we should re-enable this test. it.skip('should run the transfer function without enough sender balance', async () => { const senderBalance = new Fr(10n); const recipientBalance = new Fr(20n); diff --git a/yarn-project/acir-simulator/src/public/state_actions.ts b/yarn-project/acir-simulator/src/public/state_actions.ts new file mode 100644 index 00000000000..33688fad987 --- /dev/null +++ b/yarn-project/acir-simulator/src/public/state_actions.ts @@ -0,0 +1,80 @@ +import { AztecAddress, Fr } from '@aztec/foundation'; +import { PublicDB } from './db.js'; +import { StateRead, StateTransition } from '@aztec/circuits.js'; + +/** + * Implements state read/write operations on a contract public storage, collecting + * all state read and transitions operations, and collapsing them into a single + * read or transition per slot. + */ +export class StateActionsCollector { + // Map from slot to first read value + private readonly stateReads: Map = new Map(); + + // Map from slot to first read value and latest updated value + private readonly stateTransitions: Map = new Map(); + + constructor(private db: PublicDB, private address: AztecAddress) {} + + /** + * Returns the current value of a slot according to the latest transition for it, + * falling back to the public db. Collects the operation in state reads, + * as long as there is no existing state transition. + * @param storageSlot - slot to check + * @returns The current value as affected by all state transitions so far. + */ + public async read(storageSlot: Fr): Promise { + const slot = storageSlot.value; + const transition = this.stateTransitions.get(slot); + if (transition) return transition.newValue; + const read = this.stateReads.get(slot); + if (read) return read.value; + const value = await this.db.storageRead(this.address, storageSlot); + this.stateReads.set(slot, { value }); + return value; + } + + /** + * Sets a new value for a slot in the internal state transitions cache, + * clearing any previous state read or transition operation for the same slot. + * @param storageSlot - slot to write to + * @param newValue - value to write to it + */ + public async write(storageSlot: Fr, newValue: Fr): Promise { + const slot = storageSlot.value; + const transition = this.stateTransitions.get(slot); + if (transition) { + this.stateTransitions.set(slot, { oldValue: transition.oldValue, newValue }); + return; + } + + const read = this.stateReads.get(slot); + if (read) { + this.stateReads.delete(slot); + this.stateTransitions.set(slot, { oldValue: read.value, newValue }); + return; + } + + const oldValue = await this.db.storageRead(this.address, storageSlot); + this.stateTransitions.set(slot, { oldValue, newValue }); + return; + } + + /** + * Returns all state read and transitions performed. + * @returns all state read and transitions + */ + public collect(): [StateRead[], StateTransition[]] { + const reads = Array.from(this.stateReads.entries()).map(([slot, value]) => ({ + storageSlot: new Fr(slot), + ...value, + })); + + const transitions = Array.from(this.stateTransitions.entries()).map(([slot, values]) => ({ + storageSlot: new Fr(slot), + ...values, + })); + + return [reads, transitions]; + } +} diff --git a/yarn-project/aztec-rpc/src/account_state/account_state.ts b/yarn-project/aztec-rpc/src/account_state/account_state.ts index 470bd5d8fc0..3c618a9631b 100644 --- a/yarn-project/aztec-rpc/src/account_state/account_state.ts +++ b/yarn-project/aztec-rpc/src/account_state/account_state.ts @@ -93,7 +93,7 @@ export class AccountState { const unverifiedData = this.createUnverifiedData(outputNotes); - return new Tx(publicInputs, proof, unverifiedData); + return Tx.createPrivate(publicInputs, proof, unverifiedData); } public async process(l2BlockContexts: L2BlockContext[], unverifiedDatas: UnverifiedData[]): Promise { diff --git a/yarn-project/circuits.js/src/structs/__snapshots__/kernel.test.ts.snap b/yarn-project/circuits.js/src/structs/__snapshots__/private_kernel.test.ts.snap similarity index 100% rename from yarn-project/circuits.js/src/structs/__snapshots__/kernel.test.ts.snap rename to yarn-project/circuits.js/src/structs/__snapshots__/private_kernel.test.ts.snap diff --git a/yarn-project/circuits.js/src/structs/base_rollup.ts b/yarn-project/circuits.js/src/structs/base_rollup.ts index cf36092f5b8..9ddde8a7a8a 100644 --- a/yarn-project/circuits.js/src/structs/base_rollup.ts +++ b/yarn-project/circuits.js/src/structs/base_rollup.ts @@ -11,7 +11,7 @@ import { PRIVATE_DATA_TREE_HEIGHT, PRIVATE_DATA_TREE_ROOTS_TREE_HEIGHT, } from './constants.js'; -import { PreviousKernelData } from './kernel.js'; +import { PreviousKernelData } from './private_kernel.js'; import { AggregationObject, MembershipWitness, RollupTypes, UInt32 } from './shared.js'; export class NullifierLeafPreimage { diff --git a/yarn-project/circuits.js/src/structs/constants.ts b/yarn-project/circuits.js/src/structs/constants.ts index 3693d3ec711..c0f2c32bc95 100644 --- a/yarn-project/circuits.js/src/structs/constants.ts +++ b/yarn-project/circuits.js/src/structs/constants.ts @@ -27,6 +27,7 @@ export const VK_TREE_HEIGHT = 3; export const FUNCTION_TREE_HEIGHT = 4; export const CONTRACT_TREE_HEIGHT = 4; export const PRIVATE_DATA_TREE_HEIGHT = 8; +export const PUBLIC_DATA_TREE_HEIGHT = 32; export const NULLIFIER_TREE_HEIGHT = 8; export const PRIVATE_DATA_TREE_ROOTS_TREE_HEIGHT = 8; diff --git a/yarn-project/circuits.js/src/structs/index.ts b/yarn-project/circuits.js/src/structs/index.ts index de654063290..227844c2b53 100644 --- a/yarn-project/circuits.js/src/structs/index.ts +++ b/yarn-project/circuits.js/src/structs/index.ts @@ -2,9 +2,11 @@ export * from './base_rollup.js'; export * from './call_context.js'; export * from './constants.js'; export * from './function_data.js'; -export * from './kernel.js'; +export * from './private_kernel.js'; +export * from './public_kernel.js'; export * from './merge_rollup.js'; export * from './private_circuit_public_inputs.js'; +export * from './public_circuit_public_inputs.js'; export * from './private_call_stack_item.js'; export * from './root_rollup.js'; export * from './shared.js'; diff --git a/yarn-project/circuits.js/src/structs/kernel.test.ts b/yarn-project/circuits.js/src/structs/private_kernel.test.ts similarity index 100% rename from yarn-project/circuits.js/src/structs/kernel.test.ts rename to yarn-project/circuits.js/src/structs/private_kernel.test.ts diff --git a/yarn-project/circuits.js/src/structs/kernel.ts b/yarn-project/circuits.js/src/structs/private_kernel.ts similarity index 100% rename from yarn-project/circuits.js/src/structs/kernel.ts rename to yarn-project/circuits.js/src/structs/private_kernel.ts diff --git a/yarn-project/circuits.js/src/structs/public_circuit_public_inputs.ts b/yarn-project/circuits.js/src/structs/public_circuit_public_inputs.ts new file mode 100644 index 00000000000..83e86cda779 --- /dev/null +++ b/yarn-project/circuits.js/src/structs/public_circuit_public_inputs.ts @@ -0,0 +1,100 @@ +import { AztecAddress, Fr } from '@aztec/foundation'; +import times from 'lodash.times'; +import { FieldsOf, assertLength } from '../utils/jsUtils.js'; +import { CallContext } from './call_context.js'; +import { + ARGS_LENGTH, + EMITTED_EVENTS_LENGTH, + L1_MSG_STACK_LENGTH, + PUBLIC_CALL_STACK_LENGTH, + RETURN_VALUES_LENGTH, + STATE_READS_LENGTH, + STATE_TRANSITIONS_LENGTH, +} from './constants.js'; + +/** + * Read operations from the public state tree. + */ +export interface StateRead { + storageSlot: Fr; + value: Fr; +} + +/** + * Write operations on the public state tree. + */ +export interface StateTransition { + storageSlot: Fr; + oldValue: Fr; + newValue: Fr; +} + +/** + * Public inputs to a public circuit. + */ +export class PublicCircuitPublicInputs { + constructor( + public callContext: CallContext, + public args: Fr[], + public returnValues: Fr[], + public emittedEvents: Fr[], + public publicCallStack: Fr[], + public l1MsgStack: Fr[], + public stateTransitions: StateTransition[], + public stateReads: StateRead[], + public proverAddress: AztecAddress, + ) { + assertLength(this, 'args', ARGS_LENGTH); + assertLength(this, 'returnValues', RETURN_VALUES_LENGTH); + assertLength(this, 'emittedEvents', EMITTED_EVENTS_LENGTH); + assertLength(this, 'publicCallStack', PUBLIC_CALL_STACK_LENGTH); + assertLength(this, 'l1MsgStack', L1_MSG_STACK_LENGTH); + assertLength(this, 'stateTransitions', STATE_TRANSITIONS_LENGTH); + assertLength(this, 'stateReads', STATE_READS_LENGTH); + } + /** + * Create PublicCircuitPublicInputs from a fields dictionary. + * @param fields - The dictionary. + * @returns A PublicCircuitPublicInputs object. + */ + static from(fields: FieldsOf): PublicCircuitPublicInputs { + return new PublicCircuitPublicInputs(...PublicCircuitPublicInputs.getFields(fields)); + } + + /** + * Returns an empty instance. + * @returns an empty instance. + */ + public static empty() { + const frArray = (num: number) => times(num, () => Fr.ZERO); + return new PublicCircuitPublicInputs( + CallContext.empty(), + frArray(ARGS_LENGTH), + frArray(RETURN_VALUES_LENGTH), + frArray(EMITTED_EVENTS_LENGTH), + frArray(PUBLIC_CALL_STACK_LENGTH), + frArray(L1_MSG_STACK_LENGTH), + times(STATE_TRANSITIONS_LENGTH, () => ({ storageSlot: Fr.ZERO, oldValue: Fr.ZERO, newValue: Fr.ZERO })), + times(STATE_READS_LENGTH, () => ({ storageSlot: Fr.ZERO, value: Fr.ZERO })), + AztecAddress.ZERO, + ); + } + /** + * Serialize into a field array. Low-level utility. + * @param fields - Object with fields. + * @returns The array. + */ + static getFields(fields: FieldsOf) { + return [ + fields.callContext, + fields.args, + fields.returnValues, + fields.emittedEvents, + fields.publicCallStack, + fields.l1MsgStack, + fields.stateTransitions, + fields.stateReads, + fields.proverAddress, + ] as const; + } +} diff --git a/yarn-project/circuits.js/src/structs/public_kernel.ts b/yarn-project/circuits.js/src/structs/public_kernel.ts new file mode 100644 index 00000000000..900f0d0bbc7 --- /dev/null +++ b/yarn-project/circuits.js/src/structs/public_kernel.ts @@ -0,0 +1,112 @@ +import { AztecAddress, Fr } from '@aztec/foundation'; +import { PreviousKernelData as PreviousPrivateKernelData } from './private_kernel.js'; +import { MembershipWitness, UInt8Vector } from './shared.js'; +import { SignedTxRequest } from './tx.js'; +import { VerificationKey } from './verification_key.js'; +import { + PUBLIC_CALL_STACK_LENGTH, + PUBLIC_DATA_TREE_HEIGHT, + STATE_READS_LENGTH, + STATE_TRANSITIONS_LENGTH, +} from './constants.js'; +import { assertLength } from '../utils/jsUtils.js'; +import { FunctionData } from './function_data.js'; +import { PublicCircuitPublicInputs } from './public_circuit_public_inputs.js'; + +export type PublicKernelInputs = + | PublicKernelInputsNonFirstIteration + | PublicKernelInputsPrivateKernelInput + | PublicKernelInputsNoKernelInput; + +export class PublicKernelInputsNonFirstIteration { + public kind = 'NonFirstIteration' as const; + + constructor( + public readonly previousKernel: PreviousPublicKernelData, + public readonly witnessedPublicCall: WitnessedPublicCallData, + ) {} +} + +export class PublicKernelInputsPrivateKernelInput { + public kind = 'PrivateKernelInput' as const; + + constructor( + public readonly previousKernel: PreviousPrivateKernelData, + public readonly witnessedPublicCall: WitnessedPublicCallData, + ) {} +} + +export class PublicKernelInputsNoKernelInput { + public kind = 'NoKernelInput' as const; + + constructor( + public readonly signedTxRequest: SignedTxRequest, + public readonly witnessedPublicCall: WitnessedPublicCallData, + ) {} +} + +export class WitnessedPublicCallData { + constructor( + public readonly publicCall: PublicCallData, + // TODO: Spec uses SiblingPaths here instead of MembershipWitness, are we ok reusing this structure instead? + public readonly transitionsHashPaths: MembershipWitness[], + public readonly readsHashPaths: MembershipWitness[], + public readonly publicDataTreeRoot: Fr, + ) { + assertLength(this, 'transitionsHashPaths', STATE_TRANSITIONS_LENGTH); + assertLength(this, 'readsHashPaths', STATE_READS_LENGTH); + } +} + +export class PublicCallData { + constructor( + public readonly callStackItem: PublicCallStackItem, + public readonly publicCallStackPreimages: PublicCallStackItem[], + public readonly proof: UInt8Vector, + public readonly portalContractAddress: Fr, + public readonly bytecodeHash: Fr, + ) { + assertLength(this, 'publicCallStackPreimages', PUBLIC_CALL_STACK_LENGTH); + } +} + +export class PublicCallStackItem { + constructor( + public readonly contractAddress: AztecAddress, + public readonly functionSignature: FunctionData, + public readonly publicInputs: PublicCircuitPublicInputs, + ) {} + + static empty() { + return new this(AztecAddress.ZERO, FunctionData.empty(), PublicCircuitPublicInputs.empty()); + } +} + +export class PreviousPublicKernelData { + constructor( + public readonly publicInputs: PublicKernelPublicInputs, + public readonly proof: UInt8Vector, + public readonly vk: VerificationKey, + ) {} +} + +export class PublicKernelPublicInputs { + public readonly isPrivateKernel = false; + constructor(public readonly end: CombinedAccumulatedData, public readonly constants: CombinedConstantData) {} + + static empty() { + return new this(CombinedAccumulatedData.empty(), CombinedConstantData.empty()); + } +} + +class CombinedAccumulatedData { + static empty() { + return new this(); + } +} + +class CombinedConstantData { + static empty() { + return new this(); + } +} diff --git a/yarn-project/circuits.js/src/tests/factories.ts b/yarn-project/circuits.js/src/tests/factories.ts index 3c94b69ed2b..0704fda98e1 100644 --- a/yarn-project/circuits.js/src/tests/factories.ts +++ b/yarn-project/circuits.js/src/tests/factories.ts @@ -50,7 +50,7 @@ import { PrivateCallData, PrivateKernelInputs, PrivateKernelPublicInputs, -} from '../structs/kernel.js'; +} from '../structs/private_kernel.js'; import { PrivateCallStackItem } from '../structs/private_call_stack_item.js'; import { AffineElement, diff --git a/yarn-project/foundation/src/aztec-address/index.ts b/yarn-project/foundation/src/aztec-address/index.ts index 141bb0214aa..b1470be269e 100644 --- a/yarn-project/foundation/src/aztec-address/index.ts +++ b/yarn-project/foundation/src/aztec-address/index.ts @@ -96,6 +96,14 @@ export class AztecAddress { return `${str.slice(0, 10)}...${str.slice(-4)}`; } + /** + * Returns this address as a field element. + * @returns A field element with the same value as the address. + */ + toField() { + return Fr.fromBuffer(this.toBuffer()); + } + /** * Determines if this AztecAddress instance is equal to the given AztecAddress instance. * Equality is based on the content of their respective buffers. diff --git a/yarn-project/p2p/src/client/mocks.ts b/yarn-project/p2p/src/client/mocks.ts index f903c6ac05a..97990a29b4d 100644 --- a/yarn-project/p2p/src/client/mocks.ts +++ b/yarn-project/p2p/src/client/mocks.ts @@ -6,7 +6,7 @@ import { Tx } from '@aztec/types'; import { UnverifiedData } from '@aztec/types'; export const MockTx = () => { - return new Tx(makePrivateKernelPublicInputs(), new UInt8Vector(Buffer.alloc(0)), UnverifiedData.random(8)); + return Tx.createPrivate(makePrivateKernelPublicInputs(), new UInt8Vector(Buffer.alloc(0)), UnverifiedData.random(8)); }; export class MockBlockSource implements L2BlockSource { diff --git a/yarn-project/p2p/src/index.ts b/yarn-project/p2p/src/index.ts index e6a85a1efa9..3eff155a07e 100644 --- a/yarn-project/p2p/src/index.ts +++ b/yarn-project/p2p/src/index.ts @@ -1,24 +1 @@ -// import { InMemoryP2PCLient } from './memory_p2p_client.js'; -// import { MockBlockSource } from './mocks.js'; - export * from './client/index.js'; - -/** - * Main function of P2P in-memory client that runs at init. - */ -// async function main() { -// // TODO: replace with actual rollup source that gets instantiated with env variables -// const rollupSource = new MockBlockSource(); -// const p2pClient = new InMemoryP2PCLient(rollupSource); -// await p2pClient.start(); - -// const shutdown = async () => { -// await p2pClient.stop(); -// process.exit(0); -// }; - -// process.once('SIGINT', shutdown); -// process.once('SIGTERM', shutdown); -// } - -// main().catch(err => console.log('ERROR in main p2p function: ', err)); diff --git a/yarn-project/sequencer-client/package.json b/yarn-project/sequencer-client/package.json index 8dcb071f846..7aaefa0f550 100644 --- a/yarn-project/sequencer-client/package.json +++ b/yarn-project/sequencer-client/package.json @@ -32,6 +32,7 @@ "rootDir": "./src" }, "dependencies": { + "@aztec/acir-simulator": "workspace:^", "@aztec/circuits.js": "workspace:^", "@aztec/ethereum.js": "workspace:^", "@aztec/foundation": "workspace:^", diff --git a/yarn-project/sequencer-client/src/block_builder/circuit_block_builder.test.ts b/yarn-project/sequencer-client/src/block_builder/circuit_block_builder.test.ts index af16fedcc91..ad37429c8a6 100644 --- a/yarn-project/sequencer-client/src/block_builder/circuit_block_builder.test.ts +++ b/yarn-project/sequencer-client/src/block_builder/circuit_block_builder.test.ts @@ -13,17 +13,17 @@ import { makePrivateKernelPublicInputs, makeRootRollupPublicInputs, } from '@aztec/circuits.js/factories'; -import { Tx } from '@aztec/types'; +import { PrivateTx, Tx } from '@aztec/types'; import { MerkleTreeId, MerkleTreeOperations, MerkleTrees } from '@aztec/world-state'; import { MockProxy, mock } from 'jest-mock-extended'; import { default as levelup } from 'levelup'; import flatMap from 'lodash.flatmap'; import { default as memdown, type MemDown } from 'memdown'; -import { makeEmptyTx, makeEmptyUnverifiedData } from '../mocks/tx.js'; +import { makeEmptyPrivateTx, makeEmptyUnverifiedData } from '../mocks/tx.js'; import { VerificationKeys, getVerificationKeys } from '../mocks/verification_keys.js'; -import { EmptyProver } from '../prover/empty.js'; -import { Prover } from '../prover/index.js'; -import { Simulator } from '../simulator/index.js'; +import { EmptyRollupProver } from '../prover/empty.js'; +import { RollupProver } from '../prover/index.js'; +import { RollupSimulator } from '../simulator/index.js'; import { WasmCircuitSimulator } from '../simulator/wasm.js'; import { CircuitBlockBuilder } from './circuit_block_builder.js'; import { computeContractLeaf } from '@aztec/circuits.js/abis'; @@ -38,8 +38,8 @@ describe('sequencer/circuit_block_builder', () => { let expectsDb: MerkleTreeOperations; let vks: VerificationKeys; - let simulator: MockProxy; - let prover: MockProxy; + let simulator: MockProxy; + let prover: MockProxy; let blockNumber: number; let baseRollupOutputLeft: BaseOrMergeRollupPublicInputs; @@ -59,8 +59,8 @@ describe('sequencer/circuit_block_builder', () => { builderDb = await MerkleTrees.new(levelup(createMemDown())).then(t => t.asLatest()); expectsDb = await MerkleTrees.new(levelup(createMemDown())).then(t => t.asLatest()); vks = getVerificationKeys(); - simulator = mock(); - prover = mock(); + simulator = mock(); + prover = mock(); builder = new TestSubject(builderDb, vks, simulator, prover); // Populate root trees with first roots from the empty trees @@ -92,7 +92,7 @@ describe('sequencer/circuit_block_builder', () => { }; // Updates the expectedDb trees based on the new commitments, contracts, and nullifiers from these txs - const updateExpectedTreesFromTxs = async (txs: Tx[]) => { + const updateExpectedTreesFromTxs = async (txs: PrivateTx[]) => { const newContracts = flatMap(txs, tx => tx.data.end.newContracts.map(n => computeContractLeaf(wasm, n))); for (const [tree, leaves] of [ [MerkleTreeId.DATA_TREE, flatMap(txs, tx => tx.data.end.newCommitments.map(l => l.toBuffer()))], @@ -108,7 +108,7 @@ describe('sequencer/circuit_block_builder', () => { return new AppendOnlyTreeSnapshot(Fr.fromBuffer(treeInfo.root), Number(treeInfo.size)); }; - const setTxOldTreeRoots = async (tx: Tx) => { + const setTxOldTreeRoots = async (tx: PrivateTx) => { for (const [name, id] of [ ['privateDataTreeRoot', MerkleTreeId.DATA_TREE], ['contractTreeRoot', MerkleTreeId.CONTRACT_TREE], @@ -125,9 +125,9 @@ describe('sequencer/circuit_block_builder', () => { await builder.updateRootTrees(); // Assemble a fake transaction, we'll tweak some fields below - const tx = new Tx(makePrivateKernelPublicInputs(), emptyProof, makeEmptyUnverifiedData()); - const txsLeft = [tx, makeEmptyTx()]; - const txsRight = [makeEmptyTx(), makeEmptyTx()]; + const tx = Tx.createPrivate(makePrivateKernelPublicInputs(), emptyProof, makeEmptyUnverifiedData()); + const txsLeft = [tx, makeEmptyPrivateTx()]; + const txsRight = [makeEmptyPrivateTx(), makeEmptyPrivateTx()]; // Set tree roots to proper values in the tx await setTxOldTreeRoots(tx); @@ -157,7 +157,7 @@ describe('sequencer/circuit_block_builder', () => { ); // Actually build a block! - const txs = [tx, makeEmptyTx(), makeEmptyTx(), makeEmptyTx()]; + const txs = [tx, makeEmptyPrivateTx(), makeEmptyPrivateTx(), makeEmptyPrivateTx()]; const [l2Block, proof] = await builder.buildL2Block(blockNumber, txs); expect(l2Block.number).toEqual(blockNumber); @@ -188,13 +188,13 @@ describe('sequencer/circuit_block_builder', () => { describe('circuits simulator', () => { beforeEach(async () => { const simulator = await WasmCircuitSimulator.new(); - const prover = new EmptyProver(); + const prover = new EmptyRollupProver(); builder = new TestSubject(builderDb, vks, simulator, prover); await builder.updateRootTrees(); }); const makeContractDeployTx = async (seed = 0x1) => { - const tx = makeEmptyTx(); + const tx = makeEmptyPrivateTx(); await setTxOldTreeRoots(tx); tx.data.end.newContracts = [makeNewContractData(seed + 0x1000)]; return tx; @@ -213,7 +213,7 @@ describe('sequencer/circuit_block_builder', () => { const txs = [ ...(await Promise.all(times(deployCount, makeContractDeployTx))), - ...times(totalCount - deployCount, makeEmptyTx), + ...times(totalCount - deployCount, makeEmptyPrivateTx), ]; const [l2Block] = await builder.buildL2Block(blockNumber, txs); @@ -236,7 +236,7 @@ describe('sequencer/circuit_block_builder', () => { // This test specifically tests nullifier values which previously caused e2e_zk_token test to fail it('e2e edge case - regression test', async () => { const simulator = await WasmCircuitSimulator.new(); - const prover = new EmptyProver(); + const prover = new EmptyRollupProver(); builder = new TestSubject(builderDb, vks, simulator, prover); // update the starting tree const updateVals = Array(16).fill(0n); @@ -250,14 +250,14 @@ describe('sequencer/circuit_block_builder', () => { ); // new added values - const tx = makeEmptyTx(); + const tx = makeEmptyPrivateTx(); tx.data.end.newNullifiers[0] = new Fr( 10336601644835972678500657502133589897705389664587188571002640950065546264856n, ); tx.data.end.newNullifiers[1] = new Fr( 17490072961923661940560522096125238013953043065748521735636170028491723851741n, ); - const txs = [tx, makeEmptyTx(), makeEmptyTx(), makeEmptyTx()]; + const txs = [tx, makeEmptyPrivateTx(), makeEmptyPrivateTx(), makeEmptyPrivateTx()]; const [l2Block] = await builder.buildL2Block(blockNumber, txs); expect(l2Block.number).toEqual(blockNumber); @@ -267,7 +267,7 @@ describe('sequencer/circuit_block_builder', () => { // Test subject class that exposes internal functions for testing class TestSubject extends CircuitBlockBuilder { - public buildBaseRollupInput(tx1: Tx, tx2: Tx): Promise { + public buildBaseRollupInput(tx1: PrivateTx, tx2: PrivateTx): Promise { return super.buildBaseRollupInput(tx1, tx2); } diff --git a/yarn-project/sequencer-client/src/block_builder/circuit_block_builder.ts b/yarn-project/sequencer-client/src/block_builder/circuit_block_builder.ts index 14a28efae85..07cceeb9c61 100644 --- a/yarn-project/sequencer-client/src/block_builder/circuit_block_builder.ts +++ b/yarn-project/sequencer-client/src/block_builder/circuit_block_builder.ts @@ -23,14 +23,14 @@ import { import { computeContractLeaf } from '@aztec/circuits.js/abis'; import { Fr, createDebugLogger, toBigIntBE, toBufferBE } from '@aztec/foundation'; import { LeafData, SiblingPath } from '@aztec/merkle-tree'; -import { ContractData, L2Block, Tx } from '@aztec/types'; +import { ContractData, L2Block, PrivateTx, Tx, isPrivateTx } from '@aztec/types'; import { MerkleTreeId, MerkleTreeOperations } from '@aztec/world-state'; import chunk from 'lodash.chunk'; import flatMap from 'lodash.flatmap'; import times from 'lodash.times'; import { VerificationKeys } from '../mocks/verification_keys.js'; -import { Proof, Prover } from '../prover/index.js'; -import { Simulator } from '../simulator/index.js'; +import { Proof, RollupProver } from '../prover/index.js'; +import { RollupSimulator } from '../simulator/index.js'; import { BlockBuilder } from './index.js'; @@ -75,12 +75,14 @@ export class CircuitBlockBuilder implements BlockBuilder { constructor( protected db: MerkleTreeOperations, protected vks: VerificationKeys, - protected simulator: Simulator, - protected prover: Prover, + protected simulator: RollupSimulator, + protected prover: RollupProver, protected debug = createDebugLogger('aztec:sequencer'), ) {} - public async buildL2Block(blockNumber: number, txs: Tx[]): Promise<[L2Block, UInt8Vector]> { + public async buildL2Block(blockNumber: number, allTxs: Tx[]): Promise<[L2Block, UInt8Vector]> { + const txs: PrivateTx[] = allTxs.filter(isPrivateTx); + const [ startPrivateDataTreeSnapshot, startNullifierTreeSnapshot, @@ -143,7 +145,7 @@ export class CircuitBlockBuilder implements BlockBuilder { return new AppendOnlyTreeSnapshot(Fr.fromBuffer(treeInfo.root), Number(treeInfo.size)); } - protected async runCircuits(txs: Tx[]): Promise<[RootRollupPublicInputs, Proof]> { + protected async runCircuits(txs: PrivateTx[]): Promise<[RootRollupPublicInputs, Proof]> { // Check that the length of the array of txs is a power of two // See https://graphics.stanford.edu/~seander/bithacks.html#DetermineIfPowerOf2 if (txs.length < 4 || (txs.length & (txs.length - 1)) !== 0) { @@ -174,7 +176,7 @@ export class CircuitBlockBuilder implements BlockBuilder { return this.rootRollupCircuit(mergeOutputLeft, mergeOutputRight); } - protected async baseRollupCircuit(tx1: Tx, tx2: Tx): Promise<[BaseOrMergeRollupPublicInputs, Proof]> { + protected async baseRollupCircuit(tx1: PrivateTx, tx2: PrivateTx): Promise<[BaseOrMergeRollupPublicInputs, Proof]> { this.debug(`Running base rollup for ${await tx1.getTxHash()} ${await tx2.getTxHash()}`); const rollupInput = await this.buildBaseRollupInput(tx1, tx2); const rollupOutput = await this.simulator.baseRollupCircuit(rollupInput); @@ -349,7 +351,7 @@ export class CircuitBlockBuilder implements BlockBuilder { ); } - protected getKernelDataFor(tx: Tx) { + protected getKernelDataFor(tx: PrivateTx) { return new PreviousKernelData( tx.data, tx.proof, @@ -385,7 +387,7 @@ export class CircuitBlockBuilder implements BlockBuilder { ); } - protected getContractMembershipWitnessFor(tx: Tx) { + protected getContractMembershipWitnessFor(tx: PrivateTx) { return this.getMembershipWitnessFor( tx.data.constants.oldTreeRoots.contractTreeRoot, MerkleTreeId.CONTRACT_TREE_ROOTS_TREE, @@ -393,7 +395,7 @@ export class CircuitBlockBuilder implements BlockBuilder { ); } - protected getDataMembershipWitnessFor(tx: Tx) { + protected getDataMembershipWitnessFor(tx: PrivateTx) { return this.getMembershipWitnessFor( tx.data.constants.oldTreeRoots.privateDataTreeRoot, MerkleTreeId.DATA_TREE_ROOTS_TREE, @@ -678,7 +680,7 @@ export class CircuitBlockBuilder implements BlockBuilder { } // Builds the base rollup inputs, updating the contract, nullifier, and data trees in the process - protected async buildBaseRollupInput(tx1: Tx, tx2: Tx) { + protected async buildBaseRollupInput(tx1: PrivateTx, tx2: PrivateTx) { // Get trees info before any changes hit const constants = await this.getConstantBaseRollupData(); const startNullifierTreeSnapshot = await this.getTreeSnapshot(MerkleTreeId.NULLIFIER_TREE); diff --git a/yarn-project/sequencer-client/src/block_builder/standalone_block_builder.ts b/yarn-project/sequencer-client/src/block_builder/standalone_block_builder.ts index 3f293f97526..2a7d4400be0 100644 --- a/yarn-project/sequencer-client/src/block_builder/standalone_block_builder.ts +++ b/yarn-project/sequencer-client/src/block_builder/standalone_block_builder.ts @@ -1,19 +1,18 @@ -import { ContractData, L2Block } from '@aztec/types'; import { - KERNEL_NEW_CONTRACTS_LENGTH, + AppendOnlyTreeSnapshot, + CircuitsWasm, KERNEL_NEW_COMMITMENTS_LENGTH, + KERNEL_NEW_CONTRACTS_LENGTH, KERNEL_NEW_NULLIFIERS_LENGTH, - AppendOnlyTreeSnapshot, NewContractData, - CircuitsWasm, makeEmptyProof, } from '@aztec/circuits.js'; -import { MerkleTreeId, MerkleTreeOperations } from '@aztec/world-state'; -import { Tx } from '@aztec/types'; -import { AztecAddress, Fr, createDebugLogger } from '@aztec/foundation'; -import { BlockBuilder } from './index.js'; import { computeContractLeaf } from '@aztec/circuits.js/abis'; +import { AztecAddress, Fr, createDebugLogger } from '@aztec/foundation'; +import { ContractData, L2Block, PrivateTx } from '@aztec/types'; +import { MerkleTreeId, MerkleTreeOperations } from '@aztec/world-state'; import { Proof } from '../prover/index.js'; +import { BlockBuilder } from './index.js'; const mapContractData = (n: NewContractData) => { const contractData = new ContractData(AztecAddress.fromBuffer(n.contractAddress.toBuffer()), n.portalContractAddress); @@ -31,7 +30,7 @@ export class StandaloneBlockBuilder implements BlockBuilder { constructor(private db: MerkleTreeOperations, private log = createDebugLogger('aztec:block_builder')) {} - async buildL2Block(blockNumber: number, txs: Tx[]): Promise<[L2Block, Proof]> { + async buildL2Block(blockNumber: number, txs: PrivateTx[]): Promise<[L2Block, Proof]> { const startPrivateDataTreeSnapshot = await this.getTreeSnapshot(MerkleTreeId.DATA_TREE); const startNullifierTreeSnapshot = await this.getTreeSnapshot(MerkleTreeId.NULLIFIER_TREE); const startContractTreeSnapshot = await this.getTreeSnapshot(MerkleTreeId.CONTRACT_TREE); @@ -81,7 +80,7 @@ export class StandaloneBlockBuilder implements BlockBuilder { return new AppendOnlyTreeSnapshot(Fr.fromBuffer(treeInfo.root), Number(treeInfo.size)); } - private async updateTrees(tx: Tx) { + private async updateTrees(tx: PrivateTx) { const wasm = await CircuitsWasm.get(); const dataTreeLeaves = tx.data.end.newCommitments.map((x: Fr) => x.toBuffer()); const nullifierTreeLeaves = tx.data.end.newNullifiers.map((x: Fr) => x.toBuffer()); diff --git a/yarn-project/sequencer-client/src/client/sequencer-client.ts b/yarn-project/sequencer-client/src/client/sequencer-client.ts index 5f239358bea..55b3c9f4842 100644 --- a/yarn-project/sequencer-client/src/client/sequencer-client.ts +++ b/yarn-project/sequencer-client/src/client/sequencer-client.ts @@ -2,9 +2,12 @@ import { P2P } from '@aztec/p2p'; import { WorldStateSynchroniser } from '@aztec/world-state'; import { CircuitBlockBuilder } from '../block_builder/circuit_block_builder.js'; -import { getL1Publisher, getVerificationKeys, Sequencer } from '../index.js'; import { SequencerClientConfig } from '../config.js'; -import { EmptyProver } from '../prover/empty.js'; +import { getL1Publisher, getVerificationKeys, Sequencer } from '../index.js'; +import { EmptyPublicProver, EmptyRollupProver } from '../prover/empty.js'; +import { MockPublicProcessor } from '../sequencer/public_processor.js'; +import { FakePublicCircuitSimulator } from '../simulator/fake_public.js'; +import { MockPublicKernelCircuitSimulator } from '../simulator/mock_public_kernel.js'; import { WasmCircuitSimulator } from '../simulator/wasm.js'; /** @@ -19,13 +22,32 @@ export class SequencerClient { worldStateSynchroniser: WorldStateSynchroniser, ) { const publisher = getL1Publisher(config); + const merkleTreeDb = worldStateSynchroniser.getLatest(); + const blockBuilder = new CircuitBlockBuilder( - worldStateSynchroniser.getLatest(), + merkleTreeDb, getVerificationKeys(), await WasmCircuitSimulator.new(), - new EmptyProver(), + new EmptyRollupProver(), + ); + + // TODO: Swap with actual processor once the integration is good to go + const publicProcessor = new MockPublicProcessor( + merkleTreeDb, + new FakePublicCircuitSimulator(merkleTreeDb), + new MockPublicKernelCircuitSimulator(), + new EmptyPublicProver(), + ); + + const sequencer = new Sequencer( + publisher, + p2pClient, + worldStateSynchroniser, + blockBuilder, + publicProcessor, + config, ); - const sequencer = new Sequencer(publisher, p2pClient, worldStateSynchroniser, blockBuilder, config); + await sequencer.start(); return new SequencerClient(sequencer); } diff --git a/yarn-project/sequencer-client/src/mocks/tx.ts b/yarn-project/sequencer-client/src/mocks/tx.ts index 22101d3305b..fdd92ccf5a3 100644 --- a/yarn-project/sequencer-client/src/mocks/tx.ts +++ b/yarn-project/sequencer-client/src/mocks/tx.ts @@ -1,6 +1,6 @@ import { PrivateKernelPublicInputs, UInt8Vector } from '@aztec/circuits.js'; import { makePrivateKernelPublicInputs } from '@aztec/circuits.js/factories'; -import { Tx, UnverifiedData } from '@aztec/types'; +import { PrivateTx, Tx, UnverifiedData } from '@aztec/types'; function makeEmptyProof() { return new UInt8Vector(Buffer.alloc(0)); @@ -11,11 +11,10 @@ export function makeEmptyUnverifiedData(): UnverifiedData { return new UnverifiedData(chunks); } -export function makeEmptyTx(): Tx { - const isEmpty = true; - return new Tx(PrivateKernelPublicInputs.makeEmpty(), makeEmptyProof(), makeEmptyUnverifiedData(), isEmpty); +export function makeEmptyPrivateTx(): PrivateTx { + return Tx.createPrivate(PrivateKernelPublicInputs.makeEmpty(), makeEmptyProof(), makeEmptyUnverifiedData()); } -export function makeTx(seed = 0): Tx { - return new Tx(makePrivateKernelPublicInputs(seed), makeEmptyProof(), UnverifiedData.random(2), false); +export function makePrivateTx(seed = 0): PrivateTx { + return Tx.createPrivate(makePrivateKernelPublicInputs(seed), makeEmptyProof(), UnverifiedData.random(2)); } diff --git a/yarn-project/sequencer-client/src/prover/empty.ts b/yarn-project/sequencer-client/src/prover/empty.ts index 147bc712c7a..ee0cb96a2d9 100644 --- a/yarn-project/sequencer-client/src/prover/empty.ts +++ b/yarn-project/sequencer-client/src/prover/empty.ts @@ -1,33 +1,47 @@ +/* eslint-disable require-await */ import { AggregationObject, BaseOrMergeRollupPublicInputs, BaseRollupInputs, MergeRollupInputs, + PublicCircuitPublicInputs, + PublicKernelPublicInputs, RootRollupInputs, RootRollupPublicInputs, UInt8Vector, } from '@aztec/circuits.js'; -import { Prover } from './index.js'; - -/* eslint-disable */ +import { PublicProver, RollupProver } from './index.js'; const EMPTY_PROOF_SIZE = 42; -// TODO: Silently modifying one of the inputs is horrible. Rethink these interfaces. -export class EmptyProver implements Prover { - async getBaseRollupProof(input: BaseRollupInputs, publicInputs: BaseOrMergeRollupPublicInputs): Promise { +// TODO: Silently modifying one of the inputs to inject the aggregation object is horrible. +// We should rethink these interfaces. +export class EmptyRollupProver implements RollupProver { + async getBaseRollupProof( + _input: BaseRollupInputs, + publicInputs: BaseOrMergeRollupPublicInputs, + ): Promise { publicInputs.endAggregationObject = AggregationObject.makeFake(); return new UInt8Vector(Buffer.alloc(EMPTY_PROOF_SIZE, 0)); } async getMergeRollupProof( - input: MergeRollupInputs, + _input: MergeRollupInputs, publicInputs: BaseOrMergeRollupPublicInputs, ): Promise { publicInputs.endAggregationObject = AggregationObject.makeFake(); return new UInt8Vector(Buffer.alloc(EMPTY_PROOF_SIZE, 0)); } - async getRootRollupProof(input: RootRollupInputs, publicInputs: RootRollupPublicInputs): Promise { + async getRootRollupProof(_input: RootRollupInputs, publicInputs: RootRollupPublicInputs): Promise { publicInputs.endAggregationObject = AggregationObject.makeFake(); return new UInt8Vector(Buffer.alloc(EMPTY_PROOF_SIZE, 0)); } } + +export class EmptyPublicProver implements PublicProver { + async getPublicCircuitProof(_publicInputs: PublicCircuitPublicInputs): Promise { + return new UInt8Vector(Buffer.alloc(EMPTY_PROOF_SIZE, 0)); + } + async getPublicKernelCircuitProof(_publicInputs: PublicKernelPublicInputs): Promise { + return new UInt8Vector(Buffer.alloc(EMPTY_PROOF_SIZE, 0)); + } +} diff --git a/yarn-project/sequencer-client/src/prover/index.ts b/yarn-project/sequencer-client/src/prover/index.ts index 5e746d388cc..37a50dde64e 100644 --- a/yarn-project/sequencer-client/src/prover/index.ts +++ b/yarn-project/sequencer-client/src/prover/index.ts @@ -2,14 +2,21 @@ import { BaseOrMergeRollupPublicInputs, BaseRollupInputs, MergeRollupInputs, + PublicCircuitPublicInputs, + PublicKernelPublicInputs, RootRollupInputs, RootRollupPublicInputs, UInt8Vector, } from '@aztec/circuits.js'; export type Proof = UInt8Vector; -export interface Prover { +export interface RollupProver { getBaseRollupProof(input: BaseRollupInputs, publicInputs: BaseOrMergeRollupPublicInputs): Promise; getMergeRollupProof(input: MergeRollupInputs, publicInputs: BaseOrMergeRollupPublicInputs): Promise; getRootRollupProof(input: RootRollupInputs, publicInputs: RootRollupPublicInputs): Promise; } + +export interface PublicProver { + getPublicCircuitProof(publicInputs: PublicCircuitPublicInputs): Promise; + getPublicKernelCircuitProof(publicInputs: PublicKernelPublicInputs): Promise; +} diff --git a/yarn-project/sequencer-client/src/sequencer/public_processor.ts b/yarn-project/sequencer-client/src/sequencer/public_processor.ts new file mode 100644 index 00000000000..aa0c9f57f94 --- /dev/null +++ b/yarn-project/sequencer-client/src/sequencer/public_processor.ts @@ -0,0 +1,139 @@ +import { computeSlot } from '@aztec/acir-simulator'; +import { BarretenbergWasm } from '@aztec/barretenberg.js/wasm'; +import { + Fr, + MembershipWitness, + PUBLIC_CALL_STACK_LENGTH, + PUBLIC_DATA_TREE_HEIGHT, + PublicCallData, + PublicCallStackItem, + PublicCircuitPublicInputs, + PublicKernelInputsNoKernelInput, + PublicKernelPublicInputs, + StateRead, + StateTransition, + TxRequest, + WitnessedPublicCallData, +} from '@aztec/circuits.js'; +import { createDebugLogger } from '@aztec/foundation'; +import { PublicTx } from '@aztec/types'; +import { MerkleTreeId, MerkleTreeOperations } from '@aztec/world-state'; +import times from 'lodash.times'; +import { Proof, PublicProver } from '../prover/index.js'; +import { PublicCircuitSimulator, PublicKernelCircuitSimulator } from '../simulator/index.js'; + +type ProcessedPublicTx = { + tx: PublicTx; + publicKernelOutput: PublicKernelPublicInputs; +}; + +export class PublicProcessor { + constructor( + protected db: MerkleTreeOperations, + protected publicCircuit: PublicCircuitSimulator, + protected publicKernel: PublicKernelCircuitSimulator, + protected publicProver: PublicProver, + + private log = createDebugLogger('aztec:sequencer:public-processor'), + ) {} + + /** + * Run each tx through the public circuit and the public kernel circuit. + * @param txs - public txs to process + * @returns the list of processed txs with their circuit simulation outputs. + */ + public async process(txs: PublicTx[]): Promise<[ProcessedPublicTx[], PublicTx[]]> { + const result: ProcessedPublicTx[] = []; + const failed: PublicTx[] = []; + + for (const tx of txs) { + this.log(`Processing public tx ${await tx.getTxHash()}`); + try { + result.push({ tx, publicKernelOutput: await this.processTx(tx) }); + } catch (err) { + this.log(`Error processing public tx ${await tx.getTxHash()}: ${err}`); + failed.push(tx); + } + } + return [result, failed]; + } + + protected async processTx(tx: PublicTx): Promise { + const publicCircuitOutput = await this.publicCircuit.publicCircuit(tx.txRequest.txRequest); + const proof = await this.publicProver.getPublicCircuitProof(publicCircuitOutput); + const publicCallData = await this.processPublicCallData(tx.txRequest.txRequest, publicCircuitOutput, proof); + const publicKernelInput = new PublicKernelInputsNoKernelInput(tx.txRequest, publicCallData); + const publicKernelOutput = await this.publicKernel.publicKernelCircuitNoInput(publicKernelInput); + return publicKernelOutput; + } + + protected async processPublicCallData( + txRequest: TxRequest, + publicCircuitOutput: PublicCircuitPublicInputs, + publicCircuitProof: Proof, + ) { + // The first call is built from the tx request directly with an empty stack + const contractAddress = txRequest.to; + const callStackItem = new PublicCallStackItem(contractAddress, txRequest.functionData, publicCircuitOutput); + const publicCallStackPreimages: PublicCallStackItem[] = times(PUBLIC_CALL_STACK_LENGTH, PublicCallStackItem.empty); + + // TODO: Get these from the ContractDataSource once available + const portalContractAddress = Fr.random(); + const bytecodeHash = Fr.random(); + + const publicCallData = new PublicCallData( + callStackItem, + publicCallStackPreimages, + publicCircuitProof, + portalContractAddress, + bytecodeHash, + ); + + // Get public data tree root before we make any changes + const treeRoot = await this.db.getTreeInfo(MerkleTreeId.PUBLIC_DATA_TREE).then(i => Fr.fromBuffer(i.root)); + + // Alter public data tree as we go through state transitions producing hash paths + const { stateReads, stateTransitions } = publicCircuitOutput; + const { transitionsHashPaths, readsHashPaths } = await this.processStateTransitions( + contractAddress.toField(), + stateReads, + stateTransitions, + ); + + return new WitnessedPublicCallData(publicCallData, transitionsHashPaths, readsHashPaths, treeRoot); + } + + protected async processStateTransitions(contract: Fr, stateReads: StateRead[], stateTransitions: StateTransition[]) { + const transitionsHashPaths: MembershipWitness[] = []; + const readsHashPaths: MembershipWitness[] = []; + + const wasm = await BarretenbergWasm.get(); + const getLeafIndex = (slot: Fr) => computeSlot(slot, contract, wasm).value; + + // We get all reads from the unmodified tree + for (const stateRead of stateReads) { + readsHashPaths.push(await this.getMembershipWitness(getLeafIndex(stateRead.storageSlot))); + } + + // And then apply state transitions + for (const stateTransition of stateTransitions) { + const index = getLeafIndex(stateTransition.storageSlot); + transitionsHashPaths.push(await this.getMembershipWitness(index)); + // TODO: Update tree once we got the interface for it + // this.db.updateLeaf(MerkleTreeId.PUBLIC_DATA_TREE, stateTransition.newValue, index); + } + + return { readsHashPaths, transitionsHashPaths }; + } + + protected async getMembershipWitness(leafIndex: bigint) { + const path = await this.db.getSiblingPath(MerkleTreeId.PUBLIC_DATA_TREE, leafIndex); + return new MembershipWitness(PUBLIC_DATA_TREE_HEIGHT, Number(leafIndex), path.data.map(Fr.fromBuffer)); + } +} + +export class MockPublicProcessor extends PublicProcessor { + public process(_txs: PublicTx[]): Promise<[ProcessedPublicTx[], PublicTx[]]> { + return Promise.resolve([[], []]); + } +} diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index 37e7db8bffd..1ac256cc862 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -4,8 +4,9 @@ import { L2Block, UnverifiedData } from '@aztec/types'; import { MerkleTreeId, MerkleTreeOperations, WorldStateRunningState, WorldStateSynchroniser } from '@aztec/world-state'; import { MockProxy, mock } from 'jest-mock-extended'; import { BlockBuilder } from '../block_builder/index.js'; -import { L1Publisher, makeEmptyTx, makeTx } from '../index.js'; +import { L1Publisher, makeEmptyPrivateTx, makePrivateTx } from '../index.js'; import { Sequencer } from './sequencer.js'; +import { PublicProcessor } from './public_processor.js'; describe('sequencer', () => { let publisher: MockProxy; @@ -13,6 +14,7 @@ describe('sequencer', () => { let worldState: MockProxy; let blockBuilder: MockProxy; let merkleTreeOps: MockProxy; + let publicProcessor: MockProxy; let lastBlockNumber: number; @@ -34,11 +36,14 @@ describe('sequencer', () => { status: () => Promise.resolve({ state: WorldStateRunningState.IDLE, syncedToL2Block: lastBlockNumber }), }); - sequencer = new TestSubject(publisher, p2p, worldState, blockBuilder); + publicProcessor = mock(); + publicProcessor.process.mockResolvedValue([[], []]); + + sequencer = new TestSubject(publisher, p2p, worldState, blockBuilder, publicProcessor); }); it('builds a block out of a single tx', async () => { - const tx = makeTx(); + const tx = makePrivateTx(); const block = L2Block.random(lastBlockNumber + 1); const proof = makeEmptyProof(); @@ -50,7 +55,7 @@ describe('sequencer', () => { await sequencer.initialSync(); await sequencer.work(); - const expectedTxs = [tx, makeEmptyTx(), makeEmptyTx(), makeEmptyTx()]; + const expectedTxs = [tx, makeEmptyPrivateTx(), makeEmptyPrivateTx(), makeEmptyPrivateTx()]; const expectedUnverifiedData = tx.unverifiedData; expect(blockBuilder.buildL2Block).toHaveBeenCalledWith(lastBlockNumber + 1, expectedTxs); @@ -59,7 +64,7 @@ describe('sequencer', () => { }); it('builds a block out of several txs rejecting double spends', async () => { - const txs = [makeTx(0x10000), makeTx(0x20000), makeTx(0x30000)]; + const txs = [makePrivateTx(0x10000), makePrivateTx(0x20000), makePrivateTx(0x30000)]; const doubleSpendTx = txs[1]; const block = L2Block.random(lastBlockNumber + 1); const proof = makeEmptyProof(); @@ -80,7 +85,7 @@ describe('sequencer', () => { await sequencer.initialSync(); await sequencer.work(); - const expectedTxs = [txs[0], txs[2], makeEmptyTx(), makeEmptyTx()]; + const expectedTxs = [txs[0], txs[2], makeEmptyPrivateTx(), makeEmptyPrivateTx()]; const expectedUnverifiedData = new UnverifiedData([ ...txs[0].unverifiedData.dataChunks, ...txs[2].unverifiedData.dataChunks, diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 5bda26afeeb..341edeeaac8 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -1,13 +1,14 @@ import { RunningPromise, createDebugLogger } from '@aztec/foundation'; import { P2P } from '@aztec/p2p'; -import { Tx, UnverifiedData } from '@aztec/types'; +import { PrivateTx, PublicTx, Tx, UnverifiedData, isPrivateTx, isPublicTx } from '@aztec/types'; import { MerkleTreeId, WorldStateStatus, WorldStateSynchroniser } from '@aztec/world-state'; import times from 'lodash.times'; import { BlockBuilder } from '../block_builder/index.js'; -import { makeEmptyTx } from '../index.js'; +import { makeEmptyPrivateTx } from '../index.js'; import { L1Publisher } from '../publisher/l1-publisher.js'; import { ceilPowerOfTwo } from '../utils.js'; import { SequencerConfig } from './config.js'; +import { PublicProcessor } from './public_processor.js'; /** * Sequencer client @@ -30,6 +31,7 @@ export class Sequencer { private p2pClient: P2P, private worldState: WorldStateSynchroniser, private blockBuilder: BlockBuilder, + private publicProcessor: PublicProcessor, config?: SequencerConfig, private log = createDebugLogger('aztec:sequencer'), ) { @@ -83,49 +85,35 @@ export class Sequencer { } // Do not go forward with new block if the previous one has not been mined and processed - if (!prevBlockSynced) { - return; - } + if (!prevBlockSynced) return; this.state = SequencerState.WAITING_FOR_TXS; - // Get a single tx (for now) to build the new block - // P2P client is responsible for ensuring this tx is eligible (proof ok, not mined yet, etc) - const pendingTxs = await this.p2pClient.getTxs(); //.then(txs => txs.slice(0, this.maxTxsPerBlock)); + // Get txs to build the new block + const pendingTxs = await this.p2pClient.getTxs(); if (pendingTxs.length === 0) return; this.log(`Processing ${pendingTxs.length} txs from P2P pool`); - const validTxs = []; - const doubleSpendTxs = []; - - // Process txs until we get to maxTxsPerBlock, rejecting double spends in the process - for (const tx of pendingTxs) { - // TODO(AD) - eventually we should add a limit to how many transactions we - // skip in this manner and do something more DDOS-proof (like letting the transaction fail and pay a fee). - if (await this.isTxDoubleSpend(tx)) { - doubleSpendTxs.push(tx); - } else { - validTxs.push(tx); - } - if (validTxs.length >= this.maxTxsPerBlock) { - break; - } - } - - // Make sure we remove these from the tx pool so we do not consider it again - if (doubleSpendTxs.length > 0) { - const doubleSpendTxsHashes = await Promise.all(doubleSpendTxs.map(t => t.getTxHash())); - this.log(`Deleting double spend txs ${doubleSpendTxsHashes.join(', ')}`); - await this.p2pClient.deleteTxs(doubleSpendTxsHashes); + // Filter out invalid txs + const validTxs = await this.takeValidTxs(pendingTxs); + if (validTxs.length === 0) { + this.log(`No valid txs left after processing`); + return; } - if (validTxs.length === 0) return; - - const validTxHashes = await Promise.all(validTxs.map(tx => tx.getTxHash())); - this.log(`Assembling block with txs ${validTxHashes.join(', ')}`); + this.log(`Assembling block with txs ${(await Tx.getHashes(validTxs)).join(', ')}`); this.state = SequencerState.CREATING_BLOCK; + // Process public txs and drop the ones that fail processing + const publicTxs = validTxs.filter(isPublicTx); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [processedTxs, failedTxs] = await this.publicProcessor.process(publicTxs); + if (failedTxs.length > 0) { + await this.p2pClient.deleteTxs(await Tx.getHashes(failedTxs)); + } + // Build the new block by running the rollup circuits + // TODO: Get the public tx combined outputs in here! const block = await this.buildBlock(validTxs); this.log(`Assembled block ${block.number}`); @@ -139,12 +127,10 @@ export class Sequencer { this.log(`Failed to publish block`); } - // Publishes new unverified data to the network and awaits the tx to be mined + // Publishes new unverified data for private txs to the network and awaits the tx to be mined this.state = SequencerState.PUBLISHING_UNVERIFIED_DATA; - const publishedUnverifiedData = await this.publisher.processUnverifiedData( - block.number, - UnverifiedData.join(validTxs.map(tx => tx.unverifiedData)), - ); + const unverifiedData = UnverifiedData.join(validTxs.filter(isPrivateTx).map(tx => tx.unverifiedData)); + const publishedUnverifiedData = await this.publisher.processUnverifiedData(block.number, unverifiedData); if (publishedUnverifiedData) { this.log(`Successfully published unverifiedData for block ${block.number}`); } else { @@ -156,6 +142,42 @@ export class Sequencer { } } + // TODO: It should be responsibility of the P2P layer to validate txs before passing them on here + protected async takeValidTxs(txs: Tx[]) { + const validTxs = []; + const doubleSpendTxs = []; + const invalidSigTxs = []; + + // Process txs until we get to maxTxsPerBlock, rejecting double spends in the process + for (const tx of txs) { + // TODO(AD) - eventually we should add a limit to how many transactions we + // skip in this manner and do something more DDOS-proof (like letting the transaction fail and pay a fee). + if (tx.isPrivate() && (await this.isTxDoubleSpend(tx))) { + this.log(`Deleting double spend tx ${await tx.getTxHash()}`); + doubleSpendTxs.push(tx); + continue; + } + + if (tx.isPublic() && !(await this.isValidSignature(tx))) { + this.log(`Deleting invalid signature tx ${await tx.getTxHash()}`); + invalidSigTxs.push(tx); + continue; + } + + validTxs.push(tx); + if (validTxs.length >= this.maxTxsPerBlock) { + break; + } + } + + // Make sure we remove these from the tx pool so we do not consider it again + if (doubleSpendTxs.length > 0 || invalidSigTxs.length > 0) { + await this.p2pClient.deleteTxs(await Tx.getHashes([...doubleSpendTxs, ...invalidSigTxs])); + } + + return validTxs; + } + /** * Returns whether the previous block sent has been mined, and all dependencies have caught up with it. * @returns Boolean indicating if our dependencies are synced to the latest block. @@ -170,7 +192,7 @@ export class Sequencer { protected async buildBlock(txs: Tx[]) { // Pad the txs array with empty txs to be a power of two, at least 4 const txsTargetSize = Math.max(ceilPowerOfTwo(txs.length), 4); - const allTxs = [...txs, ...times(txsTargetSize - txs.length, makeEmptyTx)]; + const allTxs = [...txs, ...times(txsTargetSize - txs.length, makeEmptyPrivateTx)]; const [block] = await this.blockBuilder.buildL2Block(this.lastBlockNumber + 1, allTxs); return block; @@ -182,7 +204,7 @@ export class Sequencer { * @param tx - The transaction. * @returns Whether this is a problematic double spend that the L1 contract would reject. */ - protected async isTxDoubleSpend(tx: Tx): Promise { + protected async isTxDoubleSpend(tx: PrivateTx): Promise { // eslint-disable-next-line @typescript-eslint/await-thenable for (const nullifier of tx.data.end.newNullifiers) { // Skip nullifier if it's empty @@ -198,6 +220,11 @@ export class Sequencer { } return false; } + + protected isValidSignature(_tx: PublicTx): Promise { + // TODO: Validate tx ECDSA signature! + return Promise.resolve(true); + } } export enum SequencerState { diff --git a/yarn-project/sequencer-client/src/simulator/fake_public.ts b/yarn-project/sequencer-client/src/simulator/fake_public.ts new file mode 100644 index 00000000000..18b1a237007 --- /dev/null +++ b/yarn-project/sequencer-client/src/simulator/fake_public.ts @@ -0,0 +1,43 @@ +import { PublicDB, PublicExecution, computeSlot } from '@aztec/acir-simulator'; +import { BarretenbergWasm } from '@aztec/barretenberg.js/wasm'; +import { AztecAddress, EthAddress, Fr, PublicCircuitPublicInputs, TxRequest } from '@aztec/circuits.js'; +import { FunctionAbi } from '@aztec/noir-contracts'; +import { MerkleTreeId, MerkleTreeOperations } from '@aztec/world-state'; +import { PublicCircuitSimulator } from './index.js'; + +export class FakePublicCircuitSimulator implements PublicCircuitSimulator { + public readonly db: WorldStatePublicDB; + + constructor(merkleTree: MerkleTreeOperations) { + this.db = new WorldStatePublicDB(merkleTree); + } + + public async publicCircuit(tx: TxRequest): Promise { + const functionAbi: FunctionAbi = undefined as any; + const portalAddress: EthAddress = undefined as any; + + const execution = PublicExecution.fromTransactionRequest(this.db, tx, functionAbi, portalAddress); + const result = await execution.run(); + return PublicCircuitPublicInputs.from({ + args: tx.args, + callContext: execution.callContext, + emittedEvents: [], + l1MsgStack: [], + proverAddress: AztecAddress.random(), + publicCallStack: [], + returnValues: result.returnValues, + stateReads: result.stateReads, + stateTransitions: result.stateTransitions, + }); + } +} + +class WorldStatePublicDB implements PublicDB { + constructor(private db: MerkleTreeOperations) {} + + public async storageRead(contract: AztecAddress, slot: Fr): Promise { + const index = computeSlot(slot, contract.toField(), await BarretenbergWasm.get()).value; + const value = await this.db.getLeafValue(MerkleTreeId.PUBLIC_DATA_TREE, index); + return value ? Fr.fromBuffer(value) : Fr.ZERO; + } +} diff --git a/yarn-project/sequencer-client/src/simulator/index.ts b/yarn-project/sequencer-client/src/simulator/index.ts index 51232c40532..c81c0d2816b 100644 --- a/yarn-project/sequencer-client/src/simulator/index.ts +++ b/yarn-project/sequencer-client/src/simulator/index.ts @@ -2,12 +2,28 @@ import { BaseOrMergeRollupPublicInputs, BaseRollupInputs, MergeRollupInputs, + PublicCircuitPublicInputs, + PublicKernelInputsNoKernelInput, + PublicKernelInputsNonFirstIteration, + PublicKernelInputsPrivateKernelInput, + PublicKernelPublicInputs, RootRollupInputs, RootRollupPublicInputs, + TxRequest, } from '@aztec/circuits.js'; -export interface Simulator { +export interface RollupSimulator { baseRollupCircuit(input: BaseRollupInputs): Promise; mergeRollupCircuit(input: MergeRollupInputs): Promise; rootRollupCircuit(input: RootRollupInputs): Promise; } + +export interface PublicCircuitSimulator { + publicCircuit(tx: TxRequest): Promise; +} + +export interface PublicKernelCircuitSimulator { + publicKernelCircuitNoInput(inputs: PublicKernelInputsNoKernelInput): Promise; + publicKernelCircuitPrivateInput(inputs: PublicKernelInputsPrivateKernelInput): Promise; + publicKernelCircuitNonFirstIteration(inputs: PublicKernelInputsNonFirstIteration): Promise; +} diff --git a/yarn-project/sequencer-client/src/simulator/mock.ts b/yarn-project/sequencer-client/src/simulator/mock.ts deleted file mode 100644 index 60090a55282..00000000000 --- a/yarn-project/sequencer-client/src/simulator/mock.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { - BaseOrMergeRollupPublicInputs, - BaseRollupInputs, - MergeRollupInputs, - RootRollupInputs, - RootRollupPublicInputs, -} from '@aztec/circuits.js'; -import { Simulator } from './index.js'; - -/* eslint-disable */ - -export class MockSimulator implements Simulator { - baseRollupCircuit(input: BaseRollupInputs): Promise { - throw new Error('Method not implemented.'); - } - mergeRollupCircuit(input: MergeRollupInputs): Promise { - throw new Error('Method not implemented.'); - } - rootRollupCircuit(input: RootRollupInputs): Promise { - throw new Error('Method not implemented.'); - } -} diff --git a/yarn-project/sequencer-client/src/simulator/mock_public_kernel.ts b/yarn-project/sequencer-client/src/simulator/mock_public_kernel.ts new file mode 100644 index 00000000000..57e6c4e1853 --- /dev/null +++ b/yarn-project/sequencer-client/src/simulator/mock_public_kernel.ts @@ -0,0 +1,21 @@ +import { + PublicKernelInputsNoKernelInput, + PublicKernelPublicInputs, + PublicKernelInputsPrivateKernelInput, + PublicKernelInputsNonFirstIteration, +} from '@aztec/circuits.js'; +import { PublicKernelCircuitSimulator } from './index.js'; + +export class MockPublicKernelCircuitSimulator implements PublicKernelCircuitSimulator { + publicKernelCircuitNoInput(_inputs: PublicKernelInputsNoKernelInput): Promise { + return Promise.resolve(PublicKernelPublicInputs.empty()); + } + publicKernelCircuitPrivateInput(_inputs: PublicKernelInputsPrivateKernelInput): Promise { + return Promise.resolve(PublicKernelPublicInputs.empty()); + } + publicKernelCircuitNonFirstIteration( + _inputs: PublicKernelInputsNonFirstIteration, + ): Promise { + return Promise.resolve(PublicKernelPublicInputs.empty()); + } +} diff --git a/yarn-project/sequencer-client/src/simulator/wasm.ts b/yarn-project/sequencer-client/src/simulator/wasm.ts index 2d083f8ca2e..995d990d7fd 100644 --- a/yarn-project/sequencer-client/src/simulator/wasm.ts +++ b/yarn-project/sequencer-client/src/simulator/wasm.ts @@ -7,9 +7,9 @@ import { RootRollupInputs, RootRollupPublicInputs, } from '@aztec/circuits.js'; -import { Simulator } from './index.js'; +import { RollupSimulator } from './index.js'; -export class WasmCircuitSimulator implements Simulator { +export class WasmCircuitSimulator implements RollupSimulator { private rollupWasmWrapper: RollupWasmWrapper; constructor(wasm: CircuitsWasm) { diff --git a/yarn-project/sequencer-client/tsconfig.json b/yarn-project/sequencer-client/tsconfig.json index bca949504f6..b9e2c05615a 100644 --- a/yarn-project/sequencer-client/tsconfig.json +++ b/yarn-project/sequencer-client/tsconfig.json @@ -6,6 +6,9 @@ "tsBuildInfoFile": ".tsbuildinfo" }, "references": [ + { + "path": "../acir-simulator" + }, { "path": "../circuits.js" }, diff --git a/yarn-project/types/src/tx.ts b/yarn-project/types/src/tx.ts index 91edeabd4d9..bf6e977fd93 100644 --- a/yarn-project/types/src/tx.ts +++ b/yarn-project/types/src/tx.ts @@ -1,8 +1,20 @@ -import { CircuitsWasm, PrivateKernelPublicInputs, UInt8Vector } from '@aztec/circuits.js'; +import { CircuitsWasm, PrivateKernelPublicInputs, SignedTxRequest, UInt8Vector } from '@aztec/circuits.js'; import { computeContractLeaf } from '@aztec/circuits.js/abis'; import { createTxHash } from './create_tx_hash.js'; import { TxHash } from './tx_hash.js'; import { UnverifiedData } from './unverified_data.js'; +import { keccak } from '@aztec/foundation'; + +export type PrivateTx = Required> & Tx; +export type PublicTx = Required> & Tx; + +export function isPublicTx(tx: Tx): tx is PublicTx { + return tx.isPublic(); +} + +export function isPrivateTx(tx: Tx): tx is PrivateTx { + return tx.isPrivate(); +} /** * The interface of an L2 transaction. @@ -10,18 +22,60 @@ import { UnverifiedData } from './unverified_data.js'; export class Tx { private hashPromise?: Promise; + public static createPrivate( + data: PrivateKernelPublicInputs, + proof: UInt8Vector, + unverifiedData: UnverifiedData, + ): PrivateTx { + return new this(data, proof, unverifiedData, undefined) as PrivateTx; + } + + public static createPublic(txRequest: SignedTxRequest): PublicTx { + return new this(undefined, undefined, undefined, txRequest) as PublicTx; + } + + public static createPrivatePublic( + data: PrivateKernelPublicInputs, + proof: UInt8Vector, + unverifiedData: UnverifiedData, + txRequest: SignedTxRequest, + ): PrivateTx & PublicTx { + return new this(data, proof, unverifiedData, txRequest) as PrivateTx & PublicTx; + } + + public static create( + data?: PrivateKernelPublicInputs, + proof?: UInt8Vector, + unverifiedData?: UnverifiedData, + txRequest?: SignedTxRequest, + ): Tx { + const tx = new this(data, proof, unverifiedData, txRequest); + if (!tx.isPrivate() && !tx.isPublic()) { + throw new Error(`Tx needs either public or private data`); + } + return tx; + } + + public isPrivate(): this is PrivateTx { + return !!this.data && !!this.proof && !!this.unverifiedData; + } + + public isPublic(): this is PublicTx { + return !!this.txRequest; + } + /** - * - * @param data - Tx inputs. - * @param proof - Tx proof. + * Creates a new instance. + * @param data - Output of the private kernel circuit for this tx. + * @param proof - Proof from the private kernel circuit. * @param unverifiedData - Information not needed to verify the tx (e.g. encrypted note pre-images etc.) - * @param isEmpty - Whether this is a placeholder empty tx. + * @param txRequest - Signed public function call data. */ - constructor( - public readonly data: PrivateKernelPublicInputs, - public readonly proof: UInt8Vector, - public readonly unverifiedData: UnverifiedData, - public readonly isEmpty = false, + protected constructor( + public readonly data?: PrivateKernelPublicInputs, + public readonly proof?: UInt8Vector, + public readonly unverifiedData?: UnverifiedData, + public readonly txRequest?: SignedTxRequest, ) {} /** @@ -35,20 +89,51 @@ export class Tx { return this.hashPromise; } + /** + * Convenience function to get array of hashes for an array of txs. + * @param txs - the txs to get the hashes from + * @returns The corresponding array of hashes + */ + static async getHashes(txs: Tx[]): Promise { + return await Promise.all(txs.map(tx => tx.getTxHash())); + } + /** * Utility function to generate tx hash. * @param tx - The transaction from which to generate the hash. * @returns A hash of the tx data that identifies the tx. */ static async createTxHash(tx: Tx): Promise { + // TODO: Until we define how txs will be represented on the L2 block, we won't know how to + // recreate their hash from the L2 block info. So for now we take the easy way out. If the + // tx has only private data, we keep the same hash as before. If it has public data, + // we hash it and return it. And if it has both, we compute both hashes + // and hash them together. We'll probably want to change this later! + // See https://github.com/AztecProtocol/aztec3-packages/issues/271 + const hashes = []; + // NOTE: We are using computeContractLeaf here to ensure consistency with how circuits compute // contract tree leaves, which then go into the L2 block, which are then used to regenerate // the tx hashes. This means we need the full circuits wasm, and cannot use the lighter primitives - // wasm. Alternatively, we could stop using computeContractLeaf and manually use the same - const wasm = await CircuitsWasm.get(); - return createTxHash({ - ...tx.data.end, - newContracts: tx.data.end.newContracts.map(cd => computeContractLeaf(wasm, cd)), - }); + // wasm. Alternatively, we could stop using computeContractLeaf and manually use the same hash. + if (tx.isPrivate()) { + const wasm = await CircuitsWasm.get(); + hashes.push( + createTxHash({ + ...tx.data.end, + newContracts: tx.data.end.newContracts.map(cd => computeContractLeaf(wasm, cd)), + }), + ); + } + + // We hash the full signed tx request object (this is, the tx request along with the signature), + // just like Ethereum does. + if (tx.isPublic()) { + hashes.push(new TxHash(keccak(tx.txRequest.toBuffer()))); + } + + // Return a tx hash if we have only one, or hash them again if we have both + if (hashes.length === 1) return hashes[0]; + else return new TxHash(keccak(Buffer.concat(hashes.map(h => h.buffer)))); } } diff --git a/yarn-project/world-state/src/world-state-db/index.ts b/yarn-project/world-state/src/world-state-db/index.ts index fc5aa0a83b4..7b470913ad2 100644 --- a/yarn-project/world-state/src/world-state-db/index.ts +++ b/yarn-project/world-state/src/world-state-db/index.ts @@ -12,6 +12,7 @@ export enum MerkleTreeId { NULLIFIER_TREE = 2, DATA_TREE = 3, DATA_TREE_ROOTS_TREE = 4, + PUBLIC_DATA_TREE = 5, } export type IndexedMerkleTreeId = MerkleTreeId.NULLIFIER_TREE; diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 39c4175a046..21fdc2a5676 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -508,6 +508,7 @@ __metadata: version: 0.0.0-use.local resolution: "@aztec/sequencer-client@workspace:sequencer-client" dependencies: + "@aztec/acir-simulator": "workspace:^" "@aztec/circuits.js": "workspace:^" "@aztec/ethereum.js": "workspace:^" "@aztec/foundation": "workspace:^"