Skip to content

Commit

Permalink
feat: Gas Utils for L1 operations (#9834)
Browse files Browse the repository at this point in the history
Fixes #9833 
Introducing `L1TxUtils` which is used for submitting & monitoring L1 transactions. The utils handle gas-pricing by adding safety buffers to ensure tx goes in + can monitor a stuck transaction and attempt to speed it up by re-submitting with a higher gas fee.
  • Loading branch information
spypsy authored Dec 4, 2024
1 parent d6985a8 commit 17fa214
Show file tree
Hide file tree
Showing 32 changed files with 1,429 additions and 447 deletions.
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"asyncify",
"auditability",
"authwit",
"Automine",
"autonat",
"autorun",
"awslogs",
Expand Down
4 changes: 2 additions & 2 deletions spartan/aztec-network/files/config/config-prover-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ set -eu

# Pass the bootnode url as an argument
# Ask the bootnode for l1 contract addresses
output=$(node --no-warnings /usr/src/yarn-project/aztec/dest/bin/index.js get-node-info -u $1)
output=$(node --no-warnings /usr/src/yarn-project/aztec/dest/bin/index.js get-node-info --node-url $1)

echo "$output"

Expand All @@ -20,7 +20,7 @@ governance_proposer_address=$(echo "$output" | grep -oP 'GovernanceProposer Addr
governance_address=$(echo "$output" | grep -oP 'Governance Address: \K0x[a-fA-F0-9]{40}')

# Write the addresses to a file in the shared volume
cat <<EOF > /shared/contracts/contracts.env
cat <<EOF >/shared/contracts/contracts.env
export BOOTSTRAP_NODES=$boot_node_enr
export ROLLUP_CONTRACT_ADDRESS=$rollup_address
export REGISTRY_CONTRACT_ADDRESS=$registry_address
Expand Down
6 changes: 2 additions & 4 deletions spartan/aztec-network/files/config/config-validator-env.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
#!/bin/bash
set -eu


# Pass the bootnode url as an argument
# Ask the bootnode for l1 contract addresses
output=$(node --no-warnings /usr/src/yarn-project/aztec/dest/bin/index.js get-node-info -u $1)
output=$(node --no-warnings /usr/src/yarn-project/aztec/dest/bin/index.js get-node-info --node-url $1)

echo "$output"

Expand All @@ -25,9 +24,8 @@ governance_address=$(echo "$output" | grep -oP 'Governance Address: \K0x[a-fA-F0
INDEX=$(echo $POD_NAME | awk -F'-' '{print $NF}')
private_key=$(jq -r ".[$INDEX]" /app/config/keys.json)


# Write the addresses to a file in the shared volume
cat <<EOF > /shared/contracts/contracts.env
cat <<EOF >/shared/contracts/contracts.env
export BOOTSTRAP_NODES=$boot_node_enr
export ROLLUP_CONTRACT_ADDRESS=$rollup_address
export REGISTRY_CONTRACT_ADDRESS=$registry_address
Expand Down
24 changes: 24 additions & 0 deletions yarn-project/aztec-node/src/aztec-node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
type L1_TO_L2_MSG_TREE_HEIGHT,
type NOTE_HASH_TREE_HEIGHT,
type NULLIFIER_TREE_HEIGHT,
type NodeInfo,
type NullifierLeafPreimage,
type PUBLIC_DATA_TREE_HEIGHT,
type PrivateLog,
Expand Down Expand Up @@ -237,6 +238,29 @@ export class AztecNodeService implements AztecNode {
return Promise.resolve(this.p2pClient.isReady() ?? false);
}

public async getNodeInfo(): Promise<NodeInfo> {
const [nodeVersion, protocolVersion, chainId, enr, contractAddresses, protocolContractAddresses] =
await Promise.all([
this.getNodeVersion(),
this.getVersion(),
this.getChainId(),
this.getEncodedEnr(),
this.getL1ContractAddresses(),
this.getProtocolContractAddresses(),
]);

const nodeInfo: NodeInfo = {
nodeVersion,
l1ChainId: chainId,
protocolVersion,
enr,
l1ContractAddresses: contractAddresses,
protocolContractAddresses: protocolContractAddresses,
};

return nodeInfo;
}

/**
* Get a block specified by its number.
* @param number - The block number being requested.
Expand Down
3 changes: 1 addition & 2 deletions yarn-project/aztec.js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export { ContractDeployer } from './deployment/index.js';
export {
AnvilTestWatcher,
CheatCodes,
EthCheatCodes,
L1FeeJuicePortalManager,
L1ToL2TokenPortalManager,
L1TokenManager,
Expand Down Expand Up @@ -165,7 +164,7 @@ export { elapsed } from '@aztec/foundation/timer';
export { type FieldsOf } from '@aztec/foundation/types';
export { fileURLToPath } from '@aztec/foundation/url';

export { deployL1Contract, deployL1Contracts, type DeployL1Contracts } from '@aztec/ethereum';
export { type DeployL1Contracts, EthCheatCodes, deployL1Contract, deployL1Contracts } from '@aztec/ethereum';

// Start of section that exports public api via granular api.
// Here you *can* do `export *` as the granular api defacto exports things explicitly.
Expand Down
247 changes: 1 addition & 246 deletions yarn-project/aztec.js/src/utils/cheat_codes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { type EpochProofClaim, type Note, type PXE } from '@aztec/circuit-types';
import { type AztecAddress, EthAddress, Fr } from '@aztec/circuits.js';
import { deriveStorageSlotInMap } from '@aztec/circuits.js/hash';
import { type L1ContractAddresses } from '@aztec/ethereum';
import { toBigIntBE, toHex } from '@aztec/foundation/bigint-buffer';
import { keccak256 } from '@aztec/foundation/crypto';
import { EthCheatCodes, type L1ContractAddresses } from '@aztec/ethereum';
import { createDebugLogger } from '@aztec/foundation/log';
import { RollupAbi } from '@aztec/l1-artifacts';

import fs from 'fs';
import {
type GetContractReturnType,
type Hex,
Expand Down Expand Up @@ -49,248 +46,6 @@ export class CheatCodes {
}
}

/**
* A class that provides utility functions for interacting with ethereum (L1).
*/
export class EthCheatCodes {
constructor(
/**
* The RPC URL to use for interacting with the chain
*/
public rpcUrl: string,
/**
* The logger to use for the eth cheatcodes
*/
public logger = createDebugLogger('aztec:cheat_codes:eth'),
) {}

async rpcCall(method: string, params: any[]) {
const paramsString = JSON.stringify(params);
const content = {
body: `{"jsonrpc":"2.0", "method": "${method}", "params": ${paramsString}, "id": 1}`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
};
return await (await fetch(this.rpcUrl, content)).json();
}

/**
* Get the auto mine status of the underlying chain
* @returns True if automine is on, false otherwise
*/
public async isAutoMining(): Promise<boolean> {
try {
const res = await this.rpcCall('anvil_getAutomine', []);
return res.result;
} catch (err) {
this.logger.error(`Calling "anvil_getAutomine" failed with:`, err);
}
return false;
}

/**
* Get the current blocknumber
* @returns The current block number
*/
public async blockNumber(): Promise<number> {
const res = await this.rpcCall('eth_blockNumber', []);
return parseInt(res.result, 16);
}

/**
* Get the current chainId
* @returns The current chainId
*/
public async chainId(): Promise<number> {
const res = await this.rpcCall('eth_chainId', []);
return parseInt(res.result, 16);
}

/**
* Get the current timestamp
* @returns The current timestamp
*/
public async timestamp(): Promise<number> {
const res = await this.rpcCall('eth_getBlockByNumber', ['latest', true]);
return parseInt(res.result.timestamp, 16);
}

/**
* Advance the chain by a number of blocks
* @param numberOfBlocks - The number of blocks to mine
* @returns The current chainId
*/
public async mine(numberOfBlocks = 1): Promise<void> {
const res = await this.rpcCall('hardhat_mine', [numberOfBlocks]);
if (res.error) {
throw new Error(`Error mining: ${res.error.message}`);
}
this.logger.verbose(`Mined ${numberOfBlocks} L1 blocks`);
}

/**
* Set the balance of an account
* @param account - The account to set the balance for
* @param balance - The balance to set
*/
public async setBalance(account: EthAddress, balance: bigint): Promise<void> {
const res = await this.rpcCall('anvil_setBalance', [account.toString(), toHex(balance)]);
if (res.error) {
throw new Error(`Error setting balance for ${account}: ${res.error.message}`);
}
this.logger.verbose(`Set balance for ${account} to ${balance}`);
}

/**
* Set the interval between blocks (block time)
* @param interval - The interval to use between blocks
*/
public async setBlockInterval(interval: number): Promise<void> {
const res = await this.rpcCall('anvil_setBlockTimestampInterval', [interval]);
if (res.error) {
throw new Error(`Error setting block interval: ${res.error.message}`);
}
this.logger.verbose(`Set L1 block interval to ${interval}`);
}

/**
* Set the next block timestamp
* @param timestamp - The timestamp to set the next block to
*/
public async setNextBlockTimestamp(timestamp: number): Promise<void> {
const res = await this.rpcCall('evm_setNextBlockTimestamp', [timestamp]);
if (res.error) {
throw new Error(`Error setting next block timestamp: ${res.error.message}`);
}
this.logger.verbose(`Set L1 next block timestamp to ${timestamp}`);
}

/**
* Set the next block timestamp and mines the block
* @param timestamp - The timestamp to set the next block to
*/
public async warp(timestamp: number | bigint): Promise<void> {
const res = await this.rpcCall('evm_setNextBlockTimestamp', [Number(timestamp)]);
if (res.error) {
throw new Error(`Error warping: ${res.error.message}`);
}
await this.mine();
this.logger.verbose(`Warped L1 timestamp to ${timestamp}`);
}

/**
* Dumps the current chain state to a file.
* @param fileName - The file name to dump state into
*/
public async dumpChainState(fileName: string): Promise<void> {
const res = await this.rpcCall('hardhat_dumpState', []);
if (res.error) {
throw new Error(`Error dumping state: ${res.error.message}`);
}
const jsonContent = JSON.stringify(res.result);
fs.writeFileSync(`${fileName}.json`, jsonContent, 'utf8');
this.logger.verbose(`Dumped state to ${fileName}`);
}

/**
* Loads the chain state from a file.
* @param fileName - The file name to load state from
*/
public async loadChainState(fileName: string): Promise<void> {
const data = JSON.parse(fs.readFileSync(`${fileName}.json`, 'utf8'));
const res = await this.rpcCall('hardhat_loadState', [data]);
if (res.error) {
throw new Error(`Error loading state: ${res.error.message}`);
}
this.logger.verbose(`Loaded state from ${fileName}`);
}

/**
* Load the value at a storage slot of a contract address on eth
* @param contract - The contract address
* @param slot - The storage slot
* @returns - The value at the storage slot
*/
public async load(contract: EthAddress, slot: bigint): Promise<bigint> {
const res = await this.rpcCall('eth_getStorageAt', [contract.toString(), toHex(slot), 'latest']);
return BigInt(res.result);
}

/**
* Set the value at a storage slot of a contract address on eth
* @param contract - The contract address
* @param slot - The storage slot
* @param value - The value to set the storage slot to
*/
public async store(contract: EthAddress, slot: bigint, value: bigint): Promise<void> {
// for the rpc call, we need to change value to be a 32 byte hex string.
const res = await this.rpcCall('hardhat_setStorageAt', [contract.toString(), toHex(slot), toHex(value, true)]);
if (res.error) {
throw new Error(`Error setting storage for contract ${contract} at ${slot}: ${res.error.message}`);
}
this.logger.verbose(`Set L1 storage for contract ${contract} at ${slot} to ${value}`);
}

/**
* Computes the slot value for a given map and key.
* @param baseSlot - The base slot of the map (specified in Aztec.nr contract)
* @param key - The key to lookup in the map
* @returns The storage slot of the value in the map
*/
public keccak256(baseSlot: bigint, key: bigint): bigint {
// abi encode (removing the 0x) - concat key and baseSlot (both padded to 32 bytes)
const abiEncoded = toHex(key, true).substring(2) + toHex(baseSlot, true).substring(2);
return toBigIntBE(keccak256(Buffer.from(abiEncoded, 'hex')));
}

/**
* Send transactions impersonating an externally owned account or contract.
* @param who - The address to impersonate
*/
public async startImpersonating(who: EthAddress | Hex): Promise<void> {
const res = await this.rpcCall('hardhat_impersonateAccount', [who.toString()]);
if (res.error) {
throw new Error(`Error impersonating ${who}: ${res.error.message}`);
}
this.logger.verbose(`Impersonating ${who}`);
}

/**
* Stop impersonating an account that you are currently impersonating.
* @param who - The address to stop impersonating
*/
public async stopImpersonating(who: EthAddress | Hex): Promise<void> {
const res = await this.rpcCall('hardhat_stopImpersonatingAccount', [who.toString()]);
if (res.error) {
throw new Error(`Error when stopping the impersonation of ${who}: ${res.error.message}`);
}
this.logger.verbose(`Stopped impersonating ${who}`);
}

/**
* Set the bytecode for a contract
* @param contract - The contract address
* @param bytecode - The bytecode to set
*/
public async etch(contract: EthAddress, bytecode: `0x${string}`): Promise<void> {
const res = await this.rpcCall('hardhat_setCode', [contract.toString(), bytecode]);
if (res.error) {
throw new Error(`Error setting bytecode for ${contract}: ${res.error.message}`);
}
this.logger.verbose(`Set bytecode for ${contract} to ${bytecode}`);
}

/**
* Get the bytecode for a contract
* @param contract - The contract address
* @returns The bytecode for the contract
*/
public async getBytecode(contract: EthAddress): Promise<`0x${string}`> {
const res = await this.rpcCall('eth_getCode', [contract.toString(), 'latest']);
return res.result;
}
}

/** Cheat codes for the L1 rollup contract. */
export class RollupCheatCodes {
private client: WalletClient & PublicClient;
Expand Down
Loading

0 comments on commit 17fa214

Please sign in to comment.