Skip to content

Commit

Permalink
refactor: simplify publisher
Browse files Browse the repository at this point in the history
  • Loading branch information
LHerskind committed Aug 19, 2024
1 parent c80908f commit 53d3bcf
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 460 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { AvailabilityOracleAbi, OutboxAbi, RollupAbi } from '@aztec/l1-artifacts
import { SHA256Trunc, StandardTree } from '@aztec/merkle-tree';
import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types';
import { TxProver } from '@aztec/prover-client';
import { type L1Publisher, getL1Publisher } from '@aztec/sequencer-client';
import { L1Publisher } from '@aztec/sequencer-client';
import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
import { MerkleTrees, ServerWorldStateSynchronizer, type WorldStateConfig } from '@aztec/world-state';

Expand Down Expand Up @@ -161,7 +161,7 @@ describe('L1Publisher integration', () => {
builder = await TxProver.new(config, new NoopTelemetryClient());
prover = builder.createBlockProver(builderDb.asLatest());

publisher = getL1Publisher(
publisher = new L1Publisher(
{
l1RpcUrl: config.l1RpcUrl,
requiredConfirmations: 1,
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/prover-node/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type AztecNode } from '@aztec/circuit-types';
import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log';
import { createStore } from '@aztec/kv-store/utils';
import { createProverClient } from '@aztec/prover-client';
import { getL1Publisher } from '@aztec/sequencer-client';
import { L1Publisher } from '@aztec/sequencer-client';
import { createSimulationProvider } from '@aztec/simulator';
import { type TelemetryClient } from '@aztec/telemetry-client';
import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
Expand Down Expand Up @@ -43,7 +43,7 @@ export async function createProverNode(
const prover = await createProverClient(config, telemetry);

// REFACTOR: Move publisher out of sequencer package and into an L1-related package
const publisher = getL1Publisher(config, telemetry);
const publisher = new L1Publisher(config, telemetry);

const txProvider = deps.aztecNodeTxProvider
? new AztecNodeTxProvider(deps.aztecNodeTxProvider)
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/sequencer-client/src/client/sequencer-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { type WorldStateSynchronizer } from '@aztec/world-state';
import { BlockBuilderFactory } from '../block_builder/index.js';
import { type SequencerClientConfig } from '../config.js';
import { GlobalVariableBuilder } from '../global_variable_builder/index.js';
import { getL1Publisher } from '../publisher/index.js';
import { L1Publisher } from '../publisher/index.js';
import { Sequencer, type SequencerConfig } from '../sequencer/index.js';
import { TxValidatorFactory } from '../tx_validator/tx_validator_factory.js';

Expand Down Expand Up @@ -43,7 +43,7 @@ export class SequencerClient {
simulationProvider: SimulationProvider,
telemetryClient: TelemetryClient,
) {
const publisher = getL1Publisher(config, telemetryClient);
const publisher = new L1Publisher(config, telemetryClient);
const globalsBuilder = new GlobalVariableBuilder(config);
const merkleTreeDb = worldStateSynchronizer.getLatest();

Expand Down
14 changes: 0 additions & 14 deletions yarn-project/sequencer-client/src/publisher/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,2 @@
import { type TelemetryClient } from '@aztec/telemetry-client';

import { type PublisherConfig, type TxSenderConfig } from './config.js';
import { L1Publisher } from './l1-publisher.js';
import { ViemTxSender } from './viem-tx-sender.js';

export { L1Publisher } from './l1-publisher.js';
export * from './config.js';

/**
* Returns a new instance of the L1Publisher.
* @param config - Configuration to initialize the new instance.
*/
export function getL1Publisher(config: PublisherConfig & TxSenderConfig, client: TelemetryClient): L1Publisher {
return new L1Publisher(new ViemTxSender(config), client, config);
}
213 changes: 149 additions & 64 deletions yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts
Original file line number Diff line number Diff line change
@@ -1,174 +1,259 @@
import { L2Block } from '@aztec/circuit-types';
import { EthAddress, Fr } from '@aztec/circuits.js';
import { sleep } from '@aztec/foundation/sleep';
import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';

import { type MockProxy, mock } from 'jest-mock-extended';
import { type GetTransactionReceiptReturnType, type PrivateKeyAccount } from 'viem';

import { L1Publisher, type L1PublisherTxSender, type MinimalTransactionReceipt } from './l1-publisher.js';
import { type PublisherConfig, type TxSenderConfig } from './config.js';
import { L1Publisher } from './l1-publisher.js';

interface MockAvailabilityOracleWrite {
publish: (args: readonly [`0x${string}`], options: { account: PrivateKeyAccount }) => Promise<`0x${string}`>;
}

interface MockAvailabilityOracleRead {
isAvailable: (args: readonly [`0x${string}`]) => Promise<boolean>;
}

class MockAvailabilityOracle {
constructor(public write: MockAvailabilityOracleWrite, public read: MockAvailabilityOracleRead) {}
}

interface MockPublicClient {
getTransactionReceipt: ({ hash }: { hash: '0x${string}' }) => Promise<GetTransactionReceiptReturnType>;
getBlock(): Promise<{ timestamp: number }>;
getTransaction: ({ hash }: { hash: '0x${string}' }) => Promise<{ input: `0x${string}`; hash: `0x${string}` }>;
}

interface MockRollupContractWrite {
publishAndProcess: (
args: readonly [`0x${string}`, `0x${string}`, `0x${string}`],
options: { account: PrivateKeyAccount },
) => Promise<`0x${string}`>;

process: (
args: readonly [`0x${string}`, `0x${string}`],
options: { account: PrivateKeyAccount },
) => Promise<`0x${string}`>;
}

interface MockRollupContractRead {
archive: () => Promise<`0x${string}`>;
}

class MockRollupContract {
constructor(public write: MockRollupContractWrite, public read: MockRollupContractRead) {}
}

describe('L1Publisher', () => {
let txSender: MockProxy<L1PublisherTxSender>;
let publishTxHash: string;
let processTxHash: string;
let publishAndProcessTxHash: string;
let processTxReceipt: MinimalTransactionReceipt;
let publishTxReceipt: MinimalTransactionReceipt;
let publishAndProcessTxReceipt: MinimalTransactionReceipt;
let rollupContractRead: MockProxy<MockRollupContractRead>;
let rollupContractWrite: MockProxy<MockRollupContractWrite>;
let rollupContract: MockRollupContract;

let availabilityOracleRead: MockProxy<MockAvailabilityOracleRead>;
let availabilityOracleWrite: MockProxy<MockAvailabilityOracleWrite>;
let availabilityOracle: MockAvailabilityOracle;

let publicClient: MockProxy<MockPublicClient>;

let processTxHash: `0x${string}`;
let publishAndProcessTxHash: `0x${string}`;
let processTxReceipt: GetTransactionReceiptReturnType;
let publishAndProcessTxReceipt: GetTransactionReceiptReturnType;
let l2Block: L2Block;

let header: Buffer;
let archive: Buffer;
let txsEffectsHash: Buffer;
let body: Buffer;

let account: PrivateKeyAccount;

let publisher: L1Publisher;

beforeEach(() => {
l2Block = L2Block.random(42);

header = l2Block.header.toBuffer();
archive = l2Block.archive.root.toBuffer();
txsEffectsHash = l2Block.body.getTxsEffectsHash();
body = l2Block.body.toBuffer();

txSender = mock<L1PublisherTxSender>();

publishTxHash = `0x${Buffer.from('txHashPublish').toString('hex')}`; // random tx hash
processTxHash = `0x${Buffer.from('txHashProcess').toString('hex')}`; // random tx hash
publishAndProcessTxHash = `0x${Buffer.from('txHashPublishAndProcess').toString('hex')}`; // random tx hash
publishTxReceipt = {
transactionHash: publishTxHash,
status: true,
logs: [{ data: txsEffectsHash.toString('hex') }],
} as MinimalTransactionReceipt;

processTxReceipt = {
transactionHash: processTxHash,
status: true,
logs: [{ data: '' }],
} as MinimalTransactionReceipt;
status: 'success',
logs: [],
} as unknown as GetTransactionReceiptReturnType;
publishAndProcessTxReceipt = {
transactionHash: publishAndProcessTxHash,
status: true,
logs: [{ data: txsEffectsHash.toString('hex') }],
} as MinimalTransactionReceipt;
txSender.sendPublishTx.mockResolvedValueOnce(publishTxHash);
txSender.sendProcessTx.mockResolvedValueOnce(processTxHash);
txSender.sendPublishAndProcessTx.mockResolvedValueOnce(publishAndProcessTxHash);
txSender.getTransactionReceipt.mockResolvedValueOnce(publishTxReceipt).mockResolvedValueOnce(processTxReceipt);
txSender.getCurrentArchive.mockResolvedValue(l2Block.header.lastArchive.root.toBuffer());

publisher = new L1Publisher(txSender, new NoopTelemetryClient(), { l1PublishRetryIntervalMS: 1 });
status: 'success',
logs: [],
} as unknown as GetTransactionReceiptReturnType;

rollupContractWrite = mock<MockRollupContractWrite>();
rollupContractRead = mock<MockRollupContractRead>();
rollupContract = new MockRollupContract(rollupContractWrite, rollupContractRead);

availabilityOracleWrite = mock<MockAvailabilityOracleWrite>();
availabilityOracleRead = mock<MockAvailabilityOracleRead>();
availabilityOracle = new MockAvailabilityOracle(availabilityOracleWrite, availabilityOracleRead);

publicClient = mock<MockPublicClient>();

const config = {
l1RpcUrl: `http://127.0.0.1:8545`,
l1ChainId: 1,
publisherPrivateKey: `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`,
l1Contracts: {
availabilityOracleAddress: EthAddress.ZERO.toString(),
rollupAddress: EthAddress.ZERO.toString(),
},
l1PublishRetryIntervalMS: 1,
} as unknown as TxSenderConfig & PublisherConfig;

publisher = new L1Publisher(config, new NoopTelemetryClient());

(publisher as any)['availabilityOracleContract'] = availabilityOracle;
(publisher as any)['rollupContract'] = rollupContract;
(publisher as any)['publicClient'] = publicClient;

account = (publisher as any)['account'];
});

it('publishes l2 block to l1', async () => {
it('publishes and process l2 block to l1', async () => {
rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`);
rollupContractWrite.publishAndProcess.mockResolvedValueOnce(publishAndProcessTxHash);
publicClient.getTransactionReceipt.mockResolvedValueOnce(publishAndProcessTxReceipt);

const result = await publisher.processL2Block(l2Block);

expect(result).toEqual(true);
expect(txSender.sendPublishAndProcessTx).toHaveBeenCalledWith({ header, archive, body });
expect(txSender.getTransactionReceipt).toHaveBeenCalledWith(publishAndProcessTxHash);

const args = [`0x${header.toString('hex')}`, `0x${archive.toString('hex')}`, `0x${body.toString('hex')}`] as const;
expect(rollupContractWrite.publishAndProcess).toHaveBeenCalledWith(args, { account: account });
expect(publicClient.getTransactionReceipt).toHaveBeenCalledWith({ hash: publishAndProcessTxHash });
});

it('publishes l2 block to l1 (already published body)', async () => {
txSender.checkIfTxsAreAvailable.mockResolvedValueOnce(true);
availabilityOracleRead.isAvailable.mockResolvedValueOnce(true);
rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`);
rollupContractWrite.process.mockResolvedValueOnce(processTxHash);
publicClient.getTransactionReceipt.mockResolvedValueOnce(processTxReceipt);

const result = await publisher.processL2Block(l2Block);

expect(result).toEqual(true);
expect(txSender.sendProcessTx).toHaveBeenCalledWith({ header, archive, body });
expect(txSender.getTransactionReceipt).toHaveBeenCalledWith(processTxHash);
const args = [`0x${header.toString('hex')}`, `0x${archive.toString('hex')}`] as const;
expect(rollupContractWrite.process).toHaveBeenCalledWith(args, { account });
expect(publicClient.getTransactionReceipt).toHaveBeenCalledWith({ hash: processTxHash });
});

it('does not publish if last archive root is different to expected', async () => {
txSender.getCurrentArchive.mockResolvedValueOnce(L2Block.random(43).archive.root.toBuffer());
rollupContractRead.archive.mockResolvedValue(Fr.random().toString());

const result = await publisher.processL2Block(l2Block);
expect(result).toBe(false);
expect(txSender.sendPublishTx).not.toHaveBeenCalled();
expect(txSender.sendProcessTx).not.toHaveBeenCalled();
expect(txSender.sendPublishAndProcessTx).not.toHaveBeenCalled();
expect(availabilityOracleWrite.publish).not.toHaveBeenCalled();
expect(rollupContractWrite.process).not.toHaveBeenCalled();
expect(rollupContractWrite.publishAndProcess).not.toHaveBeenCalled();
});

it('does not retry if sending a process tx fails', async () => {
txSender.checkIfTxsAreAvailable.mockResolvedValueOnce(true);
txSender.sendProcessTx.mockReset().mockRejectedValueOnce(new Error()).mockResolvedValueOnce(processTxHash);
availabilityOracleRead.isAvailable.mockResolvedValueOnce(true);
rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`);
rollupContractWrite.process
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce(processTxHash as `0x${string}`);

const result = await publisher.processL2Block(l2Block);

expect(result).toEqual(false);
expect(txSender.sendProcessTx).toHaveBeenCalledTimes(1);
expect(rollupContractWrite.process).toHaveBeenCalledTimes(1);
});

it('does not retry if sending a publish and process tx fails', async () => {
txSender.sendPublishAndProcessTx.mockReset().mockRejectedValueOnce(new Error());
// .mockResolvedValueOnce(publishAndProcessTxHash);
rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`);
rollupContractWrite.publishAndProcess.mockRejectedValueOnce(new Error());

const result = await publisher.processL2Block(l2Block);

expect(result).toEqual(false);
expect(txSender.sendPublishAndProcessTx).toHaveBeenCalledTimes(1);
expect(rollupContractWrite.publishAndProcess).toHaveBeenCalledTimes(1);
});

it('retries if fetching the receipt fails (process)', async () => {
txSender.checkIfTxsAreAvailable.mockResolvedValueOnce(true);
txSender.getTransactionReceipt
.mockReset()
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce(processTxReceipt);
availabilityOracleRead.isAvailable.mockResolvedValueOnce(true);
rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`);
rollupContractWrite.process.mockResolvedValueOnce(processTxHash);
publicClient.getTransactionReceipt.mockRejectedValueOnce(new Error()).mockResolvedValueOnce(processTxReceipt);

const result = await publisher.processL2Block(l2Block);

expect(result).toEqual(true);
expect(txSender.getTransactionReceipt).toHaveBeenCalledTimes(2);
expect(publicClient.getTransactionReceipt).toHaveBeenCalledTimes(2);
});

it('retries if fetching the receipt fails (publish process)', async () => {
txSender.getTransactionReceipt
.mockReset()
rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`);
rollupContractWrite.publishAndProcess.mockResolvedValueOnce(publishAndProcessTxHash as `0x${string}`);
publicClient.getTransactionReceipt
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce(publishAndProcessTxReceipt);

const result = await publisher.processL2Block(l2Block);

expect(result).toEqual(true);
expect(txSender.getTransactionReceipt).toHaveBeenCalledTimes(2);
expect(publicClient.getTransactionReceipt).toHaveBeenCalledTimes(2);
});

it('returns false if publish and process tx reverts', async () => {
txSender.getTransactionReceipt.mockReset().mockResolvedValueOnce({ ...publishAndProcessTxReceipt, status: false });
rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`);
rollupContractWrite.publishAndProcess.mockResolvedValueOnce(publishAndProcessTxHash);
publicClient.getTransactionReceipt.mockResolvedValueOnce({ ...publishAndProcessTxReceipt, status: 'reverted' });

const result = await publisher.processL2Block(l2Block);

expect(result).toEqual(false);
});

it('returns false if process tx reverts', async () => {
txSender.checkIfTxsAreAvailable.mockResolvedValueOnce(true);
txSender.getTransactionReceipt.mockReset().mockResolvedValueOnce({ ...processTxReceipt, status: false });
availabilityOracleRead.isAvailable.mockResolvedValueOnce(true);
rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`);

publicClient.getTransactionReceipt.mockResolvedValueOnce({ ...processTxReceipt, status: 'reverted' });

const result = await publisher.processL2Block(l2Block);

expect(result).toEqual(false);
});

it('returns false if sending publish and progress tx is interrupted', async () => {
txSender.sendPublishAndProcessTx.mockReset().mockImplementationOnce(() => sleep(10, publishAndProcessTxHash));
rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`);
rollupContractWrite.publishAndProcess.mockImplementationOnce(
() => sleep(10, publishAndProcessTxHash) as Promise<`0x${string}`>,
);

const resultPromise = publisher.processL2Block(l2Block);
publisher.interrupt();
const result = await resultPromise;

expect(result).toEqual(false);
expect(txSender.getTransactionReceipt).not.toHaveBeenCalled();
expect(publicClient.getTransactionReceipt).not.toHaveBeenCalled();
});

it('returns false if sending process tx is interrupted', async () => {
txSender.checkIfTxsAreAvailable.mockResolvedValueOnce(true);
txSender.sendProcessTx.mockReset().mockImplementationOnce(() => sleep(10, processTxHash));
availabilityOracleRead.isAvailable.mockResolvedValueOnce(true);
rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`);
rollupContractWrite.process.mockImplementationOnce(() => sleep(10, processTxHash) as Promise<`0x${string}`>);

const resultPromise = publisher.processL2Block(l2Block);
publisher.interrupt();
const result = await resultPromise;

expect(result).toEqual(false);
expect(txSender.getTransactionReceipt).not.toHaveBeenCalled();
expect(publicClient.getTransactionReceipt).not.toHaveBeenCalled();
});
});
Loading

0 comments on commit 53d3bcf

Please sign in to comment.