Skip to content

Commit

Permalink
feat: Poor man's fernet (#6918)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
spalladino committed Jun 6, 2024
1 parent 14e0c1d commit 19c2a97
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 7 deletions.
32 changes: 32 additions & 0 deletions l1-contracts/src/core/Rollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions l1-contracts/src/core/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
95 changes: 95 additions & 0 deletions yarn-project/cli/src/cmds/sequencers.ts
Original file line number Diff line number Diff line change
@@ -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}`);
}
}
33 changes: 33 additions & 0 deletions yarn-project/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,5 +641,38 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
computeSelector(functionSignature, log);
});

program
.command('sequencers')
.argument('<command>', '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 <string>',
'Url of the ethereum host. Chain identifiers localhost and testnet can be used',
ETHEREUM_HOST,
)
.option('-a, --api-key <string>', 'Api key for the ethereum host', API_KEY)
.option(
'-m, --mnemonic <string>',
'The mnemonic for the sender of the tx',
'test test test test test test test test test test test junk',
)
.option('--block-number <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;
}
12 changes: 11 additions & 1 deletion yarn-project/sequencer-client/src/publisher/l1-publisher.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -42,6 +42,10 @@ export type MinimalTransactionReceipt = {
* Pushes txs to the L1 chain and waits for their completion.
*/
export interface L1PublisherTxSender {
getSenderAddress(): Promise<EthAddress>;

getSubmitterAddressForBlock(blockNumber: number): Promise<EthAddress>;

/**
* Publishes tx effects to Availability Oracle.
* @param encodedBody - Encoded block body.
Expand Down Expand Up @@ -117,6 +121,12 @@ export class L1Publisher implements L2BlockReceiver {
this.sleepTimeMs = config?.l1BlockPublishRetryIntervalMS ?? 60_000;
}

public async isItMyTurnToSubmit(blockNumber: number): Promise<boolean> {
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.
Expand Down
10 changes: 10 additions & 0 deletions yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -71,6 +72,15 @@ export class ViemTxSender implements L1PublisherTxSender {
});
}

getSenderAddress(): Promise<EthAddress> {
return Promise.resolve(EthAddress.fromString(this.account.address));
}

async getSubmitterAddressForBlock(blockNumber: number): Promise<EthAddress> {
const submitter = await this.rollupContract.read.whoseTurnIsIt([BigInt(blockNumber)]);
return EthAddress.fromString(submitter);
}

async getCurrentArchive(): Promise<Buffer> {
const archive = await this.rollupContract.read.archive();
return Buffer.from(archive.replace('0x', ''), 'hex');
Expand Down
40 changes: 40 additions & 0 deletions yarn-project/sequencer-client/src/sequencer/sequencer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ describe('sequencer', () => {
lastBlockNumber = 0;

publisher = mock<L1Publisher>();
publisher.isItMyTurnToSubmit.mockResolvedValue(true);

globalVariableBuilder = mock<GlobalVariableBuilder>();
merkleTreeOps = mock<MerkleTreeOperations>();
proverClient = mock<ProverClient>();
Expand Down Expand Up @@ -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 => {
Expand Down
21 changes: 15 additions & 6 deletions yarn-project/sequencer-client/src/sequencer/sequencer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
*/
Expand All @@ -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(
Expand Down

0 comments on commit 19c2a97

Please sign in to comment.