-
Notifications
You must be signed in to change notification settings - Fork 310
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
362 additions
and
460 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
213
yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
Oops, something went wrong.