From 19c2a97784c917da212e76f3307d47e1beb8099f Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 6 Jun 2024 10:46:48 +0100 Subject: [PATCH] feat: Poor man's fernet (#6918) Allows sequencers to register themselves on the rollup contract, and they take turns to submit blocks. If there are no sequeners registered, it's a free-for-all. --- l1-contracts/src/core/Rollup.sol | 32 +++++++ l1-contracts/src/core/libraries/Errors.sol | 1 + yarn-project/cli/src/cmds/sequencers.ts | 95 +++++++++++++++++++ yarn-project/cli/src/index.ts | 33 +++++++ .../src/publisher/l1-publisher.ts | 12 ++- .../src/publisher/viem-tx-sender.ts | 10 ++ .../src/sequencer/sequencer.test.ts | 40 ++++++++ .../src/sequencer/sequencer.ts | 21 ++-- 8 files changed, 237 insertions(+), 7 deletions(-) create mode 100644 yarn-project/cli/src/cmds/sequencers.ts diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index eb2a9d71ab3..752306fbddf 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -16,6 +16,7 @@ import {HeaderLib} from "./libraries/HeaderLib.sol"; import {Hash} from "./libraries/Hash.sol"; import {Errors} from "./libraries/Errors.sol"; import {Constants} from "./libraries/ConstantsGen.sol"; +import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; // Contracts import {MockVerifier} from "../mock/MockVerifier.sol"; @@ -43,6 +44,10 @@ contract Rollup is IRollup { // See https://github.com/AztecProtocol/aztec-packages/issues/1614 uint256 public lastWarpedBlockTs; + using EnumerableSet for EnumerableSet.AddressSet; + + EnumerableSet.AddressSet private sequencers; + constructor(IRegistry _registry, IAvailabilityOracle _availabilityOracle, IERC20 _gasToken) { verifier = new MockVerifier(); REGISTRY = _registry; @@ -53,6 +58,27 @@ contract Rollup is IRollup { VERSION = 1; } + // HACK: Add a sequencer to set of potential sequencers + function addSequencer(address sequencer) external { + sequencers.add(sequencer); + } + + // HACK: Remove a sequencer from the set of potential sequencers + function removeSequencer(address sequencer) external { + sequencers.remove(sequencer); + } + + // HACK: Return whose turn it is to submit a block + function whoseTurnIsIt(uint256 blockNumber) public view returns (address) { + return + sequencers.length() == 0 ? address(0x0) : sequencers.at(blockNumber % sequencers.length()); + } + + // HACK: Return all the registered sequencers + function getSequencers() external view returns (address[] memory) { + return sequencers.values(); + } + function setVerifier(address _verifier) external override(IRollup) { // TODO remove, only needed for testing verifier = IVerifier(_verifier); @@ -79,6 +105,12 @@ contract Rollup is IRollup { revert Errors.Rollup__UnavailableTxs(header.contentCommitment.txsEffectsHash); } + // Check that this is the current sequencer's turn + address sequencer = whoseTurnIsIt(header.globalVariables.blockNumber); + if (sequencer != address(0x0) && sequencer != msg.sender) { + revert Errors.Rollup__InvalidSequencer(msg.sender); + } + bytes32[] memory publicInputs = new bytes32[](2 + Constants.HEADER_LENGTH + Constants.AGGREGATION_OBJECT_LENGTH); // the archive tree root diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index 3168976f28f..7481445954d 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -46,6 +46,7 @@ library Errors { error Rollup__TimestampInFuture(); // 0xbc1ce916 error Rollup__TimestampTooOld(); // 0x72ed9c81 error Rollup__UnavailableTxs(bytes32 txsHash); // 0x414906c3 + error Rollup__InvalidSequencer(address sequencer); // Registry error Registry__RollupNotRegistered(address rollup); // 0xa1fee4cf diff --git a/yarn-project/cli/src/cmds/sequencers.ts b/yarn-project/cli/src/cmds/sequencers.ts new file mode 100644 index 00000000000..cc53c083547 --- /dev/null +++ b/yarn-project/cli/src/cmds/sequencers.ts @@ -0,0 +1,95 @@ +import { createEthereumChain } from '@aztec/ethereum'; +import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; +import { RollupAbi } from '@aztec/l1-artifacts'; + +import { createPublicClient, createWalletClient, getContract, http } from 'viem'; +import { mnemonicToAccount } from 'viem/accounts'; + +import { createCompatibleClient } from '../client.js'; + +export async function sequencers(opts: { + command: 'list' | 'add' | 'remove' | 'who-next'; + who?: string; + mnemonic?: string; + rpcUrl: string; + l1RpcUrl: string; + apiKey: string; + blockNumber?: number; + log: LogFn; + debugLogger: DebugLogger; +}) { + const { + blockNumber: maybeBlockNumber, + command, + who: maybeWho, + mnemonic, + rpcUrl, + l1RpcUrl, + apiKey, + log, + debugLogger, + } = opts; + const client = await createCompatibleClient(rpcUrl, debugLogger); + const { l1ContractAddresses } = await client.getNodeInfo(); + + const chain = createEthereumChain(l1RpcUrl, apiKey); + const publicClient = createPublicClient({ chain: chain.chainInfo, transport: http(chain.rpcUrl) }); + + const walletClient = mnemonic + ? createWalletClient({ + account: mnemonicToAccount(mnemonic), + chain: chain.chainInfo, + transport: http(chain.rpcUrl), + }) + : undefined; + + const rollup = getContract({ + address: l1ContractAddresses.rollupAddress.toString(), + abi: RollupAbi, + client: publicClient, + }); + + const writeableRollup = walletClient + ? getContract({ + address: l1ContractAddresses.rollupAddress.toString(), + abi: RollupAbi, + client: walletClient, + }) + : undefined; + + const who = (maybeWho as `0x{string}`) ?? walletClient?.account.address.toString(); + + if (command === 'list') { + const sequencers = await rollup.read.getSequencers(); + if (sequencers.length === 0) { + log(`No sequencers registered on rollup`); + } else { + log(`Registered sequencers on rollup:`); + for (const sequencer of sequencers) { + log(' ' + sequencer.toString()); + } + } + } else if (command === 'add') { + if (!who || !writeableRollup) { + throw new Error(`Missing sequencer address`); + } + log(`Adding ${who} as sequencer`); + const hash = await writeableRollup.write.addSequencer([who]); + await publicClient.waitForTransactionReceipt({ hash }); + log(`Added in tx ${hash}`); + } else if (command === 'remove') { + if (!who || !writeableRollup) { + throw new Error(`Missing sequencer address`); + } + log(`Removing ${who} as sequencer`); + const hash = await writeableRollup.write.removeSequencer([who]); + await publicClient.waitForTransactionReceipt({ hash }); + log(`Removed in tx ${hash}`); + } else if (command === 'who-next') { + const blockNumber = maybeBlockNumber ?? (await client.getBlockNumber()) + 1; + const next = await rollup.read.whoseTurnIsIt([BigInt(blockNumber)]); + log(`Next sequencer expected to build ${blockNumber} is ${next}`); + } else { + throw new Error(`Unknown command ${command}`); + } +} diff --git a/yarn-project/cli/src/index.ts b/yarn-project/cli/src/index.ts index b56d129def9..05f34d22db9 100644 --- a/yarn-project/cli/src/index.ts +++ b/yarn-project/cli/src/index.ts @@ -641,5 +641,38 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { computeSelector(functionSignature, log); }); + program + .command('sequencers') + .argument('', 'Command to run: list, add, remove, who-next') + .argument('[who]', 'Who to add/remove') + .description('Manages or queries registered sequencers on the L1 rollup contract.') + .requiredOption( + '--l1-rpc-url ', + 'Url of the ethereum host. Chain identifiers localhost and testnet can be used', + ETHEREUM_HOST, + ) + .option('-a, --api-key ', 'Api key for the ethereum host', API_KEY) + .option( + '-m, --mnemonic ', + 'The mnemonic for the sender of the tx', + 'test test test test test test test test test test test junk', + ) + .option('--block-number ', 'Block number to query next sequencer for', parseOptionalInteger) + .addOption(pxeOption) + .action(async (command, who, options) => { + const { sequencers } = await import('./cmds/sequencers.js'); + await sequencers({ + command: command, + who, + mnemonic: options.mnemonic, + rpcUrl: options.rpcUrl, + l1RpcUrl: options.l1RpcUrl, + apiKey: options.apiKey ?? '', + blockNumber: options.blockNumber, + log, + debugLogger, + }); + }); + return program; } diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index 5be120677d2..9cc82d3b6e2 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -1,6 +1,6 @@ import { type L2Block } from '@aztec/circuit-types'; import { type L1PublishStats } from '@aztec/circuit-types/stats'; -import { type Fr, type Proof } from '@aztec/circuits.js'; +import { type EthAddress, type Fr, type Proof } from '@aztec/circuits.js'; import { createDebugLogger } from '@aztec/foundation/log'; import { serializeToBuffer } from '@aztec/foundation/serialize'; import { InterruptibleSleep } from '@aztec/foundation/sleep'; @@ -42,6 +42,10 @@ export type MinimalTransactionReceipt = { * Pushes txs to the L1 chain and waits for their completion. */ export interface L1PublisherTxSender { + getSenderAddress(): Promise; + + getSubmitterAddressForBlock(blockNumber: number): Promise; + /** * Publishes tx effects to Availability Oracle. * @param encodedBody - Encoded block body. @@ -117,6 +121,12 @@ export class L1Publisher implements L2BlockReceiver { this.sleepTimeMs = config?.l1BlockPublishRetryIntervalMS ?? 60_000; } + public async isItMyTurnToSubmit(blockNumber: number): Promise { + const submitter = await this.txSender.getSubmitterAddressForBlock(blockNumber); + const sender = await this.txSender.getSenderAddress(); + return submitter.isZero() || submitter.equals(sender); + } + /** * Publishes L2 block on L1. * @param block - L2 block to publish. diff --git a/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts b/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts index a33466a73e1..96bfabad35b 100644 --- a/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts +++ b/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts @@ -1,4 +1,5 @@ import { type L2Block } from '@aztec/circuit-types'; +import { EthAddress } from '@aztec/circuits.js'; import { createEthereumChain } from '@aztec/ethereum'; import { createDebugLogger } from '@aztec/foundation/log'; import { AvailabilityOracleAbi, RollupAbi } from '@aztec/l1-artifacts'; @@ -71,6 +72,15 @@ export class ViemTxSender implements L1PublisherTxSender { }); } + getSenderAddress(): Promise { + return Promise.resolve(EthAddress.fromString(this.account.address)); + } + + async getSubmitterAddressForBlock(blockNumber: number): Promise { + const submitter = await this.rollupContract.read.whoseTurnIsIt([BigInt(blockNumber)]); + return EthAddress.fromString(submitter); + } + async getCurrentArchive(): Promise { const archive = await this.rollupContract.read.archive(); return Buffer.from(archive.replace('0x', ''), 'hex'); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index 37c8a7aaa16..fbcec5e886c 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -63,6 +63,8 @@ describe('sequencer', () => { lastBlockNumber = 0; publisher = mock(); + publisher.isItMyTurnToSubmit.mockResolvedValue(true); + globalVariableBuilder = mock(); merkleTreeOps = mock(); proverClient = mock(); @@ -148,6 +150,44 @@ describe('sequencer', () => { expect(proverClient.cancelBlock).toHaveBeenCalledTimes(0); }); + it('builds a block when it is their turn', async () => { + const tx = mockTxForRollup(); + tx.data.constants.txContext.chainId = chainId; + const block = L2Block.random(lastBlockNumber + 1); + const proof = makeEmptyProof(); + const result: ProvingSuccess = { + status: PROVING_STATUS.SUCCESS, + }; + const ticket: ProvingTicket = { + provingPromise: Promise.resolve(result), + }; + + p2p.getTxs.mockResolvedValueOnce([tx]); + proverClient.startNewBlock.mockResolvedValueOnce(ticket); + proverClient.finaliseBlock.mockResolvedValue({ block, aggregationObject: [], proof }); + publisher.processL2Block.mockResolvedValueOnce(true); + globalVariableBuilder.buildGlobalVariables.mockResolvedValueOnce( + new GlobalVariables(chainId, version, new Fr(lastBlockNumber + 1), Fr.ZERO, coinbase, feeRecipient, gasFees), + ); + + // Not your turn! + publisher.isItMyTurnToSubmit.mockClear().mockResolvedValue(false); + await sequencer.initialSync(); + await sequencer.work(); + expect(proverClient.startNewBlock).not.toHaveBeenCalled(); + + // Now it is! + publisher.isItMyTurnToSubmit.mockClear().mockResolvedValue(true); + await sequencer.work(); + expect(proverClient.startNewBlock).toHaveBeenCalledWith( + 2, + new GlobalVariables(chainId, version, new Fr(lastBlockNumber + 1), Fr.ZERO, coinbase, feeRecipient, gasFees), + Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(new Fr(0n)), + ); + expect(publisher.processL2Block).toHaveBeenCalledWith(block, [], proof); + expect(proverClient.cancelBlock).toHaveBeenCalledTimes(0); + }); + it('builds a block out of several txs rejecting double spends', async () => { const txs = [mockTxForRollup(0x10000), mockTxForRollup(0x20000), mockTxForRollup(0x30000)]; txs.forEach(tx => { diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 593e6c85f0c..78de0e16bc5 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -162,6 +162,18 @@ export class Sequencer { return; } + const historicalHeader = (await this.l2BlockSource.getBlock(-1))?.header; + const newBlockNumber = + (historicalHeader === undefined + ? await this.l2BlockSource.getBlockNumber() + : Number(historicalHeader.globalVariables.blockNumber.toBigInt())) + 1; + + // Do not go forward with new block if not my turn + if (!(await this.publisher.isItMyTurnToSubmit(newBlockNumber))) { + this.log.verbose('Not my turn to submit block'); + return; + } + const workTimer = new Timer(); this.state = SequencerState.WAITING_FOR_TXS; @@ -172,12 +184,6 @@ export class Sequencer { } this.log.debug(`Retrieved ${pendingTxs.length} txs from P2P pool`); - const historicalHeader = (await this.l2BlockSource.getBlock(-1))?.header; - const newBlockNumber = - (historicalHeader === undefined - ? await this.l2BlockSource.getBlockNumber() - : Number(historicalHeader.globalVariables.blockNumber.toBigInt())) + 1; - /** * We'll call this function before running expensive operations to avoid wasted work. */ @@ -186,6 +192,9 @@ export class Sequencer { if (currentBlockNumber + 1 !== newBlockNumber) { throw new Error('New block was emitted while building block'); } + if (!(await this.publisher.isItMyTurnToSubmit(newBlockNumber))) { + throw new Error(`Not this sequencer turn to submit block`); + } }; const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(