diff --git a/yarn-project/circuit-types/src/interfaces/aztec-node.test.ts b/yarn-project/circuit-types/src/interfaces/aztec-node.test.ts index d415249c3f4b..042ff45987fa 100644 --- a/yarn-project/circuit-types/src/interfaces/aztec-node.test.ts +++ b/yarn-project/circuit-types/src/interfaces/aztec-node.test.ts @@ -9,6 +9,7 @@ import { L1_TO_L2_MSG_TREE_HEIGHT, NOTE_HASH_TREE_HEIGHT, NULLIFIER_TREE_HEIGHT, + type NodeInfo, PUBLIC_DATA_TREE_HEIGHT, type ProtocolContractAddresses, ProtocolContractsNames, @@ -155,6 +156,11 @@ describe('AztecNodeApiSchema', () => { expect(response).toBe(true); }); + it('getNodeInfo', async () => { + const response = await context.client.getNodeInfo(); + expect(response).toEqual(await handler.getNodeInfo()); + }); + it('getBlocks', async () => { const response = await context.client.getBlocks(1, 1); expect(response).toHaveLength(1); @@ -404,6 +410,20 @@ class MockAztecNode implements AztecNode { isReady(): Promise { return Promise.resolve(true); } + getNodeInfo(): Promise { + return Promise.resolve({ + nodeVersion: '1.0', + l1ChainId: 1, + protocolVersion: 1, + enr: 'enr', + l1ContractAddresses: Object.fromEntries( + L1ContractsNames.map(name => [name, EthAddress.random()]), + ) as L1ContractAddresses, + protocolContractAddresses: Object.fromEntries( + ProtocolContractsNames.map(name => [name, AztecAddress.random()]), + ) as ProtocolContractAddresses, + }); + } getBlocks(from: number, limit: number): Promise { return Promise.resolve(times(limit, i => L2Block.random(from + i))); } diff --git a/yarn-project/circuit-types/src/interfaces/aztec-node.ts b/yarn-project/circuit-types/src/interfaces/aztec-node.ts index fb23b2464a74..149172864c83 100644 --- a/yarn-project/circuit-types/src/interfaces/aztec-node.ts +++ b/yarn-project/circuit-types/src/interfaces/aztec-node.ts @@ -8,7 +8,8 @@ import { L1_TO_L2_MSG_TREE_HEIGHT, NOTE_HASH_TREE_HEIGHT, NULLIFIER_TREE_HEIGHT, - NodeInfo, + type NodeInfo, + NodeInfoSchema, PUBLIC_DATA_TREE_HEIGHT, type ProtocolContractAddresses, ProtocolContractAddressesSchema, @@ -464,6 +465,8 @@ export const AztecNodeApiSchema: ApiSchemaFor = { isReady: z.function().returns(z.boolean()), + getNodeInfo: z.function().returns(NodeInfoSchema), + getBlocks: z.function().args(z.number(), z.number()).returns(z.array(L2Block.schema)), getNodeVersion: z.function().returns(z.string()), diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 5542200da746..eb3c65d46688 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -617,7 +617,7 @@ export async function deployL1Contract( let txHash: Hex | undefined = undefined; let resultingAddress: Hex | null | undefined = undefined; - const gasUtils = new GasUtils(publicClient, logger); + const gasUtils = new GasUtils(publicClient, walletClient, logger); if (libraries) { // @note Assumes that we wont have nested external libraries. @@ -667,7 +667,7 @@ export async function deployL1Contract( const existing = await publicClient.getBytecode({ address: resultingAddress }); if (existing === undefined || existing === '0x') { - const receipt = await gasUtils.sendAndMonitorTransaction(walletClient, walletClient.account!, { + const receipt = await gasUtils.sendAndMonitorTransaction({ to: deployer, data: concatHex([salt, calldata]), }); @@ -680,7 +680,7 @@ export async function deployL1Contract( } else { // Regular deployment path const deployData = encodeDeployData({ abi, bytecode, args }); - const receipt = await gasUtils.sendAndMonitorTransaction(walletClient, walletClient.account!, { + const receipt = await gasUtils.sendAndMonitorTransaction({ to: '0x', // Contract creation data: deployData, }); diff --git a/yarn-project/ethereum/src/gas_utils.test.ts b/yarn-project/ethereum/src/gas_utils.test.ts index b96ad3aa4cbc..f9727070e013 100644 --- a/yarn-project/ethereum/src/gas_utils.test.ts +++ b/yarn-project/ethereum/src/gas_utils.test.ts @@ -29,7 +29,6 @@ describe('GasUtils', () => { let gasUtils: GasUtils; let publicClient: any; let walletClient: any; - let account: any; let anvil: Anvil; let initialBaseFee: bigint; const logger = createDebugLogger('l1_gas_test'); @@ -43,7 +42,7 @@ describe('GasUtils', () => { throw new Error('Failed to get private key'); } const privKey = Buffer.from(privKeyRaw).toString('hex'); - account = privateKeyToAccount(`0x${privKey}`); + const account = privateKeyToAccount(`0x${privKey}`); publicClient = createPublicClient({ transport: http(rpcUrl), @@ -58,6 +57,7 @@ describe('GasUtils', () => { gasUtils = new GasUtils( publicClient, + walletClient, logger, { bufferPercentage: 20n, @@ -77,7 +77,7 @@ describe('GasUtils', () => { }, 5000); it('sends and monitors a simple transaction', async () => { - const receipt = await gasUtils.sendAndMonitorTransaction(walletClient, account, { + const receipt = await gasUtils.sendAndMonitorTransaction({ to: '0x1234567890123456789012345678901234567890', data: '0x', value: 0n, @@ -92,7 +92,7 @@ describe('GasUtils', () => { initialBaseFee = initialBlock.baseFeePerGas ?? 0n; // Start a transaction - const sendPromise = gasUtils.sendAndMonitorTransaction(walletClient, account, { + const sendPromise = gasUtils.sendAndMonitorTransaction({ to: '0x1234567890123456789012345678901234567890', data: '0x', value: 0n, @@ -131,7 +131,7 @@ describe('GasUtils', () => { params: [], }); - const receipt = await gasUtils.sendAndMonitorTransaction(walletClient, account, { + const receipt = await gasUtils.sendAndMonitorTransaction({ to: '0x1234567890123456789012345678901234567890', data: '0x', value: 0n, @@ -154,6 +154,7 @@ describe('GasUtils', () => { // First deploy without any buffer const baselineGasUtils = new GasUtils( publicClient, + walletClient, logger, { bufferPercentage: 0n, @@ -168,7 +169,7 @@ describe('GasUtils', () => { }, ); - const baselineTx = await baselineGasUtils.sendAndMonitorTransaction(walletClient, account, { + const baselineTx = await baselineGasUtils.sendAndMonitorTransaction({ to: EthAddress.ZERO.toString(), data: SIMPLE_CONTRACT_BYTECODE, }); @@ -181,6 +182,7 @@ describe('GasUtils', () => { // Now deploy with 20% buffer const bufferedGasUtils = new GasUtils( publicClient, + walletClient, logger, { bufferPercentage: 20n, @@ -195,7 +197,7 @@ describe('GasUtils', () => { }, ); - const bufferedTx = await bufferedGasUtils.sendAndMonitorTransaction(walletClient, account, { + const bufferedTx = await bufferedGasUtils.sendAndMonitorTransaction({ to: EthAddress.ZERO.toString(), data: SIMPLE_CONTRACT_BYTECODE, }); diff --git a/yarn-project/ethereum/src/gas_utils.ts b/yarn-project/ethereum/src/gas_utils.ts index 5cbd0916e255..4edc36aed2a8 100644 --- a/yarn-project/ethereum/src/gas_utils.ts +++ b/yarn-project/ethereum/src/gas_utils.ts @@ -4,7 +4,9 @@ import { sleep } from '@aztec/foundation/sleep'; import { type Account, type Address, + type Chain, type Hex, + type HttpTransport, type PublicClient, type TransactionReceipt, type WalletClient, @@ -80,6 +82,7 @@ export class GasUtils { constructor( private readonly publicClient: PublicClient, + private readonly walletClient: WalletClient, private readonly logger?: DebugLogger, gasConfig?: GasConfig, monitorConfig?: L1TxMonitorConfig, @@ -102,15 +105,13 @@ export class GasUtils { * @returns The hash of the successful transaction */ public async sendAndMonitorTransaction( - walletClient: WalletClient, - account: Account, request: L1TxRequest, _gasConfig?: Partial, _monitorConfig?: Partial, ): Promise { const monitorConfig = { ...this.monitorConfig, ..._monitorConfig }; const gasConfig = { ...this.gasConfig, ..._gasConfig }; - + const account = this.walletClient.account; // Estimate gas const gasLimit = await this.estimateGas(account, request); @@ -118,9 +119,7 @@ export class GasUtils { const nonce = await this.publicClient.getTransactionCount({ address: account.address }); // Send initial tx - const txHash = await walletClient.sendTransaction({ - chain: null, - account, + const txHash = await this.walletClient.sendTransaction({ ...request, gas: gasLimit, maxFeePerGas: gasPrice, @@ -163,15 +162,19 @@ export class GasUtils { // Check if current tx is pending const tx = await this.publicClient.getTransaction({ hash: currentTxHash }); - if (tx) { - this.logger?.debug(`L1 Transaction ${currentTxHash} pending`); + + // Get time passed + const timePassed = Date.now() - lastSeen; + + if (tx && timePassed < monitorConfig.stallTimeMs) { + this.logger?.debug(`L1 Transaction ${currentTxHash} pending. Time passed: ${timePassed}ms`); lastSeen = Date.now(); await sleep(monitorConfig.checkIntervalMs); continue; } - // tx not found and enough time has passed - might be stuck - if (Date.now() - lastSeen > monitorConfig.stallTimeMs && attempts < monitorConfig.maxAttempts) { + // Enough time has passed - might be stuck + if (timePassed > monitorConfig.stallTimeMs && attempts < monitorConfig.maxAttempts) { attempts++; const newGasPrice = await this.getGasPrice(); @@ -181,9 +184,7 @@ export class GasUtils { ); // Send replacement tx with higher gas price - currentTxHash = await walletClient.sendTransaction({ - chain: null, - account, + currentTxHash = await this.walletClient.sendTransaction({ ...request, nonce, gas: gasLimit, diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index 83359da60f18..b0a29474a4c7 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -44,6 +44,7 @@ import { type PublicActions, type PublicClient, type PublicRpcSchema, + type TransactionReceipt, type WalletActions, type WalletClient, type WalletRpcSchema, @@ -205,6 +206,7 @@ export class L1Publisher { this.gasUtils = new GasUtils( this.publicClient, + this.walletClient, this.log, { bufferPercentage: 20n, @@ -216,7 +218,6 @@ export class L1Publisher { maxAttempts: 5, checkIntervalMs: 30_000, stallTimeMs: 120_000, - gasPriceIncrease: 75n, }, ); } @@ -517,28 +518,24 @@ export class L1Publisher { this.log.verbose(`Submitting propose transaction`); - const tx = proofQuote + const result = proofQuote ? await this.sendProposeAndClaimTx(proposeTxArgs, proofQuote) : await this.sendProposeTx(proposeTxArgs); - if (!tx) { + if (!result || result?.receipt) { this.log.info(`Failed to publish block ${block.number} to L1`, ctx); return false; } - const { hash: txHash, args, functionName, gasLimit } = tx; - - const receipt = await this.getTransactionReceipt(txHash); - if (!receipt) { - this.log.info(`Failed to get receipt for tx ${txHash}`, ctx); - return false; - } + const { receipt, args, functionName } = result; // Tx was mined successfully - if (receipt.status) { - const tx = await this.getTransactionStats(txHash); + if (receipt.status === 'success') { + const tx = await this.getTransactionStats(receipt.transactionHash); const stats: L1PublishBlockStats = { - ...pick(receipt, 'gasPrice', 'gasUsed', 'transactionHash'), + gasPrice: receipt.effectiveGasPrice, + gasUsed: receipt.gasUsed, + transactionHash: receipt.transactionHash, ...pick(tx!, 'calldataGas', 'calldataSize', 'sender'), ...block.getStats(), eventName: 'rollup-published-to-l1', @@ -554,7 +551,6 @@ export class L1Publisher { const errorMsg = await this.tryGetErrorFromRevertedTx({ args, functionName, - gasLimit, abi: RollupAbi, address: this.rollupContract.address, blockNumber: receipt.blockNumber, @@ -570,7 +566,6 @@ export class L1Publisher { private async tryGetErrorFromRevertedTx(args: { args: any[]; functionName: string; - gasLimit: bigint; abi: any; address: Hex; blockNumber: bigint | undefined; @@ -721,59 +716,23 @@ export class L1Publisher { const txArgs = [...this.getSubmitEpochProofArgs(args), proofHex] as const; this.log.info(`SubmitEpochProof proofSize=${args.proof.withoutPublicInputs().length} bytes`); - // Estimate gas and get price - const gasLimit = await this.gasUtils.estimateGas(() => - this.rollupContract.estimateGas.submitEpochRootProof(txArgs, { account: this.account }), - ); - const maxFeePerGas = await this.gasUtils.getGasPrice(); - - const txHash = await this.rollupContract.write.submitEpochRootProof(txArgs, { - account: this.account, - gas: gasLimit, - maxFeePerGas, - }); - - // Monitor transaction - await this.gasUtils.monitorTransaction(txHash, this.walletClient, { + const txReceipt = await this.gasUtils.sendAndMonitorTransaction({ to: this.rollupContract.address, data: encodeFunctionData({ abi: this.rollupContract.abi, functionName: 'submitEpochRootProof', args: txArgs, }), - nonce: await this.publicClient.getTransactionCount({ address: this.account.address }), - gasLimit, - maxFeePerGas, }); - return txHash; + return txReceipt.transactionHash; } catch (err) { this.log.error(`Rollup submit epoch proof failed`, err); return undefined; } } - private async prepareProposeTx(encodedData: L1ProcessArgs, gasGuess: bigint) { - // We have to jump a few hoops because viem is not happy around estimating gas for view functions - const computeTxsEffectsHashGas = await this.gasUtils.estimateGas(() => - this.publicClient.estimateGas({ - to: this.rollupContract.address, - data: encodeFunctionData({ - abi: this.rollupContract.abi, - functionName: 'computeTxsEffectsHash', - args: [`0x${encodedData.body.toString('hex')}`], - }), - }), - ); - - // @note We perform this guesstimate instead of the usual `gasEstimate` since - // viem will use the current state to simulate against, which means that - // we will fail estimation in the case where we are simulating for the - // first ethereum block within our slot (as current time is not in the - // slot yet). - const gasGuesstimate = computeTxsEffectsHashGas + gasGuess; - const maxFeePerGas = await this.gasUtils.getGasPrice(); - + private prepareProposeTx(encodedData: L1ProcessArgs) { const attestations = encodedData.attestations ? encodedData.attestations.map(attest => attest.toViemSignature()) : []; @@ -787,7 +746,7 @@ export class L1Publisher { `0x${encodedData.body.toString('hex')}`, ] as const; - return { args, gasGuesstimate, maxFeePerGas }; + return args; } private getSubmitEpochProofArgs(args: { @@ -818,40 +777,31 @@ export class L1Publisher { private async sendProposeTx( encodedData: L1ProcessArgs, - ): Promise<{ hash: string; args: any; functionName: string; gasLimit: bigint } | undefined> { + ): Promise<{ receipt: TransactionReceipt; args: any; functionName: string } | undefined> { if (this.interrupted) { return undefined; } try { - const { args, gasGuesstimate, maxFeePerGas } = await this.prepareProposeTx( - encodedData, - L1Publisher.PROPOSE_GAS_GUESS, + const args = this.prepareProposeTx(encodedData); + + const receipt = await this.gasUtils.sendAndMonitorTransaction( + { + to: this.rollupContract.address, + data: encodeFunctionData({ + abi: this.rollupContract.abi, + functionName: 'propose', + args, + }), + }, + { + bufferFixed: L1Publisher.PROPOSE_GAS_GUESS, + }, ); - const txHash = await this.rollupContract.write.propose(args, { - account: this.account, - gas: gasGuesstimate, - maxFeePerGas, - }); - - // Monitor the transaction and speed it up if needed - const receipt = await this.gasUtils.monitorTransaction(txHash, this.walletClient, { - to: this.rollupContract.address, - data: encodeFunctionData({ - abi: this.rollupContract.abi, - functionName: 'propose', - args, - }), - nonce: await this.publicClient.getTransactionCount({ address: this.account.address }), - gasLimit: gasGuesstimate, - maxFeePerGas, - }); - return { - hash: receipt.transactionHash, + receipt, args, functionName: 'propose', - gasLimit: gasGuesstimate, }; } catch (err) { prettyLogViemError(err, this.log); @@ -863,27 +813,28 @@ export class L1Publisher { private async sendProposeAndClaimTx( encodedData: L1ProcessArgs, quote: EpochProofQuote, - ): Promise<{ hash: string; args: any; functionName: string; gasLimit: bigint } | undefined> { + ): Promise<{ receipt: TransactionReceipt; args: any; functionName: string } | undefined> { if (this.interrupted) { return undefined; } try { - const { args, gasGuesstimate, maxFeePerGas } = await this.prepareProposeTx( - encodedData, - L1Publisher.PROPOSE_AND_CLAIM_GAS_GUESS, - ); this.log.info(`ProposeAndClaim`); this.log.info(inspect(quote.payload)); - return { - hash: await this.rollupContract.write.proposeAndClaim([...args, quote.toViemArgs()], { - account: this.account, - gas: gasGuesstimate, - maxFeePerGas, + const args = this.prepareProposeTx(encodedData); + const receipt = await this.gasUtils.sendAndMonitorTransaction({ + to: this.rollupContract.address, + data: encodeFunctionData({ + abi: this.rollupContract.abi, + functionName: 'proposeAndClaim', + args: [...args, quote.toViemArgs()], }), - functionName: 'proposeAndClaim', + }); + + return { + receipt, args, - gasLimit: gasGuesstimate, + functionName: 'proposeAndClaim', }; } catch (err) { prettyLogViemError(err, this.log);