From 7dca6df2db01569ccc1deb3f356dd31df7450b13 Mon Sep 17 00:00:00 2001 From: spypsy Date: Fri, 8 Nov 2024 13:29:24 +0000 Subject: [PATCH 01/45] feat: Gas Utils for L1 operations --- .../aztec-node/src/aztec-node/server.ts | 24 +++ .../aztec/terraform/prover-node/main.tf | 2 +- .../src/interfaces/aztec-node.ts | 8 + yarn-project/cli/src/cmds/l1/index.ts | 2 +- .../cli/src/cmds/l1/update_l1_validators.ts | 10 +- .../cli/src/cmds/pxe/get_node_info.ts | 11 +- yarn-project/cli/src/cmds/pxe/index.ts | 16 +- .../scripts/native-network/boot-node.sh | 8 +- .../native-network/deploy-l1-contracts.sh | 33 ++- .../scripts/native-network/prover-node.sh | 6 +- .../end-to-end/scripts/native-network/pxe.sh | 11 +- .../scripts/native-network/test-transfer.sh | 1 + .../scripts/native-network/transaction-bot.sh | 23 +- .../scripts/native-network/validator.sh | 54 +++-- .../ethereum/src/deploy_l1_contracts.ts | 59 ++++- yarn-project/ethereum/src/gas_utils.ts | 201 ++++++++++++++++++ yarn-project/ethereum/src/index.ts | 3 +- .../src/publisher/l1-publisher.ts | 101 +++++++-- yarn-project/telemetry-client/src/config.ts | 6 +- 19 files changed, 493 insertions(+), 86 deletions(-) create mode 100644 yarn-project/ethereum/src/gas_utils.ts diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 5fcdc048252..6f78e95c4cb 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -43,6 +43,7 @@ import { type L1_TO_L2_MSG_TREE_HEIGHT, type NOTE_HASH_TREE_HEIGHT, type NULLIFIER_TREE_HEIGHT, + NodeInfo, type NullifierLeafPreimage, type PUBLIC_DATA_TREE_HEIGHT, type ProtocolContractAddresses, @@ -228,6 +229,29 @@ export class AztecNodeService implements AztecNode { return Promise.resolve(this.p2pClient.isReady() ?? false); } + public async getNodeInfo(): Promise { + 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. diff --git a/yarn-project/aztec/terraform/prover-node/main.tf b/yarn-project/aztec/terraform/prover-node/main.tf index 45bdfcb0be8..9200fb719d6 100644 --- a/yarn-project/aztec/terraform/prover-node/main.tf +++ b/yarn-project/aztec/terraform/prover-node/main.tf @@ -73,7 +73,7 @@ resource "aws_cloudwatch_log_group" "aztec-prover-node-log-group" { resource "aws_service_discovery_service" "aztec-prover-node" { count = local.node_count - name = "${var.DEPLOY_TAG}-aztec-prover-node-${count.index + 1}" + name = "${var.DEPLOY_TAG}aztecprovernode${count.index + 1}" health_check_custom_config { failure_threshold = 1 diff --git a/yarn-project/circuit-types/src/interfaces/aztec-node.ts b/yarn-project/circuit-types/src/interfaces/aztec-node.ts index e12578d7c44..d7f646685bc 100644 --- a/yarn-project/circuit-types/src/interfaces/aztec-node.ts +++ b/yarn-project/circuit-types/src/interfaces/aztec-node.ts @@ -6,6 +6,7 @@ import type { L1_TO_L2_MSG_TREE_HEIGHT, NOTE_HASH_TREE_HEIGHT, NULLIFIER_TREE_HEIGHT, + NodeInfo, PUBLIC_DATA_TREE_HEIGHT, ProtocolContractAddresses, } from '@aztec/circuits.js'; @@ -188,6 +189,13 @@ export interface AztecNode extends ProverCoordination { */ isReady(): Promise; + /** + * Returns the information about the server's node. Includes current Node version, compatible Noir version, + * L1 chain identifier, protocol version, and L1 address of the rollup contract. + * @returns - The node information. + */ + getNodeInfo(): Promise; + /** * Method to request blocks. Will attempt to return all requested blocks but will return only those available. * @param from - The start of the range of blocks to return. diff --git a/yarn-project/cli/src/cmds/l1/index.ts b/yarn-project/cli/src/cmds/l1/index.ts index 80c0d514c3b..5bb1ff71240 100644 --- a/yarn-project/cli/src/cmds/l1/index.ts +++ b/yarn-project/cli/src/cmds/l1/index.ts @@ -109,7 +109,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL 'test test test test test test test test test test test junk', ) .addOption(l1ChainIdOption) - .option('--validator ', 'ethereum address of the validator', parseEthereumAddress) + .option('--validator
', 'ethereum address of the validator', parseEthereumAddress) .option('--rollup
', 'ethereum address of the rollup contract', parseEthereumAddress) .action(async options => { const { removeL1Validator } = await import('./update_l1_validators.js'); diff --git a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts index e51863b39b9..ecc8ba6b504 100644 --- a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts +++ b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts @@ -1,6 +1,6 @@ import { EthCheatCodes } from '@aztec/aztec.js'; import { ETHEREUM_SLOT_DURATION, type EthAddress } from '@aztec/circuits.js'; -import { createEthereumChain } from '@aztec/ethereum'; +import { createEthereumChain, isAnvilTestChain } from '@aztec/ethereum'; import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; import { RollupAbi } from '@aztec/l1-artifacts'; @@ -53,9 +53,11 @@ export async function addL1Validator({ const txHash = await rollup.write.addValidator([validatorAddress.toString()]); dualLog(`Transaction hash: ${txHash}`); await publicClient.waitForTransactionReceipt({ hash: txHash }); - dualLog(`Funding validator on L1`); - const cheatCodes = new EthCheatCodes(rpcUrl, debugLogger); - await cheatCodes.setBalance(validatorAddress, 10n ** 20n); + if (isAnvilTestChain(chainId)) { + dualLog(`Funding validator on L1`); + const cheatCodes = new EthCheatCodes(rpcUrl, debugLogger); + await cheatCodes.setBalance(validatorAddress, 10n ** 20n); + } } export async function removeL1Validator({ diff --git a/yarn-project/cli/src/cmds/pxe/get_node_info.ts b/yarn-project/cli/src/cmds/pxe/get_node_info.ts index 473fb690e7d..19e2571aa35 100644 --- a/yarn-project/cli/src/cmds/pxe/get_node_info.ts +++ b/yarn-project/cli/src/cmds/pxe/get_node_info.ts @@ -1,8 +1,13 @@ -import { createCompatibleClient } from '@aztec/aztec.js'; +import { type AztecNode, type PXE, createAztecNodeClient, createCompatibleClient } from '@aztec/aztec.js'; import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; -export async function getNodeInfo(rpcUrl: string, debugLogger: DebugLogger, log: LogFn) { - const client = await createCompatibleClient(rpcUrl, debugLogger); +export async function getNodeInfo(rpcUrl: string, pxeRequest: boolean, debugLogger: DebugLogger, log: LogFn) { + let client: AztecNode | PXE; + if (pxeRequest) { + client = await createCompatibleClient(rpcUrl, debugLogger); + } else { + client = createAztecNodeClient(rpcUrl); + } const info = await client.getNodeInfo(); log(`Node Version: ${info.nodeVersion}`); log(`Chain Id: ${info.l1ChainId}`); diff --git a/yarn-project/cli/src/cmds/pxe/index.ts b/yarn-project/cli/src/cmds/pxe/index.ts index bc3e4969a88..1a63c2e9499 100644 --- a/yarn-project/cli/src/cmds/pxe/index.ts +++ b/yarn-project/cli/src/cmds/pxe/index.ts @@ -4,7 +4,9 @@ import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; import { type Command } from 'commander'; import { + LOCALHOST, logJson, + makePxeOption, parseAztecAddress, parseEthereumAddress, parseField, @@ -133,11 +135,19 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL program .command('get-node-info') - .description('Gets the information of an aztec node at a URL.') - .addOption(pxeOption) + .description('Gets the information of an Aztec node from a PXE or directly from an Aztec node.') + .option('--node-url ', 'URL of the node.', `http://${LOCALHOST}:8080`) + .option('--from-node', 'Get the info directly from an Aztec node.', false) + .addOption(makePxeOption(false)) .action(async options => { const { getNodeInfo } = await import('./get_node_info.js'); - await getNodeInfo(options.rpcUrl, debugLogger, log); + let url: string; + if (options.pxe) { + url = options.rpcUrl; + } else { + url = options.nodeUrl; + } + await getNodeInfo(url, options.pxe, debugLogger, log); }); program diff --git a/yarn-project/end-to-end/scripts/native-network/boot-node.sh b/yarn-project/end-to-end/scripts/native-network/boot-node.sh index 943bcdf4a4f..39067971ab9 100755 --- a/yarn-project/end-to-end/scripts/native-network/boot-node.sh +++ b/yarn-project/end-to-end/scripts/native-network/boot-node.sh @@ -13,7 +13,7 @@ exec > >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log") 2> >(tee -a "$(dirname export PORT=${PORT:-"8080"} export DEBUG=${DEBUG:-"aztec:*,-aztec:avm_simulator*,-aztec:libp2p_service*,-aztec:circuits:artifact_hash,-json-rpc*,-aztec:l2_block_stream,-aztec:world-state:*"} export LOG_LEVEL=${LOG_LEVEL:-"debug"} -export ETHEREUM_HOST="http://127.0.0.1:8545" +export ETHEREUM_HOST=${ETHEREUM_HOST:-"http://127.0.0.1:8545"} export P2P_ENABLED="true" export VALIDATOR_DISABLED="true" export SEQ_MAX_SECONDS_BETWEEN_BLOCKS="0" @@ -26,11 +26,11 @@ export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="${OTEL_EXPORTER_OTLP_METRICS_ENDPOIN export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-}" export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="${OTEL_EXPORTER_OTLP_LOGS_ENDPOINT:-}" export OTEL_RESOURCE_ATTRIBUTES="service.name=boot-node" -export VALIDATOR_PRIVATE_KEY="0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a" +export VALIDATOR_PRIVATE_KEY=${VALIDATOR_PRIVATE_KEY:-"0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a"} REPO=$(git rev-parse --show-toplevel) echo "Waiting for l1 contracts to be deployed..." -until [ -f "$REPO"/yarn-project/end-to-end/scripts/native-network/state/l1-contracts.env ] ; do +until [ -f "$REPO"/yarn-project/end-to-end/scripts/native-network/state/l1-contracts.env ]; do sleep 1 done echo "Done waiting." @@ -42,4 +42,4 @@ function filter_noise() { } # Start the Aztec node with the sequencer and archiver -node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js start --node --archiver --sequencer --pxe 2>&1 | filter_noise \ No newline at end of file +node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js start --node --archiver --sequencer 2>&1 | filter_noise diff --git a/yarn-project/end-to-end/scripts/native-network/deploy-l1-contracts.sh b/yarn-project/end-to-end/scripts/native-network/deploy-l1-contracts.sh index 4ee84d7f391..7afa9f60649 100755 --- a/yarn-project/end-to-end/scripts/native-network/deploy-l1-contracts.sh +++ b/yarn-project/end-to-end/scripts/native-network/deploy-l1-contracts.sh @@ -18,21 +18,33 @@ else INIT_VALIDATORS="false" fi -echo "Waiting for Anvil to be up at port 8545..." +export ETHEREUM_HOST=${ETHEREUM_HOST:-"http://127.0.0.1:8545"} +export L1_CHAIN_ID=${L1_CHAIN_ID:-"31337"} +export PRIVATE_KEY=${PRIVATE_KEY:-""} +export SALT=${SALT:-"1337"} + +echo "Waiting for Ethereum node to be up..." until curl -s -X POST -H 'Content-Type: application/json' \ --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ - http://127.0.0.1:8545 2>/dev/null | grep -q 'result' ; do + $ETHEREUM_HOST 2>/dev/null | grep -q 'result'; do sleep 1 done echo "Done waiting." -# Run the deploy-l1-contracts command and capture the output -export ETHEREUM_HOST="http://127.0.0.1:8545" -if [ "$INIT_VALIDATORS" = "true" ]; then - output=$(node --no-warnings $(git rev-parse --show-toplevel)/yarn-project/aztec/dest/bin/index.js deploy-l1-contracts --validators "$VALIDATOR_ADDRESSES" --salt 1337) -else - output=$(node --no-warnings $(git rev-parse --show-toplevel)/yarn-project/aztec/dest/bin/index.js deploy-l1-contracts --salt 1337) -fi +# Construct base command +COMMAND="node --no-warnings $(git rev-parse --show-toplevel)/yarn-project/aztec/dest/bin/index.js \ + deploy-l1-contracts \ + --rpc-url $ETHEREUM_HOST \ + --l1-chain-id $L1_CHAIN_ID \ + --salt $SALT" + +# Add validators if specified +[ "$INIT_VALIDATORS" = "true" ] && COMMAND="$COMMAND --validators $VALIDATOR_ADDRESSES" + +# Add private key if provided +[ -n "$PRIVATE_KEY" ] && COMMAND="$COMMAND --private-key $PRIVATE_KEY" + +output=$($COMMAND) echo "$output" @@ -48,9 +60,8 @@ SYSSTIA_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'Sysstia Address: \K0x[a-fA GEROUSIA_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'Gerousia Address: \K0x[a-fA-F0-9]{40}') APELLA_CONTRACT_ADDRESS=$(echo "$output" | grep -oP 'Apella Address: \K0x[a-fA-F0-9]{40}') - # Save contract addresses to state/l1-contracts.env -cat << EOCONFIG > $(git rev-parse --show-toplevel)/yarn-project/end-to-end/scripts/native-network/state/l1-contracts.env +cat <$(git rev-parse --show-toplevel)/yarn-project/end-to-end/scripts/native-network/state/l1-contracts.env export ROLLUP_CONTRACT_ADDRESS=$ROLLUP_CONTRACT_ADDRESS export REGISTRY_CONTRACT_ADDRESS=$REGISTRY_CONTRACT_ADDRESS export INBOX_CONTRACT_ADDRESS=$INBOX_CONTRACT_ADDRESS diff --git a/yarn-project/end-to-end/scripts/native-network/prover-node.sh b/yarn-project/end-to-end/scripts/native-network/prover-node.sh index c6388c91e39..0ee6bc55a68 100755 --- a/yarn-project/end-to-end/scripts/native-network/prover-node.sh +++ b/yarn-project/end-to-end/scripts/native-network/prover-node.sh @@ -14,11 +14,11 @@ exec > >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log") 2> >(tee -a "$(dirname REPO=$(git rev-parse --show-toplevel) echo "Waiting for l1 contracts to be deployed..." -until [ -f "$REPO"/yarn-project/end-to-end/scripts/native-network/state/l1-contracts.env ] ; do +until [ -f "$REPO"/yarn-project/end-to-end/scripts/native-network/state/l1-contracts.env ]; do sleep 1 done echo "Waiting for Aztec Node..." -until curl -s http://127.0.0.1:8080/status >/dev/null ; do +until curl -s http://127.0.0.1:8080/status >/dev/null; do sleep 1 done echo "Done waiting." @@ -34,7 +34,7 @@ export BOOTSTRAP_NODES=$(echo "$output" | grep -oP 'Node ENR: \K.*') # Set environment variables export LOG_LEVEL=${LOG_LEVEL:-"debug"} export DEBUG=${DEBUG:-"aztec:*,-aztec:avm_simulator*,-aztec:libp2p_service*,-aztec:circuits:artifact_hash,-json-rpc*,-aztec:l2_block_stream,-aztec:world-state:*"} -export ETHEREUM_HOST="http://127.0.0.1:8545" +export ETHEREUM_HOST=${ETHEREUM_HOST:-"http://127.0.0.1:8545"} export PROVER_AGENT_ENABLED="true" export PROVER_PUBLISHER_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" export PROVER_COORDINATION_NODE_URL="http://127.0.0.1:8080" diff --git a/yarn-project/end-to-end/scripts/native-network/pxe.sh b/yarn-project/end-to-end/scripts/native-network/pxe.sh index e02133cf943..c7db13a4c56 100755 --- a/yarn-project/end-to-end/scripts/native-network/pxe.sh +++ b/yarn-project/end-to-end/scripts/native-network/pxe.sh @@ -9,19 +9,20 @@ exec > >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log") 2> >(tee -a "$(dirname # Starts the PXE (Private eXecution Environment) service # Set environment variables -export ETHEREUM_HOST="http://127.0.0.1:8545" -export AZTEC_NODE_URL="http://127.0.0.1:8080" +export ETHEREUM_HOST=${ETHEREUM_HOST:-"http://127.0.0.1:8545"} +export AZTEC_NODE_URL=${AZTEC_NODE_URL:-"http://127.0.0.1:8080"} +export VALIDATOR_NODE_URL=${VALIDATOR_NODE_URL:-"http://127.0.0.1:8081"} export LOG_LEVEL=${LOG_LEVEL:-"debug"} export DEBUG="aztec:*" echo "Waiting for Aztec Node..." -until curl -s http://127.0.0.1:8080/status >/dev/null ; do +until curl -s $AZTEC_NODE_URL/status >/dev/null; do sleep 1 done # We need to also wait for the validator, as the initial node cannot # Produce blocks on it's own echo "Waiting for Validator 0..." -until curl -s http://127.0.0.1:8081/status >/dev/null ; do +until curl -s $VALIDATOR_NODE_URL/status >/dev/null; do sleep 1 done echo "Done waiting." @@ -31,4 +32,4 @@ function filter_noise() { } # Start the PXE service -node --no-warnings $(git rev-parse --show-toplevel)/yarn-project/aztec/dest/bin/index.js start --port=8079 --pxe 2>&1 | filter_noise \ No newline at end of file +node --no-warnings $(git rev-parse --show-toplevel)/yarn-project/aztec/dest/bin/index.js start --port=8079 --pxe 2>&1 | filter_noise diff --git a/yarn-project/end-to-end/scripts/native-network/test-transfer.sh b/yarn-project/end-to-end/scripts/native-network/test-transfer.sh index 50790afbe3e..e54d8966ede 100755 --- a/yarn-project/end-to-end/scripts/native-network/test-transfer.sh +++ b/yarn-project/end-to-end/scripts/native-network/test-transfer.sh @@ -11,6 +11,7 @@ exec > >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log") 2> >(tee -a "$(dirname export BOOTNODE_URL=${BOOTNODE_URL:-http://127.0.0.1:8080} export PXE_URL=${PXE_URL:-http://127.0.0.1:8079} export ETHEREUM_HOST=${ETHEREUM_HOST:-http://127.0.0.1:8545} +export K8S=${K8S:-false} REPO=$(git rev-parse --show-toplevel) # Run our test assuming the port in pxe.sh diff --git a/yarn-project/end-to-end/scripts/native-network/transaction-bot.sh b/yarn-project/end-to-end/scripts/native-network/transaction-bot.sh index 722bfdcf0ce..db65f208818 100755 --- a/yarn-project/end-to-end/scripts/native-network/transaction-bot.sh +++ b/yarn-project/end-to-end/scripts/native-network/transaction-bot.sh @@ -4,6 +4,9 @@ set -eu # Get the name of the script without the path and extension SCRIPT_NAME=$(basename "$0" .sh) +# Set the token contract to use +export BOT_TOKEN_CONTRACT=${BOT_TOKEN_CONTRACT:-"TokenContract"} + # Redirect stdout and stderr to .log while also printing to the console exec > >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log") 2> >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log" >&2) @@ -11,7 +14,7 @@ exec > >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log") 2> >(tee -a "$(dirname REPO=$(git rev-parse --show-toplevel) echo "Waiting for Aztec Node..." -until curl -s http://127.0.0.1:8080/status >/dev/null ; do +until curl -s http://127.0.0.1:8080/status >/dev/null; do sleep 1 done echo "Waiting for PXE service..." @@ -20,15 +23,19 @@ until curl -s -X POST -H 'content-type: application/json' \ http://127.0.0.1:8079 | grep -q '"enr:-'; do sleep 1 done -echo "Waiting for l2 contracts to be deployed..." -until [ -f "$REPO"/yarn-project/end-to-end/scripts/native-network/state/l2-contracts.env ] ; do - sleep 1 -done -echo "Done waiting." + +# Don't wait for l2 contracts if using EasyPrivateTokenContract +if [ "${BOT_TOKEN_CONTRACT:-TokenContract}" != "EasyPrivateTokenContract" ]; then + echo "Waiting for l2 contracts to be deployed..." + until [ -f "$REPO"/yarn-project/end-to-end/scripts/native-network/state/l2-contracts.env ]; do + sleep 1 + done + echo "Done waiting." +fi # Set environment variables -export ETHEREUM_HOST="http://127.0.0.1:8545" -export AZTEC_NODE_URL="http://127.0.0.1:8080" +export ETHEREUM_HOST=${ETHEREUM_HOST:-"http://127.0.0.1:8545"} +export AZTEC_NODE_URL=${AZTEC_NODE_URL:-"http://127.0.0.1:8080"} export LOG_LEVEL=${LOG_LEVEL:-"debug"} export DEBUG="aztec:*,-aztec:avm_simulator*,-aztec:libp2p_service*,-aztec:circuits:artifact_hash,-json-rpc*,-aztec:l2_block_stream,-aztec:world-state:*" export BOT_PRIVATE_KEY="0xcafe" diff --git a/yarn-project/end-to-end/scripts/native-network/validator.sh b/yarn-project/end-to-end/scripts/native-network/validator.sh index 518dbb9db97..10c86200bef 100755 --- a/yarn-project/end-to-end/scripts/native-network/validator.sh +++ b/yarn-project/end-to-end/scripts/native-network/validator.sh @@ -15,14 +15,14 @@ P2P_PORT="$2" REPO=$(git rev-parse --show-toplevel) echo "Waiting for l1 contracts to be deployed..." -until [ -f "$REPO"/yarn-project/end-to-end/scripts/native-network/state/l1-contracts.env ] ; do +until [ -f "$REPO"/yarn-project/end-to-end/scripts/native-network/state/l1-contracts.env ]; do sleep 1 done source "$REPO"/yarn-project/end-to-end/scripts/native-network/state/l1-contracts.env echo "Waiting for Aztec Node..." -until curl -s http://127.0.0.1:8080/status >/dev/null ; do +until curl -s http://127.0.0.1:8080/status >/dev/null; do sleep 1 done echo "Done waiting." @@ -37,15 +37,26 @@ output=$(node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js get-nod export BOOTSTRAP_NODES=$(echo "$output" | grep -oP 'Node ENR: \K.*') echo "BOOTSTRAP_NODES: $BOOTSTRAP_NODES" -# Generate a private key for the validator -json_account=$(node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js generate-l1-account) -export ADDRESS=$(echo $json_account | jq -r '.address') -export LOG_LEVEL=${LOG_LEVEL:-"debug"} -export VALIDATOR_PRIVATE_KEY=$(echo $json_account | jq -r '.privateKey') +# Generate a private key for the validator only if not already set +if [ -z "${VALIDATOR_PRIVATE_KEY:-}" ] || [ -z "${ADDRESS:-}" ]; then + echo "" + echo "" + echo "Generating L1 Validator account..." + echo "" + echo "" + json_account=$(node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js generate-l1-account) + export ADDRESS=$(echo $json_account | jq -r '.address') + export VALIDATOR_PRIVATE_KEY=$(echo $json_account | jq -r '.privateKey') + echo "Validator address: $ADDRESS" + echo "Validator private key: $VALIDATOR_PRIVATE_KEY" +fi + +export PRIVATE_KEY=${PRIVATE_KEY:-} export L1_PRIVATE_KEY=$VALIDATOR_PRIVATE_KEY export SEQ_PUBLISHER_PRIVATE_KEY=$VALIDATOR_PRIVATE_KEY export DEBUG=${DEBUG:-"aztec:*,-aztec:avm_simulator*,-aztec:libp2p_service*,-aztec:circuits:artifact_hash,-json-rpc*,-aztec:l2_block_stream,-aztec:world-state:*"} -export ETHEREUM_HOST="http://127.0.0.1:8545" +export ETHEREUM_HOST=${ETHEREUM_HOST:-"http://127.0.0.1:8545"} +export IS_ANVIL=${IS_ANVIL:-"true"} export P2P_ENABLED="true" export VALIDATOR_DISABLED="false" export SEQ_MAX_SECONDS_BETWEEN_BLOCKS="0" @@ -59,15 +70,24 @@ export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="${OTEL_EXPORTER_OTLP_METRICS_ENDPOIN export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-}" export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="${OTEL_EXPORTER_OTLP_LOGS_ENDPOINT:-}" -# Add L1 validator -# this may fail, so try 3 times -for i in {1..3}; do - node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js add-l1-validator --validator $ADDRESS --rollup $ROLLUP_CONTRACT_ADDRESS && break - sleep 1 -done +# Check if validator is already registered +echo "Checking if validator is already registered..." +debug_output=$(node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js debug-rollup --rollup $ROLLUP_CONTRACT_ADDRESS) +if echo "$debug_output" | grep -q "Validators:.*$ADDRESS"; then + echo "Validator $ADDRESS is already registered" +else + # Add L1 validator + # this may fail, so try 3 times + echo "Adding validator $ADDRESS..." + for i in {1..3}; do + node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js add-l1-validator --validator $ADDRESS --rollup $ROLLUP_CONTRACT_ADDRESS && break + sleep 1 + done +fi -# Fast forward epochs -node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js fast-forward-epochs --rollup $ROLLUP_CONTRACT_ADDRESS --count 1 +# Fast forward epochs if we're on an anvil chain +if [ "$IS_ANVIL" = "true" ]; then + node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js fast-forward-epochs --rollup $ROLLUP_CONTRACT_ADDRESS --count 1 +fi # Start the Validator Node with the sequencer and archiver node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js start --port="$PORT" --node --archiver --sequencer - diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 3bc0f440a6b..39a249f1518 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -51,6 +51,7 @@ import { type HDAccount, type PrivateKeyAccount, mnemonicToAccount, privateKeyTo import { foundry } from 'viem/chains'; import { isAnvilTestChain } from './ethereum_chain.js'; +import { GasUtils } from './gas_utils.js'; import { type L1ContractAddresses } from './l1_contract_addresses.js'; /** @@ -656,15 +657,63 @@ export async function deployL1Contract( const existing = await publicClient.getBytecode({ address }); if (existing === undefined || existing === '0x') { - txHash = await walletClient.sendTransaction({ to: deployer, data: concatHex([salt, calldata]) }); - logger?.verbose(`Deploying contract with salt ${salt} to address ${address} in tx ${txHash}`); + // Add gas estimation and price buffering for CREATE2 deployment + const deployData = encodeDeployData({ abi, bytecode, args }); + const gasUtils = new GasUtils(publicClient, logger); + 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})`, + ); } else { logger?.verbose(`Skipping existing deployment of contract with salt ${salt} to address ${address}`); } } else { - txHash = await walletClient.deployContract({ abi, bytecode, args }); - logger?.verbose(`Deploying contract in tx ${txHash}`); - const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash, pollingInterval: 100 }); + // Regular deployment path + const deployData = encodeDeployData({ abi, bytecode, args }); + const gasUtils = new GasUtils(publicClient, logger); + 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, { + 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) { throw new Error( diff --git a/yarn-project/ethereum/src/gas_utils.ts b/yarn-project/ethereum/src/gas_utils.ts new file mode 100644 index 00000000000..b23de48af52 --- /dev/null +++ b/yarn-project/ethereum/src/gas_utils.ts @@ -0,0 +1,201 @@ +import { type DebugLogger } from '@aztec/foundation/log'; +import { makeBackoff, retry } from '@aztec/foundation/retry'; + +import { + type Address, + type Hex, + type PublicClient, + type TransactionReceipt, + type WalletClient, + formatGwei, +} from 'viem'; + +// 1_000_000_000 Gwei = 1 ETH +// 1_000_000_000 Wei = 1 Gwei +// 1_000_000_000_000_000_000 Wei = 1 ETH + +export interface GasConfig { + /** + * How much to increase gas price by each attempt (percentage) + */ + bufferPercentage: bigint; + /** + * Maximum gas price in gwei + */ + maxGwei: bigint; + /** + * Minimum gas price in gwei + */ + minGwei: bigint; + /** + * Priority fee in gwei + */ + priorityFeeGwei: bigint; +} + +export interface TransactionMonitorConfig { + /** + * Maximum number of speed-up attempts + */ + maxAttempts: number; + /** + * How often to check tx status + */ + checkIntervalMs: number; + /** + * How long before considering tx stalled + */ + stallTimeMs: number; + /** + * How much to increase gas price by each attempt (percentage) + */ + gasPriceIncrease: bigint; +} + +const DEFAULT_GAS_CONFIG: GasConfig = { + bufferPercentage: 20n, + maxGwei: 500n, + minGwei: 1n, + priorityFeeGwei: 2n, +}; + +const DEFAULT_MONITOR_CONFIG: Required = { + maxAttempts: 3, + checkIntervalMs: 30_000, + stallTimeMs: 180_000, + gasPriceIncrease: 50n, +}; + +export class GasUtils { + private readonly gasConfig: GasConfig; + private readonly monitorConfig: TransactionMonitorConfig; + + constructor( + private readonly publicClient: PublicClient, + private readonly logger?: DebugLogger, + gasConfig?: GasConfig, + monitorConfig?: TransactionMonitorConfig, + ) { + this.gasConfig! = gasConfig ?? DEFAULT_GAS_CONFIG; + this.monitorConfig! = monitorConfig ?? DEFAULT_MONITOR_CONFIG; + } + + /** + * Gets the current gas price with safety buffer and bounds checking + */ + public async getGasPrice(): Promise { + const block = await this.publicClient.getBlock({ blockTag: 'latest' }); + const baseFee = block.baseFeePerGas ?? 0n; + const priorityFee = this.gasConfig.priorityFeeGwei * 1_000_000_000n; + + const baseWithBuffer = baseFee + (baseFee * this.gasConfig.bufferPercentage) / 100n; + const totalGasPrice = baseWithBuffer + priorityFee; + + const maxGasPrice = this.gasConfig.maxGwei * 1_000_000_000n; + const minGasPrice = this.gasConfig.minGwei * 1_000_000_000n; + + let finalGasPrice: bigint; + if (totalGasPrice < minGasPrice) { + finalGasPrice = minGasPrice; + } else if (totalGasPrice > maxGasPrice) { + finalGasPrice = maxGasPrice; + } else { + finalGasPrice = totalGasPrice; + } + + this.logger?.debug( + `Gas price calculation: baseFee=${baseFee}, withBuffer=${baseWithBuffer}, priority=${priorityFee}, final=${finalGasPrice}`, + ); + + return finalGasPrice; + } + + /** + * Estimates gas with retries and adds buffer + */ + public estimateGas Promise>(estimator: T): Promise { + return retry( + async () => { + const gas = await estimator(); + return gas + (gas * this.gasConfig.bufferPercentage) / 100n; + }, + 'gas estimation', + makeBackoff([1, 2, 3]), + this.logger, + false, + ); + } + + /** + * Monitors a transaction and attempts to speed it up if it gets stuck + * @param txHash - The hash of the transaction to monitor + * @param walletClient - The wallet client for sending replacement tx + * @param originalTx - The original transaction data for replacement + * @param config - Monitor configuration + */ + public async monitorTransaction( + txHash: Hex, + walletClient: WalletClient, + originalTx: { + to: Address; + data: Hex; + nonce: number; + gasLimit: bigint; + maxFeePerGas: bigint; + }, + config?: TransactionMonitorConfig, + ): Promise { + const cfg = { ...this.monitorConfig, ...config }; + let attempts = 0; + let lastSeen = Date.now(); + let currentTxHash = txHash; + + while (true) { + try { + // Check transaction status + const receipt = await this.publicClient.getTransactionReceipt({ hash: currentTxHash }); + if (receipt) { + return receipt; + } + + // Check if transaction is pending + const tx = await this.publicClient.getTransaction({ hash: currentTxHash }); + if (tx) { + lastSeen = Date.now(); + await new Promise(resolve => setTimeout(resolve, cfg.checkIntervalMs)); + continue; + } + + // Transaction not found and enough time has passed - might be stuck + if (Date.now() - lastSeen > cfg.stallTimeMs && attempts < cfg.maxAttempts) { + attempts++; + const newGasPrice = (originalTx.maxFeePerGas * (100n + cfg.gasPriceIncrease)) / 100n; + + this.logger?.info( + `Transaction ${currentTxHash} appears stuck. Attempting speed-up ${attempts}/${cfg.maxAttempts} ` + + `with new gas price ${formatGwei(newGasPrice)} gwei`, + ); + + // Send replacement transaction with higher gas price + const account = await walletClient.getAddresses().then(addresses => addresses[0]); + currentTxHash = await walletClient.sendTransaction({ + chain: null, + account, + to: originalTx.to, + data: originalTx.data, + nonce: originalTx.nonce, + gas: originalTx.gasLimit, + maxFeePerGas: newGasPrice, + }); + + lastSeen = Date.now(); + } + + await new Promise(resolve => setTimeout(resolve, cfg.checkIntervalMs)); + } catch (err: any) { + this.logger?.warn(`Error monitoring transaction ${currentTxHash}:`, err); + await new Promise(resolve => setTimeout(resolve, cfg.checkIntervalMs)); + } + } + } +} diff --git a/yarn-project/ethereum/src/index.ts b/yarn-project/ethereum/src/index.ts index 2e39ebaa8f5..198fc960156 100644 --- a/yarn-project/ethereum/src/index.ts +++ b/yarn-project/ethereum/src/index.ts @@ -1,6 +1,7 @@ export * from './constants.js'; export * from './deploy_l1_contracts.js'; +export * from './ethereum_chain.js'; +export * from './gas_utils.js'; export * from './l1_contract_addresses.js'; export * from './l1_reader.js'; -export * from './ethereum_chain.js'; export * from './utils.js'; diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index 4486cd6c859..89538d48ec8 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -17,7 +17,7 @@ import { type Proof, type RootRollupPublicInputs, } from '@aztec/circuits.js'; -import { createEthereumChain } from '@aztec/ethereum'; +import { GasUtils, createEthereumChain } from '@aztec/ethereum'; import { makeTuple } from '@aztec/foundation/array'; import { areArraysEqual, compactArray, times } from '@aztec/foundation/collection'; import { type Signature } from '@aztec/foundation/eth-signature'; @@ -161,6 +161,8 @@ export class L1Publisher { public static PROPOSE_GAS_GUESS: bigint = 12_000_000n; public static PROPOSE_AND_CLAIM_GAS_GUESS: bigint = this.PROPOSE_GAS_GUESS + 100_000n; + private readonly gasUtils: GasUtils; + constructor(config: TxSenderConfig & PublisherConfig, client: TelemetryClient) { this.sleepTimeMs = config?.l1PublishRetryIntervalMS ?? 60_000; this.metrics = new L1PublisherMetrics(client, 'L1Publisher'); @@ -195,6 +197,23 @@ export class L1Publisher { client: this.walletClient, }); } + + this.gasUtils = new GasUtils( + this.publicClient, + this.log, + { + bufferPercentage: 20n, + maxGwei: 500n, + minGwei: 1n, + priorityFeeGwei: 15n / 10n, + }, + { + maxAttempts: 5, + checkIntervalMs: 30_000, + stallTimeMs: 120_000, + gasPriceIncrease: 75n, + }, + ); } public getPayLoad() { @@ -693,8 +712,33 @@ export class L1Publisher { const proofHex: Hex = `0x${args.proof.withoutPublicInputs().toString('hex')}`; const txArgs = [...this.getSubmitEpochProofArgs(args), proofHex] as const; this.log.info(`SubmitEpochProof proofSize=${args.proof.withoutPublicInputs().length} bytes`); - await this.rollupContract.simulate.submitEpochRootProof(txArgs, { account: this.account }); - return await this.rollupContract.write.submitEpochRootProof(txArgs, { account: this.account }); + + // 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, { + 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; } catch (err) { this.log.error(`Rollup submit epoch proof failed`, err); return undefined; @@ -703,14 +747,16 @@ export class L1Publisher { 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.publicClient.estimateGas({ - to: this.rollupContract.address, - data: encodeFunctionData({ - abi: this.rollupContract.abi, - functionName: 'computeTxsEffectsHash', - args: [`0x${encodedData.body.toString('hex')}`], + 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 @@ -718,6 +764,7 @@ export class L1Publisher { // 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(); const attestations = encodedData.attestations ? encodedData.attestations.map(attest => attest.toViemSignature()) @@ -732,7 +779,7 @@ export class L1Publisher { `0x${encodedData.body.toString('hex')}`, ] as const; - return { args, gasGuesstimate }; + return { args, gasGuesstimate, maxFeePerGas }; } private getSubmitEpochProofArgs(args: { @@ -768,13 +815,32 @@ export class L1Publisher { return undefined; } try { - const { args, gasGuesstimate } = await this.prepareProposeTx(encodedData, L1Publisher.PROPOSE_GAS_GUESS); + const { args, gasGuesstimate, maxFeePerGas } = await this.prepareProposeTx( + encodedData, + L1Publisher.PROPOSE_GAS_GUESS, + ); - return { - hash: await this.rollupContract.write.propose(args, { - account: this.account, - gas: gasGuesstimate, + 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, args, functionName: 'propose', gasLimit: gasGuesstimate, @@ -794,7 +860,7 @@ export class L1Publisher { return undefined; } try { - const { args, gasGuesstimate } = await this.prepareProposeTx( + const { args, gasGuesstimate, maxFeePerGas } = await this.prepareProposeTx( encodedData, L1Publisher.PROPOSE_AND_CLAIM_GAS_GUESS, ); @@ -805,6 +871,7 @@ export class L1Publisher { hash: await this.rollupContract.write.proposeAndClaim([...args, quote.toViemArgs()], { account: this.account, gas: gasGuesstimate, + maxFeePerGas, }), functionName: 'proposeAndClaim', args, diff --git a/yarn-project/telemetry-client/src/config.ts b/yarn-project/telemetry-client/src/config.ts index c48a9be6bc4..2ea4d90c5aa 100644 --- a/yarn-project/telemetry-client/src/config.ts +++ b/yarn-project/telemetry-client/src/config.ts @@ -12,17 +12,17 @@ export const telemetryClientConfigMappings: ConfigMappingsType new URL(val), + parseEnv: (val: string) => val && new URL(val), }, tracesCollectorUrl: { env: 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', description: 'The URL of the telemetry collector for traces', - parseEnv: (val: string) => new URL(val), + parseEnv: (val: string) => val && new URL(val), }, logsCollectorUrl: { env: 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT', description: 'The URL of the telemetry collector for logs', - parseEnv: (val: string) => new URL(val), + parseEnv: (val: string) => val && new URL(val), }, serviceName: { env: 'OTEL_SERVICE_NAME', From 8a0edaa55b779e15bce287bfeb573ecbdf8398d3 Mon Sep 17 00:00:00 2001 From: spypsy Date: Fri, 8 Nov 2024 13:35:01 +0000 Subject: [PATCH 02/45] undo irrelevant change --- yarn-project/aztec/terraform/prover-node/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/aztec/terraform/prover-node/main.tf b/yarn-project/aztec/terraform/prover-node/main.tf index 9200fb719d6..45bdfcb0be8 100644 --- a/yarn-project/aztec/terraform/prover-node/main.tf +++ b/yarn-project/aztec/terraform/prover-node/main.tf @@ -73,7 +73,7 @@ resource "aws_cloudwatch_log_group" "aztec-prover-node-log-group" { resource "aws_service_discovery_service" "aztec-prover-node" { count = local.node_count - name = "${var.DEPLOY_TAG}aztecprovernode${count.index + 1}" + name = "${var.DEPLOY_TAG}-aztec-prover-node-${count.index + 1}" health_check_custom_config { failure_threshold = 1 From fdfc143918bb96bce0200c866a6ad8dc19423ba9 Mon Sep 17 00:00:00 2001 From: spypsy Date: Fri, 8 Nov 2024 13:36:22 +0000 Subject: [PATCH 03/45] update some logging --- .../end-to-end/scripts/native-network/validator.sh | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/yarn-project/end-to-end/scripts/native-network/validator.sh b/yarn-project/end-to-end/scripts/native-network/validator.sh index 10c86200bef..c7a07108271 100755 --- a/yarn-project/end-to-end/scripts/native-network/validator.sh +++ b/yarn-project/end-to-end/scripts/native-network/validator.sh @@ -39,16 +39,10 @@ echo "BOOTSTRAP_NODES: $BOOTSTRAP_NODES" # Generate a private key for the validator only if not already set if [ -z "${VALIDATOR_PRIVATE_KEY:-}" ] || [ -z "${ADDRESS:-}" ]; then - echo "" - echo "" - echo "Generating L1 Validator account..." - echo "" - echo "" + echo "Generating new L1 Validator account..." json_account=$(node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js generate-l1-account) export ADDRESS=$(echo $json_account | jq -r '.address') export VALIDATOR_PRIVATE_KEY=$(echo $json_account | jq -r '.privateKey') - echo "Validator address: $ADDRESS" - echo "Validator private key: $VALIDATOR_PRIVATE_KEY" fi export PRIVATE_KEY=${PRIVATE_KEY:-} From 347d750f3a79eed8f7f50d9a94cb4087ed90a57c Mon Sep 17 00:00:00 2001 From: spypsy Date: Mon, 11 Nov 2024 14:59:23 +0000 Subject: [PATCH 04/45] add e2e test --- .../scripts/native-network/prover-node.sh | 2 +- .../scripts/native-network/transaction-bot.sh | 6 +- .../end-to-end/src/e2e_l1_gas.test.ts | 104 ++++++++++++++++++ yarn-project/ethereum/src/gas_utils.ts | 20 ++-- 4 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 yarn-project/end-to-end/src/e2e_l1_gas.test.ts diff --git a/yarn-project/end-to-end/scripts/native-network/prover-node.sh b/yarn-project/end-to-end/scripts/native-network/prover-node.sh index 0ee6bc55a68..3decda02873 100755 --- a/yarn-project/end-to-end/scripts/native-network/prover-node.sh +++ b/yarn-project/end-to-end/scripts/native-network/prover-node.sh @@ -36,7 +36,7 @@ export LOG_LEVEL=${LOG_LEVEL:-"debug"} export DEBUG=${DEBUG:-"aztec:*,-aztec:avm_simulator*,-aztec:libp2p_service*,-aztec:circuits:artifact_hash,-json-rpc*,-aztec:l2_block_stream,-aztec:world-state:*"} export ETHEREUM_HOST=${ETHEREUM_HOST:-"http://127.0.0.1:8545"} export PROVER_AGENT_ENABLED="true" -export PROVER_PUBLISHER_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +export PROVER_PUBLISHER_PRIVATE_KEY=${PROVER_PUBLISHER_PRIVATE_KEY:-"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"} export PROVER_COORDINATION_NODE_URL="http://127.0.0.1:8080" export AZTEC_NODE_URL="http://127.0.0.1:8080" export PROVER_JOB_SOURCE_URL="http://127.0.0.1:$PORT" diff --git a/yarn-project/end-to-end/scripts/native-network/transaction-bot.sh b/yarn-project/end-to-end/scripts/native-network/transaction-bot.sh index db65f208818..a42c2417ffd 100755 --- a/yarn-project/end-to-end/scripts/native-network/transaction-bot.sh +++ b/yarn-project/end-to-end/scripts/native-network/transaction-bot.sh @@ -6,6 +6,7 @@ SCRIPT_NAME=$(basename "$0" .sh) # Set the token contract to use export BOT_TOKEN_CONTRACT=${BOT_TOKEN_CONTRACT:-"TokenContract"} +export BOT_PXE_URL=${BOT_PXE_URL:-"http://127.0.0.1:8079"} # Redirect stdout and stderr to .log while also printing to the console exec > >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log") 2> >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log" >&2) @@ -20,7 +21,7 @@ done echo "Waiting for PXE service..." until curl -s -X POST -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","method":"pxe_getNodeInfo","params":[],"id":67}' \ - http://127.0.0.1:8079 | grep -q '"enr:-'; do + $BOT_PXE_URL | grep -q '"enr:-'; do sleep 1 done @@ -49,4 +50,5 @@ export PXE_PROVER_ENABLED="false" export PROVER_REAL_PROOFS="false" # Start the bot -node --no-warnings $(git rev-parse --show-toplevel)/yarn-project/aztec/dest/bin/index.js start --port=8077 --pxe --bot + +node --no-warnings $REPO/yarn-project/aztec/dest/bin/index.js start --port=8077 --bot --pxe diff --git a/yarn-project/end-to-end/src/e2e_l1_gas.test.ts b/yarn-project/end-to-end/src/e2e_l1_gas.test.ts new file mode 100644 index 00000000000..580eb45f23a --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_l1_gas.test.ts @@ -0,0 +1,104 @@ +import { GasUtils } from '@aztec/ethereum'; +import { createDebugLogger } from '@aztec/foundation/log'; + +import { createPublicClient, createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { foundry } from 'viem/chains'; + +import { getPrivateKeyFromIndex, startAnvil } from './fixtures/utils.js'; + +describe('e2e_l1_gas', () => { + let gasUtils: GasUtils; + let publicClient: any; + let walletClient: any; + const logger = createDebugLogger('l1_gas_test'); + + beforeAll(async () => { + const res = await startAnvil(12); + const rpcUrl = res.rpcUrl; + const privKey = getPrivateKeyFromIndex(0); + const account = privateKeyToAccount(`0x${privKey?.toString('hex')}`); + + publicClient = createPublicClient({ + transport: http(rpcUrl), + chain: foundry, + }); + + walletClient = createWalletClient({ + transport: http(rpcUrl), + chain: foundry, + account, + }); + + gasUtils = new GasUtils( + publicClient, + logger, + { + bufferPercentage: 20n, + maxGwei: 500n, + minGwei: 1n, + priorityFeeGwei: 2n, + }, + { + maxAttempts: 3, + checkIntervalMs: 1000, + stallTimeMs: 3000, + gasPriceIncrease: 50n, + }, + ); + }); + + 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; + + // Send initial transaction with current gas price + const initialGasPrice = await gasUtils.getGasPrice(); + expect(initialGasPrice).toBeGreaterThanOrEqual(initialBaseFee); // Sanity check + + const initialTxHash = await walletClient.sendTransaction({ + to: '0x1234567890123456789012345678901234567890', + 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()], + }); + + // 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 + 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 + await publicClient.transport.request({ + method: 'anvil_setNextBlockBaseFeePerGas', + params: [initialBaseFee.toString()], + }); + }); + + it('respects max gas price limits during spikes', async () => { + const maxGwei = 500n; + const gasPrice = await gasUtils.getGasPrice(); + + // Even with huge base fee, should not exceed max + expect(gasPrice).toBeLessThanOrEqual(maxGwei * 1000000000n); + }); +}); diff --git a/yarn-project/ethereum/src/gas_utils.ts b/yarn-project/ethereum/src/gas_utils.ts index b23de48af52..742e37dac61 100644 --- a/yarn-project/ethereum/src/gas_utils.ts +++ b/yarn-project/ethereum/src/gas_utils.ts @@ -1,5 +1,6 @@ import { type DebugLogger } from '@aztec/foundation/log'; import { makeBackoff, retry } from '@aztec/foundation/retry'; +import { sleep } from '@aztec/foundation/sleep'; import { type Address, @@ -143,9 +144,9 @@ export class GasUtils { gasLimit: bigint; maxFeePerGas: bigint; }, - config?: TransactionMonitorConfig, + txMonitorConfig?: TransactionMonitorConfig, ): Promise { - const cfg = { ...this.monitorConfig, ...config }; + const config = { ...this.monitorConfig, ...txMonitorConfig }; let attempts = 0; let lastSeen = Date.now(); let currentTxHash = txHash; @@ -155,24 +156,26 @@ export class GasUtils { // Check transaction status const receipt = await this.publicClient.getTransactionReceipt({ hash: currentTxHash }); if (receipt) { + this.logger?.info(`Transaction ${currentTxHash} confirmed`); return receipt; } // Check if transaction is pending const tx = await this.publicClient.getTransaction({ hash: currentTxHash }); + this.logger?.info(`Transaction ${currentTxHash} pending`); if (tx) { lastSeen = Date.now(); - await new Promise(resolve => setTimeout(resolve, cfg.checkIntervalMs)); + await new Promise(resolve => setTimeout(resolve, config.checkIntervalMs)); continue; } // Transaction not found and enough time has passed - might be stuck - if (Date.now() - lastSeen > cfg.stallTimeMs && attempts < cfg.maxAttempts) { + if (Date.now() - lastSeen > config.stallTimeMs && attempts < config.maxAttempts) { attempts++; - const newGasPrice = (originalTx.maxFeePerGas * (100n + cfg.gasPriceIncrease)) / 100n; + const newGasPrice = (originalTx.maxFeePerGas * (100n + config.gasPriceIncrease)) / 100n; this.logger?.info( - `Transaction ${currentTxHash} appears stuck. Attempting speed-up ${attempts}/${cfg.maxAttempts} ` + + `Transaction ${currentTxHash} appears stuck. Attempting speed-up ${attempts}/${config.maxAttempts} ` + `with new gas price ${formatGwei(newGasPrice)} gwei`, ); @@ -191,10 +194,11 @@ export class GasUtils { lastSeen = Date.now(); } - await new Promise(resolve => setTimeout(resolve, cfg.checkIntervalMs)); + // await new Promise(resolve => setTimeout(resolve, cfg.checkIntervalMs)); + await sleep(config.checkIntervalMs); } catch (err: any) { this.logger?.warn(`Error monitoring transaction ${currentTxHash}:`, err); - await new Promise(resolve => setTimeout(resolve, cfg.checkIntervalMs)); + await new Promise(resolve => setTimeout(resolve, config.checkIntervalMs)); } } } From d01a31625adf5e4a8d20566e2071189168fec874 Mon Sep 17 00:00:00 2001 From: spypsy Date: Mon, 11 Nov 2024 15:00:13 +0000 Subject: [PATCH 05/45] add to E2E test config --- yarn-project/end-to-end/scripts/e2e_test_config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn-project/end-to-end/scripts/e2e_test_config.yml b/yarn-project/end-to-end/scripts/e2e_test_config.yml index 869813c0f3d..92c5b834064 100644 --- a/yarn-project/end-to-end/scripts/e2e_test_config.yml +++ b/yarn-project/end-to-end/scripts/e2e_test_config.yml @@ -53,6 +53,7 @@ tests: test_path: 'e2e_fees/private_refunds.test.ts' e2e_keys: {} e2e_l1_with_wall_time: {} + e2e_l1_gas: {} e2e_lending_contract: {} e2e_event_logs: {} e2e_max_block_number: {} From 427c836f646a276dea7727770765bb1cac515b4d Mon Sep 17 00:00:00 2001 From: spypsy Date: Mon, 11 Nov 2024 15:40:31 +0000 Subject: [PATCH 06/45] rm old import --- yarn-project/cli/src/cmds/l1/update_l1_validators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts index 18c175f36b7..eab092c3edf 100644 --- a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts +++ b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts @@ -1,6 +1,6 @@ import { EthCheatCodes } from '@aztec/aztec.js'; -import { ETHEREUM_SLOT_DURATION, type EthAddress } from '@aztec/circuits.js'; +import { type EthAddress } from '@aztec/circuits.js'; import { createEthereumChain, isAnvilTestChain, getL1ContractsConfigEnvVars } from '@aztec/ethereum'; import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; import { RollupAbi } from '@aztec/l1-artifacts'; From 1adc20e5042db2153e813aa3ce9b97de1b44d3df Mon Sep 17 00:00:00 2001 From: spypsy Date: Wed, 13 Nov 2024 11:09:21 +0000 Subject: [PATCH 07/45] small fixes --- .../end-to-end/scripts/native-network/validator.sh | 7 ++++++- .../end-to-end/scripts/native-network/validators.sh | 7 ++++++- yarn-project/ethereum/src/gas_utils.ts | 9 +++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/yarn-project/end-to-end/scripts/native-network/validator.sh b/yarn-project/end-to-end/scripts/native-network/validator.sh index c7a07108271..e3c56de9880 100755 --- a/yarn-project/end-to-end/scripts/native-network/validator.sh +++ b/yarn-project/end-to-end/scripts/native-network/validator.sh @@ -10,6 +10,11 @@ exec > >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log") 2> >(tee -a "$(dirname # PORTS PORT="$1" P2P_PORT="$2" +ADDRESS="${3:-${ADDRESS:-}}" +export VALIDATOR_PRIVATE_KEY="${4:-${VALIDATOR_PRIVATE_KEY:-}}" + +echo "ADDRESS: $ADDRESS" +echo "VALIDATOR_PRIVATE_KEY: $VALIDATOR_PRIVATE_KEY" # Starts the Validator Node REPO=$(git rev-parse --show-toplevel) @@ -45,7 +50,7 @@ if [ -z "${VALIDATOR_PRIVATE_KEY:-}" ] || [ -z "${ADDRESS:-}" ]; then export VALIDATOR_PRIVATE_KEY=$(echo $json_account | jq -r '.privateKey') fi -export PRIVATE_KEY=${PRIVATE_KEY:-} +export PRIVATE_KEY=${VALIDATOR_PRIVATE_KEY:-} export L1_PRIVATE_KEY=$VALIDATOR_PRIVATE_KEY export SEQ_PUBLISHER_PRIVATE_KEY=$VALIDATOR_PRIVATE_KEY export DEBUG=${DEBUG:-"aztec:*,-aztec:avm_simulator*,-aztec:libp2p_service*,-aztec:circuits:artifact_hash,-json-rpc*,-aztec:l2_block_stream,-aztec:world-state:*"} diff --git a/yarn-project/end-to-end/scripts/native-network/validators.sh b/yarn-project/end-to-end/scripts/native-network/validators.sh index 6a9ac7f4f40..97c9c2251a5 100755 --- a/yarn-project/end-to-end/scripts/native-network/validators.sh +++ b/yarn-project/end-to-end/scripts/native-network/validators.sh @@ -20,7 +20,12 @@ for ((i=0; i Date: Wed, 13 Nov 2024 11:14:57 +0000 Subject: [PATCH 08/45] rm pk logging --- yarn-project/end-to-end/scripts/native-network/validator.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/yarn-project/end-to-end/scripts/native-network/validator.sh b/yarn-project/end-to-end/scripts/native-network/validator.sh index e3c56de9880..d8634f8ad3a 100755 --- a/yarn-project/end-to-end/scripts/native-network/validator.sh +++ b/yarn-project/end-to-end/scripts/native-network/validator.sh @@ -13,9 +13,6 @@ P2P_PORT="$2" ADDRESS="${3:-${ADDRESS:-}}" export VALIDATOR_PRIVATE_KEY="${4:-${VALIDATOR_PRIVATE_KEY:-}}" -echo "ADDRESS: $ADDRESS" -echo "VALIDATOR_PRIVATE_KEY: $VALIDATOR_PRIVATE_KEY" - # Starts the Validator Node REPO=$(git rev-parse --show-toplevel) From 13d731a962c7d543cfea47a14248b1ce09df0350 Mon Sep 17 00:00:00 2001 From: spypsy Date: Wed, 13 Nov 2024 12:56:20 +0000 Subject: [PATCH 09/45] formatting & test issues --- .../aztec-node/src/aztec-node/server.ts | 2 +- .../src/publisher/l1-publisher.test.ts | 21 ++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 0e9081ceea9..cf3f9b787d3 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -43,7 +43,7 @@ import { type L1_TO_L2_MSG_TREE_HEIGHT, type NOTE_HASH_TREE_HEIGHT, type NULLIFIER_TREE_HEIGHT, - NodeInfo, + type NodeInfo, type NullifierLeafPreimage, type PUBLIC_DATA_TREE_HEIGHT, type ProtocolContractAddresses, diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts index 3fd8ced0837..9805fb59ae9 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts @@ -7,7 +7,7 @@ import { RollupAbi } from '@aztec/l1-artifacts'; import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; import { type MockProxy, mock } from 'jest-mock-extended'; -import { type GetTransactionReceiptReturnType, type PrivateKeyAccount } from 'viem'; +import { type GetTransactionReceiptReturnType, type PrivateKeyAccount, type WalletClient } from 'viem'; import { type PublisherConfig, type TxSenderConfig } from './config.js'; import { L1Publisher } from './l1-publisher.js'; @@ -19,6 +19,16 @@ interface MockPublicClient { estimateGas: ({ to, data }: { to: '0x${string}'; data: '0x${string}' }) => Promise; } +interface MockGasUtils { + getGasPrice: () => Promise; + estimateGas: ({ to, data }: { to: '0x${string}'; data: '0x${string}' }) => Promise; + monitorTransaction: ( + txHash: `0x${string}`, + walletClient: WalletClient, + options: { to: `0x${string}`; data: `0x${string}`; nonce: bigint; gasLimit: bigint; maxFeePerGas: bigint }, + ) => Promise; +} + interface MockRollupContractWrite { propose: ( args: readonly [`0x${string}`, `0x${string}`] | readonly [`0x${string}`, `0x${string}`, `0x${string}`], @@ -50,6 +60,7 @@ describe('L1Publisher', () => { let rollupContract: MockRollupContract; let publicClient: MockProxy; + let gasUtils: MockProxy; let proposeTxHash: `0x${string}`; let proposeTxReceipt: GetTransactionReceiptReturnType; @@ -87,7 +98,7 @@ describe('L1Publisher', () => { rollupContract = new MockRollupContract(rollupContractWrite, rollupContractRead); publicClient = mock(); - + gasUtils = mock(); const config = { l1RpcUrl: `http://127.0.0.1:8545`, l1ChainId: 1, @@ -101,12 +112,15 @@ describe('L1Publisher', () => { (publisher as any)['rollupContract'] = rollupContract; (publisher as any)['publicClient'] = publicClient; - + (publisher as any)['gasUtils'] = gasUtils; account = (publisher as any)['account']; rollupContractRead.getCurrentSlot.mockResolvedValue(l2Block.header.globalVariables.slotNumber.toBigInt()); publicClient.getBlock.mockResolvedValue({ timestamp: 12n }); publicClient.estimateGas.mockResolvedValue(GAS_GUESS); + gasUtils.getGasPrice.mockResolvedValue(100_000_000n); + gasUtils.estimateGas.mockResolvedValue(GAS_GUESS); + gasUtils.monitorTransaction.mockResolvedValue(proposeTxReceipt); }); it('publishes and propose l2 block to l1', async () => { @@ -130,6 +144,7 @@ describe('L1Publisher', () => { expect(rollupContractWrite.propose).toHaveBeenCalledWith(args, { account: account, gas: L1Publisher.PROPOSE_GAS_GUESS + GAS_GUESS, + maxFeePerGas: 100_000_000n, }); expect(publicClient.getTransactionReceipt).toHaveBeenCalledWith({ hash: proposeTxHash }); }); From e688b8da6fc90ab566a3b25286f9b76abc25b81c Mon Sep 17 00:00:00 2001 From: spypsy Date: Wed, 13 Nov 2024 13:31:42 +0000 Subject: [PATCH 10/45] formatting --- yarn-project/cli/src/cmds/l1/update_l1_validators.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts index eab092c3edf..c84cff6e754 100644 --- a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts +++ b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts @@ -1,7 +1,6 @@ import { EthCheatCodes } from '@aztec/aztec.js'; - import { type EthAddress } from '@aztec/circuits.js'; -import { createEthereumChain, isAnvilTestChain, getL1ContractsConfigEnvVars } from '@aztec/ethereum'; +import { createEthereumChain, getL1ContractsConfigEnvVars, isAnvilTestChain } from '@aztec/ethereum'; import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; import { RollupAbi } from '@aztec/l1-artifacts'; From b280ed484652b6474ea429f9c394a212470a58c7 Mon Sep 17 00:00:00 2001 From: spypsy Date: Wed, 13 Nov 2024 13:36:29 +0000 Subject: [PATCH 11/45] don't fail script when ADDRESS & pk are unset --- .../scripts/native-network/validators.sh | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/yarn-project/end-to-end/scripts/native-network/validators.sh b/yarn-project/end-to-end/scripts/native-network/validators.sh index 97c9c2251a5..ce71d6708e9 100755 --- a/yarn-project/end-to-end/scripts/native-network/validators.sh +++ b/yarn-project/end-to-end/scripts/native-network/validators.sh @@ -15,25 +15,32 @@ cd "$(dirname "${BASH_SOURCE[0]}")" CMD=() +echo "NUM_VALIDATORS: $NUM_VALIDATORS" + # Generate validator commands -for ((i=0; i Date: Wed, 13 Nov 2024 13:44:58 +0000 Subject: [PATCH 12/45] remove logs --- yarn-project/end-to-end/scripts/native-network/validators.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/yarn-project/end-to-end/scripts/native-network/validators.sh b/yarn-project/end-to-end/scripts/native-network/validators.sh index ce71d6708e9..996ddb7cf68 100755 --- a/yarn-project/end-to-end/scripts/native-network/validators.sh +++ b/yarn-project/end-to-end/scripts/native-network/validators.sh @@ -15,8 +15,6 @@ cd "$(dirname "${BASH_SOURCE[0]}")" CMD=() -echo "NUM_VALIDATORS: $NUM_VALIDATORS" - # Generate validator commands for ((i = 0; i < NUM_VALIDATORS; i++)); do PORT=$((8081 + i)) @@ -29,9 +27,6 @@ for ((i = 0; i < NUM_VALIDATORS; i++)); do ADDRESS="${!ADDRESS_VAR:-}" VALIDATOR_PRIVATE_KEY="${!PRIVATE_KEY_VAR:-}" - echo "ADDRESS: $ADDRESS" - echo "VALIDATOR_PRIVATE_KEY: $VALIDATOR_PRIVATE_KEY" - CMD+=("./validator.sh $PORT $P2P_PORT $ADDRESS $VALIDATOR_PRIVATE_KEY") done From 22613520390b5abd39b92e817c8fd4faeb0454bc Mon Sep 17 00:00:00 2001 From: spypsy Date: Wed, 13 Nov 2024 14:19:50 +0000 Subject: [PATCH 13/45] update default private_key assignment in validator.sh --- yarn-project/end-to-end/scripts/native-network/validator.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/end-to-end/scripts/native-network/validator.sh b/yarn-project/end-to-end/scripts/native-network/validator.sh index d8634f8ad3a..7af88e3a88c 100755 --- a/yarn-project/end-to-end/scripts/native-network/validator.sh +++ b/yarn-project/end-to-end/scripts/native-network/validator.sh @@ -47,7 +47,7 @@ if [ -z "${VALIDATOR_PRIVATE_KEY:-}" ] || [ -z "${ADDRESS:-}" ]; then export VALIDATOR_PRIVATE_KEY=$(echo $json_account | jq -r '.privateKey') fi -export PRIVATE_KEY=${VALIDATOR_PRIVATE_KEY:-} +export PRIVATE_KEY=${PRIVATE_KEY:-${VALIDATOR_PRIVATE_KEY:-}} export L1_PRIVATE_KEY=$VALIDATOR_PRIVATE_KEY export SEQ_PUBLISHER_PRIVATE_KEY=$VALIDATOR_PRIVATE_KEY export DEBUG=${DEBUG:-"aztec:*,-aztec:avm_simulator*,-aztec:libp2p_service*,-aztec:circuits:artifact_hash,-json-rpc*,-aztec:l2_block_stream,-aztec:world-state:*"} From 70923275cfdb8e9ab0160d82b3b4d13634ee0923 Mon Sep 17 00:00:00 2001 From: spypsy Date: Wed, 13 Nov 2024 14:41:36 +0000 Subject: [PATCH 14/45] use const for WEI --- yarn-project/ethereum/src/gas_utils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/yarn-project/ethereum/src/gas_utils.ts b/yarn-project/ethereum/src/gas_utils.ts index 33489849681..ead367472fa 100644 --- a/yarn-project/ethereum/src/gas_utils.ts +++ b/yarn-project/ethereum/src/gas_utils.ts @@ -15,6 +15,8 @@ import { // 1_000_000_000 Wei = 1 Gwei // 1_000_000_000_000_000_000 Wei = 1 ETH +const WEI_CONST = 1_000_000_000n; + export interface GasConfig { /** * How much to increase gas price by each attempt (percentage) @@ -87,13 +89,13 @@ export class GasUtils { public async getGasPrice(): Promise { const block = await this.publicClient.getBlock({ blockTag: 'latest' }); const baseFee = block.baseFeePerGas ?? 0n; - const priorityFee = this.gasConfig.priorityFeeGwei * 1_000_000_000n; + const priorityFee = this.gasConfig.priorityFeeGwei * WEI_CONST; const baseWithBuffer = baseFee + (baseFee * this.gasConfig.bufferPercentage) / 100n; const totalGasPrice = baseWithBuffer + priorityFee; - const maxGasPrice = this.gasConfig.maxGwei * 1_000_000_000n; - const minGasPrice = this.gasConfig.minGwei * 1_000_000_000n; + const maxGasPrice = this.gasConfig.maxGwei * WEI_CONST; + const minGasPrice = this.gasConfig.minGwei * WEI_CONST; let finalGasPrice: bigint; if (totalGasPrice < minGasPrice) { From 1b4c8c3bf87f6b7268814b8e158c3782e7d84beb Mon Sep 17 00:00:00 2001 From: spypsy Date: Wed, 13 Nov 2024 15:51:49 +0000 Subject: [PATCH 15/45] skip PRIVATE_KEY assignment --- yarn-project/end-to-end/scripts/native-network/validator.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/yarn-project/end-to-end/scripts/native-network/validator.sh b/yarn-project/end-to-end/scripts/native-network/validator.sh index 7af88e3a88c..622e56e3c28 100755 --- a/yarn-project/end-to-end/scripts/native-network/validator.sh +++ b/yarn-project/end-to-end/scripts/native-network/validator.sh @@ -47,7 +47,6 @@ if [ -z "${VALIDATOR_PRIVATE_KEY:-}" ] || [ -z "${ADDRESS:-}" ]; then export VALIDATOR_PRIVATE_KEY=$(echo $json_account | jq -r '.privateKey') fi -export PRIVATE_KEY=${PRIVATE_KEY:-${VALIDATOR_PRIVATE_KEY:-}} export L1_PRIVATE_KEY=$VALIDATOR_PRIVATE_KEY export SEQ_PUBLISHER_PRIVATE_KEY=$VALIDATOR_PRIVATE_KEY export DEBUG=${DEBUG:-"aztec:*,-aztec:avm_simulator*,-aztec:libp2p_service*,-aztec:circuits:artifact_hash,-json-rpc*,-aztec:l2_block_stream,-aztec:world-state:*"} From 31449f73e12c89d87a34820739f2a9194244d56b Mon Sep 17 00:00:00 2001 From: spypsy Date: Thu, 14 Nov 2024 17:57:38 +0000 Subject: [PATCH 16/45] WIP: PR fixes --- .../cli/src/cmds/l1/update_l1_validators.ts | 7 ++ yarn-project/cli/src/cmds/pxe/index.ts | 7 +- .../scripts/native-network/validator.sh | 9 ++- .../scripts/native-network/validators.sh | 2 +- yarn-project/ethereum/package.json | 2 + .../ethereum/src/deploy_l1_contracts.ts | 65 +++++++++++++------ .../src/gas_utils.test.ts} | 33 ++++++++-- yarn-project/ethereum/src/gas_utils.ts | 4 +- yarn-project/yarn.lock | 30 ++++++--- 9 files changed, 115 insertions(+), 44 deletions(-) rename yarn-project/{end-to-end/src/e2e_l1_gas.test.ts => ethereum/src/gas_utils.test.ts} (74%) diff --git a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts index c84cff6e754..e08231e5b7f 100644 --- a/yarn-project/cli/src/cmds/l1/update_l1_validators.ts +++ b/yarn-project/cli/src/cmds/l1/update_l1_validators.ts @@ -57,6 +57,13 @@ export async function addL1Validator({ dualLog(`Funding validator on L1`); const cheatCodes = new EthCheatCodes(rpcUrl, debugLogger); await cheatCodes.setBalance(validatorAddress, 10n ** 20n); + } else { + const balance = await publicClient.getBalance({ address: validatorAddress.toString() }); + const balanceInEth = Number(balance) / 10 ** 18; + dualLog(`Validator balance: ${balanceInEth.toFixed(6)} ETH`); + if (balanceInEth === 0) { + dualLog(`WARNING: Validator has no balance. Remember to fund it!`); + } } } diff --git a/yarn-project/cli/src/cmds/pxe/index.ts b/yarn-project/cli/src/cmds/pxe/index.ts index 1a63c2e9499..42fa8f0da34 100644 --- a/yarn-project/cli/src/cmds/pxe/index.ts +++ b/yarn-project/cli/src/cmds/pxe/index.ts @@ -137,15 +137,14 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL .command('get-node-info') .description('Gets the information of an Aztec node from a PXE or directly from an Aztec node.') .option('--node-url ', 'URL of the node.', `http://${LOCALHOST}:8080`) - .option('--from-node', 'Get the info directly from an Aztec node.', false) .addOption(makePxeOption(false)) .action(async options => { const { getNodeInfo } = await import('./get_node_info.js'); let url: string; - if (options.pxe) { - url = options.rpcUrl; - } else { + if (options.nodeUrl) { url = options.nodeUrl; + } else { + url = options.rpcUrl; } await getNodeInfo(url, options.pxe, debugLogger, log); }); diff --git a/yarn-project/end-to-end/scripts/native-network/validator.sh b/yarn-project/end-to-end/scripts/native-network/validator.sh index 622e56e3c28..92d2e6d6932 100755 --- a/yarn-project/end-to-end/scripts/native-network/validator.sh +++ b/yarn-project/end-to-end/scripts/native-network/validator.sh @@ -51,7 +51,14 @@ export L1_PRIVATE_KEY=$VALIDATOR_PRIVATE_KEY export SEQ_PUBLISHER_PRIVATE_KEY=$VALIDATOR_PRIVATE_KEY export DEBUG=${DEBUG:-"aztec:*,-aztec:avm_simulator*,-aztec:libp2p_service*,-aztec:circuits:artifact_hash,-json-rpc*,-aztec:l2_block_stream,-aztec:world-state:*"} export ETHEREUM_HOST=${ETHEREUM_HOST:-"http://127.0.0.1:8545"} -export IS_ANVIL=${IS_ANVIL:-"true"} + +# Automatically detect if we're using Anvil +if curl -s -H "Content-Type: application/json" -X POST --data '{"method":"web3_clientVersion","params":[],"id":49,"jsonrpc":"2.0"}' $ETHEREUM_HOST | jq .result | grep -q anvil; then + IS_ANVIL="true" +else + IS_ANVIL="false" +fi + export P2P_ENABLED="true" export VALIDATOR_DISABLED="false" export SEQ_MAX_SECONDS_BETWEEN_BLOCKS="0" diff --git a/yarn-project/end-to-end/scripts/native-network/validators.sh b/yarn-project/end-to-end/scripts/native-network/validators.sh index 996ddb7cf68..c2454b87481 100755 --- a/yarn-project/end-to-end/scripts/native-network/validators.sh +++ b/yarn-project/end-to-end/scripts/native-network/validators.sh @@ -21,7 +21,7 @@ for ((i = 0; i < NUM_VALIDATORS; i++)); do P2P_PORT=$((40401 + i)) IDX=$((i + 1)) - # Use empty string as default if variables are not set + # These variables should be set in public networks if we have funded validators already. Leave empty for test environments. ADDRESS_VAR="ADDRESS_${IDX}" PRIVATE_KEY_VAR="VALIDATOR_PRIVATE_KEY_${IDX}" ADDRESS="${!ADDRESS_VAR:-}" diff --git a/yarn-project/ethereum/package.json b/yarn-project/ethereum/package.json index 87c18abfca7..8f5490a56e0 100644 --- a/yarn-project/ethereum/package.json +++ b/yarn-project/ethereum/package.json @@ -34,6 +34,8 @@ "@jest/globals": "^29.5.0", "@types/jest": "^29.5.0", "@types/node": "^18.14.6", + "@viem/anvil": "^0.0.10", + "get-port": "^7.1.0", "jest": "^29.5.0", "ts-node": "^10.9.1", "typescript": "^5.0.4" diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index ed9307588e9..a2a1d28971e 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -223,6 +223,19 @@ export type L1Clients = { walletClient: WalletClient; }; +export const deployerAbi = [ + { + inputs: [ + { internalType: 'bytes32', name: 'salt', type: 'bytes32' }, + { internalType: 'bytes', name: 'init_code', type: 'bytes' }, + ], + name: 'deploy', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'nonpayable', + type: 'function', + }, +]; + /** * Creates a wallet and a public viem client for interacting with L1. * @param rpcUrl - RPC URL to connect to L1. @@ -604,6 +617,8 @@ export async function deployL1Contract( let txHash: Hex | undefined = undefined; let address: Hex | null | undefined = undefined; + const gasUtils = new GasUtils(publicClient, logger); + if (libraries) { // @note Assumes that we wont have nested external libraries. @@ -652,33 +667,43 @@ export async function deployL1Contract( const existing = await publicClient.getBytecode({ address }); if (existing === undefined || existing === '0x') { - // Add gas estimation and price buffering for CREATE2 deployment - const deployData = encodeDeployData({ abi, bytecode, args }); - const gasUtils = new GasUtils(publicClient, logger); - 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, + const { request } = await publicClient.simulateContract({ + account: walletClient.account, + address: deployer, + abi: deployerAbi, + functionName: 'deploy', + args: [salt, calldata], }); - logger?.verbose( - `Deploying contract with salt ${salt} to address ${address} in tx ${txHash} (gas: ${gasEstimate}, price: ${gasPrice})`, - ); + + 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})`, + // ); } else { logger?.verbose(`Skipping existing deployment of contract with salt ${salt} to address ${address}`); } } else { // Regular deployment path const deployData = encodeDeployData({ abi, bytecode, args }); - const gasUtils = new GasUtils(publicClient, logger); const gasEstimate = await gasUtils.estimateGas(() => publicClient.estimateGas({ account: walletClient.account?.address, diff --git a/yarn-project/end-to-end/src/e2e_l1_gas.test.ts b/yarn-project/ethereum/src/gas_utils.test.ts similarity index 74% rename from yarn-project/end-to-end/src/e2e_l1_gas.test.ts rename to yarn-project/ethereum/src/gas_utils.test.ts index 580eb45f23a..a08e3cfee97 100644 --- a/yarn-project/end-to-end/src/e2e_l1_gas.test.ts +++ b/yarn-project/ethereum/src/gas_utils.test.ts @@ -1,11 +1,25 @@ -import { GasUtils } from '@aztec/ethereum'; import { createDebugLogger } from '@aztec/foundation/log'; +import { createAnvil } from '@viem/anvil'; +import getPort from 'get-port'; import { createPublicClient, createWalletClient, http } from 'viem'; -import { privateKeyToAccount } from 'viem/accounts'; +import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; import { foundry } from 'viem/chains'; -import { getPrivateKeyFromIndex, startAnvil } from './fixtures/utils.js'; +import { GasUtils } from './gas_utils.js'; + +const MNEMONIC = 'test test test test test test test test test test test junk'; + +const startAnvil = async (l1BlockTime?: number) => { + const ethereumHostPort = await getPort(); + const rpcUrl = `http://127.0.0.1:${ethereumHostPort}`; + const anvil = createAnvil({ + port: ethereumHostPort, + blockTime: l1BlockTime, + }); + await anvil.start(); + return { anvil, rpcUrl }; +}; describe('e2e_l1_gas', () => { let gasUtils: GasUtils; @@ -14,10 +28,15 @@ describe('e2e_l1_gas', () => { const logger = createDebugLogger('l1_gas_test'); beforeAll(async () => { - const res = await startAnvil(12); - const rpcUrl = res.rpcUrl; - const privKey = getPrivateKeyFromIndex(0); - const account = privateKeyToAccount(`0x${privKey?.toString('hex')}`); + const { rpcUrl } = await startAnvil(12); + 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}`); publicClient = createPublicClient({ transport: http(rpcUrl), diff --git a/yarn-project/ethereum/src/gas_utils.ts b/yarn-project/ethereum/src/gas_utils.ts index ead367472fa..1ba4e0d13dd 100644 --- a/yarn-project/ethereum/src/gas_utils.ts +++ b/yarn-project/ethereum/src/gas_utils.ts @@ -176,7 +176,7 @@ export class GasUtils { this.logger?.info(`Transaction ${currentTxHash} pending`); if (tx) { lastSeen = Date.now(); - await new Promise(resolve => setTimeout(resolve, config.checkIntervalMs)); + await sleep(config.checkIntervalMs); continue; } @@ -204,8 +204,6 @@ export class GasUtils { lastSeen = Date.now(); } - - // await new Promise(resolve => setTimeout(resolve, cfg.checkIntervalMs)); await sleep(config.checkIntervalMs); } catch (err: any) { this.logger?.warn(`Error monitoring transaction ${currentTxHash}:`, err); diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 335dbacd01c..0e5aea3e7c7 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -616,7 +616,9 @@ __metadata: "@jest/globals": ^29.5.0 "@types/jest": ^29.5.0 "@types/node": ^18.14.6 + "@viem/anvil": ^0.0.10 dotenv: ^16.0.3 + get-port: ^7.1.0 jest: ^29.5.0 ts-node: ^10.9.1 tslib: ^2.4.0 @@ -3485,7 +3487,7 @@ __metadata: version: 0.0.0-use.local resolution: "@noir-lang/noir_codegen@portal:../noir/packages/noir_codegen::locator=%40aztec%2Faztec3-packages%40workspace%3A." dependencies: - "@noir-lang/types": 0.37.0 + "@noir-lang/types": 0.38.0 glob: ^10.3.10 ts-command-line-args: ^2.5.1 bin: @@ -3494,13 +3496,13 @@ __metadata: linkType: soft "@noir-lang/noir_js@file:../noir/packages/noir_js::locator=%40aztec%2Faztec3-packages%40workspace%3A.": - version: 0.37.0 - resolution: "@noir-lang/noir_js@file:../noir/packages/noir_js#../noir/packages/noir_js::hash=7257df&locator=%40aztec%2Faztec3-packages%40workspace%3A." + version: 0.38.0 + resolution: "@noir-lang/noir_js@file:../noir/packages/noir_js#../noir/packages/noir_js::hash=9fc813&locator=%40aztec%2Faztec3-packages%40workspace%3A." dependencies: - "@noir-lang/acvm_js": 0.53.0 - "@noir-lang/noirc_abi": 0.37.0 - "@noir-lang/types": 0.37.0 - checksum: 50419bebe0146ec664232712abb190148a9bb4e9bcfe538a13a0107192e31045ec22efddeeb8eac3eb5ac6cb50f52782adddaaeb87df328ebea3caf55579a3c8 + "@noir-lang/acvm_js": 0.54.0 + "@noir-lang/noirc_abi": 0.38.0 + "@noir-lang/types": 0.38.0 + checksum: 99cdc1f1e352d45a8968261dc7f1d68d58b5bca8177e25c610667eb60954436356c56852e6a23fbf69ddb8db9b92494736438c0852f4702d33e8d0e981e60552 languageName: node linkType: hard @@ -3508,7 +3510,7 @@ __metadata: version: 0.0.0-use.local resolution: "@noir-lang/noirc_abi@portal:../noir/packages/noirc_abi::locator=%40aztec%2Faztec3-packages%40workspace%3A." dependencies: - "@noir-lang/types": 0.37.0 + "@noir-lang/types": 0.38.0 languageName: node linkType: soft @@ -5437,6 +5439,18 @@ __metadata: languageName: node linkType: hard +"@viem/anvil@npm:^0.0.10": + version: 0.0.10 + resolution: "@viem/anvil@npm:0.0.10" + dependencies: + execa: ^7.1.1 + get-port: ^6.1.2 + http-proxy: ^1.18.1 + ws: ^8.13.0 + checksum: fb475055f36c753cea26fa0c02a0278301dddcdc8003418576395cfc31e97ba5a236fbc66ff093bd8d39ea05286487adb86b7499308e446b6cfe90dc08089b38 + languageName: node + linkType: hard + "@viem/anvil@npm:^0.0.9": version: 0.0.9 resolution: "@viem/anvil@npm:0.0.9" From d066d51ed56b818f3d2b16606d957e934a3a5917 Mon Sep 17 00:00:00 2001 From: spypsy Date: Mon, 18 Nov 2024 13:20:08 +0000 Subject: [PATCH 17/45] refactor GasUtils --- yarn-project/ethereum/package.json | 3 +- .../ethereum/src/deploy_l1_contracts.ts | 76 +----- yarn-project/ethereum/src/gas_utils.test.ts | 164 ++++++++--- yarn-project/ethereum/src/gas_utils.ts | 256 ++++++++++-------- 4 files changed, 289 insertions(+), 210 deletions(-) diff --git a/yarn-project/ethereum/package.json b/yarn-project/ethereum/package.json index 5e68426ab3d..b3e6eba9531 100644 --- a/yarn-project/ethereum/package.json +++ b/yarn-project/ethereum/package.json @@ -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" diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 2671e2c8f22..5542200da74 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -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); @@ -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, @@ -744,6 +696,6 @@ export async function deployL1Contract( } } - return { address: EthAddress.fromString(address!), txHash }; + return { address: EthAddress.fromString(resultingAddress!), txHash }; } // docs:end:deployL1Contract diff --git a/yarn-project/ethereum/src/gas_utils.test.ts b/yarn-project/ethereum/src/gas_utils.test.ts index a08e3cfee97..b96ad3aa4cb 100644 --- a/yarn-project/ethereum/src/gas_utils.test.ts +++ b/yarn-project/ethereum/src/gas_utils.test.ts @@ -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'; @@ -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(); @@ -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), @@ -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()], @@ -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); }); diff --git a/yarn-project/ethereum/src/gas_utils.ts b/yarn-project/ethereum/src/gas_utils.ts index 1ba4e0d13dd..5cbd0916e25 100644 --- a/yarn-project/ethereum/src/gas_utils.ts +++ b/yarn-project/ethereum/src/gas_utils.ts @@ -1,8 +1,8 @@ import { type DebugLogger } from '@aztec/foundation/log'; -import { makeBackoff, retry } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; import { + type Account, type Address, type Hex, type PublicClient, @@ -21,7 +21,11 @@ export interface GasConfig { /** * How much to increase gas price by each attempt (percentage) */ - bufferPercentage: bigint; + bufferPercentage?: bigint; + /** + * Fixed buffer to add to gas price + */ + bufferFixed?: bigint; /** * Maximum gas price in gwei */ @@ -36,7 +40,7 @@ export interface GasConfig { priorityFeeGwei: bigint; } -export interface TransactionMonitorConfig { +export interface L1TxMonitorConfig { /** * Maximum number of speed-up attempts */ @@ -49,10 +53,12 @@ export interface TransactionMonitorConfig { * How long before considering tx stalled */ stallTimeMs: number; - /** - * How much to increase gas price by each attempt (percentage) - */ - gasPriceIncrease: bigint; +} + +export interface L1TxRequest { + to: Address; + data: Hex; + value?: bigint; } const DEFAULT_GAS_CONFIG: GasConfig = { @@ -62,153 +68,185 @@ const DEFAULT_GAS_CONFIG: GasConfig = { priorityFeeGwei: 2n, }; -const DEFAULT_MONITOR_CONFIG: Required = { +const DEFAULT_MONITOR_CONFIG: Required = { maxAttempts: 3, checkIntervalMs: 30_000, - stallTimeMs: 180_000, - gasPriceIncrease: 50n, + stallTimeMs: 60_000, }; export class GasUtils { private readonly gasConfig: GasConfig; - private readonly monitorConfig: TransactionMonitorConfig; + private readonly monitorConfig: L1TxMonitorConfig; constructor( private readonly publicClient: PublicClient, private readonly logger?: DebugLogger, gasConfig?: GasConfig, - monitorConfig?: TransactionMonitorConfig, + monitorConfig?: L1TxMonitorConfig, ) { - this.gasConfig! = gasConfig ?? DEFAULT_GAS_CONFIG; - this.monitorConfig! = monitorConfig ?? DEFAULT_MONITOR_CONFIG; - } - - /** - * Gets the current gas price with safety buffer and bounds checking - */ - public async getGasPrice(): Promise { - const block = await this.publicClient.getBlock({ blockTag: 'latest' }); - const baseFee = block.baseFeePerGas ?? 0n; - const priorityFee = this.gasConfig.priorityFeeGwei * WEI_CONST; - - const baseWithBuffer = baseFee + (baseFee * this.gasConfig.bufferPercentage) / 100n; - const totalGasPrice = baseWithBuffer + priorityFee; - - const maxGasPrice = this.gasConfig.maxGwei * WEI_CONST; - const minGasPrice = this.gasConfig.minGwei * WEI_CONST; - - let finalGasPrice: bigint; - if (totalGasPrice < minGasPrice) { - finalGasPrice = minGasPrice; - } else if (totalGasPrice > maxGasPrice) { - finalGasPrice = maxGasPrice; - } else { - finalGasPrice = totalGasPrice; - } - - this.logger?.debug( - `Gas price calculation: baseFee=${baseFee}, withBuffer=${baseWithBuffer}, priority=${priorityFee}, final=${finalGasPrice}`, - ); - - return finalGasPrice; + this.gasConfig! = { + ...DEFAULT_GAS_CONFIG, + ...(gasConfig || {}), + }; + this.monitorConfig! = { + ...DEFAULT_MONITOR_CONFIG, + ...(monitorConfig || {}), + }; } /** - * Estimates gas with retries and adds buffer + * Sends a transaction and monitors it until completion, handling gas estimation and price bumping + * @param walletClient - The wallet client for sending the transaction + * @param request - The transaction request (to, data, value) + * @param monitorConfig - Optional monitoring configuration + * @returns The hash of the successful transaction */ - public estimateGas Promise>(estimator: T): Promise { - return retry( - async () => { - const gas = await estimator(); - return gas + (gas * this.gasConfig.bufferPercentage) / 100n; - }, - 'gas estimation', - makeBackoff([1, 2, 3]), - this.logger, - false, - ); - } - - /** - * Monitors a transaction and attempts to speed it up if it gets stuck - * @param txHash - The hash of the transaction to monitor - * @param walletClient - The wallet client for sending replacement tx - * @param originalTx - The original transaction data for replacement - * @param config - Monitor configuration - */ - public async monitorTransaction( - txHash: Hex, + public async sendAndMonitorTransaction( walletClient: WalletClient, - originalTx: { - to: Address; - data: Hex; - nonce: number; - gasLimit: bigint; - maxFeePerGas: bigint; - }, - txMonitorConfig?: TransactionMonitorConfig, + account: Account, + request: L1TxRequest, + _gasConfig?: Partial, + _monitorConfig?: Partial, ): Promise { - const config = { ...this.monitorConfig, ...txMonitorConfig }; + const monitorConfig = { ...this.monitorConfig, ..._monitorConfig }; + const gasConfig = { ...this.gasConfig, ..._gasConfig }; + + // Estimate gas + const gasLimit = await this.estimateGas(account, request); + + const gasPrice = await this.getGasPrice(gasConfig); + const nonce = await this.publicClient.getTransactionCount({ address: account.address }); + + // Send initial tx + const txHash = await walletClient.sendTransaction({ + chain: null, + account, + ...request, + gas: gasLimit, + maxFeePerGas: gasPrice, + nonce, + }); + + this.logger?.verbose( + `Sent L1 transaction ${txHash} with gas limit ${gasLimit} and price ${formatGwei(gasPrice)} gwei`, + ); + + // Track all tx hashes we send + const txHashes = new Set([txHash]); + let currentTxHash = txHash; let attempts = 0; let lastSeen = Date.now(); - let currentTxHash = txHash; - - // Flag to use for first run to avoid logging errors. - let firstRun = true; - while (true) { try { - // Check transaction status - const receipt = await this.publicClient.getTransactionReceipt({ hash: currentTxHash }); - if (receipt) { - this.logger?.info(`Transaction ${currentTxHash} confirmed`); - return receipt; + const currentNonce = await this.publicClient.getTransactionCount({ address: account.address }); + if (currentNonce > nonce) { + // A tx with this nonce has been mined - check all our tx hashes + for (const hash of txHashes) { + try { + const receipt = await this.publicClient.getTransactionReceipt({ hash }); + if (receipt) { + this.logger?.debug(`L1 Transaction ${hash} confirmed`); + if (receipt.status === 'reverted') { + this.logger?.error(`L1 Transaction ${hash} reverted`); + throw new Error(`Transaction ${hash} reverted`); + } + return receipt; + } + } catch (err) { + if (err instanceof Error && err.message.includes('reverted')) { + throw err; + } + // We can ignore other errors - just try the next hash + } + } } - if (firstRun) { - firstRun = false; - // Wait a second for tx to be broadcast - await sleep(1000); - } - - // Check if transaction is pending + // Check if current tx is pending const tx = await this.publicClient.getTransaction({ hash: currentTxHash }); - this.logger?.info(`Transaction ${currentTxHash} pending`); if (tx) { + this.logger?.debug(`L1 Transaction ${currentTxHash} pending`); lastSeen = Date.now(); - await sleep(config.checkIntervalMs); + await sleep(monitorConfig.checkIntervalMs); continue; } - // Transaction not found and enough time has passed - might be stuck - if (Date.now() - lastSeen > config.stallTimeMs && attempts < config.maxAttempts) { + // tx not found and enough time has passed - might be stuck + if (Date.now() - lastSeen > monitorConfig.stallTimeMs && attempts < monitorConfig.maxAttempts) { attempts++; - const newGasPrice = (originalTx.maxFeePerGas * (100n + config.gasPriceIncrease)) / 100n; + const newGasPrice = await this.getGasPrice(); - this.logger?.info( - `Transaction ${currentTxHash} appears stuck. Attempting speed-up ${attempts}/${config.maxAttempts} ` + + this.logger?.debug( + `L1 Transaction ${currentTxHash} appears stuck. Attempting speed-up ${attempts}/${monitorConfig.maxAttempts} ` + `with new gas price ${formatGwei(newGasPrice)} gwei`, ); - // Send replacement transaction with higher gas price - const account = await walletClient.getAddresses().then(addresses => addresses[0]); + // Send replacement tx with higher gas price currentTxHash = await walletClient.sendTransaction({ chain: null, account, - to: originalTx.to, - data: originalTx.data, - nonce: originalTx.nonce, - gas: originalTx.gasLimit, + ...request, + nonce, + gas: gasLimit, maxFeePerGas: newGasPrice, }); + // Record new tx hash + txHashes.add(currentTxHash); lastSeen = Date.now(); } - await sleep(config.checkIntervalMs); + await sleep(monitorConfig.checkIntervalMs); } catch (err: any) { - this.logger?.warn(`Error monitoring transaction ${currentTxHash}:`, err); - await new Promise(resolve => setTimeout(resolve, config.checkIntervalMs)); + this.logger?.warn(`Error monitoring tx ${currentTxHash}:`, err); + if (err.message?.includes('reverted')) { + throw err; + } + await sleep(monitorConfig.checkIntervalMs); } } } + + /** + * Gets the current gas price with bounds checking + */ + private async getGasPrice(_gasConfig?: GasConfig): Promise { + const gasConfig = { ...this.gasConfig, ..._gasConfig }; + const block = await this.publicClient.getBlock({ blockTag: 'latest' }); + const baseFee = block.baseFeePerGas ?? 0n; // Keep in Wei + const priorityFee = gasConfig.priorityFeeGwei * WEI_CONST; + + // First ensure we're at least meeting the base fee + const minRequired = baseFee; + const maxAllowed = gasConfig.maxGwei * WEI_CONST; + + // Our gas price must be at least the base fee + let finalGasPrice = minRequired; + + // Add priority fee if we have room under the max + if (finalGasPrice + priorityFee <= maxAllowed) { + finalGasPrice += priorityFee; + } + + this.logger?.debug( + `Gas price calculation: baseFee=${formatGwei(baseFee)}, priority=${gasConfig.priorityFeeGwei}, final=${formatGwei( + finalGasPrice, + )}`, + ); + + return finalGasPrice; + } + + /** + * Estimates gas and adds buffer + */ + private async estimateGas(account: Account, request: L1TxRequest, _gasConfig?: GasConfig): Promise { + const gasConfig = { ...this.gasConfig, ..._gasConfig }; + const initialEstimate = await this.publicClient.estimateGas({ account, ...request }); + + // Add buffer based on either fixed amount or percentage + const withBuffer = gasConfig.bufferFixed + ? initialEstimate + gasConfig.bufferFixed + : initialEstimate + (initialEstimate * (gasConfig.bufferPercentage ?? 0n)) / 100n; + + return withBuffer; + } } From f96b1d849e776e2bbce3cd4c9625676afa00e8db Mon Sep 17 00:00:00 2001 From: spypsy Date: Mon, 18 Nov 2024 14:08:01 +0000 Subject: [PATCH 18/45] simplify acc & wallets --- .../src/interfaces/aztec-node.test.ts | 20 +++ .../src/interfaces/aztec-node.ts | 5 +- .../ethereum/src/deploy_l1_contracts.ts | 6 +- yarn-project/ethereum/src/gas_utils.test.ts | 16 ++- yarn-project/ethereum/src/gas_utils.ts | 29 ++-- .../src/publisher/l1-publisher.ts | 135 ++++++------------ 6 files changed, 94 insertions(+), 117 deletions(-) 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 d415249c3f4..042ff45987f 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 fb23b2464a7..149172864c8 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 5542200da74..eb3c65d4668 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 b96ad3aa4cb..f9727070e01 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 5cbd0916e25..da466cf7457 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, @@ -211,7 +212,7 @@ export class GasUtils { private async getGasPrice(_gasConfig?: GasConfig): Promise { const gasConfig = { ...this.gasConfig, ..._gasConfig }; const block = await this.publicClient.getBlock({ blockTag: 'latest' }); - const baseFee = block.baseFeePerGas ?? 0n; // Keep in Wei + const baseFee = block.baseFeePerGas ?? 0n; const priorityFee = gasConfig.priorityFeeGwei * WEI_CONST; // First ensure we're at least meeting the base fee diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index 83359da60f1..b0a29474a4c 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); From e18c4f937abb68558659adb5bf47e5757e3ef3f7 Mon Sep 17 00:00:00 2001 From: spypsy Date: Mon, 18 Nov 2024 14:20:36 +0000 Subject: [PATCH 19/45] bump priorityFee on retries --- yarn-project/ethereum/src/gas_utils.ts | 52 +++++++++++++------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/yarn-project/ethereum/src/gas_utils.ts b/yarn-project/ethereum/src/gas_utils.ts index da466cf7457..45eb26875a5 100644 --- a/yarn-project/ethereum/src/gas_utils.ts +++ b/yarn-project/ethereum/src/gas_utils.ts @@ -37,9 +37,9 @@ export interface GasConfig { */ minGwei: bigint; /** - * Priority fee in gwei + * How much to increase priority fee by each attempt (percentage) */ - priorityFeeGwei: bigint; + priorityFeeBumpPercentage?: bigint; } export interface L1TxMonitorConfig { @@ -67,7 +67,7 @@ const DEFAULT_GAS_CONFIG: GasConfig = { bufferPercentage: 20n, maxGwei: 500n, minGwei: 1n, - priorityFeeGwei: 2n, + priorityFeeBumpPercentage: 20n, }; const DEFAULT_MONITOR_CONFIG: Required = { @@ -76,6 +76,11 @@ const DEFAULT_MONITOR_CONFIG: Required = { stallTimeMs: 60_000, }; +interface GasPrice { + maxFeePerGas: bigint; + maxPriorityFeePerGas: bigint; +} + export class GasUtils { private readonly gasConfig: GasConfig; private readonly monitorConfig: L1TxMonitorConfig; @@ -122,12 +127,13 @@ export class GasUtils { const txHash = await this.walletClient.sendTransaction({ ...request, gas: gasLimit, - maxFeePerGas: gasPrice, + maxFeePerGas: gasPrice.maxFeePerGas, + maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas, nonce, }); this.logger?.verbose( - `Sent L1 transaction ${txHash} with gas limit ${gasLimit} and price ${formatGwei(gasPrice)} gwei`, + `Sent L1 transaction ${txHash} with gas limit ${gasLimit} and price ${formatGwei(gasPrice.maxFeePerGas)} gwei`, ); // Track all tx hashes we send @@ -176,19 +182,19 @@ export class GasUtils { // Enough time has passed - might be stuck if (timePassed > monitorConfig.stallTimeMs && attempts < monitorConfig.maxAttempts) { attempts++; - const newGasPrice = await this.getGasPrice(); + const newGasPrice = await this.getGasPrice(gasConfig, attempts); this.logger?.debug( `L1 Transaction ${currentTxHash} appears stuck. Attempting speed-up ${attempts}/${monitorConfig.maxAttempts} ` + - `with new gas price ${formatGwei(newGasPrice)} gwei`, + `with new priority fee ${formatGwei(newGasPrice.maxPriorityFeePerGas)} gwei`, ); - // Send replacement tx with higher gas price currentTxHash = await this.walletClient.sendTransaction({ ...request, nonce, gas: gasLimit, - maxFeePerGas: newGasPrice, + maxFeePerGas: newGasPrice.maxFeePerGas, + maxPriorityFeePerGas: newGasPrice.maxPriorityFeePerGas, }); // Record new tx hash @@ -209,31 +215,27 @@ export class GasUtils { /** * Gets the current gas price with bounds checking */ - private async getGasPrice(_gasConfig?: GasConfig): Promise { + private async getGasPrice(_gasConfig?: GasConfig, attempt: number = 0): Promise { const gasConfig = { ...this.gasConfig, ..._gasConfig }; const block = await this.publicClient.getBlock({ blockTag: 'latest' }); const baseFee = block.baseFeePerGas ?? 0n; - const priorityFee = gasConfig.priorityFeeGwei * WEI_CONST; - - // First ensure we're at least meeting the base fee - const minRequired = baseFee; - const maxAllowed = gasConfig.maxGwei * WEI_CONST; - // Our gas price must be at least the base fee - let finalGasPrice = minRequired; - - // Add priority fee if we have room under the max - if (finalGasPrice + priorityFee <= maxAllowed) { - finalGasPrice += priorityFee; + // Get initial priority fee from the network + let priorityFee = await this.publicClient.estimateMaxPriorityFeePerGas(); + if (attempt > 0) { + // Bump priority fee by configured percentage for each attempt + priorityFee = (priorityFee * (100n + (gasConfig.priorityFeeBumpPercentage ?? 20n) * BigInt(attempt))) / 100n; } + const maxFeePerGas = gasConfig.maxGwei * WEI_CONST; + const maxPriorityFeePerGas = priorityFee; + this.logger?.debug( - `Gas price calculation: baseFee=${formatGwei(baseFee)}, priority=${gasConfig.priorityFeeGwei}, final=${formatGwei( - finalGasPrice, - )}`, + `Gas price calculation (attempt ${attempt}): baseFee=${formatGwei(baseFee)}, ` + + `maxPriorityFee=${formatGwei(maxPriorityFeePerGas)}, maxFee=${formatGwei(maxFeePerGas)}`, ); - return finalGasPrice; + return { maxFeePerGas, maxPriorityFeePerGas }; } /** From f853f219bae8024e08d62c60d080aade275ad6cf Mon Sep 17 00:00:00 2001 From: spypsy Date: Mon, 18 Nov 2024 17:17:10 +0000 Subject: [PATCH 20/45] configs --- yarn-project/ethereum/src/gas_utils.test.ts | 72 ++++------ yarn-project/ethereum/src/gas_utils.ts | 125 +++++++++++------- yarn-project/foundation/src/config/env_var.ts | 10 +- yarn-project/sequencer-client/src/config.ts | 2 + .../sequencer-client/src/publisher/config.ts | 11 +- .../src/publisher/l1-publisher.test.ts | 10 +- .../src/publisher/l1-publisher.ts | 21 +-- 7 files changed, 129 insertions(+), 122 deletions(-) diff --git a/yarn-project/ethereum/src/gas_utils.test.ts b/yarn-project/ethereum/src/gas_utils.test.ts index f9727070e01..5cdde77bc7e 100644 --- a/yarn-project/ethereum/src/gas_utils.test.ts +++ b/yarn-project/ethereum/src/gas_utils.test.ts @@ -55,22 +55,14 @@ describe('GasUtils', () => { account, }); - gasUtils = new GasUtils( - publicClient, - walletClient, - logger, - { - bufferPercentage: 20n, - maxGwei: 500n, - minGwei: 1n, - priorityFeeGwei: 2n, - }, - { - maxAttempts: 3, - checkIntervalMs: 100, - stallTimeMs: 1000, - }, - ); + gasUtils = new GasUtils(publicClient, walletClient, logger, { + bufferPercentage: 20n, + maxGwei: 500n, + minGwei: 1n, + maxAttempts: 3, + checkIntervalMs: 100, + stallTimeMs: 1000, + }); }); afterAll(async () => { await anvil.stop(); @@ -152,22 +144,14 @@ describe('GasUtils', () => { it('adds appropriate buffer to gas estimation', async () => { // First deploy without any buffer - const baselineGasUtils = new GasUtils( - publicClient, - walletClient, - logger, - { - bufferPercentage: 0n, - maxGwei: 500n, - minGwei: 1n, - priorityFeeGwei: 2n, - }, - { - maxAttempts: 3, - checkIntervalMs: 100, - stallTimeMs: 1000, - }, - ); + const baselineGasUtils = new GasUtils(publicClient, walletClient, logger, { + bufferPercentage: 0n, + maxGwei: 500n, + minGwei: 1n, + maxAttempts: 3, + checkIntervalMs: 100, + stallTimeMs: 1000, + }); const baselineTx = await baselineGasUtils.sendAndMonitorTransaction({ to: EthAddress.ZERO.toString(), @@ -180,22 +164,14 @@ describe('GasUtils', () => { }); // Now deploy with 20% buffer - const bufferedGasUtils = new GasUtils( - publicClient, - walletClient, - logger, - { - bufferPercentage: 20n, - maxGwei: 500n, - minGwei: 1n, - priorityFeeGwei: 2n, - }, - { - maxAttempts: 3, - checkIntervalMs: 100, - stallTimeMs: 1000, - }, - ); + const bufferedGasUtils = new GasUtils(publicClient, walletClient, logger, { + bufferPercentage: 20n, + maxGwei: 500n, + minGwei: 1n, + maxAttempts: 3, + checkIntervalMs: 100, + stallTimeMs: 1000, + }); const bufferedTx = await bufferedGasUtils.sendAndMonitorTransaction({ to: EthAddress.ZERO.toString(), diff --git a/yarn-project/ethereum/src/gas_utils.ts b/yarn-project/ethereum/src/gas_utils.ts index 45eb26875a5..a9caba73190 100644 --- a/yarn-project/ethereum/src/gas_utils.ts +++ b/yarn-project/ethereum/src/gas_utils.ts @@ -1,3 +1,9 @@ +import { + type ConfigMappingsType, + bigintConfigHelper, + getDefaultConfig, + numberConfigHelper, +} from '@aztec/foundation/config'; import { type DebugLogger } from '@aztec/foundation/log'; import { sleep } from '@aztec/foundation/sleep'; @@ -19,9 +25,9 @@ import { const WEI_CONST = 1_000_000_000n; -export interface GasConfig { +export interface GasUtilsConfig { /** - * How much to increase gas price by each attempt (percentage) + * How much to increase calculated gas limit. */ bufferPercentage?: bigint; /** @@ -31,74 +37,97 @@ export interface GasConfig { /** * Maximum gas price in gwei */ - maxGwei: bigint; + maxGwei?: bigint; /** * Minimum gas price in gwei */ - minGwei: bigint; + minGwei?: bigint; /** * How much to increase priority fee by each attempt (percentage) */ priorityFeeBumpPercentage?: bigint; -} - -export interface L1TxMonitorConfig { /** * Maximum number of speed-up attempts */ - maxAttempts: number; + maxAttempts?: number; /** * How often to check tx status */ - checkIntervalMs: number; + checkIntervalMs?: number; /** * How long before considering tx stalled */ - stallTimeMs: number; + stallTimeMs?: number; } +export const gasUtilsConfigMappings: ConfigMappingsType = { + bufferPercentage: { + description: 'How much to increase gas price by each attempt (percentage)', + env: 'L1_GAS_LIMIT_BUFFER_PERCENTAGE', + ...bigintConfigHelper(20n), + }, + bufferFixed: { + description: 'Fixed buffer to add to gas price', + env: 'L1_GAS_LIMIT_BUFFER_FIXED', + ...bigintConfigHelper(), + }, + minGwei: { + description: 'Minimum gas price in gwei', + env: 'L1_GAS_PRICE_MIN', + ...bigintConfigHelper(1n), + }, + maxGwei: { + description: 'Maximum gas price in gwei', + env: 'L1_GAS_PRICE_MAX', + ...bigintConfigHelper(100n), + }, + priorityFeeBumpPercentage: { + description: 'How much to increase priority fee by each attempt (percentage)', + env: 'L1_PRIORITY_FEE_BUMP_PERCENTAGE', + ...bigintConfigHelper(20n), + }, + maxAttempts: { + description: 'Maximum number of speed-up attempts', + env: 'L1_TX_MONITOR_MAX_ATTEMPTS', + ...numberConfigHelper(3), + }, + checkIntervalMs: { + description: 'How often to check tx status', + env: 'L1_TX_MONITOR_CHECK_INTERVAL_MS', + ...numberConfigHelper(30_000), + }, + stallTimeMs: { + description: 'How long before considering tx stalled', + env: 'L1_TX_MONITOR_STALL_TIME_MS', + ...numberConfigHelper(60_000), + }, +}; + +export const defaultGasUtilsConfig = getDefaultConfig(gasUtilsConfigMappings); + export interface L1TxRequest { to: Address; data: Hex; value?: bigint; } -const DEFAULT_GAS_CONFIG: GasConfig = { - bufferPercentage: 20n, - maxGwei: 500n, - minGwei: 1n, - priorityFeeBumpPercentage: 20n, -}; - -const DEFAULT_MONITOR_CONFIG: Required = { - maxAttempts: 3, - checkIntervalMs: 30_000, - stallTimeMs: 60_000, -}; - interface GasPrice { maxFeePerGas: bigint; maxPriorityFeePerGas: bigint; } export class GasUtils { - private readonly gasConfig: GasConfig; - private readonly monitorConfig: L1TxMonitorConfig; + private readonly config: GasUtilsConfig; constructor( private readonly publicClient: PublicClient, private readonly walletClient: WalletClient, private readonly logger?: DebugLogger, - gasConfig?: GasConfig, - monitorConfig?: L1TxMonitorConfig, + config?: Partial, ) { - this.gasConfig! = { - ...DEFAULT_GAS_CONFIG, - ...(gasConfig || {}), - }; - this.monitorConfig! = { - ...DEFAULT_MONITOR_CONFIG, - ...(monitorConfig || {}), + this.config = { + ...defaultGasUtilsConfig, + ...(config || {}), }; } @@ -111,11 +140,9 @@ export class GasUtils { */ public async sendAndMonitorTransaction( request: L1TxRequest, - _gasConfig?: Partial, - _monitorConfig?: Partial, + _gasConfig?: Partial, ): Promise { - const monitorConfig = { ...this.monitorConfig, ..._monitorConfig }; - const gasConfig = { ...this.gasConfig, ..._gasConfig }; + const gasConfig = { ...this.config, ..._gasConfig }; const account = this.walletClient.account; // Estimate gas const gasLimit = await this.estimateGas(account, request); @@ -172,20 +199,20 @@ export class GasUtils { // Get time passed const timePassed = Date.now() - lastSeen; - if (tx && timePassed < monitorConfig.stallTimeMs) { + if (tx && timePassed < gasConfig.stallTimeMs!) { this.logger?.debug(`L1 Transaction ${currentTxHash} pending. Time passed: ${timePassed}ms`); lastSeen = Date.now(); - await sleep(monitorConfig.checkIntervalMs); + await sleep(gasConfig.checkIntervalMs!); continue; } // Enough time has passed - might be stuck - if (timePassed > monitorConfig.stallTimeMs && attempts < monitorConfig.maxAttempts) { + if (timePassed > gasConfig.stallTimeMs! && attempts < gasConfig.maxAttempts!) { attempts++; const newGasPrice = await this.getGasPrice(gasConfig, attempts); this.logger?.debug( - `L1 Transaction ${currentTxHash} appears stuck. Attempting speed-up ${attempts}/${monitorConfig.maxAttempts} ` + + `L1 Transaction ${currentTxHash} appears stuck. Attempting speed-up ${attempts}/${gasConfig.maxAttempts} ` + `with new priority fee ${formatGwei(newGasPrice.maxPriorityFeePerGas)} gwei`, ); @@ -201,13 +228,13 @@ export class GasUtils { txHashes.add(currentTxHash); lastSeen = Date.now(); } - await sleep(monitorConfig.checkIntervalMs); + await sleep(gasConfig.checkIntervalMs!); } catch (err: any) { this.logger?.warn(`Error monitoring tx ${currentTxHash}:`, err); if (err.message?.includes('reverted')) { throw err; } - await sleep(monitorConfig.checkIntervalMs); + await sleep(gasConfig.checkIntervalMs!); } } } @@ -215,8 +242,8 @@ export class GasUtils { /** * Gets the current gas price with bounds checking */ - private async getGasPrice(_gasConfig?: GasConfig, attempt: number = 0): Promise { - const gasConfig = { ...this.gasConfig, ..._gasConfig }; + private async getGasPrice(_gasConfig?: GasUtilsConfig, attempt: number = 0): Promise { + const gasConfig = { ...this.config, ..._gasConfig }; const block = await this.publicClient.getBlock({ blockTag: 'latest' }); const baseFee = block.baseFeePerGas ?? 0n; @@ -227,7 +254,7 @@ export class GasUtils { priorityFee = (priorityFee * (100n + (gasConfig.priorityFeeBumpPercentage ?? 20n) * BigInt(attempt))) / 100n; } - const maxFeePerGas = gasConfig.maxGwei * WEI_CONST; + const maxFeePerGas = gasConfig.maxGwei! * WEI_CONST; const maxPriorityFeePerGas = priorityFee; this.logger?.debug( @@ -241,8 +268,8 @@ export class GasUtils { /** * Estimates gas and adds buffer */ - private async estimateGas(account: Account, request: L1TxRequest, _gasConfig?: GasConfig): Promise { - const gasConfig = { ...this.gasConfig, ..._gasConfig }; + private async estimateGas(account: Account, request: L1TxRequest, _gasConfig?: GasUtilsConfig): Promise { + const gasConfig = { ...this.config, ..._gasConfig }; const initialEstimate = await this.publicClient.estimateGas({ account, ...request }); // Add buffer based on either fixed amount or percentage diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 60967110193..f4175d91e82 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -155,4 +155,12 @@ export type EnvVar = | 'AZTEC_SLOT_DURATION' | 'AZTEC_EPOCH_DURATION' | 'AZTEC_TARGET_COMMITTEE_SIZE' - | 'AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS'; + | 'AZTEC_EPOCH_PROOF_CLAIM_WINDOW_IN_L2_SLOTS' + | 'L1_GAS_LIMIT_BUFFER_PERCENTAGE' + | 'L1_GAS_LIMIT_BUFFER_FIXED' + | 'L1_GAS_PRICE_MIN' + | 'L1_GAS_PRICE_MAX' + | 'L1_PRIORITY_FEE_BUMP_PERCENTAGE' + | 'L1_TX_MONITOR_MAX_ATTEMPTS' + | 'L1_TX_MONITOR_CHECK_INTERVAL_MS' + | 'L1_TX_MONITOR_STALL_TIME_MS'; diff --git a/yarn-project/sequencer-client/src/config.ts b/yarn-project/sequencer-client/src/config.ts index 76130083211..de1c1ac80b9 100644 --- a/yarn-project/sequencer-client/src/config.ts +++ b/yarn-project/sequencer-client/src/config.ts @@ -1,8 +1,10 @@ import { type AllowedElement } from '@aztec/circuit-types'; import { AztecAddress, Fr, FunctionSelector, getContractClassFromArtifact } from '@aztec/circuits.js'; import { + type GasUtilsConfig, type L1ContractsConfig, type L1ReaderConfig, + gasUtilsConfigMappings, l1ContractsConfigMappings, l1ReaderConfigMappings, } from '@aztec/ethereum'; diff --git a/yarn-project/sequencer-client/src/publisher/config.ts b/yarn-project/sequencer-client/src/publisher/config.ts index 561add17597..7cb894f3c69 100644 --- a/yarn-project/sequencer-client/src/publisher/config.ts +++ b/yarn-project/sequencer-client/src/publisher/config.ts @@ -1,4 +1,4 @@ -import { type L1ReaderConfig, NULL_KEY } from '@aztec/ethereum'; +import { type GasUtilsConfig, type L1ReaderConfig, NULL_KEY, gasUtilsConfigMappings } from '@aztec/ethereum'; import { type ConfigMappingsType, getConfigFromMappings, numberConfigHelper } from '@aztec/foundation/config'; /** @@ -19,12 +19,12 @@ export type TxSenderConfig = L1ReaderConfig & { /** * Configuration of the L1Publisher. */ -export interface PublisherConfig { +export type PublisherConfig = GasUtilsConfig & { /** * The interval to wait between publish retries. */ l1PublishRetryIntervalMS: number; -} +}; export const getTxSenderConfigMappings: ( scope: 'PROVER' | 'SEQ', @@ -62,13 +62,16 @@ export function getTxSenderConfigFromEnv(scope: 'PROVER' | 'SEQ'): Omit ConfigMappingsType = scope => ({ +export const getPublisherConfigMappings: ( + scope: 'PROVER' | 'SEQ', +) => ConfigMappingsType = scope => ({ l1PublishRetryIntervalMS: { env: `${scope}_PUBLISH_RETRY_INTERVAL_MS`, parseEnv: (val: string) => +val, defaultValue: 1000, description: 'The interval to wait between publish retries.', }, + ...gasUtilsConfigMappings, }); export function getPublisherConfigFromEnv(scope: 'PROVER' | 'SEQ'): PublisherConfig { diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts index 9805fb59ae9..6943b8fbacc 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts @@ -1,6 +1,11 @@ import { L2Block } from '@aztec/circuit-types'; import { EthAddress } from '@aztec/circuits.js'; -import { type L1ContractsConfig, getL1ContractsConfigEnvVars } from '@aztec/ethereum'; +import { + type GasUtilsConfig, + type L1ContractsConfig, + defaultGasUtilsConfig, + getL1ContractsConfigEnvVars, +} from '@aztec/ethereum'; import { type ViemSignature } from '@aztec/foundation/eth-signature'; import { sleep } from '@aztec/foundation/sleep'; import { RollupAbi } from '@aztec/l1-artifacts'; @@ -106,7 +111,8 @@ describe('L1Publisher', () => { l1Contracts: { rollupAddress: EthAddress.ZERO.toString() }, l1PublishRetryIntervalMS: 1, ethereumSlotDuration: getL1ContractsConfigEnvVars().ethereumSlotDuration, - } as unknown as TxSenderConfig & PublisherConfig & Pick; + ...defaultGasUtilsConfig, + } as unknown as TxSenderConfig & PublisherConfig & Pick & GasUtilsConfig; publisher = new L1Publisher(config, new NoopTelemetryClient()); diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index b0a29474a4c..c358b80d9c7 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -17,7 +17,7 @@ import { type Proof, type RootRollupPublicInputs, } from '@aztec/circuits.js'; -import { GasUtils, type L1ContractsConfig, createEthereumChain } from '@aztec/ethereum'; +import { GasUtils, type GasUtilsConfig, type L1ContractsConfig, createEthereumChain } from '@aztec/ethereum'; import { makeTuple } from '@aztec/foundation/array'; import { areArraysEqual, compactArray, times } from '@aztec/foundation/collection'; import { type Signature } from '@aztec/foundation/eth-signature'; @@ -166,7 +166,7 @@ export class L1Publisher { private readonly gasUtils: GasUtils; constructor( - config: TxSenderConfig & PublisherConfig & Pick, + config: TxSenderConfig & PublisherConfig & Pick & GasUtilsConfig, client: TelemetryClient, ) { this.sleepTimeMs = config?.l1PublishRetryIntervalMS ?? 60_000; @@ -204,22 +204,7 @@ export class L1Publisher { }); } - this.gasUtils = new GasUtils( - this.publicClient, - this.walletClient, - this.log, - { - bufferPercentage: 20n, - maxGwei: 500n, - minGwei: 1n, - priorityFeeGwei: 15n / 10n, - }, - { - maxAttempts: 5, - checkIntervalMs: 30_000, - stallTimeMs: 120_000, - }, - ); + this.gasUtils = new GasUtils(this.publicClient, this.walletClient, this.log, config); } public getPayLoad() { From b3223d7233bd72ed6f213729dedcdbcc2795c5d9 Mon Sep 17 00:00:00 2001 From: spypsy Date: Tue, 19 Nov 2024 09:39:14 +0000 Subject: [PATCH 21/45] rename to L1TxUtils --- yarn-project/ethereum/src/deploy_l1_contracts.ts | 8 ++++---- yarn-project/ethereum/src/index.ts | 2 +- .../src/{gas_utils.test.ts => l1_tx_utils.test.ts} | 10 +++++----- .../ethereum/src/{gas_utils.ts => l1_tx_utils.ts} | 2 +- .../sequencer-client/src/publisher/l1-publisher.ts | 12 ++++++------ 5 files changed, 17 insertions(+), 17 deletions(-) rename yarn-project/ethereum/src/{gas_utils.test.ts => l1_tx_utils.test.ts} (94%) rename yarn-project/ethereum/src/{gas_utils.ts => l1_tx_utils.ts} (99%) diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index eb3c65d4668..b63268b9e76 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -52,8 +52,8 @@ import { foundry } from 'viem/chains'; import { type L1ContractsConfig } from './config.js'; import { isAnvilTestChain } from './ethereum_chain.js'; -import { GasUtils } from './gas_utils.js'; import { type L1ContractAddresses } from './l1_contract_addresses.js'; +import { L1TxUtils } from './l1_tx_utils.js'; /** * Return type of the deployL1Contract function. @@ -617,7 +617,7 @@ export async function deployL1Contract( let txHash: Hex | undefined = undefined; let resultingAddress: Hex | null | undefined = undefined; - const gasUtils = new GasUtils(publicClient, walletClient, logger); + const l1TxUtils = new L1TxUtils(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({ + const receipt = await l1TxUtils.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({ + const receipt = await l1TxUtils.sendAndMonitorTransaction({ to: '0x', // Contract creation data: deployData, }); diff --git a/yarn-project/ethereum/src/index.ts b/yarn-project/ethereum/src/index.ts index c90dec0599a..ba742f2304b 100644 --- a/yarn-project/ethereum/src/index.ts +++ b/yarn-project/ethereum/src/index.ts @@ -1,7 +1,7 @@ export * from './constants.js'; export * from './deploy_l1_contracts.js'; export * from './ethereum_chain.js'; -export * from './gas_utils.js'; +export * from './l1_tx_utils.js'; export * from './l1_contract_addresses.js'; export * from './l1_reader.js'; export * from './utils.js'; diff --git a/yarn-project/ethereum/src/gas_utils.test.ts b/yarn-project/ethereum/src/l1_tx_utils.test.ts similarity index 94% rename from yarn-project/ethereum/src/gas_utils.test.ts rename to yarn-project/ethereum/src/l1_tx_utils.test.ts index 5cdde77bc7e..87809e1af2a 100644 --- a/yarn-project/ethereum/src/gas_utils.test.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.test.ts @@ -7,7 +7,7 @@ import { createPublicClient, createWalletClient, http } from 'viem'; import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; import { foundry } from 'viem/chains'; -import { GasUtils } from './gas_utils.js'; +import { L1TxUtils } from './l1_tx_utils.js'; const MNEMONIC = 'test test test test test test test test test test test junk'; const WEI_CONST = 1_000_000_000n; @@ -26,7 +26,7 @@ const startAnvil = async (l1BlockTime?: number) => { }; describe('GasUtils', () => { - let gasUtils: GasUtils; + let gasUtils: L1TxUtils; let publicClient: any; let walletClient: any; let anvil: Anvil; @@ -55,7 +55,7 @@ describe('GasUtils', () => { account, }); - gasUtils = new GasUtils(publicClient, walletClient, logger, { + gasUtils = new L1TxUtils(publicClient, walletClient, logger, { bufferPercentage: 20n, maxGwei: 500n, minGwei: 1n, @@ -144,7 +144,7 @@ describe('GasUtils', () => { it('adds appropriate buffer to gas estimation', async () => { // First deploy without any buffer - const baselineGasUtils = new GasUtils(publicClient, walletClient, logger, { + const baselineGasUtils = new L1TxUtils(publicClient, walletClient, logger, { bufferPercentage: 0n, maxGwei: 500n, minGwei: 1n, @@ -164,7 +164,7 @@ describe('GasUtils', () => { }); // Now deploy with 20% buffer - const bufferedGasUtils = new GasUtils(publicClient, walletClient, logger, { + const bufferedGasUtils = new L1TxUtils(publicClient, walletClient, logger, { bufferPercentage: 20n, maxGwei: 500n, minGwei: 1n, diff --git a/yarn-project/ethereum/src/gas_utils.ts b/yarn-project/ethereum/src/l1_tx_utils.ts similarity index 99% rename from yarn-project/ethereum/src/gas_utils.ts rename to yarn-project/ethereum/src/l1_tx_utils.ts index a9caba73190..aa949393fba 100644 --- a/yarn-project/ethereum/src/gas_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.ts @@ -116,7 +116,7 @@ interface GasPrice { maxPriorityFeePerGas: bigint; } -export class GasUtils { +export class L1TxUtils { private readonly config: GasUtilsConfig; constructor( diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index c358b80d9c7..c0111a16de3 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -17,7 +17,7 @@ import { type Proof, type RootRollupPublicInputs, } from '@aztec/circuits.js'; -import { GasUtils, type GasUtilsConfig, type L1ContractsConfig, createEthereumChain } from '@aztec/ethereum'; +import { type GasUtilsConfig, type L1ContractsConfig, L1TxUtils, createEthereumChain } from '@aztec/ethereum'; import { makeTuple } from '@aztec/foundation/array'; import { areArraysEqual, compactArray, times } from '@aztec/foundation/collection'; import { type Signature } from '@aztec/foundation/eth-signature'; @@ -163,7 +163,7 @@ export class L1Publisher { public static PROPOSE_GAS_GUESS: bigint = 12_000_000n; public static PROPOSE_AND_CLAIM_GAS_GUESS: bigint = this.PROPOSE_GAS_GUESS + 100_000n; - private readonly gasUtils: GasUtils; + private readonly l1TxUtils: L1TxUtils; constructor( config: TxSenderConfig & PublisherConfig & Pick & GasUtilsConfig, @@ -204,7 +204,7 @@ export class L1Publisher { }); } - this.gasUtils = new GasUtils(this.publicClient, this.walletClient, this.log, config); + this.l1TxUtils = new L1TxUtils(this.publicClient, this.walletClient, this.log, config); } public getPayLoad() { @@ -701,7 +701,7 @@ export class L1Publisher { const txArgs = [...this.getSubmitEpochProofArgs(args), proofHex] as const; this.log.info(`SubmitEpochProof proofSize=${args.proof.withoutPublicInputs().length} bytes`); - const txReceipt = await this.gasUtils.sendAndMonitorTransaction({ + const txReceipt = await this.l1TxUtils.sendAndMonitorTransaction({ to: this.rollupContract.address, data: encodeFunctionData({ abi: this.rollupContract.abi, @@ -769,7 +769,7 @@ export class L1Publisher { try { const args = this.prepareProposeTx(encodedData); - const receipt = await this.gasUtils.sendAndMonitorTransaction( + const receipt = await this.l1TxUtils.sendAndMonitorTransaction( { to: this.rollupContract.address, data: encodeFunctionData({ @@ -807,7 +807,7 @@ export class L1Publisher { this.log.info(inspect(quote.payload)); const args = this.prepareProposeTx(encodedData); - const receipt = await this.gasUtils.sendAndMonitorTransaction({ + const receipt = await this.l1TxUtils.sendAndMonitorTransaction({ to: this.rollupContract.address, data: encodeFunctionData({ abi: this.rollupContract.abi, From 47f15a4640899779547db41f78bb3ae265697f78 Mon Sep 17 00:00:00 2001 From: spypsy Date: Tue, 19 Nov 2024 10:13:37 +0000 Subject: [PATCH 22/45] don't use 0x --- yarn-project/ethereum/src/deploy_l1_contracts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index b63268b9e76..df68b7043d0 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -681,7 +681,7 @@ export async function deployL1Contract( // Regular deployment path const deployData = encodeDeployData({ abi, bytecode, args }); const receipt = await l1TxUtils.sendAndMonitorTransaction({ - to: '0x', // Contract creation + to: EthAddress.ZERO.toString(), // Contract creation data: deployData, }); From 03fbf79e0a4bb9a1bb36f91b558298540ce10bae Mon Sep 17 00:00:00 2001 From: spypsy Date: Tue, 19 Nov 2024 11:12:33 +0000 Subject: [PATCH 23/45] send contract deployments to null --- yarn-project/ethereum/src/deploy_l1_contracts.ts | 2 +- yarn-project/ethereum/src/l1_tx_utils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index df68b7043d0..d90b6fab71d 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -681,7 +681,7 @@ export async function deployL1Contract( // Regular deployment path const deployData = encodeDeployData({ abi, bytecode, args }); const receipt = await l1TxUtils.sendAndMonitorTransaction({ - to: EthAddress.ZERO.toString(), // Contract creation + to: null, data: deployData, }); diff --git a/yarn-project/ethereum/src/l1_tx_utils.ts b/yarn-project/ethereum/src/l1_tx_utils.ts index aa949393fba..8bd6b7da9e7 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.ts @@ -106,7 +106,7 @@ export const gasUtilsConfigMappings: ConfigMappingsType = { export const defaultGasUtilsConfig = getDefaultConfig(gasUtilsConfigMappings); export interface L1TxRequest { - to: Address; + to: Address | null; data: Hex; value?: bigint; } From 280c9a7f0c71e9a919b6f29d984d47266a68fd0a Mon Sep 17 00:00:00 2001 From: spypsy Date: Tue, 19 Nov 2024 12:52:41 +0000 Subject: [PATCH 24/45] fix publisher tests --- yarn-project/ethereum/src/l1_tx_utils.ts | 19 ++- yarn-project/sequencer-client/src/config.ts | 2 - .../sequencer-client/src/publisher/config.ts | 8 +- .../src/publisher/l1-publisher.test.ts | 109 ++++++++---------- .../src/publisher/l1-publisher.ts | 8 +- 5 files changed, 62 insertions(+), 84 deletions(-) diff --git a/yarn-project/ethereum/src/l1_tx_utils.ts b/yarn-project/ethereum/src/l1_tx_utils.ts index 8bd6b7da9e7..0fbbab54e5e 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.ts @@ -25,7 +25,7 @@ import { const WEI_CONST = 1_000_000_000n; -export interface GasUtilsConfig { +export interface L1TxUtilsConfig { /** * How much to increase calculated gas limit. */ @@ -60,7 +60,7 @@ export interface GasUtilsConfig { stallTimeMs?: number; } -export const gasUtilsConfigMappings: ConfigMappingsType = { +export const l1TxUtilsConfigMappings: ConfigMappingsType = { bufferPercentage: { description: 'How much to increase gas price by each attempt (percentage)', env: 'L1_GAS_LIMIT_BUFFER_PERCENTAGE', @@ -103,7 +103,7 @@ export const gasUtilsConfigMappings: ConfigMappingsType = { }, }; -export const defaultGasUtilsConfig = getDefaultConfig(gasUtilsConfigMappings); +export const defaultL1TxUtilsConfig = getDefaultConfig(l1TxUtilsConfigMappings); export interface L1TxRequest { to: Address | null; @@ -117,16 +117,16 @@ interface GasPrice { } export class L1TxUtils { - private readonly config: GasUtilsConfig; + private readonly config: L1TxUtilsConfig; constructor( private readonly publicClient: PublicClient, private readonly walletClient: WalletClient, private readonly logger?: DebugLogger, - config?: Partial, + config?: Partial, ) { this.config = { - ...defaultGasUtilsConfig, + ...defaultL1TxUtilsConfig, ...(config || {}), }; } @@ -140,7 +140,7 @@ export class L1TxUtils { */ public async sendAndMonitorTransaction( request: L1TxRequest, - _gasConfig?: Partial, + _gasConfig?: Partial, ): Promise { const gasConfig = { ...this.config, ..._gasConfig }; const account = this.walletClient.account; @@ -149,7 +149,6 @@ export class L1TxUtils { const gasPrice = await this.getGasPrice(gasConfig); const nonce = await this.publicClient.getTransactionCount({ address: account.address }); - // Send initial tx const txHash = await this.walletClient.sendTransaction({ ...request, @@ -242,7 +241,7 @@ export class L1TxUtils { /** * Gets the current gas price with bounds checking */ - private async getGasPrice(_gasConfig?: GasUtilsConfig, attempt: number = 0): Promise { + private async getGasPrice(_gasConfig?: L1TxUtilsConfig, attempt: number = 0): Promise { const gasConfig = { ...this.config, ..._gasConfig }; const block = await this.publicClient.getBlock({ blockTag: 'latest' }); const baseFee = block.baseFeePerGas ?? 0n; @@ -268,7 +267,7 @@ export class L1TxUtils { /** * Estimates gas and adds buffer */ - private async estimateGas(account: Account, request: L1TxRequest, _gasConfig?: GasUtilsConfig): Promise { + private async estimateGas(account: Account, request: L1TxRequest, _gasConfig?: L1TxUtilsConfig): Promise { const gasConfig = { ...this.config, ..._gasConfig }; const initialEstimate = await this.publicClient.estimateGas({ account, ...request }); diff --git a/yarn-project/sequencer-client/src/config.ts b/yarn-project/sequencer-client/src/config.ts index de1c1ac80b9..76130083211 100644 --- a/yarn-project/sequencer-client/src/config.ts +++ b/yarn-project/sequencer-client/src/config.ts @@ -1,10 +1,8 @@ import { type AllowedElement } from '@aztec/circuit-types'; import { AztecAddress, Fr, FunctionSelector, getContractClassFromArtifact } from '@aztec/circuits.js'; import { - type GasUtilsConfig, type L1ContractsConfig, type L1ReaderConfig, - gasUtilsConfigMappings, l1ContractsConfigMappings, l1ReaderConfigMappings, } from '@aztec/ethereum'; diff --git a/yarn-project/sequencer-client/src/publisher/config.ts b/yarn-project/sequencer-client/src/publisher/config.ts index 7cb894f3c69..367f2aa6677 100644 --- a/yarn-project/sequencer-client/src/publisher/config.ts +++ b/yarn-project/sequencer-client/src/publisher/config.ts @@ -1,4 +1,4 @@ -import { type GasUtilsConfig, type L1ReaderConfig, NULL_KEY, gasUtilsConfigMappings } from '@aztec/ethereum'; +import { type L1ReaderConfig, type L1TxUtilsConfig, NULL_KEY, l1TxUtilsConfigMappings } from '@aztec/ethereum'; import { type ConfigMappingsType, getConfigFromMappings, numberConfigHelper } from '@aztec/foundation/config'; /** @@ -19,7 +19,7 @@ export type TxSenderConfig = L1ReaderConfig & { /** * Configuration of the L1Publisher. */ -export type PublisherConfig = GasUtilsConfig & { +export type PublisherConfig = L1TxUtilsConfig & { /** * The interval to wait between publish retries. */ @@ -64,14 +64,14 @@ export function getTxSenderConfigFromEnv(scope: 'PROVER' | 'SEQ'): Omit ConfigMappingsType = scope => ({ +) => ConfigMappingsType = scope => ({ l1PublishRetryIntervalMS: { env: `${scope}_PUBLISH_RETRY_INTERVAL_MS`, parseEnv: (val: string) => +val, defaultValue: 1000, description: 'The interval to wait between publish retries.', }, - ...gasUtilsConfigMappings, + ...l1TxUtilsConfigMappings, }); export function getPublisherConfigFromEnv(scope: 'PROVER' | 'SEQ'): PublisherConfig { diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts index 6943b8fbacc..0a1f8c5cbdb 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts @@ -1,9 +1,10 @@ import { L2Block } from '@aztec/circuit-types'; import { EthAddress } from '@aztec/circuits.js'; import { - type GasUtilsConfig, type L1ContractsConfig, - defaultGasUtilsConfig, + type L1TxRequest, + type L1TxUtilsConfig, + defaultL1TxUtilsConfig, getL1ContractsConfigEnvVars, } from '@aztec/ethereum'; import { type ViemSignature } from '@aztec/foundation/eth-signature'; @@ -12,11 +13,18 @@ import { RollupAbi } from '@aztec/l1-artifacts'; import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; import { type MockProxy, mock } from 'jest-mock-extended'; -import { type GetTransactionReceiptReturnType, type PrivateKeyAccount, type WalletClient } from 'viem'; +import { + type GetTransactionReceiptReturnType, + type PrivateKeyAccount, + type TransactionReceipt, + encodeFunctionData, +} from 'viem'; import { type PublisherConfig, type TxSenderConfig } from './config.js'; import { L1Publisher } from './l1-publisher.js'; +const mockRollupAddress = '0xcafe'; + interface MockPublicClient { getTransactionReceipt: ({ hash }: { hash: '0x${string}' }) => Promise; getBlock(): Promise<{ timestamp: bigint }>; @@ -24,14 +32,11 @@ interface MockPublicClient { estimateGas: ({ to, data }: { to: '0x${string}'; data: '0x${string}' }) => Promise; } -interface MockGasUtils { - getGasPrice: () => Promise; - estimateGas: ({ to, data }: { to: '0x${string}'; data: '0x${string}' }) => Promise; - monitorTransaction: ( - txHash: `0x${string}`, - walletClient: WalletClient, - options: { to: `0x${string}`; data: `0x${string}`; nonce: bigint; gasLimit: bigint; maxFeePerGas: bigint }, - ) => Promise; +interface MockL1TxUtils { + sendAndMonitorTransaction: ( + request: L1TxRequest, + _gasConfig?: Partial, + ) => Promise; } interface MockRollupContractWrite { @@ -57,6 +62,9 @@ interface MockRollupContractRead { class MockRollupContract { constructor(public write: MockRollupContractWrite, public read: MockRollupContractRead, public abi = RollupAbi) {} + get address() { + return mockRollupAddress; + } } describe('L1Publisher', () => { @@ -65,7 +73,7 @@ describe('L1Publisher', () => { let rollupContract: MockRollupContract; let publicClient: MockProxy; - let gasUtils: MockProxy; + let l1TxUtils: MockProxy; let proposeTxHash: `0x${string}`; let proposeTxReceipt: GetTransactionReceiptReturnType; @@ -76,8 +84,6 @@ describe('L1Publisher', () => { let blockHash: Buffer; let body: Buffer; - let account: PrivateKeyAccount; - let publisher: L1Publisher; const GAS_GUESS = 300_000n; @@ -103,7 +109,7 @@ describe('L1Publisher', () => { rollupContract = new MockRollupContract(rollupContractWrite, rollupContractRead); publicClient = mock(); - gasUtils = mock(); + l1TxUtils = mock(); const config = { l1RpcUrl: `http://127.0.0.1:8545`, l1ChainId: 1, @@ -111,30 +117,29 @@ describe('L1Publisher', () => { l1Contracts: { rollupAddress: EthAddress.ZERO.toString() }, l1PublishRetryIntervalMS: 1, ethereumSlotDuration: getL1ContractsConfigEnvVars().ethereumSlotDuration, - ...defaultGasUtilsConfig, - } as unknown as TxSenderConfig & PublisherConfig & Pick & GasUtilsConfig; + ...defaultL1TxUtilsConfig, + } as unknown as TxSenderConfig & + PublisherConfig & + Pick & + L1TxUtilsConfig; publisher = new L1Publisher(config, new NoopTelemetryClient()); (publisher as any)['rollupContract'] = rollupContract; (publisher as any)['publicClient'] = publicClient; - (publisher as any)['gasUtils'] = gasUtils; - account = (publisher as any)['account']; + (publisher as any)['l1TxUtils'] = l1TxUtils; + publisher as any; rollupContractRead.getCurrentSlot.mockResolvedValue(l2Block.header.globalVariables.slotNumber.toBigInt()); publicClient.getBlock.mockResolvedValue({ timestamp: 12n }); publicClient.estimateGas.mockResolvedValue(GAS_GUESS); - gasUtils.getGasPrice.mockResolvedValue(100_000_000n); - gasUtils.estimateGas.mockResolvedValue(GAS_GUESS); - gasUtils.monitorTransaction.mockResolvedValue(proposeTxReceipt); + l1TxUtils.sendAndMonitorTransaction.mockResolvedValue(proposeTxReceipt); }); it('publishes and propose l2 block to l1', async () => { rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`); rollupContractWrite.propose.mockResolvedValueOnce(proposeTxHash); - publicClient.getTransactionReceipt.mockResolvedValueOnce(proposeTxReceipt); - const result = await publisher.proposeL2Block(l2Block); expect(result).toEqual(true); @@ -147,22 +152,22 @@ describe('L1Publisher', () => { [], `0x${body.toString('hex')}`, ] as const; - expect(rollupContractWrite.propose).toHaveBeenCalledWith(args, { - account: account, - gas: L1Publisher.PROPOSE_GAS_GUESS + GAS_GUESS, - maxFeePerGas: 100_000_000n, - }); - expect(publicClient.getTransactionReceipt).toHaveBeenCalledWith({ hash: proposeTxHash }); + expect(l1TxUtils.sendAndMonitorTransaction).toHaveBeenCalledWith( + { + to: mockRollupAddress, + data: encodeFunctionData({ abi: rollupContract.abi, functionName: 'propose', args }), + }, + { bufferFixed: L1Publisher.PROPOSE_GAS_GUESS }, + ); }); it('does not retry if sending a propose tx fails', async () => { rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`); - rollupContractWrite.propose.mockRejectedValueOnce(new Error()).mockResolvedValueOnce(proposeTxHash); + l1TxUtils.sendAndMonitorTransaction.mockRejectedValueOnce(new Error()).mockResolvedValueOnce(proposeTxReceipt); const result = await publisher.proposeL2Block(l2Block); expect(result).toEqual(false); - expect(rollupContractWrite.propose).toHaveBeenCalledTimes(1); }); it('does not retry if simulating a publish and propose tx fails', async () => { @@ -172,45 +177,20 @@ describe('L1Publisher', () => { await expect(publisher.proposeL2Block(l2Block)).rejects.toThrow(); expect(rollupContractRead.validateHeader).toHaveBeenCalledTimes(1); - expect(rollupContractWrite.propose).toHaveBeenCalledTimes(0); }); it('does not retry if sending a publish and propose tx fails', async () => { rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`); - rollupContractWrite.propose.mockRejectedValueOnce(new Error()); + l1TxUtils.sendAndMonitorTransaction.mockRejectedValueOnce(new Error()).mockResolvedValueOnce(proposeTxReceipt); const result = await publisher.proposeL2Block(l2Block); expect(result).toEqual(false); - expect(rollupContractWrite.propose).toHaveBeenCalledTimes(1); - }); - - it('retries if fetching the receipt fails (propose)', async () => { - rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`); - rollupContractWrite.propose.mockResolvedValueOnce(proposeTxHash); - publicClient.getTransactionReceipt.mockRejectedValueOnce(new Error()).mockResolvedValueOnce(proposeTxReceipt); - - const result = await publisher.proposeL2Block(l2Block); - - expect(result).toEqual(true); - expect(publicClient.getTransactionReceipt).toHaveBeenCalledTimes(2); - }); - - it('retries if fetching the receipt fails (publish propose)', async () => { - rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`); - rollupContractWrite.propose.mockResolvedValueOnce(proposeTxHash as `0x${string}`); - publicClient.getTransactionReceipt.mockRejectedValueOnce(new Error()).mockResolvedValueOnce(proposeTxReceipt); - - const result = await publisher.proposeL2Block(l2Block); - - expect(result).toEqual(true); - expect(publicClient.getTransactionReceipt).toHaveBeenCalledTimes(2); }); it('returns false if publish and propose tx reverts', async () => { rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`); - rollupContractWrite.propose.mockResolvedValueOnce(proposeTxHash); - publicClient.getTransactionReceipt.mockResolvedValueOnce({ ...proposeTxReceipt, status: 'reverted' }); + l1TxUtils.sendAndMonitorTransaction.mockResolvedValueOnce({ ...proposeTxReceipt, status: 'reverted' }); const result = await publisher.proposeL2Block(l2Block); @@ -220,7 +200,7 @@ describe('L1Publisher', () => { it('returns false if propose tx reverts', async () => { rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`); - publicClient.getTransactionReceipt.mockResolvedValueOnce({ ...proposeTxReceipt, status: 'reverted' }); + l1TxUtils.sendAndMonitorTransaction.mockResolvedValueOnce({ ...proposeTxReceipt, status: 'reverted' }); const result = await publisher.proposeL2Block(l2Block); @@ -229,8 +209,9 @@ describe('L1Publisher', () => { it('returns false if sending publish and progress tx is interrupted', async () => { rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`); - rollupContractWrite.propose.mockImplementationOnce(() => sleep(10, proposeTxHash) as Promise<`0x${string}`>); - + l1TxUtils.sendAndMonitorTransaction.mockImplementationOnce( + () => sleep(10, proposeTxReceipt) as Promise, + ); const resultPromise = publisher.proposeL2Block(l2Block); publisher.interrupt(); const result = await resultPromise; @@ -241,7 +222,9 @@ describe('L1Publisher', () => { it('returns false if sending propose tx is interrupted', async () => { rollupContractRead.archive.mockResolvedValue(l2Block.header.lastArchive.root.toString() as `0x${string}`); - rollupContractWrite.propose.mockImplementationOnce(() => sleep(10, proposeTxHash) as Promise<`0x${string}`>); + l1TxUtils.sendAndMonitorTransaction.mockImplementationOnce( + () => sleep(10, proposeTxReceipt) as Promise, + ); const resultPromise = publisher.proposeL2Block(l2Block); publisher.interrupt(); diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index c0111a16de3..3a032cb6d25 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -17,7 +17,7 @@ import { type Proof, type RootRollupPublicInputs, } from '@aztec/circuits.js'; -import { type GasUtilsConfig, type L1ContractsConfig, L1TxUtils, createEthereumChain } from '@aztec/ethereum'; +import { type L1ContractsConfig, L1TxUtils, type L1TxUtilsConfig, createEthereumChain } from '@aztec/ethereum'; import { makeTuple } from '@aztec/foundation/array'; import { areArraysEqual, compactArray, times } from '@aztec/foundation/collection'; import { type Signature } from '@aztec/foundation/eth-signature'; @@ -166,7 +166,7 @@ export class L1Publisher { private readonly l1TxUtils: L1TxUtils; constructor( - config: TxSenderConfig & PublisherConfig & Pick & GasUtilsConfig, + config: TxSenderConfig & PublisherConfig & Pick & L1TxUtilsConfig, client: TelemetryClient, ) { this.sleepTimeMs = config?.l1PublishRetryIntervalMS ?? 60_000; @@ -507,7 +507,7 @@ export class L1Publisher { ? await this.sendProposeAndClaimTx(proposeTxArgs, proofQuote) : await this.sendProposeTx(proposeTxArgs); - if (!result || result?.receipt) { + if (!result || !result?.receipt) { this.log.info(`Failed to publish block ${block.number} to L1`, ctx); return false; } @@ -768,7 +768,6 @@ export class L1Publisher { } try { const args = this.prepareProposeTx(encodedData); - const receipt = await this.l1TxUtils.sendAndMonitorTransaction( { to: this.rollupContract.address, @@ -782,7 +781,6 @@ export class L1Publisher { bufferFixed: L1Publisher.PROPOSE_GAS_GUESS, }, ); - return { receipt, args, From 0b45fe6d6548bae7c0cac26f38048cc619808dec Mon Sep 17 00:00:00 2001 From: spypsy Date: Tue, 19 Nov 2024 14:03:50 +0000 Subject: [PATCH 25/45] fix getNodeInfo test --- .../circuit-types/src/interfaces/aztec-node.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 042ff45987f..65f159579ab 100644 --- a/yarn-project/circuit-types/src/interfaces/aztec-node.test.ts +++ b/yarn-project/circuit-types/src/interfaces/aztec-node.test.ts @@ -158,7 +158,15 @@ describe('AztecNodeApiSchema', () => { it('getNodeInfo', async () => { const response = await context.client.getNodeInfo(); - expect(response).toEqual(await handler.getNodeInfo()); + expect(response).toEqual({ + ...(await handler.getNodeInfo()), + l1ContractAddresses: Object.fromEntries( + L1ContractsNames.map(name => [name, expect.any(EthAddress)]), + ) as L1ContractAddresses, + protocolContractAddresses: Object.fromEntries( + ProtocolContractsNames.map(name => [name, expect.any(AztecAddress)]), + ) as ProtocolContractAddresses, + }); }); it('getBlocks', async () => { From 653a0834aeafdfe55d4d5f4a67a93b18c3659c6d Mon Sep 17 00:00:00 2001 From: spypsy Date: Tue, 19 Nov 2024 16:47:48 +0000 Subject: [PATCH 26/45] split sendTx & monitorTx for separate use --- .../ethereum/src/deploy_l1_contracts.ts | 4 +- yarn-project/ethereum/src/l1_tx_utils.ts | 75 +++++++++++++------ 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index d90b6fab71d..c12d5814959 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -667,11 +667,11 @@ export async function deployL1Contract( const existing = await publicClient.getBytecode({ address: resultingAddress }); if (existing === undefined || existing === '0x') { - const receipt = await l1TxUtils.sendAndMonitorTransaction({ + const res = await l1TxUtils.sendTransaction({ to: deployer, data: concatHex([salt, calldata]), }); - txHash = receipt.transactionHash; + txHash = res.txHash; logger?.verbose(`Deployed contract with salt ${salt} to address ${resultingAddress} in tx ${txHash}.`); } else { diff --git a/yarn-project/ethereum/src/l1_tx_utils.ts b/yarn-project/ethereum/src/l1_tx_utils.ts index 0fbbab54e5e..6fe5a954565 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.ts @@ -132,46 +132,60 @@ export class L1TxUtils { } /** - * Sends a transaction and monitors it until completion, handling gas estimation and price bumping - * @param walletClient - The wallet client for sending the transaction + * Sends a transaction with gas estimation and pricing * @param request - The transaction request (to, data, value) - * @param monitorConfig - Optional monitoring configuration - * @returns The hash of the successful transaction + * @param gasConfig - Optional gas configuration + * @returns The transaction hash and parameters used */ - public async sendAndMonitorTransaction( + public async sendTransaction( request: L1TxRequest, _gasConfig?: Partial, - ): Promise { + ): Promise<{ txHash: Hex; gasLimit: bigint; gasPrice: GasPrice }> { const gasConfig = { ...this.config, ..._gasConfig }; const account = this.walletClient.account; - // Estimate gas - const gasLimit = await this.estimateGas(account, request); + const gasLimit = await this.estimateGas(account, request); const gasPrice = await this.getGasPrice(gasConfig); - const nonce = await this.publicClient.getTransactionCount({ address: account.address }); - // Send initial tx + const txHash = await this.walletClient.sendTransaction({ ...request, gas: gasLimit, maxFeePerGas: gasPrice.maxFeePerGas, maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas, - nonce, }); this.logger?.verbose( `Sent L1 transaction ${txHash} with gas limit ${gasLimit} and price ${formatGwei(gasPrice.maxFeePerGas)} gwei`, ); - // Track all tx hashes we send - const txHashes = new Set([txHash]); - let currentTxHash = txHash; + return { txHash, gasLimit, gasPrice }; + } + + /** + * Monitors a transaction until completion, handling speed-ups if needed + * @param request - Original transaction request (needed for speed-ups) + * @param initialTxHash - Hash of the initial transaction + * @param params - Parameters used in the initial transaction + * @param gasConfig - Optional gas configuration + */ + public async monitorTransaction( + request: L1TxRequest, + initialTxHash: Hex, + params: { nonce: number; gasLimit: bigint }, + _gasConfig?: Partial, + ): Promise { + const gasConfig = { ...this.config, ..._gasConfig }; + const account = this.walletClient.account; + + const txHashes = new Set([initialTxHash]); + let currentTxHash = initialTxHash; let attempts = 0; let lastSeen = Date.now(); + while (true) { try { const currentNonce = await this.publicClient.getTransactionCount({ address: account.address }); - if (currentNonce > nonce) { - // A tx with this nonce has been mined - check all our tx hashes + if (currentNonce > params.nonce) { for (const hash of txHashes) { try { const receipt = await this.publicClient.getTransactionReceipt({ hash }); @@ -187,15 +201,11 @@ export class L1TxUtils { if (err instanceof Error && err.message.includes('reverted')) { throw err; } - // We can ignore other errors - just try the next hash } } } - // Check if current tx is pending const tx = await this.publicClient.getTransaction({ hash: currentTxHash }); - - // Get time passed const timePassed = Date.now() - lastSeen; if (tx && timePassed < gasConfig.stallTimeMs!) { @@ -205,7 +215,6 @@ export class L1TxUtils { continue; } - // Enough time has passed - might be stuck if (timePassed > gasConfig.stallTimeMs! && attempts < gasConfig.maxAttempts!) { attempts++; const newGasPrice = await this.getGasPrice(gasConfig, attempts); @@ -217,13 +226,12 @@ export class L1TxUtils { currentTxHash = await this.walletClient.sendTransaction({ ...request, - nonce, - gas: gasLimit, + nonce: params.nonce, + gas: params.gasLimit, maxFeePerGas: newGasPrice.maxFeePerGas, maxPriorityFeePerGas: newGasPrice.maxPriorityFeePerGas, }); - // Record new tx hash txHashes.add(currentTxHash); lastSeen = Date.now(); } @@ -238,6 +246,25 @@ export class L1TxUtils { } } + /** + * Sends a transaction and monitors it until completion + * @param request - The transaction request (to, data, value) + * @param gasConfig - Optional gas configuration + * @returns The receipt of the successful transaction + */ + public async sendAndMonitorTransaction( + request: L1TxRequest, + gasConfig?: Partial, + ): Promise { + const { txHash, gasLimit } = await this.sendTransaction(request, gasConfig); + const tx = await this.publicClient.getTransaction({ hash: txHash }); + if (!tx?.nonce) { + throw new Error(`Failed to get L1 transaction ${txHash} nonce`); + } + + return this.monitorTransaction(request, txHash, { nonce: tx.nonce, gasLimit }, gasConfig); + } + /** * Gets the current gas price with bounds checking */ From c15d6b1b204febcc3d3b0392724e6be3aea13b77 Mon Sep 17 00:00:00 2001 From: spypsy Date: Tue, 19 Nov 2024 16:56:31 +0000 Subject: [PATCH 27/45] calc nonce in monitor function --- yarn-project/ethereum/src/l1_tx_utils.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/yarn-project/ethereum/src/l1_tx_utils.ts b/yarn-project/ethereum/src/l1_tx_utils.ts index 6fe5a954565..7697cd9696f 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.ts @@ -171,12 +171,18 @@ export class L1TxUtils { public async monitorTransaction( request: L1TxRequest, initialTxHash: Hex, - params: { nonce: number; gasLimit: bigint }, + params: { gasLimit: bigint }, _gasConfig?: Partial, ): Promise { const gasConfig = { ...this.config, ..._gasConfig }; const account = this.walletClient.account; + const tx = await this.publicClient.getTransaction({ hash: initialTxHash }); + if (!tx?.nonce) { + throw new Error(`Failed to get L1 transaction ${initialTxHash} nonce`); + } + const nonce = tx.nonce; + const txHashes = new Set([initialTxHash]); let currentTxHash = initialTxHash; let attempts = 0; @@ -185,7 +191,7 @@ export class L1TxUtils { while (true) { try { const currentNonce = await this.publicClient.getTransactionCount({ address: account.address }); - if (currentNonce > params.nonce) { + if (currentNonce > nonce) { for (const hash of txHashes) { try { const receipt = await this.publicClient.getTransactionReceipt({ hash }); @@ -226,7 +232,7 @@ export class L1TxUtils { currentTxHash = await this.walletClient.sendTransaction({ ...request, - nonce: params.nonce, + nonce, gas: params.gasLimit, maxFeePerGas: newGasPrice.maxFeePerGas, maxPriorityFeePerGas: newGasPrice.maxPriorityFeePerGas, @@ -262,7 +268,7 @@ export class L1TxUtils { throw new Error(`Failed to get L1 transaction ${txHash} nonce`); } - return this.monitorTransaction(request, txHash, { nonce: tx.nonce, gasLimit }, gasConfig); + return this.monitorTransaction(request, txHash, { gasLimit }, gasConfig); } /** From 9508cb7b89a038d754fbdac54523383f7863ffb7 Mon Sep 17 00:00:00 2001 From: spypsy Date: Tue, 19 Nov 2024 17:47:36 +0000 Subject: [PATCH 28/45] fix null-check --- yarn-project/ethereum/src/l1_tx_utils.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/yarn-project/ethereum/src/l1_tx_utils.ts b/yarn-project/ethereum/src/l1_tx_utils.ts index 7697cd9696f..11217aa8a92 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.ts @@ -178,7 +178,7 @@ export class L1TxUtils { const account = this.walletClient.account; const tx = await this.publicClient.getTransaction({ hash: initialTxHash }); - if (!tx?.nonce) { + if (tx?.nonce === undefined || tx?.nonce === null) { throw new Error(`Failed to get L1 transaction ${initialTxHash} nonce`); } const nonce = tx.nonce; @@ -263,11 +263,6 @@ export class L1TxUtils { gasConfig?: Partial, ): Promise { const { txHash, gasLimit } = await this.sendTransaction(request, gasConfig); - const tx = await this.publicClient.getTransaction({ hash: txHash }); - if (!tx?.nonce) { - throw new Error(`Failed to get L1 transaction ${txHash} nonce`); - } - return this.monitorTransaction(request, txHash, { gasLimit }, gasConfig); } From d80ece634ccb745c5634d70b95fa094a51078e84 Mon Sep 17 00:00:00 2001 From: spypsy Date: Wed, 20 Nov 2024 09:43:56 +0000 Subject: [PATCH 29/45] remove from e2e config --- yarn-project/end-to-end/scripts/e2e_test_config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/yarn-project/end-to-end/scripts/e2e_test_config.yml b/yarn-project/end-to-end/scripts/e2e_test_config.yml index 3d65c783427..85d30516ee7 100644 --- a/yarn-project/end-to-end/scripts/e2e_test_config.yml +++ b/yarn-project/end-to-end/scripts/e2e_test_config.yml @@ -51,7 +51,6 @@ tests: test_path: 'e2e_fees/private_payments.test.ts' e2e_keys: {} e2e_l1_with_wall_time: {} - e2e_l1_gas: {} e2e_lending_contract: {} e2e_event_logs: {} e2e_max_block_number: {} From 47edcd83b1550ba8a8000b8d9f3ff0a9c296518d Mon Sep 17 00:00:00 2001 From: spypsy Date: Wed, 20 Nov 2024 13:14:39 +0000 Subject: [PATCH 30/45] temp fix integration publisher test --- .../composed/integration_l1_publisher.test.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts b/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts index 7b47386c6ac..2c3397ae010 100644 --- a/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts +++ b/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts @@ -544,15 +544,17 @@ describe('L1Publisher integration', () => { // Expect a proper error to be logged. Full message looks like: // aztec:sequencer:publisher [ERROR] Rollup process tx reverted. The contract function "propose" reverted. Error: Rollup__InvalidInHash(bytes32 expected, bytes32 actual) (0x00089a9d421a82c4a25f7acbebe69e638d5b064fa8a60e018793dcb0be53752c, 0x00a5a12af159e0608de45d825718827a36d8a7cdfa9ecc7955bc62180ae78e51) blockNumber=1 slotNumber=49 blockHash=0x131c59ebc2ce21224de6473fe954b0d4eb918043432a3a95406bb7e7a4297fbd txHash=0xc01c3c26b6b67003a8cce352afe475faf7e0196a5a3bba963cfda3792750ed28 - expect(loggerErrorSpy).toHaveBeenCalledWith( - expect.stringMatching(/Rollup__InvalidInHash/), - undefined, - expect.objectContaining({ - blockHash: expect.any(String), - blockNumber: expect.any(Number), - slotNumber: expect.any(BigInt), - }), - ); + expect(loggerErrorSpy).toHaveBeenCalledWith('Rollup publish failed', expect.stringContaining('0xcd6f4233')); + // NOTE: Reinstate check below with #10066 + // expect(loggerErrorSpy).toHaveBeenCalledWith( + // expect.stringMatching(/Rollup__InvalidInHash/), + // undefined, + // expect.objectContaining({ + // blockHash: expect.any(String), + // blockNumber: expect.any(Number), + // slotNumber: expect.any(BigInt), + // }), + // ); }); }); }); From d1fa40434f631003369ae0fac57836a3e003be9f Mon Sep 17 00:00:00 2001 From: spypsy Date: Wed, 20 Nov 2024 13:17:38 +0000 Subject: [PATCH 31/45] temp fix integration publisher test --- .../sequencer-client/src/publisher/l1-publisher.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index 93bd93f16bf..bb815fa0736 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -507,7 +507,7 @@ export class L1Publisher { ? await this.sendProposeAndClaimTx(proposeTxArgs, proofQuote) : await this.sendProposeTx(proposeTxArgs); - if (!result || !result?.receipt) { + if (!result?.receipt) { this.log.info(`Failed to publish block ${block.number} to L1`, ctx); return false; } @@ -527,7 +527,6 @@ export class L1Publisher { }; this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...ctx }); this.metrics.recordProcessBlockTx(timer.ms(), stats); - return true; } @@ -790,7 +789,8 @@ export class L1Publisher { }; } catch (err) { prettyLogViemError(err, this.log); - this.log.error(`Rollup publish failed`, err); + const errorMessage = err instanceof Error ? err.message : String(err); + this.log.error(`Rollup publish failed`, errorMessage); return undefined; } } @@ -823,7 +823,8 @@ export class L1Publisher { }; } catch (err) { prettyLogViemError(err, this.log); - this.log.error(`Rollup publish failed`, err); + const errorMessage = err instanceof Error ? err.message : String(err); + this.log.error(`Rollup publish failed`, errorMessage); return undefined; } } From 09339a4d1922ad5148247de44dd8dbd37000b025 Mon Sep 17 00:00:00 2001 From: spypsy Date: Thu, 21 Nov 2024 09:46:47 +0000 Subject: [PATCH 32/45] some PR fixes --- yarn-project/ethereum/package.json | 5 +-- .../ethereum/src/deploy_l1_contracts.ts | 13 ------ yarn-project/ethereum/src/l1_tx_utils.test.ts | 42 ++++++++++--------- 3 files changed, 25 insertions(+), 35 deletions(-) diff --git a/yarn-project/ethereum/package.json b/yarn-project/ethereum/package.json index b3e6eba9531..054f301ccbe 100644 --- a/yarn-project/ethereum/package.json +++ b/yarn-project/ethereum/package.json @@ -18,8 +18,7 @@ "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", - "gas": "node ./dest/test_gas_stuff.js" + "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests" }, "inherits": [ "../package.common.json" @@ -85,4 +84,4 @@ "engines": { "node": ">=18" } -} +} \ No newline at end of file diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index c12d5814959..a9796d9be3a 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -223,19 +223,6 @@ export type L1Clients = { walletClient: WalletClient; }; -export const deployerAbi = [ - { - inputs: [ - { internalType: 'bytes32', name: 'salt', type: 'bytes32' }, - { internalType: 'bytes', name: 'init_code', type: 'bytes' }, - ], - name: 'deploy', - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'nonpayable', - type: 'function', - }, -]; - /** * Creates a wallet and a public viem client for interacting with L1. * @param rpcUrl - RPC URL to connect to L1. diff --git a/yarn-project/ethereum/src/l1_tx_utils.test.ts b/yarn-project/ethereum/src/l1_tx_utils.test.ts index 87809e1af2a..3a1eba67a51 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.test.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.test.ts @@ -3,7 +3,16 @@ import { createDebugLogger } from '@aztec/foundation/log'; import { type Anvil, createAnvil } from '@viem/anvil'; import getPort from 'get-port'; -import { createPublicClient, createWalletClient, http } from 'viem'; +import { + type Account, + type Chain, + type HttpTransport, + type PublicClient, + type WalletClient, + createPublicClient, + createWalletClient, + http, +} from 'viem'; import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; import { foundry } from 'viem/chains'; @@ -27,8 +36,8 @@ const startAnvil = async (l1BlockTime?: number) => { describe('GasUtils', () => { let gasUtils: L1TxUtils; - let publicClient: any; - let walletClient: any; + let walletClient: WalletClient; + let publicClient: PublicClient; let anvil: Anvil; let initialBaseFee: bigint; const logger = createDebugLogger('l1_gas_test'); @@ -64,6 +73,17 @@ describe('GasUtils', () => { stallTimeMs: 1000, }); }); + afterEach(async () => { + // Reset base fee + await publicClient.transport.request({ + method: 'anvil_setNextBlockBaseFeePerGas', + params: [initialBaseFee.toString()], + }); + await publicClient.transport.request({ + method: 'evm_mine', + params: [], + }); + }); afterAll(async () => { await anvil.stop(); }, 5000); @@ -99,12 +119,6 @@ describe('GasUtils', () => { // Transaction should still complete const receipt = await sendPromise; expect(receipt.status).toBe('success'); - - // Reset base fee - await publicClient.transport.request({ - method: 'anvil_setNextBlockBaseFeePerGas', - params: [initialBaseFee.toString()], - }); }); it('respects max gas price limits during spikes', async () => { @@ -130,16 +144,6 @@ describe('GasUtils', () => { }); 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 () => { From a331d9ed2ad58b52e6686526dd535103490c3bf9 Mon Sep 17 00:00:00 2001 From: spypsy Date: Thu, 21 Nov 2024 11:17:48 +0000 Subject: [PATCH 33/45] use default-defined value --- yarn-project/ethereum/src/l1_tx_utils.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/yarn-project/ethereum/src/l1_tx_utils.ts b/yarn-project/ethereum/src/l1_tx_utils.ts index 11217aa8a92..af41ec9e496 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.ts @@ -278,7 +278,12 @@ export class L1TxUtils { let priorityFee = await this.publicClient.estimateMaxPriorityFeePerGas(); if (attempt > 0) { // Bump priority fee by configured percentage for each attempt - priorityFee = (priorityFee * (100n + (gasConfig.priorityFeeBumpPercentage ?? 20n) * BigInt(attempt))) / 100n; + priorityFee = + (priorityFee * + (100n + + (gasConfig.priorityFeeBumpPercentage ?? defaultL1TxUtilsConfig.priorityFeeBumpPercentage!) * + BigInt(attempt))) / + 100n; } const maxFeePerGas = gasConfig.maxGwei! * WEI_CONST; From 1b169b4db4c84c21485f6a099a5882eb4feef182 Mon Sep 17 00:00:00 2001 From: spypsy Date: Thu, 21 Nov 2024 14:06:00 +0000 Subject: [PATCH 34/45] more PR fixes --- yarn-project/ethereum/src/l1_tx_utils.ts | 35 ++++++++++++++++--- yarn-project/foundation/src/config/env_var.ts | 3 +- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/yarn-project/ethereum/src/l1_tx_utils.ts b/yarn-project/ethereum/src/l1_tx_utils.ts index af41ec9e496..d376668e45a 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.ts @@ -5,12 +5,14 @@ import { numberConfigHelper, } from '@aztec/foundation/config'; import { type DebugLogger } from '@aztec/foundation/log'; +import { makeBackoff, retry } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; import { type Account, type Address, type Chain, + type GetTransactionReturnType, type Hex, type HttpTransport, type PublicClient, @@ -58,6 +60,10 @@ export interface L1TxUtilsConfig { * How long before considering tx stalled */ stallTimeMs?: number; + /** + * How long to wait for a tx to be mined before giving up + */ + txTimeoutMs?: number; } export const l1TxUtilsConfigMappings: ConfigMappingsType = { @@ -94,12 +100,17 @@ export const l1TxUtilsConfigMappings: ConfigMappingsType = { checkIntervalMs: { description: 'How often to check tx status', env: 'L1_TX_MONITOR_CHECK_INTERVAL_MS', - ...numberConfigHelper(30_000), + ...numberConfigHelper(10_000), }, stallTimeMs: { description: 'How long before considering tx stalled', env: 'L1_TX_MONITOR_STALL_TIME_MS', - ...numberConfigHelper(60_000), + ...numberConfigHelper(30_000), + }, + txTimeoutMs: { + description: 'How long to wait for a tx to be mined before giving up. Set to 0 to disable.', + env: 'L1_TX_MONITOR_TX_TIMEOUT_MS', + ...numberConfigHelper(300_000), // 5 mins }, }; @@ -177,7 +188,15 @@ export class L1TxUtils { const gasConfig = { ...this.config, ..._gasConfig }; const account = this.walletClient.account; - const tx = await this.publicClient.getTransaction({ hash: initialTxHash }); + // Retry a few times, in case the tx is not yet propagated. + const tx = await retry( + () => this.publicClient.getTransaction({ hash: initialTxHash }), + `Getting L1 transaction ${initialTxHash} nonce`, + makeBackoff([1, 2, 3]), + this.logger, + true, + ); + if (tx?.nonce === undefined || tx?.nonce === null) { throw new Error(`Failed to get L1 transaction ${initialTxHash} nonce`); } @@ -187,8 +206,10 @@ export class L1TxUtils { let currentTxHash = initialTxHash; let attempts = 0; let lastSeen = Date.now(); + const initialTxTime = lastSeen; + let txTimedOut = false; - while (true) { + while (!txTimedOut) { try { const currentNonce = await this.publicClient.getTransactionCount({ address: account.address }); if (currentNonce > nonce) { @@ -199,7 +220,6 @@ export class L1TxUtils { this.logger?.debug(`L1 Transaction ${hash} confirmed`); if (receipt.status === 'reverted') { this.logger?.error(`L1 Transaction ${hash} reverted`); - throw new Error(`Transaction ${hash} reverted`); } return receipt; } @@ -249,7 +269,12 @@ export class L1TxUtils { } await sleep(gasConfig.checkIntervalMs!); } + // Check if tx has timed out. + if (gasConfig.txTimeoutMs) { + txTimedOut = Date.now() - initialTxTime > gasConfig.txTimeoutMs!; + } } + throw new Error(`L1 Transaction ${currentTxHash} timed out`); } /** diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index b3d7435e4da..61a687271ca 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -165,4 +165,5 @@ export type EnvVar = | 'L1_PRIORITY_FEE_BUMP_PERCENTAGE' | 'L1_TX_MONITOR_MAX_ATTEMPTS' | 'L1_TX_MONITOR_CHECK_INTERVAL_MS' - | 'L1_TX_MONITOR_STALL_TIME_MS'; + | 'L1_TX_MONITOR_STALL_TIME_MS' + | 'L1_TX_MONITOR_TX_TIMEOUT_MS'; From 8818d1f59ae5059400a645a3c438ad22bab43c76 Mon Sep 17 00:00:00 2001 From: spypsy Date: Fri, 22 Nov 2024 12:28:37 +0000 Subject: [PATCH 35/45] 10% min fee bump, ensure test sends replacement tx --- yarn-project/ethereum/src/l1_tx_utils.test.ts | 92 +++++++++++++++---- yarn-project/ethereum/src/l1_tx_utils.ts | 26 ++++-- 2 files changed, 93 insertions(+), 25 deletions(-) diff --git a/yarn-project/ethereum/src/l1_tx_utils.test.ts b/yarn-project/ethereum/src/l1_tx_utils.test.ts index 3a1eba67a51..a3d6222ee2f 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.test.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.test.ts @@ -1,5 +1,6 @@ import { EthAddress } from '@aztec/foundation/eth-address'; import { createDebugLogger } from '@aztec/foundation/log'; +import { sleep } from '@aztec/foundation/sleep'; import { type Anvil, createAnvil } from '@viem/anvil'; import getPort from 'get-port'; @@ -39,7 +40,7 @@ describe('GasUtils', () => { let walletClient: WalletClient; let publicClient: PublicClient; let anvil: Anvil; - let initialBaseFee: bigint; + const initialBaseFee = WEI_CONST; // 1 gwei const logger = createDebugLogger('l1_gas_test'); beforeAll(async () => { @@ -85,8 +86,13 @@ describe('GasUtils', () => { }); }); afterAll(async () => { + // disabling interval mining as it seems to cause issues with stopping anvil + await publicClient.transport.request({ + method: 'evm_setIntervalMining', + params: [0], // Disable interval mining + }); await anvil.stop(); - }, 5000); + }, 5_000); it('sends and monitors a simple transaction', async () => { const receipt = await gasUtils.sendAndMonitorTransaction({ @@ -99,27 +105,81 @@ describe('GasUtils', () => { }, 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; + // Disable all forms of mining + await publicClient.transport.request({ + method: 'evm_setAutomine', + params: [false], + }); + await publicClient.transport.request({ + method: 'evm_setIntervalMining', + params: [0], + }); - // Start a transaction - const sendPromise = gasUtils.sendAndMonitorTransaction({ - to: '0x1234567890123456789012345678901234567890', - data: '0x', + // Ensure initial base fee is low + await publicClient.transport.request({ + method: 'anvil_setNextBlockBaseFeePerGas', + params: [initialBaseFee.toString()], + }); + + const request = { + to: '0x1234567890123456789012345678901234567890' as `0x${string}`, + data: '0x' as `0x${string}`, value: 0n, + }; + + const estimatedGas = await publicClient.estimateGas(request); + + const txHash = await walletClient.sendTransaction({ + ...request, + gas: estimatedGas, + maxFeePerGas: WEI_CONST * 10n, + maxPriorityFeePerGas: WEI_CONST, + }); + + const rawTx = await publicClient.transport.request({ + method: 'debug_getRawTransaction', + params: [txHash], }); - // Spike gas price to 3x the initial base fee + // Temporarily drop the transaction + await publicClient.transport.request({ + method: 'anvil_dropTransaction', + params: [txHash], + }); + + // Mine a block with higher base fee await publicClient.transport.request({ method: 'anvil_setNextBlockBaseFeePerGas', - params: [(initialBaseFee * 3n).toString()], + params: [((WEI_CONST * 15n) / 10n).toString()], + }); + await publicClient.transport.request({ + method: 'evm_mine', + params: [], + }); + + // Re-add the original tx + await publicClient.transport.request({ + method: 'eth_sendRawTransaction', + params: [rawTx], }); - // Transaction should still complete - const receipt = await sendPromise; + // keeping auto-mining disabled to simulate a stuck transaction + // The monitor should detect the stall and create a replacement tx + + // Monitor should detect stall and replace with higher gas price + const monitorFn = gasUtils.monitorTransaction(request, txHash, { gasLimit: estimatedGas }); + + await sleep(2000); + // re-enable mining + await publicClient.transport.request({ + method: 'evm_setIntervalMining', + params: [1], + }); + const receipt = await monitorFn; expect(receipt.status).toBe('success'); - }); + // Verify that a replacement transaction was created + expect(receipt.transactionHash).not.toBe(txHash); + }, 20_000); it('respects max gas price limits during spikes', async () => { const maxGwei = 500n; @@ -144,7 +204,7 @@ describe('GasUtils', () => { }); expect(receipt.effectiveGasPrice).toBeLessThanOrEqual(maxGwei * WEI_CONST); - }); + }, 60_000); it('adds appropriate buffer to gas estimation', async () => { // First deploy without any buffer @@ -152,7 +212,7 @@ describe('GasUtils', () => { bufferPercentage: 0n, maxGwei: 500n, minGwei: 1n, - maxAttempts: 3, + maxAttempts: 5, checkIntervalMs: 100, stallTimeMs: 1000, }); diff --git a/yarn-project/ethereum/src/l1_tx_utils.ts b/yarn-project/ethereum/src/l1_tx_utils.ts index d376668e45a..561092f1d99 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.ts @@ -27,6 +27,10 @@ import { const WEI_CONST = 1_000_000_000n; +// setting a minimum bump percentage to 10% due to geth's implementation +// https://github.com/ethereum/go-ethereum/blob/e3d61e6db028c412f74bc4d4c7e117a9e29d0de0/core/txpool/legacypool/list.go#L298 +const MIN_REPLACEMENT_BUMP_PERCENTAGE = 10n; + export interface L1TxUtilsConfig { /** * How much to increase calculated gas limit. @@ -236,7 +240,6 @@ export class L1TxUtils { if (tx && timePassed < gasConfig.stallTimeMs!) { this.logger?.debug(`L1 Transaction ${currentTxHash} pending. Time passed: ${timePassed}ms`); - lastSeen = Date.now(); await sleep(gasConfig.checkIntervalMs!); continue; } @@ -301,17 +304,22 @@ export class L1TxUtils { // Get initial priority fee from the network let priorityFee = await this.publicClient.estimateMaxPriorityFeePerGas(); + let maxFeePerGas = gasConfig.maxGwei! * WEI_CONST; + if (attempt > 0) { - // Bump priority fee by configured percentage for each attempt - priorityFee = - (priorityFee * - (100n + - (gasConfig.priorityFeeBumpPercentage ?? defaultL1TxUtilsConfig.priorityFeeBumpPercentage!) * - BigInt(attempt))) / - 100n; + // Ensure minimum 10% bump for replacement transactions + const bumpPercentage = BigInt( + Math.max( + Number(gasConfig.priorityFeeBumpPercentage ?? defaultL1TxUtilsConfig.priorityFeeBumpPercentage!), + Number(MIN_REPLACEMENT_BUMP_PERCENTAGE), + ), + ); + + // Bump both priority fee and max fee by at least 10% + priorityFee = (priorityFee * (100n + bumpPercentage * BigInt(attempt))) / 100n; + maxFeePerGas = (maxFeePerGas * (100n + bumpPercentage * BigInt(attempt))) / 100n; } - const maxFeePerGas = gasConfig.maxGwei! * WEI_CONST; const maxPriorityFeePerGas = priorityFee; this.logger?.debug( From 34170d105e7cb71c62f9f03626844103ebcf5973 Mon Sep 17 00:00:00 2001 From: spypsy Date: Tue, 26 Nov 2024 11:13:40 +0000 Subject: [PATCH 36/45] undo submodule changes --- l1-contracts/lib/forge-std | 2 +- l1-contracts/lib/openzeppelin-contracts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/l1-contracts/lib/forge-std b/l1-contracts/lib/forge-std index 75f1746c949..0e709775091 160000 --- a/l1-contracts/lib/forge-std +++ b/l1-contracts/lib/forge-std @@ -1 +1 @@ -Subproject commit 75f1746c949ae56377611e5f2128aa93d381431f +Subproject commit 0e7097750918380d84dd3cfdef595bee74dabb70 diff --git a/l1-contracts/lib/openzeppelin-contracts b/l1-contracts/lib/openzeppelin-contracts index 7222a31d548..448efeea664 160000 --- a/l1-contracts/lib/openzeppelin-contracts +++ b/l1-contracts/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 7222a31d548695998a475c9661fa159ef45a0e88 +Subproject commit 448efeea6640bbbc09373f03fbc9c88e280147ba From 3d46d87db79c823ef7a283a80663af3f27946dbc Mon Sep 17 00:00:00 2001 From: spypsy Date: Wed, 27 Nov 2024 12:19:44 +0000 Subject: [PATCH 37/45] fix get-node-info opts --- yarn-project/cli/src/cmds/pxe/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/cli/src/cmds/pxe/index.ts b/yarn-project/cli/src/cmds/pxe/index.ts index 42fa8f0da34..47fe99eba64 100644 --- a/yarn-project/cli/src/cmds/pxe/index.ts +++ b/yarn-project/cli/src/cmds/pxe/index.ts @@ -146,7 +146,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL } else { url = options.rpcUrl; } - await getNodeInfo(url, options.pxe, debugLogger, log); + await getNodeInfo(url, !options.nodeUrl, debugLogger, log); }); program From af2d3e69aa15a489aaebb1afe5eef85b3f3928cc Mon Sep 17 00:00:00 2001 From: spypsy Date: Wed, 27 Nov 2024 12:51:01 +0000 Subject: [PATCH 38/45] requesting info from node, not pxe --- spartan/aztec-network/files/config/config-prover-env.sh | 4 ++-- spartan/aztec-network/files/config/config-validator-env.sh | 6 ++---- .../end-to-end/scripts/native-network/prover-node.sh | 2 +- yarn-project/end-to-end/scripts/native-network/validator.sh | 2 +- yarn-project/ethereum/package.json | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/spartan/aztec-network/files/config/config-prover-env.sh b/spartan/aztec-network/files/config/config-prover-env.sh index 11c4ad5aef2..a3eccd01c1b 100644 --- a/spartan/aztec-network/files/config/config-prover-env.sh +++ b/spartan/aztec-network/files/config/config-prover-env.sh @@ -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" @@ -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 < /shared/contracts/contracts.env +cat </shared/contracts/contracts.env export BOOTSTRAP_NODES=$boot_node_enr export ROLLUP_CONTRACT_ADDRESS=$rollup_address export REGISTRY_CONTRACT_ADDRESS=$registry_address diff --git a/spartan/aztec-network/files/config/config-validator-env.sh b/spartan/aztec-network/files/config/config-validator-env.sh index 71d03fbbc98..6483168f16d 100644 --- a/spartan/aztec-network/files/config/config-validator-env.sh +++ b/spartan/aztec-network/files/config/config-validator-env.sh @@ -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" @@ -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 < /shared/contracts/contracts.env +cat </shared/contracts/contracts.env export BOOTSTRAP_NODES=$boot_node_enr export ROLLUP_CONTRACT_ADDRESS=$rollup_address export REGISTRY_CONTRACT_ADDRESS=$registry_address diff --git a/yarn-project/end-to-end/scripts/native-network/prover-node.sh b/yarn-project/end-to-end/scripts/native-network/prover-node.sh index 3decda02873..af5dc590056 100755 --- a/yarn-project/end-to-end/scripts/native-network/prover-node.sh +++ b/yarn-project/end-to-end/scripts/native-network/prover-node.sh @@ -26,7 +26,7 @@ echo "Done waiting." source "$REPO"/yarn-project/end-to-end/scripts/native-network/state/l1-contracts.env # Get node info from the boot node -output=$(node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js get-node-info -u http://127.0.0.1:8080) +output=$(node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js get-node-info --node-url http://127.0.0.1:8080) # Extract boot node ENR export BOOTSTRAP_NODES=$(echo "$output" | grep -oP 'Node ENR: \K.*') diff --git a/yarn-project/end-to-end/scripts/native-network/validator.sh b/yarn-project/end-to-end/scripts/native-network/validator.sh index 92d2e6d6932..fa183829d61 100755 --- a/yarn-project/end-to-end/scripts/native-network/validator.sh +++ b/yarn-project/end-to-end/scripts/native-network/validator.sh @@ -33,7 +33,7 @@ echo "Done waiting." BOOT_NODE_URL="http://127.0.0.1:8080" # Get node info from the boot node -output=$(node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js get-node-info -u $BOOT_NODE_URL) +output=$(node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js get-node-info --node-url $BOOT_NODE_URL) # Extract boot node ENR export BOOTSTRAP_NODES=$(echo "$output" | grep -oP 'Node ENR: \K.*') diff --git a/yarn-project/ethereum/package.json b/yarn-project/ethereum/package.json index f03aabb09ab..f6be604435c 100644 --- a/yarn-project/ethereum/package.json +++ b/yarn-project/ethereum/package.json @@ -90,4 +90,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} From 0834d5025c782a8354173a11c50b70c5f7b4992a Mon Sep 17 00:00:00 2001 From: spypsy Date: Fri, 29 Nov 2024 15:04:26 +0000 Subject: [PATCH 39/45] More PR fixes --- yarn-project/ethereum/src/l1_tx_utils.test.ts | 47 ++++--- yarn-project/ethereum/src/l1_tx_utils.ts | 115 ++++++++++++------ yarn-project/foundation/src/config/env_var.ts | 1 + .../src/publisher/l1-publisher.ts | 45 +++++-- 4 files changed, 139 insertions(+), 69 deletions(-) diff --git a/yarn-project/ethereum/src/l1_tx_utils.test.ts b/yarn-project/ethereum/src/l1_tx_utils.test.ts index a3d6222ee2f..d35cf4be19c 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.test.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.test.ts @@ -2,8 +2,7 @@ import { EthAddress } from '@aztec/foundation/eth-address'; import { createDebugLogger } from '@aztec/foundation/log'; import { sleep } from '@aztec/foundation/sleep'; -import { type Anvil, createAnvil } from '@viem/anvil'; -import getPort from 'get-port'; +import { type Anvil } from '@viem/anvil'; import { type Account, type Chain, @@ -18,23 +17,13 @@ import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; import { foundry } from 'viem/chains'; import { L1TxUtils } from './l1_tx_utils.js'; +import { startAnvil } from './test/start_anvil.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(); - const rpcUrl = `http://127.0.0.1:${ethereumHostPort}`; - const anvil = createAnvil({ - port: ethereumHostPort, - blockTime: l1BlockTime, - }); - await anvil.start(); - return { anvil, rpcUrl }; -}; - describe('GasUtils', () => { let gasUtils: L1TxUtils; let walletClient: WalletClient; @@ -66,7 +55,7 @@ describe('GasUtils', () => { }); gasUtils = new L1TxUtils(publicClient, walletClient, logger, { - bufferPercentage: 20n, + gasLimitBufferPercentage: 20n, maxGwei: 500n, minGwei: 1n, maxAttempts: 3, @@ -74,6 +63,7 @@ describe('GasUtils', () => { stallTimeMs: 1000, }); }); + afterEach(async () => { // Reset base fee await publicClient.transport.request({ @@ -129,11 +119,14 @@ describe('GasUtils', () => { const estimatedGas = await publicClient.estimateGas(request); + const originalMaxFeePerGas = WEI_CONST * 10n; + const originalMaxPriorityFeePerGas = WEI_CONST; + const txHash = await walletClient.sendTransaction({ ...request, gas: estimatedGas, - maxFeePerGas: WEI_CONST * 10n, - maxPriorityFeePerGas: WEI_CONST, + maxFeePerGas: originalMaxFeePerGas, + maxPriorityFeePerGas: originalMaxPriorityFeePerGas, }); const rawTx = await publicClient.transport.request({ @@ -179,6 +172,12 @@ describe('GasUtils', () => { expect(receipt.status).toBe('success'); // Verify that a replacement transaction was created expect(receipt.transactionHash).not.toBe(txHash); + + // Get details of replacement tx to verify higher gas price + const replacementTx = await publicClient.getTransaction({ hash: receipt.transactionHash }); + + expect(replacementTx.maxFeePerGas!).toBeGreaterThan(originalMaxFeePerGas); + expect(replacementTx.maxPriorityFeePerGas!).toBeGreaterThan(originalMaxPriorityFeePerGas); }, 20_000); it('respects max gas price limits during spikes', async () => { @@ -207,11 +206,21 @@ describe('GasUtils', () => { }, 60_000); it('adds appropriate buffer to gas estimation', async () => { + const stableBaseFee = WEI_CONST * 10n; + await publicClient.transport.request({ + method: 'anvil_setNextBlockBaseFeePerGas', + params: [stableBaseFee.toString()], + }); + await publicClient.transport.request({ + method: 'evm_mine', + params: [], + }); + // First deploy without any buffer const baselineGasUtils = new L1TxUtils(publicClient, walletClient, logger, { - bufferPercentage: 0n, + gasLimitBufferPercentage: 0n, maxGwei: 500n, - minGwei: 1n, + minGwei: 10n, // Increased minimum gas price maxAttempts: 5, checkIntervalMs: 100, stallTimeMs: 1000, @@ -229,7 +238,7 @@ describe('GasUtils', () => { // Now deploy with 20% buffer const bufferedGasUtils = new L1TxUtils(publicClient, walletClient, logger, { - bufferPercentage: 20n, + gasLimitBufferPercentage: 20n, maxGwei: 500n, minGwei: 1n, maxAttempts: 3, diff --git a/yarn-project/ethereum/src/l1_tx_utils.ts b/yarn-project/ethereum/src/l1_tx_utils.ts index 561092f1d99..d06db27ca05 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.ts @@ -31,15 +31,14 @@ const WEI_CONST = 1_000_000_000n; // https://github.com/ethereum/go-ethereum/blob/e3d61e6db028c412f74bc4d4c7e117a9e29d0de0/core/txpool/legacypool/list.go#L298 const MIN_REPLACEMENT_BUMP_PERCENTAGE = 10n; +// Avg ethereum block time is ~12s +const BLOCK_TIME_MS = 12_000; + export interface L1TxUtilsConfig { /** * How much to increase calculated gas limit. */ - bufferPercentage?: bigint; - /** - * Fixed buffer to add to gas price - */ - bufferFixed?: bigint; + gasLimitBufferPercentage?: bigint; /** * Maximum gas price in gwei */ @@ -49,9 +48,13 @@ export interface L1TxUtilsConfig { */ minGwei?: bigint; /** - * How much to increase priority fee by each attempt (percentage) + * Priority fee bump percentage */ priorityFeeBumpPercentage?: bigint; + /** + * How much to increase priority fee by each attempt (percentage) + */ + priorityFeeRetryBumpPercentage?: bigint; /** * Maximum number of speed-up attempts */ @@ -71,16 +74,11 @@ export interface L1TxUtilsConfig { } export const l1TxUtilsConfigMappings: ConfigMappingsType = { - bufferPercentage: { + gasLimitBufferPercentage: { description: 'How much to increase gas price by each attempt (percentage)', env: 'L1_GAS_LIMIT_BUFFER_PERCENTAGE', ...bigintConfigHelper(20n), }, - bufferFixed: { - description: 'Fixed buffer to add to gas price', - env: 'L1_GAS_LIMIT_BUFFER_FIXED', - ...bigintConfigHelper(), - }, minGwei: { description: 'Minimum gas price in gwei', env: 'L1_GAS_PRICE_MIN', @@ -96,6 +94,11 @@ export const l1TxUtilsConfigMappings: ConfigMappingsType = { env: 'L1_PRIORITY_FEE_BUMP_PERCENTAGE', ...bigintConfigHelper(20n), }, + priorityFeeRetryBumpPercentage: { + description: 'How much to increase priority fee by each retry attempt (percentage)', + env: 'L1_PRIORITY_FEE_RETRY_BUMP_PERCENTAGE', + ...bigintConfigHelper(50n), + }, maxAttempts: { description: 'Maximum number of speed-up attempts', env: 'L1_TX_MONITOR_MAX_ATTEMPTS', @@ -154,12 +157,18 @@ export class L1TxUtils { */ public async sendTransaction( request: L1TxRequest, - _gasConfig?: Partial, + _gasConfig?: Partial & { fixedGas?: bigint }, ): Promise<{ txHash: Hex; gasLimit: bigint; gasPrice: GasPrice }> { const gasConfig = { ...this.config, ..._gasConfig }; const account = this.walletClient.account; + let gasLimit: bigint; + + if (gasConfig.fixedGas) { + gasLimit = gasConfig.fixedGas; + } else { + gasLimit = await this.estimateGas(account, request); + } - const gasLimit = await this.estimateGas(account, request); const gasPrice = await this.getGasPrice(gasConfig); const txHash = await this.walletClient.sendTransaction({ @@ -209,8 +218,8 @@ export class L1TxUtils { const txHashes = new Set([initialTxHash]); let currentTxHash = initialTxHash; let attempts = 0; - let lastSeen = Date.now(); - const initialTxTime = lastSeen; + let lastAttemptSent = Date.now(); + const initialTxTime = lastAttemptSent; let txTimedOut = false; while (!txTimedOut) { @@ -236,17 +245,32 @@ export class L1TxUtils { } const tx = await this.publicClient.getTransaction({ hash: currentTxHash }); - const timePassed = Date.now() - lastSeen; + const timePassed = Date.now() - lastAttemptSent; if (tx && timePassed < gasConfig.stallTimeMs!) { this.logger?.debug(`L1 Transaction ${currentTxHash} pending. Time passed: ${timePassed}ms`); + + // Check timeout before continuing + if (gasConfig.txTimeoutMs) { + txTimedOut = Date.now() - initialTxTime > gasConfig.txTimeoutMs; + if (txTimedOut) { + break; + } + } + await sleep(gasConfig.checkIntervalMs!); continue; } if (timePassed > gasConfig.stallTimeMs! && attempts < gasConfig.maxAttempts!) { attempts++; - const newGasPrice = await this.getGasPrice(gasConfig, attempts); + const newGasPrice = await this.getGasPrice( + gasConfig, + attempts, + tx.maxFeePerGas && tx.maxPriorityFeePerGas + ? { maxFeePerGas: tx.maxFeePerGas, maxPriorityFeePerGas: tx.maxPriorityFeePerGas } + : undefined, + ); this.logger?.debug( `L1 Transaction ${currentTxHash} appears stuck. Attempting speed-up ${attempts}/${gasConfig.maxAttempts} ` + @@ -262,7 +286,7 @@ export class L1TxUtils { }); txHashes.add(currentTxHash); - lastSeen = Date.now(); + lastAttemptSent = Date.now(); } await sleep(gasConfig.checkIntervalMs!); } catch (err: any) { @@ -288,7 +312,7 @@ export class L1TxUtils { */ public async sendAndMonitorTransaction( request: L1TxRequest, - gasConfig?: Partial, + gasConfig?: Partial & { fixedGas?: bigint }, ): Promise { const { txHash, gasLimit } = await this.sendTransaction(request, gasConfig); return this.monitorTransaction(request, txHash, { gasLimit }, gasConfig); @@ -297,29 +321,48 @@ export class L1TxUtils { /** * Gets the current gas price with bounds checking */ - private async getGasPrice(_gasConfig?: L1TxUtilsConfig, attempt: number = 0): Promise { + private async getGasPrice( + _gasConfig?: L1TxUtilsConfig, + attempt: number = 0, + previousGasPrice?: typeof attempt extends 0 ? never : GasPrice, + ): Promise { const gasConfig = { ...this.config, ..._gasConfig }; const block = await this.publicClient.getBlock({ blockTag: 'latest' }); const baseFee = block.baseFeePerGas ?? 0n; // Get initial priority fee from the network let priorityFee = await this.publicClient.estimateMaxPriorityFeePerGas(); - let maxFeePerGas = gasConfig.maxGwei! * WEI_CONST; + let maxFeePerGas = baseFee; // gasConfig.maxGwei! * WEI_CONST; + + // Bump base fee so it's valid for next blocks if it stalls + const numBlocks = Math.ceil(gasConfig.stallTimeMs! / BLOCK_TIME_MS); + for (let i = 0; i < numBlocks; i++) { + // each block can go up 12.5% from previous baseFee + maxFeePerGas = (maxFeePerGas * (1_000n + 125n)) / 1_000n; + } if (attempt > 0) { - // Ensure minimum 10% bump for replacement transactions - const bumpPercentage = BigInt( - Math.max( - Number(gasConfig.priorityFeeBumpPercentage ?? defaultL1TxUtilsConfig.priorityFeeBumpPercentage!), - Number(MIN_REPLACEMENT_BUMP_PERCENTAGE), - ), - ); - - // Bump both priority fee and max fee by at least 10% - priorityFee = (priorityFee * (100n + bumpPercentage * BigInt(attempt))) / 100n; - maxFeePerGas = (maxFeePerGas * (100n + bumpPercentage * BigInt(attempt))) / 100n; + const configBump = + gasConfig.priorityFeeRetryBumpPercentage ?? defaultL1TxUtilsConfig.priorityFeeRetryBumpPercentage!; + const bumpPercentage = + configBump > MIN_REPLACEMENT_BUMP_PERCENTAGE ? configBump : MIN_REPLACEMENT_BUMP_PERCENTAGE; + + // Calculate minimum required fees based on previous attempt + const minPriorityFee = (previousGasPrice!.maxPriorityFeePerGas * (100n + bumpPercentage)) / 100n; + const minMaxFee = (previousGasPrice!.maxFeePerGas * (100n + bumpPercentage)) / 100n; + + // Use maximum between current network values and minimum required values + priorityFee = priorityFee > minPriorityFee ? priorityFee : minPriorityFee; + maxFeePerGas = maxFeePerGas > minMaxFee ? maxFeePerGas : minMaxFee; + } else { + // first attempt, just bump priority fee + priorityFee = (priorityFee * (100n + (gasConfig.priorityFeeBumpPercentage || 0n))) / 100n; } + // Ensure we don't exceed maxGwei + const maxGweiInWei = gasConfig.maxGwei! * WEI_CONST; + maxFeePerGas = maxFeePerGas > maxGweiInWei ? maxGweiInWei : maxFeePerGas; + const maxPriorityFeePerGas = priorityFee; this.logger?.debug( @@ -333,14 +376,12 @@ export class L1TxUtils { /** * Estimates gas and adds buffer */ - private async estimateGas(account: Account, request: L1TxRequest, _gasConfig?: L1TxUtilsConfig): Promise { + public async estimateGas(account: Account, request: L1TxRequest, _gasConfig?: L1TxUtilsConfig): Promise { const gasConfig = { ...this.config, ..._gasConfig }; const initialEstimate = await this.publicClient.estimateGas({ account, ...request }); // Add buffer based on either fixed amount or percentage - const withBuffer = gasConfig.bufferFixed - ? initialEstimate + gasConfig.bufferFixed - : initialEstimate + (initialEstimate * (gasConfig.bufferPercentage ?? 0n)) / 100n; + const withBuffer = initialEstimate + (initialEstimate * (gasConfig.gasLimitBufferPercentage ?? 0n)) / 100n; return withBuffer; } diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 7ed05594286..2c21d26d55a 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -174,6 +174,7 @@ export type EnvVar = | 'L1_GAS_PRICE_MIN' | 'L1_GAS_PRICE_MAX' | 'L1_PRIORITY_FEE_BUMP_PERCENTAGE' + | 'L1_PRIORITY_FEE_RETRY_BUMP_PERCENTAGE' | 'L1_TX_MONITOR_MAX_ATTEMPTS' | 'L1_TX_MONITOR_CHECK_INTERVAL_MS' | 'L1_TX_MONITOR_STALL_TIME_MS' diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index 382332e8d64..f42b8c0f2e9 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -741,7 +741,23 @@ export class L1Publisher { } } - private prepareProposeTx(encodedData: L1ProcessArgs) { + private async prepareProposeTx(encodedData: L1ProcessArgs) { + const computeTxsEffectsHashGas = await this.l1TxUtils.estimateGas(this.account, { + 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 + L1Publisher.PROPOSE_GAS_GUESS; + const attestations = encodedData.attestations ? encodedData.attestations.map(attest => attest.toViemSignature()) : []; @@ -762,7 +778,7 @@ export class L1Publisher { `0x${encodedData.body.toString('hex')}`, ] as const; - return args; + return { args, gas: gasGuesstimate }; } private getSubmitEpochProofArgs(args: { @@ -798,7 +814,7 @@ export class L1Publisher { return undefined; } try { - const args = this.prepareProposeTx(encodedData); + const { args, gas } = this.prepareProposeTx(encodedData); const receipt = await this.l1TxUtils.sendAndMonitorTransaction( { to: this.rollupContract.address, @@ -809,7 +825,7 @@ export class L1Publisher { }), }, { - bufferFixed: L1Publisher.PROPOSE_GAS_GUESS, + fixedGas: gas, }, ); return { @@ -836,15 +852,18 @@ export class L1Publisher { this.log.info(`ProposeAndClaim`); this.log.info(inspect(quote.payload)); - const args = this.prepareProposeTx(encodedData); - const receipt = await this.l1TxUtils.sendAndMonitorTransaction({ - to: this.rollupContract.address, - data: encodeFunctionData({ - abi: this.rollupContract.abi, - functionName: 'proposeAndClaim', - args: [...args, quote.toViemArgs()], - }), - }); + const { args, gas } = this.prepareProposeTx(encodedData); + const receipt = await this.l1TxUtils.sendAndMonitorTransaction( + { + to: this.rollupContract.address, + data: encodeFunctionData({ + abi: this.rollupContract.abi, + functionName: 'proposeAndClaim', + args: [...args, quote.toViemArgs()], + }), + }, + { fixedGas: gas }, + ); return { receipt, From 0c20390f11b7f27b598b3c2f9bd47f166bee1abd Mon Sep 17 00:00:00 2001 From: spypsy Date: Fri, 29 Nov 2024 17:30:01 +0000 Subject: [PATCH 40/45] fix await --- yarn-project/accounts/package.json | 2 +- yarn-project/circuits.js/package.json | 2 +- yarn-project/sequencer-client/src/publisher/l1-publisher.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn-project/accounts/package.json b/yarn-project/accounts/package.json index d73f4c9ebf3..6af84aee1c9 100644 --- a/yarn-project/accounts/package.json +++ b/yarn-project/accounts/package.json @@ -102,4 +102,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/circuits.js/package.json b/yarn-project/circuits.js/package.json index 9406e50c6ee..009150b8807 100644 --- a/yarn-project/circuits.js/package.json +++ b/yarn-project/circuits.js/package.json @@ -99,4 +99,4 @@ ] ] } -} \ No newline at end of file +} diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index f42b8c0f2e9..09132883480 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -814,7 +814,7 @@ export class L1Publisher { return undefined; } try { - const { args, gas } = this.prepareProposeTx(encodedData); + const { args, gas } = await this.prepareProposeTx(encodedData); const receipt = await this.l1TxUtils.sendAndMonitorTransaction( { to: this.rollupContract.address, @@ -852,7 +852,7 @@ export class L1Publisher { this.log.info(`ProposeAndClaim`); this.log.info(inspect(quote.payload)); - const { args, gas } = this.prepareProposeTx(encodedData); + const { args, gas } = await this.prepareProposeTx(encodedData); const receipt = await this.l1TxUtils.sendAndMonitorTransaction( { to: this.rollupContract.address, From 5fd231b90e0478ed69328f46d142c63c4b709c43 Mon Sep 17 00:00:00 2001 From: spypsy Date: Mon, 2 Dec 2024 12:11:08 +0000 Subject: [PATCH 41/45] add cheat codes --- yarn-project/aztec.js/src/index.ts | 3 +- .../aztec.js/src/utils/cheat_codes.ts | 247 +------------- yarn-project/ethereum/src/eth_cheat_codes.ts | 316 ++++++++++++++++++ yarn-project/ethereum/src/index.ts | 1 + yarn-project/ethereum/src/l1_tx_utils.test.ts | 85 ++--- yarn-project/ethereum/src/l1_tx_utils.ts | 5 +- 6 files changed, 347 insertions(+), 310 deletions(-) create mode 100644 yarn-project/ethereum/src/eth_cheat_codes.ts diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index ca6f1011172..3a67fd9a6fd 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -42,7 +42,6 @@ export { ContractDeployer } from './deployment/index.js'; export { AnvilTestWatcher, CheatCodes, - EthCheatCodes, L1FeeJuicePortalManager, L1ToL2TokenPortalManager, L1TokenManager, @@ -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. diff --git a/yarn-project/aztec.js/src/utils/cheat_codes.ts b/yarn-project/aztec.js/src/utils/cheat_codes.ts index f35ae53c25f..87048d1c0e1 100644 --- a/yarn-project/aztec.js/src/utils/cheat_codes.ts +++ b/yarn-project/aztec.js/src/utils/cheat_codes.ts @@ -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, @@ -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 { - 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 { - const res = await this.rpcCall('eth_blockNumber', []); - return parseInt(res.result, 16); - } - - /** - * Get the current chainId - * @returns The current chainId - */ - public async chainId(): Promise { - const res = await this.rpcCall('eth_chainId', []); - return parseInt(res.result, 16); - } - - /** - * Get the current timestamp - * @returns The current timestamp - */ - public async timestamp(): Promise { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - // 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 { - 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 { - 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 { - 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; diff --git a/yarn-project/ethereum/src/eth_cheat_codes.ts b/yarn-project/ethereum/src/eth_cheat_codes.ts new file mode 100644 index 00000000000..74918bf4653 --- /dev/null +++ b/yarn-project/ethereum/src/eth_cheat_codes.ts @@ -0,0 +1,316 @@ +import { toBigIntBE, toHex } from '@aztec/foundation/bigint-buffer'; +import { keccak256 } from '@aztec/foundation/crypto'; +import { type EthAddress } from '@aztec/foundation/eth-address'; +import { createDebugLogger } from '@aztec/foundation/log'; + +import fs from 'fs'; +import { type Hex } from 'viem'; + +/** + * 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 { + 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 { + const res = await this.rpcCall('eth_blockNumber', []); + return parseInt(res.result, 16); + } + + /** + * Get the current chainId + * @returns The current chainId + */ + public async chainId(): Promise { + const res = await this.rpcCall('eth_chainId', []); + return parseInt(res.result, 16); + } + + /** + * Get the current timestamp + * @returns The current timestamp + */ + public async timestamp(): Promise { + 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 + */ + public async mine(numberOfBlocks = 1): Promise { + 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`); + } + + /** + * Mines a single block with evm_mine + */ + public async evmMine(): Promise { + const res = await this.rpcCall('evm_mine', []); + if (res.error) { + throw new Error(`Error mining: ${res.error.message}`); + } + } + + /** + * 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 { + 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 { + 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 base fee per gas + * @param baseFee - The base fee to set + */ + public async setNextBlockBaseFeePerGas(baseFee: bigint): Promise { + const res = await this.rpcCall('anvil_setNextBlockBaseFeePerGas', [baseFee.toString()]); + if (res.error) { + throw new Error(`Error setting next block base fee per gas: ${res.error.message}`); + } + this.logger.verbose(`Set L1 next block base fee per gas to ${baseFee}`); + } + + /** + * Set the interval between blocks (block time) + * @param seconds - The interval to use between blocks + */ + public async setIntervalMining(seconds: number): Promise { + const res = await this.rpcCall('anvil_setIntervalMining', [seconds]); + if (res.error) { + throw new Error(`Error setting interval mining: ${res.error.message}`); + } + this.logger.verbose(`Set L1 interval mining to ${seconds} seconds`); + } + + /** + * Set the automine status of the underlying anvil chain + * @param automine - The automine status to set + */ + public async setAutomine(automine: boolean): Promise { + const res = await this.rpcCall('anvil_setAutomine', [automine]); + if (res.error) { + throw new Error(`Error setting automine: ${res.error.message}`); + } + this.logger.verbose(`Set L1 automine to ${automine}`); + } + + /** + * Drop a transaction from the mempool + * @param txHash - The transaction hash + */ + public async dropTransaction(txHash: Hex): Promise { + const res = await this.rpcCall('anvil_dropTransaction', [txHash]); + if (res.error) { + throw new Error(`Error dropping transaction: ${res.error.message}`); + } + this.logger.verbose(`Dropped transaction ${txHash}`); + } + + /** + * Set the next block timestamp + * @param timestamp - The timestamp to set the next block to + */ + public async setNextBlockTimestamp(timestamp: number): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + // 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 { + 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 { + 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 { + 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; + } + + /** + * Get the raw transaction object for a given transaction hash + * @param txHash - The transaction hash + * @returns The raw transaction + */ + public async getRawTransaction(txHash: Hex): Promise<`0x${string}`> { + const res = await this.rpcCall('debug_getRawTransaction', [txHash]); + return res.result; + } +} diff --git a/yarn-project/ethereum/src/index.ts b/yarn-project/ethereum/src/index.ts index f52fc691795..d6393560093 100644 --- a/yarn-project/ethereum/src/index.ts +++ b/yarn-project/ethereum/src/index.ts @@ -1,6 +1,7 @@ export * from './constants.js'; export * from './deploy_l1_contracts.js'; export * from './ethereum_chain.js'; +export * from './eth_cheat_codes.js'; export * from './l1_tx_utils.js'; export * from './l1_contract_addresses.js'; export * from './l1_reader.js'; diff --git a/yarn-project/ethereum/src/l1_tx_utils.test.ts b/yarn-project/ethereum/src/l1_tx_utils.test.ts index d35cf4be19c..31f77588dcf 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.test.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.test.ts @@ -16,6 +16,7 @@ import { import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; import { foundry } from 'viem/chains'; +import { EthCheatCodes } from './eth_cheat_codes.js'; import { L1TxUtils } from './l1_tx_utils.js'; import { startAnvil } from './test/start_anvil.js'; @@ -29,12 +30,14 @@ describe('GasUtils', () => { let walletClient: WalletClient; let publicClient: PublicClient; let anvil: Anvil; + let cheatCodes: EthCheatCodes; const initialBaseFee = WEI_CONST; // 1 gwei const logger = createDebugLogger('l1_gas_test'); beforeAll(async () => { const { anvil: anvilInstance, rpcUrl } = await startAnvil(1); anvil = anvilInstance; + cheatCodes = new EthCheatCodes(rpcUrl); const hdAccount = mnemonicToAccount(MNEMONIC, { addressIndex: 0 }); const privKeyRaw = hdAccount.getHdKey().privateKey; if (!privKeyRaw) { @@ -54,6 +57,13 @@ describe('GasUtils', () => { account, }); + // set base fee + await publicClient.transport.request({ + method: 'anvil_setNextBlockBaseFeePerGas', + params: [initialBaseFee.toString()], + }); + await cheatCodes.evmMine(); + gasUtils = new L1TxUtils(publicClient, walletClient, logger, { gasLimitBufferPercentage: 20n, maxGwei: 500n, @@ -66,21 +76,12 @@ describe('GasUtils', () => { afterEach(async () => { // Reset base fee - await publicClient.transport.request({ - method: 'anvil_setNextBlockBaseFeePerGas', - params: [initialBaseFee.toString()], - }); - await publicClient.transport.request({ - method: 'evm_mine', - params: [], - }); + await cheatCodes.setNextBlockBaseFeePerGas(initialBaseFee); + await cheatCodes.evmMine(); }); afterAll(async () => { // disabling interval mining as it seems to cause issues with stopping anvil - await publicClient.transport.request({ - method: 'evm_setIntervalMining', - params: [0], // Disable interval mining - }); + await cheatCodes.setIntervalMining(0); // Disable interval mining await anvil.stop(); }, 5_000); @@ -96,20 +97,11 @@ describe('GasUtils', () => { it('handles gas price spikes by retrying with higher gas price', async () => { // Disable all forms of mining - await publicClient.transport.request({ - method: 'evm_setAutomine', - params: [false], - }); - await publicClient.transport.request({ - method: 'evm_setIntervalMining', - params: [0], - }); + await cheatCodes.setAutomine(false); + await cheatCodes.setIntervalMining(0); // Ensure initial base fee is low - await publicClient.transport.request({ - method: 'anvil_setNextBlockBaseFeePerGas', - params: [initialBaseFee.toString()], - }); + await cheatCodes.setNextBlockBaseFeePerGas(initialBaseFee); const request = { to: '0x1234567890123456789012345678901234567890' as `0x${string}`, @@ -129,26 +121,14 @@ describe('GasUtils', () => { maxPriorityFeePerGas: originalMaxPriorityFeePerGas, }); - const rawTx = await publicClient.transport.request({ - method: 'debug_getRawTransaction', - params: [txHash], - }); + const rawTx = await cheatCodes.getRawTransaction(txHash); // Temporarily drop the transaction - await publicClient.transport.request({ - method: 'anvil_dropTransaction', - params: [txHash], - }); + await cheatCodes.dropTransaction(txHash); // Mine a block with higher base fee - await publicClient.transport.request({ - method: 'anvil_setNextBlockBaseFeePerGas', - params: [((WEI_CONST * 15n) / 10n).toString()], - }); - await publicClient.transport.request({ - method: 'evm_mine', - params: [], - }); + await cheatCodes.setNextBlockBaseFeePerGas((WEI_CONST * 15n) / 10n); + await cheatCodes.evmMine(); // Re-add the original tx await publicClient.transport.request({ @@ -164,10 +144,7 @@ describe('GasUtils', () => { await sleep(2000); // re-enable mining - await publicClient.transport.request({ - method: 'evm_setIntervalMining', - params: [1], - }); + await cheatCodes.setIntervalMining(1); const receipt = await monitorFn; expect(receipt.status).toBe('success'); // Verify that a replacement transaction was created @@ -185,16 +162,10 @@ describe('GasUtils', () => { 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()], - }); + await cheatCodes.setNextBlockBaseFeePerGas(newBaseFee); // Mine a new block to make the base fee change take effect - await publicClient.transport.request({ - method: 'evm_mine', - params: [], - }); + await cheatCodes.evmMine(); const receipt = await gasUtils.sendAndMonitorTransaction({ to: '0x1234567890123456789012345678901234567890', @@ -207,14 +178,8 @@ describe('GasUtils', () => { it('adds appropriate buffer to gas estimation', async () => { const stableBaseFee = WEI_CONST * 10n; - await publicClient.transport.request({ - method: 'anvil_setNextBlockBaseFeePerGas', - params: [stableBaseFee.toString()], - }); - await publicClient.transport.request({ - method: 'evm_mine', - params: [], - }); + await cheatCodes.setNextBlockBaseFeePerGas(stableBaseFee); + await cheatCodes.evmMine(); // First deploy without any buffer const baselineGasUtils = new L1TxUtils(publicClient, walletClient, logger, { diff --git a/yarn-project/ethereum/src/l1_tx_utils.ts b/yarn-project/ethereum/src/l1_tx_utils.ts index d06db27ca05..350dfde4b9a 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.ts @@ -332,7 +332,7 @@ export class L1TxUtils { // Get initial priority fee from the network let priorityFee = await this.publicClient.estimateMaxPriorityFeePerGas(); - let maxFeePerGas = baseFee; // gasConfig.maxGwei! * WEI_CONST; + let maxFeePerGas = baseFee; // Bump base fee so it's valid for next blocks if it stalls const numBlocks = Math.ceil(gasConfig.stallTimeMs! / BLOCK_TIME_MS); @@ -363,7 +363,8 @@ export class L1TxUtils { const maxGweiInWei = gasConfig.maxGwei! * WEI_CONST; maxFeePerGas = maxFeePerGas > maxGweiInWei ? maxGweiInWei : maxFeePerGas; - const maxPriorityFeePerGas = priorityFee; + // Ensure priority fee doesn't exceed max fee + const maxPriorityFeePerGas = priorityFee > maxFeePerGas ? maxFeePerGas : priorityFee; this.logger?.debug( `Gas price calculation (attempt ${attempt}): baseFee=${formatGwei(baseFee)}, ` + From a17a0ae1eb0497278fba587eecfaa32f742b5390 Mon Sep 17 00:00:00 2001 From: spypsy Date: Mon, 2 Dec 2024 12:51:45 +0000 Subject: [PATCH 42/45] expect two error calls --- .../composed/integration_l1_publisher.test.ts | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts b/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts index 38891cfbc35..45d2099f12b 100644 --- a/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts +++ b/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts @@ -584,19 +584,29 @@ describe('L1Publisher integration', () => { // Expect the tx to revert await expect(publisher.proposeL2Block(block)).resolves.toEqual(false); - // Expect a proper error to be logged. Full message looks like: - // aztec:sequencer:publisher [ERROR] Rollup process tx reverted. The contract function "propose" reverted. Error: Rollup__InvalidInHash(bytes32 expected, bytes32 actual) (0x00089a9d421a82c4a25f7acbebe69e638d5b064fa8a60e018793dcb0be53752c, 0x00a5a12af159e0608de45d825718827a36d8a7cdfa9ecc7955bc62180ae78e51) blockNumber=1 slotNumber=49 blockHash=0x131c59ebc2ce21224de6473fe954b0d4eb918043432a3a95406bb7e7a4297fbd txHash=0xc01c3c26b6b67003a8cce352afe475faf7e0196a5a3bba963cfda3792750ed28 - expect(loggerErrorSpy).toHaveBeenCalledWith('Rollup publish failed', expect.stringContaining('0xcd6f4233')); - // NOTE: Reinstate check below with #10066 - // expect(loggerErrorSpy).toHaveBeenCalledWith( - // expect.stringMatching(/Rollup__InvalidInHash/), - // undefined, - // expect.objectContaining({ - // blockHash: expect.any(String), - // blockNumber: expect.any(Number), - // slotNumber: expect.any(BigInt), - // }), - // ); + // Test for both calls + expect(loggerErrorSpy).toHaveBeenCalledTimes(2); + + // Test first call + expect(loggerErrorSpy).toHaveBeenNthCalledWith( + 1, + expect.stringMatching(/^L1 Transaction 0x[a-f0-9]{64} reverted$/), + ); + + // Test second call + expect(loggerErrorSpy).toHaveBeenNthCalledWith( + 2, + expect.stringMatching( + /^Rollup process tx reverted\. The contract function "propose" reverted\. Error: Rollup__InvalidInHash/, + ), + undefined, + expect.objectContaining({ + blockHash: expect.any(String), + blockNumber: expect.any(Number), + slotNumber: expect.any(BigInt), + txHash: expect.any(String), + }), + ); }); }); }); From 0c84a64e04259e593a6095404fb17a2ce66c1560 Mon Sep 17 00:00:00 2001 From: spypsy Date: Mon, 2 Dec 2024 14:25:34 +0000 Subject: [PATCH 43/45] update l1-publisher tests --- .../sequencer-client/src/publisher/l1-publisher.test.ts | 3 ++- yarn-project/sequencer-client/src/publisher/l1-publisher.ts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts index 878201f1952..cedbfbe0d7d 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts @@ -134,6 +134,7 @@ describe('L1Publisher', () => { publicClient.getBlock.mockResolvedValue({ timestamp: 12n }); publicClient.estimateGas.mockResolvedValue(GAS_GUESS); l1TxUtils.sendAndMonitorTransaction.mockResolvedValue(proposeTxReceipt); + (l1TxUtils as any).estimateGas.mockResolvedValue(GAS_GUESS); }); it('publishes and propose l2 block to l1', async () => { @@ -163,7 +164,7 @@ describe('L1Publisher', () => { to: mockRollupAddress, data: encodeFunctionData({ abi: rollupContract.abi, functionName: 'propose', args }), }, - { bufferFixed: L1Publisher.PROPOSE_GAS_GUESS }, + { fixedGas: GAS_GUESS + L1Publisher.PROPOSE_GAS_GUESS }, ); }); diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index 09132883480..10e4b61f967 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -514,7 +514,6 @@ export class L1Publisher { }); this.log.verbose(`Submitting propose transaction`); - const result = proofQuote ? await this.sendProposeAndClaimTx(proposeTxArgs, proofQuote) : await this.sendProposeTx(proposeTxArgs); From de03935f1adfd42d4b4925986c51f7b9e6ffdecb Mon Sep 17 00:00:00 2001 From: spypsy Date: Tue, 3 Dec 2024 18:00:08 +0000 Subject: [PATCH 44/45] add priorityFee to maxFeePerGas calculation, retry fetching l1 tx --- yarn-project/ethereum/src/l1_tx_utils.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/yarn-project/ethereum/src/l1_tx_utils.ts b/yarn-project/ethereum/src/l1_tx_utils.ts index 350dfde4b9a..b4770836d29 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.ts @@ -204,7 +204,7 @@ export class L1TxUtils { // Retry a few times, in case the tx is not yet propagated. const tx = await retry( () => this.publicClient.getTransaction({ hash: initialTxHash }), - `Getting L1 transaction ${initialTxHash} nonce`, + `Getting L1 transaction ${initialTxHash}`, makeBackoff([1, 2, 3]), this.logger, true, @@ -244,7 +244,14 @@ export class L1TxUtils { } } - const tx = await this.publicClient.getTransaction({ hash: currentTxHash }); + // Retry a few times, in case the tx is not yet propagated. + const tx = await retry( + () => this.publicClient.getTransaction({ hash: currentTxHash }), + `Getting L1 transaction ${currentTxHash}`, + makeBackoff([1, 2, 3]), + this.logger, + true, + ); const timePassed = Date.now() - lastAttemptSent; if (tx && timePassed < gasConfig.stallTimeMs!) { @@ -359,6 +366,9 @@ export class L1TxUtils { priorityFee = (priorityFee * (100n + (gasConfig.priorityFeeBumpPercentage || 0n))) / 100n; } + // Add priority fee to maxFeePerGas + maxFeePerGas += priorityFee; + // Ensure we don't exceed maxGwei const maxGweiInWei = gasConfig.maxGwei! * WEI_CONST; maxFeePerGas = maxFeePerGas > maxGweiInWei ? maxGweiInWei : maxFeePerGas; From e62ba9c886e10f3946f9402a633e9cac235efda5 Mon Sep 17 00:00:00 2001 From: spypsy Date: Wed, 4 Dec 2024 10:48:13 +0000 Subject: [PATCH 45/45] fix priorityFee addition, add gas calculationt ests --- cspell.json | 1 + yarn-project/ethereum/src/l1_tx_utils.test.ts | 77 ++++++++++++++++++- yarn-project/ethereum/src/l1_tx_utils.ts | 7 +- 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/cspell.json b/cspell.json index 6a62badf5be..8b9e5d15e7e 100644 --- a/cspell.json +++ b/cspell.json @@ -11,6 +11,7 @@ "asyncify", "auditability", "authwit", + "Automine", "autonat", "autorun", "awslogs", diff --git a/yarn-project/ethereum/src/l1_tx_utils.test.ts b/yarn-project/ethereum/src/l1_tx_utils.test.ts index 31f77588dcf..7dffaf011ce 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.test.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.test.ts @@ -17,7 +17,7 @@ import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; import { foundry } from 'viem/chains'; import { EthCheatCodes } from './eth_cheat_codes.js'; -import { L1TxUtils } from './l1_tx_utils.js'; +import { L1TxUtils, defaultL1TxUtilsConfig } from './l1_tx_utils.js'; import { startAnvil } from './test/start_anvil.js'; const MNEMONIC = 'test test test test test test test test test test test junk'; @@ -25,6 +25,12 @@ const WEI_CONST = 1_000_000_000n; // Simple contract that just returns 42 const SIMPLE_CONTRACT_BYTECODE = '0x69602a60005260206000f3600052600a6016f3'; +export type PendingTransaction = { + hash: `0x${string}`; + maxFeePerGas: bigint; + maxPriorityFeePerGas: bigint; +}; + describe('GasUtils', () => { let gasUtils: L1TxUtils; let walletClient: WalletClient; @@ -224,4 +230,73 @@ describe('GasUtils', () => { expect(bufferedDetails.gas).toBeGreaterThan(baselineDetails.gas); expect(bufferedDetails.gas).toBeLessThanOrEqual((baselineDetails.gas * 120n) / 100n); }, 20_000); + + it('calculates correct gas prices for initial attempt', async () => { + // Set base fee to 1 gwei + await cheatCodes.setNextBlockBaseFeePerGas(WEI_CONST); + await cheatCodes.evmMine(); + + const basePriorityFee = await publicClient.estimateMaxPriorityFeePerGas(); + const gasPrice = await gasUtils['getGasPrice'](); + + // With default config, priority fee should be bumped by 20% + const expectedPriorityFee = (basePriorityFee * 120n) / 100n; + + // Base fee should be bumped for potential stalls (1.125^(stallTimeMs/12000) = ~1.125 for default config) + const expectedMaxFee = (WEI_CONST * 1125n) / 1000n + expectedPriorityFee; + + expect(gasPrice.maxPriorityFeePerGas).toBe(expectedPriorityFee); + expect(gasPrice.maxFeePerGas).toBe(expectedMaxFee); + }); + + it('calculates correct gas prices for retry attempts', async () => { + await cheatCodes.setNextBlockBaseFeePerGas(WEI_CONST); + await cheatCodes.evmMine(); + + const initialGasPrice = await gasUtils['getGasPrice'](); + + // Get retry gas price for 2nd attempt + const retryGasPrice = await gasUtils['getGasPrice'](undefined, 1, initialGasPrice); + + // With default config, retry should bump fees by 50% + const expectedPriorityFee = (initialGasPrice.maxPriorityFeePerGas * 150n) / 100n; + const expectedMaxFee = (initialGasPrice.maxFeePerGas * 150n) / 100n; + + expect(retryGasPrice.maxPriorityFeePerGas).toBe(expectedPriorityFee); + expect(retryGasPrice.maxFeePerGas).toBe(expectedMaxFee); + }); + + it('respects minimum gas price bump for replacements', async () => { + const gasUtils = new L1TxUtils(publicClient, walletClient, logger, { + ...defaultL1TxUtilsConfig, + priorityFeeRetryBumpPercentage: 5n, // Set lower than minimum 10% + }); + + const initialGasPrice = await gasUtils['getGasPrice'](); + + // Get retry gas price with attempt = 1 + const retryGasPrice = await gasUtils['getGasPrice'](undefined, 1, initialGasPrice); + + // Should use 10% minimum bump even though config specified 5% + const expectedPriorityFee = (initialGasPrice.maxPriorityFeePerGas * 110n) / 100n; + const expectedMaxFee = (initialGasPrice.maxFeePerGas * 110n) / 100n; + + expect(retryGasPrice.maxPriorityFeePerGas).toBe(expectedPriorityFee); + expect(retryGasPrice.maxFeePerGas).toBe(expectedMaxFee); + }); + + it('adds correct buffer to gas estimation', async () => { + const request = { + to: '0x1234567890123456789012345678901234567890' as `0x${string}`, + data: '0x' as `0x${string}`, + value: 0n, + }; + + const baseEstimate = await publicClient.estimateGas(request); + const bufferedEstimate = await gasUtils.estimateGas(walletClient.account!, request); + + // adds 20% buffer + const expectedEstimate = baseEstimate + (baseEstimate * 20n) / 100n; + expect(bufferedEstimate).toBe(expectedEstimate); + }); }); diff --git a/yarn-project/ethereum/src/l1_tx_utils.ts b/yarn-project/ethereum/src/l1_tx_utils.ts index b4770836d29..f95610303b7 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.ts @@ -358,17 +358,18 @@ export class L1TxUtils { const minPriorityFee = (previousGasPrice!.maxPriorityFeePerGas * (100n + bumpPercentage)) / 100n; const minMaxFee = (previousGasPrice!.maxFeePerGas * (100n + bumpPercentage)) / 100n; + // Add priority fee to maxFeePerGas + maxFeePerGas += priorityFee; + // Use maximum between current network values and minimum required values priorityFee = priorityFee > minPriorityFee ? priorityFee : minPriorityFee; maxFeePerGas = maxFeePerGas > minMaxFee ? maxFeePerGas : minMaxFee; } else { // first attempt, just bump priority fee priorityFee = (priorityFee * (100n + (gasConfig.priorityFeeBumpPercentage || 0n))) / 100n; + maxFeePerGas += priorityFee; } - // Add priority fee to maxFeePerGas - maxFeePerGas += priorityFee; - // Ensure we don't exceed maxGwei const maxGweiInWei = gasConfig.maxGwei! * WEI_CONST; maxFeePerGas = maxFeePerGas > maxGweiInWei ? maxGweiInWei : maxFeePerGas;