Skip to content

Commit

Permalink
feat: forwarder contract
Browse files Browse the repository at this point in the history
refactor: different publishers for sequencer and prover
feat: sequencer publisher uses forwarder contract
  • Loading branch information
just-mitch committed Jan 30, 2025
1 parent 1c7d208 commit 02060aa
Show file tree
Hide file tree
Showing 67 changed files with 2,834 additions and 2,081 deletions.
22 changes: 22 additions & 0 deletions l1-contracts/src/periphery/Forwarder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Aztec Labs.
pragma solidity >=0.8.27;

import {Ownable} from "@oz/access/Ownable.sol";
import {Address} from "@oz/utils/Address.sol";
import {IForwarder} from "./interfaces/IForwarder.sol";

contract Forwarder is Ownable, IForwarder {
using Address for address;

constructor(address __owner) Ownable(__owner) {}

function forward(address[] calldata _to, bytes[] calldata _data) external override onlyOwner {
require(
_to.length == _data.length, IForwarder.ForwarderLengthMismatch(_to.length, _data.length)
);
for (uint256 i = 0; i < _to.length; i++) {
_to[i].functionCall(_data[i]);
}
}
}
9 changes: 9 additions & 0 deletions l1-contracts/src/periphery/interfaces/IForwarder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Aztec Labs.
pragma solidity >=0.8.27;

interface IForwarder {
error ForwarderLengthMismatch(uint256 toLength, uint256 dataLength); // 3a2aeb4d

function forward(address[] calldata _to, bytes[] calldata _data) external;
}
83 changes: 83 additions & 0 deletions l1-contracts/test/Forwarder.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Aztec Labs.
pragma solidity >=0.8.27;

import {Test} from "forge-std/Test.sol";
import {Forwarder} from "../src/periphery/Forwarder.sol";
import {IForwarder} from "../src/periphery/interfaces/IForwarder.sol";
import {TestERC20} from "@aztec/mock/TestERC20.sol";
import {Ownable} from "@oz/access/Ownable.sol";
// solhint-disable comprehensive-interface

contract ForwarderTest is Test {
Forwarder public forwarder;
TestERC20 public token1;
TestERC20 public token2;
address public owner;
address public user;

function setUp() public {
owner = makeAddr("owner");
user = makeAddr("user");

vm.prank(owner);
forwarder = new Forwarder(owner);

token1 = new TestERC20("Token1", "TK1", address(forwarder));
token2 = new TestERC20("Token2", "TK2", address(forwarder));
}

function testForward() public {
// Setup test data
address[] memory targets = new address[](2);
targets[0] = address(token1);
targets[1] = address(token2);

bytes[] memory data = new bytes[](2);
data[0] = abi.encodeCall(TestERC20.mint, (address(this), 100));
data[1] = abi.encodeCall(TestERC20.mint, (address(this), 200));

// Execute forward call
vm.prank(owner);
forwarder.forward(targets, data);

// Verify results
assertEq(token1.balanceOf(address(this)), 100);
assertEq(token2.balanceOf(address(this)), 200);
}

function testRevertWhenNotOwner(address _user) public {
address[] memory targets = new address[](1);
bytes[] memory data = new bytes[](1);

vm.assume(_user != owner);
vm.prank(_user);
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _user));
forwarder.forward(targets, data);
}

function testRevertWhenLengthMismatch() public {
address[] memory targets = new address[](2);
bytes[] memory data = new bytes[](1);

vm.prank(owner);
vm.expectRevert(abi.encodeWithSelector(IForwarder.ForwarderLengthMismatch.selector, 2, 1));
forwarder.forward(targets, data);
}

function testRevertWhenCallToInvalidAddress(address _invalidAddress) public {
vm.assume(_invalidAddress != address(token1));
vm.assume(_invalidAddress != address(token2));
vm.assume(_invalidAddress != address(forwarder));

address[] memory targets = new address[](1);
targets[0] = _invalidAddress;

bytes[] memory data = new bytes[](1);
data[0] = hex"12345678";

vm.prank(owner);
vm.expectRevert();
forwarder.forward(targets, data);
}
}
23 changes: 23 additions & 0 deletions spartan/aztec-network/values/1-validators.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
telemetry:
enabled: true

validator:
replicas: 1
validatorKeys:
Expand All @@ -10,3 +13,23 @@ validator:
bootNode:
validator:
disabled: true

ethereum:
execution:
resources:
requests:
memory: "2Gi"
cpu: "1"
storageSize: "10Gi"
beacon:
resources:
requests:
memory: "2Gi"
cpu: "1"
storageSize: "10Gi"
validator:
resources:
requests:
memory: "2Gi"
cpu: "1"
storageSize: "10Gi"
15 changes: 12 additions & 3 deletions yarn-project/archiver/src/archiver/archiver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { EthAddress } from '@aztec/foundation/eth-address';
import { Fr } from '@aztec/foundation/fields';
import { type Logger, createLogger } from '@aztec/foundation/log';
import { sleep } from '@aztec/foundation/sleep';
import { type InboxAbi, RollupAbi } from '@aztec/l1-artifacts';
import { ForwarderAbi, type InboxAbi, RollupAbi } from '@aztec/l1-artifacts';
import { getTelemetryClient } from '@aztec/telemetry-client';

import { jest } from '@jest/globals';
Expand Down Expand Up @@ -80,6 +80,7 @@ describe('Archiver', () => {
let mockRollup: {
read: typeof mockRollupRead;
getEvents: typeof mockRollupEvents;
address: string;
};
let mockInbox: {
read: typeof mockInboxRead;
Expand Down Expand Up @@ -147,6 +148,7 @@ describe('Archiver', () => {
mockRollup = {
read: mockRollupRead,
getEvents: mockRollupEvents,
address: rollupAddress.toString(),
};

(archiver as any).rollup = mockRollup;
Expand Down Expand Up @@ -571,7 +573,7 @@ async function makeRollupTx(l2Block: L2Block) {
const blobInput = Blob.getEthBlobEvaluationInputs(await Blob.getBlobs(l2Block.body.toBlobFields()));
const archive = toHex(l2Block.archive.root.toBuffer());
const blockHash = toHex((await l2Block.header.hash()).toBuffer());
const input = encodeFunctionData({
const rollupInput = encodeFunctionData({
abi: RollupAbi,
functionName: 'propose',
args: [
Expand All @@ -581,7 +583,14 @@ async function makeRollupTx(l2Block: L2Block) {
blobInput,
],
});
return { input } as Transaction<bigint, number>;

const forwarderInput = encodeFunctionData({
abi: ForwarderAbi,
functionName: 'forward',
args: [[EthAddress.ZERO.toString()], [rollupInput]],
});

return { input: forwarderInput } as Transaction<bigint, number>;
}

/**
Expand Down
72 changes: 64 additions & 8 deletions yarn-project/archiver/src/archiver/data_retrieval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { type EthAddress } from '@aztec/foundation/eth-address';
import { type ViemSignature } from '@aztec/foundation/eth-signature';
import { type Logger, createLogger } from '@aztec/foundation/log';
import { numToUInt32BE } from '@aztec/foundation/serialize';
import { type InboxAbi, RollupAbi } from '@aztec/l1-artifacts';
import { ForwarderAbi, type InboxAbi, RollupAbi } from '@aztec/l1-artifacts';

import {
type Chain,
Expand Down Expand Up @@ -108,6 +108,7 @@ export async function processL2BlockProposedLogs(
log.transactionHash!,
blobHashes,
l2BlockNumber,
rollup.address,
);

const l1: L1PublishedData = {
Expand All @@ -133,6 +134,57 @@ export async function getL1BlockTime(publicClient: PublicClient, blockNumber: bi
return block.timestamp;
}

/**
* Extracts the first 'propose' method calldata from a forwarder transaction's data.
* @param forwarderData - The forwarder transaction input data
* @param rollupAddress - The address of the rollup contract
* @returns The calldata for the first 'propose' method call to the rollup contract
*/
function extractRollupProposeCalldata(forwarderData: Hex, rollupAddress: Hex): Hex {
// TODO(#11451): custom forwarders
const { functionName: forwarderFunctionName, args: forwarderArgs } = decodeFunctionData({
abi: ForwarderAbi,
data: forwarderData,
});

if (forwarderFunctionName !== 'forward') {
throw new Error(`Unexpected forwarder method called ${forwarderFunctionName}`);
}

if (forwarderArgs.length !== 2) {
throw new Error(`Unexpected number of arguments for forwarder`);
}

const [to, data] = forwarderArgs;

// Find all rollup calls
const rollupAddressLower = rollupAddress.toLowerCase();

for (let i = 0; i < to.length; i++) {
const addr = to[i];
if (addr.toLowerCase() !== rollupAddressLower) {
continue;
}
const callData = data[i];

try {
const { functionName: rollupFunctionName } = decodeFunctionData({
abi: RollupAbi,
data: callData,
});

if (rollupFunctionName === 'propose') {
return callData;
}
} catch (err) {
// Skip invalid function data
continue;
}
}

throw new Error(`Rollup address not found in forwarder args`);
}

/**
* Gets block from the calldata of an L1 transaction.
* Assumes that the block was published from an EOA.
Expand All @@ -148,18 +200,22 @@ async function getBlockFromRollupTx(
txHash: `0x${string}`,
blobHashes: Buffer[], // WORKTODO(md): buffer32?
l2BlockNum: bigint,
rollupAddress: Hex,
): Promise<L2Block> {
const { input: data, blockHash } = await publicClient.getTransaction({ hash: txHash });

const { functionName, args } = decodeFunctionData({ abi: RollupAbi, data });
const { input: forwarderData, blockHash } = await publicClient.getTransaction({ hash: txHash });

const allowedMethods = ['propose', 'proposeAndClaim'];
const rollupData = extractRollupProposeCalldata(forwarderData, rollupAddress);
const { functionName: rollupFunctionName, args: rollupArgs } = decodeFunctionData({
abi: RollupAbi,
data: rollupData,
});

if (!allowedMethods.includes(functionName)) {
throw new Error(`Unexpected method called ${functionName}`);
if (rollupFunctionName !== 'propose') {
throw new Error(`Unexpected rollup method called ${rollupFunctionName}`);
}

// TODO(#9101): 'bodyHex' will be removed from below
const [decodedArgs, , bodyHex, blobInputs] = args! as readonly [
const [decodedArgs, , bodyHex, blobInputs] = rollupArgs! as readonly [
{
header: Hex;
archive: Hex;
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/aztec-node/src/aztec-node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ import { type P2P, createP2PClient } from '@aztec/p2p';
import { ProtocolContractAddress } from '@aztec/protocol-contracts';
import {
GlobalVariableBuilder,
type L1Publisher,
SequencerClient,
type SequencerPublisher,
createSlasherClient,
createValidatorForAcceptingTxs,
getDefaultAllowedSetupFunctions,
Expand Down Expand Up @@ -144,7 +144,7 @@ export class AztecNodeService implements AztecNode, Traceable {
deps: {
telemetry?: TelemetryClient;
logger?: Logger;
publisher?: L1Publisher;
publisher?: SequencerPublisher;
dateProvider?: DateProvider;
blobSinkClient?: BlobSinkClientInterface;
} = {},
Expand Down
35 changes: 35 additions & 0 deletions yarn-project/aztec.js/src/utils/anvil_test_watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ import type * as chains from 'viem/chains';
* block within the slot. And if so, it will time travel into the next slot.
*/
export class AnvilTestWatcher {
private isSandbox: boolean = false;

private rollup: GetContractReturnType<typeof RollupAbi, PublicClient<HttpTransport, chains.Chain>>;

private filledRunningPromise?: RunningPromise;
private mineIfOutdatedPromise?: RunningPromise;

private logger: Logger = createLogger(`aztecjs:utils:watcher`);

Expand All @@ -36,6 +39,10 @@ export class AnvilTestWatcher {
this.logger.debug(`Watcher created for rollup at ${rollupAddress}`);
}

setIsSandbox(isSandbox: boolean) {
this.isSandbox = isSandbox;
}

async start() {
if (this.filledRunningPromise) {
throw new Error('Watcher already watching for filled slot');
Expand All @@ -50,6 +57,8 @@ export class AnvilTestWatcher {
if (isAutoMining) {
this.filledRunningPromise = new RunningPromise(() => this.warpTimeIfNeeded(), this.logger, 1000);
this.filledRunningPromise.start();
this.mineIfOutdatedPromise = new RunningPromise(() => this.mineIfOutdated(), this.logger, 1000);
this.mineIfOutdatedPromise.start();
this.logger.info(`Watcher started for rollup at ${this.rollup.address}`);
} else {
this.logger.info(`Watcher not started because not auto mining`);
Expand All @@ -58,6 +67,27 @@ export class AnvilTestWatcher {

async stop() {
await this.filledRunningPromise?.stop();
await this.mineIfOutdatedPromise?.stop();
}

async mineIfOutdated() {
// this doesn't apply to the sandbox, because we don't have a date provider in the sandbox
if (!this.dateProvider) {
return;
}

const l1Time = (await this.cheatcodes.timestamp()) * 1000;
const wallTime = this.dateProvider.now();

// If the wall time is more than 24 seconds away from L1 time,
// mine a block and sync the clocks
if (Math.abs(wallTime - l1Time) > 24 * 1000) {
this.logger.warn(`Wall time is more than 24 seconds away from L1 time, mining a block and syncing clocks`);
await this.cheatcodes.evmMine();
const newL1Time = await this.cheatcodes.timestamp();
this.logger.info(`New L1 time: ${newL1Time}`);
this.dateProvider.setTime(newL1Time * 1000);
}
}

async warpTimeIfNeeded() {
Expand All @@ -80,6 +110,11 @@ export class AnvilTestWatcher {
return;
}

// If we are not in sandbox, we don't need to warp time
if (!this.isSandbox) {
return;
}

const currentTimestamp = this.dateProvider?.now() ?? Date.now();
if (currentTimestamp > nextSlotTimestamp * 1000) {
try {
Expand Down
Loading

0 comments on commit 02060aa

Please sign in to comment.