Skip to content

Commit

Permalink
refactor GasUtils
Browse files Browse the repository at this point in the history
  • Loading branch information
spypsy committed Nov 18, 2024
1 parent 9af92ea commit d066d51
Show file tree
Hide file tree
Showing 4 changed files with 289 additions and 210 deletions.
3 changes: 2 additions & 1 deletion yarn-project/ethereum/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"formatting:fix": "run -T eslint --fix ./src && run -T prettier -w ./src",
"start:dev": "tsc-watch -p tsconfig.json --onSuccess 'yarn start'",
"start": "node ./dest/index.js",
"test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests"
"test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests",
"gas": "node ./dest/test_gas_stuff.js"
},
"inherits": [
"../package.common.json"
Expand Down
76 changes: 14 additions & 62 deletions yarn-project/ethereum/src/deploy_l1_contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ export async function deployL1Contract(
logger?: DebugLogger,
): Promise<{ address: EthAddress; txHash: Hex | undefined }> {
let txHash: Hex | undefined = undefined;
let address: Hex | null | undefined = undefined;
let resultingAddress: Hex | null | undefined = undefined;

const gasUtils = new GasUtils(publicClient, logger);

Expand Down Expand Up @@ -663,79 +663,31 @@ export async function deployL1Contract(
const salt = padHex(maybeSalt, { size: 32 });
const deployer: Hex = '0x4e59b44847b379578588920cA78FbF26c0B4956C';
const calldata = encodeDeployData({ abi, bytecode, args });
address = getContractAddress({ from: deployer, salt, bytecode: calldata, opcode: 'CREATE2' });
const existing = await publicClient.getBytecode({ address });
resultingAddress = getContractAddress({ from: deployer, salt, bytecode: calldata, opcode: 'CREATE2' });
const existing = await publicClient.getBytecode({ address: resultingAddress });

if (existing === undefined || existing === '0x') {
const { request } = await publicClient.simulateContract({
account: walletClient.account,
address: deployer,
abi: deployerAbi,
functionName: 'deploy',
args: [salt, calldata],
const receipt = await gasUtils.sendAndMonitorTransaction(walletClient, walletClient.account!, {
to: deployer,
data: concatHex([salt, calldata]),
});
txHash = receipt.transactionHash;

console.log('REQUEST', request);

txHash = await walletClient.writeContract(request);

// Add gas estimation and price buffering for CREATE2 deployment
// const deployData = encodeDeployData({ abi, bytecode, args });
// const gasEstimate = await gasUtils.estimateGas(() =>
// publicClient.estimateGas({
// account: walletClient.account?.address,
// data: deployData,
// }),
// );
// const gasPrice = await gasUtils.getGasPrice();

// txHash = await walletClient.sendTransaction({
// to: deployer,
// data: concatHex([salt, calldata]),
// gas: gasEstimate,
// maxFeePerGas: gasPrice,
// });
// logger?.verbose(
// `Deploying contract with salt ${salt} to address ${address} in tx ${txHash} (gas: ${gasEstimate}, price: ${gasPrice})`,
// );
logger?.verbose(`Deployed contract with salt ${salt} to address ${resultingAddress} in tx ${txHash}.`);
} else {
logger?.verbose(`Skipping existing deployment of contract with salt ${salt} to address ${address}`);
logger?.verbose(`Skipping existing deployment of contract with salt ${salt} to address ${resultingAddress}`);
}
} else {
// Regular deployment path
const deployData = encodeDeployData({ abi, bytecode, args });
const gasEstimate = await gasUtils.estimateGas(() =>
publicClient.estimateGas({
account: walletClient.account?.address,
data: deployData,
}),
);
const gasPrice = await gasUtils.getGasPrice();

txHash = await walletClient.deployContract({
abi,
bytecode,
args,
gas: gasEstimate,
maxFeePerGas: gasPrice,
});

// Monitor deployment transaction
await gasUtils.monitorTransaction(txHash, walletClient, {
const receipt = await gasUtils.sendAndMonitorTransaction(walletClient, walletClient.account!, {
to: '0x', // Contract creation
data: deployData,
nonce: await publicClient.getTransactionCount({ address: walletClient.account!.address }),
gasLimit: gasEstimate,
maxFeePerGas: gasPrice,
});

const receipt = await publicClient.waitForTransactionReceipt({
hash: txHash,
pollingInterval: 100,
timeout: 60_000, // 1min
});
address = receipt.contractAddress;
if (!address) {
txHash = receipt.transactionHash;
resultingAddress = receipt.contractAddress;
if (!resultingAddress) {
throw new Error(
`No contract address found in receipt: ${JSON.stringify(receipt, (_, val) =>
typeof val === 'bigint' ? String(val) : val,
Expand All @@ -744,6 +696,6 @@ export async function deployL1Contract(
}
}

return { address: EthAddress.fromString(address!), txHash };
return { address: EthAddress.fromString(resultingAddress!), txHash };
}
// docs:end:deployL1Contract
164 changes: 126 additions & 38 deletions yarn-project/ethereum/src/gas_utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { EthAddress } from '@aztec/foundation/eth-address';
import { createDebugLogger } from '@aztec/foundation/log';

import { createAnvil } from '@viem/anvil';
import { type Anvil, createAnvil } from '@viem/anvil';
import getPort from 'get-port';
import { createPublicClient, createWalletClient, http } from 'viem';
import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
Expand All @@ -9,6 +10,9 @@ import { foundry } from 'viem/chains';
import { GasUtils } from './gas_utils.js';

const MNEMONIC = 'test test test test test test test test test test test junk';
const WEI_CONST = 1_000_000_000n;
// Simple contract that just returns 42
const SIMPLE_CONTRACT_BYTECODE = '0x69602a60005260206000f3600052600a6016f3';

const startAnvil = async (l1BlockTime?: number) => {
const ethereumHostPort = await getPort();
Expand All @@ -21,22 +25,25 @@ const startAnvil = async (l1BlockTime?: number) => {
return { anvil, rpcUrl };
};

describe('e2e_l1_gas', () => {
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');

beforeAll(async () => {
const { rpcUrl } = await startAnvil(12);
const { anvil: anvilInstance, rpcUrl } = await startAnvil(1);
anvil = anvilInstance;
const hdAccount = mnemonicToAccount(MNEMONIC, { addressIndex: 0 });
const privKeyRaw = hdAccount.getHdKey().privateKey;
if (!privKeyRaw) {
// should never happen, used for types
throw new Error('Failed to get private key');
}
const privKey = Buffer.from(privKeyRaw).toString('hex');
const account = privateKeyToAccount(`0x${privKey}`);
account = privateKeyToAccount(`0x${privKey}`);

publicClient = createPublicClient({
transport: http(rpcUrl),
Expand All @@ -60,53 +67,48 @@ describe('e2e_l1_gas', () => {
},
{
maxAttempts: 3,
checkIntervalMs: 1000,
stallTimeMs: 3000,
gasPriceIncrease: 50n,
checkIntervalMs: 100,
stallTimeMs: 1000,
},
);
});
afterAll(async () => {
await anvil.stop();
}, 5000);

it('handles gas price spikes by increasing gas price', async () => {
// Get initial base fee and verify we're starting from a known state
const initialBlock = await publicClient.getBlock({ blockTag: 'latest' });
const initialBaseFee = initialBlock.baseFeePerGas ?? 0n;
it('sends and monitors a simple transaction', async () => {
const receipt = await gasUtils.sendAndMonitorTransaction(walletClient, account, {
to: '0x1234567890123456789012345678901234567890',
data: '0x',
value: 0n,
});

// Send initial transaction with current gas price
const initialGasPrice = await gasUtils.getGasPrice();
expect(initialGasPrice).toBeGreaterThanOrEqual(initialBaseFee); // Sanity check
expect(receipt.status).toBe('success');
}, 10_000);

it('handles gas price spikes by retrying with higher gas price', async () => {
// Get initial base fee
const initialBlock = await publicClient.getBlock({ blockTag: 'latest' });
initialBaseFee = initialBlock.baseFeePerGas ?? 0n;

const initialTxHash = await walletClient.sendTransaction({
// Start a transaction
const sendPromise = gasUtils.sendAndMonitorTransaction(walletClient, account, {
to: '0x1234567890123456789012345678901234567890',
data: '0x',
value: 0n,
maxFeePerGas: initialGasPrice,
gas: 21000n,
});

// Spike gas price to 3x the initial base fee
const spikeBaseFee = initialBaseFee * 3n;
await publicClient.transport.request({
method: 'anvil_setNextBlockBaseFeePerGas',
params: [spikeBaseFee.toString()],
params: [(initialBaseFee * 3n).toString()],
});

// Monitor the transaction - it should automatically increase gas price
const receipt = await gasUtils.monitorTransaction(initialTxHash, walletClient, {
to: '0x1234567890123456789012345678901234567890',
data: '0x',
nonce: await publicClient.getTransactionCount({ address: walletClient.account.address }),
gasLimit: 21000n,
maxFeePerGas: initialGasPrice,
});

// Transaction should eventually succeed
// Transaction should still complete
const receipt = await sendPromise;
expect(receipt.status).toBe('success');

// Gas price should have been increased from initial price
const finalGasPrice = receipt.effectiveGasPrice;
expect(finalGasPrice).toBeGreaterThan(initialGasPrice);

// Reset base fee to initial value for cleanup
// Reset base fee
await publicClient.transport.request({
method: 'anvil_setNextBlockBaseFeePerGas',
params: [initialBaseFee.toString()],
Expand All @@ -115,9 +117,95 @@ describe('e2e_l1_gas', () => {

it('respects max gas price limits during spikes', async () => {
const maxGwei = 500n;
const gasPrice = await gasUtils.getGasPrice();
const newBaseFee = (maxGwei - 10n) * WEI_CONST;

// Set base fee high but still under our max
await publicClient.transport.request({
method: 'anvil_setNextBlockBaseFeePerGas',
params: [newBaseFee.toString()],
});

// Mine a new block to make the base fee change take effect
await publicClient.transport.request({
method: 'evm_mine',
params: [],
});

const receipt = await gasUtils.sendAndMonitorTransaction(walletClient, account, {
to: '0x1234567890123456789012345678901234567890',
data: '0x',
value: 0n,
});

// Even with huge base fee, should not exceed max
expect(gasPrice).toBeLessThanOrEqual(maxGwei * 1000000000n);
expect(receipt.effectiveGasPrice).toBeLessThanOrEqual(maxGwei * WEI_CONST);

// Reset base fee
await publicClient.transport.request({
method: 'anvil_setNextBlockBaseFeePerGas',
params: [initialBaseFee.toString()],
});
await publicClient.transport.request({
method: 'evm_mine',
params: [],
});
});

it('adds appropriate buffer to gas estimation', async () => {
// First deploy without any buffer
const baselineGasUtils = new GasUtils(
publicClient,
logger,
{
bufferPercentage: 0n,
maxGwei: 500n,
minGwei: 1n,
priorityFeeGwei: 2n,
},
{
maxAttempts: 3,
checkIntervalMs: 100,
stallTimeMs: 1000,
},
);

const baselineTx = await baselineGasUtils.sendAndMonitorTransaction(walletClient, account, {
to: EthAddress.ZERO.toString(),
data: SIMPLE_CONTRACT_BYTECODE,
});

// Get the transaction details to see the gas limit
const baselineDetails = await publicClient.getTransaction({
hash: baselineTx.transactionHash,
});

// Now deploy with 20% buffer
const bufferedGasUtils = new GasUtils(
publicClient,
logger,
{
bufferPercentage: 20n,
maxGwei: 500n,
minGwei: 1n,
priorityFeeGwei: 2n,
},
{
maxAttempts: 3,
checkIntervalMs: 100,
stallTimeMs: 1000,
},
);

const bufferedTx = await bufferedGasUtils.sendAndMonitorTransaction(walletClient, account, {
to: EthAddress.ZERO.toString(),
data: SIMPLE_CONTRACT_BYTECODE,
});

const bufferedDetails = await publicClient.getTransaction({
hash: bufferedTx.transactionHash,
});

// The gas limit should be ~20% higher
expect(bufferedDetails.gas).toBeGreaterThan(baselineDetails.gas);
expect(bufferedDetails.gas).toBeLessThanOrEqual((baselineDetails.gas * 120n) / 100n);
}, 20_000);
});
Loading

0 comments on commit d066d51

Please sign in to comment.