diff --git a/.github/workflows/deploy-staging-networks.yml b/.github/workflows/deploy-staging-networks.yml index 21a9f07e3e12..ce939c304287 100644 --- a/.github/workflows/deploy-staging-networks.yml +++ b/.github/workflows/deploy-staging-networks.yml @@ -115,8 +115,15 @@ jobs: ETHERSCAN_API_KEY=${{ secrets.ETHERSCAN_API_KEY }} DEPLOY_INTERNAL_BOOTNODE=false STORE_SNAPSHOT_URL="${{ secrets.GCS_TESTNET_SNAPSHOT_URL }}/staging-public/" + BOT_TRANSFERS_REPLICAS=1 + BOT_TRANSFERS_TX_INTERVAL_SECONDS=250 + BOT_TRANSFERS_FOLLOW_CHAIN=PENDING + BOT_SWAPS_REPLICAS=1 + BOT_SWAPS_FOLLOW_CHAIN=PENDING + BOT_SWAPS_TX_INTERVAL_SECONDS=350 + EOF echo "NAMESPACE=$NAMESPACE" >> $GITHUB_ENV diff --git a/spartan/scripts/deploy_network.sh b/spartan/scripts/deploy_network.sh index 2976b3ec8721..fd21b108c5a1 100755 --- a/spartan/scripts/deploy_network.sh +++ b/spartan/scripts/deploy_network.sh @@ -72,8 +72,8 @@ BOT_TRANSFERS_REPLICAS=${BOT_TRANSFERS_REPLICAS:-0} BOT_SWAPS_REPLICAS=${BOT_SWAPS_REPLICAS:-0} BOT_TRANSFERS_TX_INTERVAL_SECONDS=${BOT_TRANSFERS_TX_INTERVAL_SECONDS:-60} BOT_SWAPS_TX_INTERVAL_SECONDS=${BOT_SWAPS_TX_INTERVAL_SECONDS:-60} -BOT_TRANSFERS_FOLLOW_CHAIN=${BOT_TRANSFERS_FOLLOW_CHAIN:-NONE} -BOT_SWAPS_FOLLOW_CHAIN=${BOT_SWAPS_FOLLOW_CHAIN:-NONE} +BOT_TRANSFERS_FOLLOW_CHAIN=${BOT_TRANSFERS_FOLLOW_CHAIN:-PENDING} +BOT_SWAPS_FOLLOW_CHAIN=${BOT_SWAPS_FOLLOW_CHAIN:-PENDING} ######################## # CHAOS MESH VARIABLES diff --git a/spartan/terraform/deploy-aztec-infra/main.tf b/spartan/terraform/deploy-aztec-infra/main.tf index 149918ab12a1..7b44975cc209 100644 --- a/spartan/terraform/deploy-aztec-infra/main.tf +++ b/spartan/terraform/deploy-aztec-infra/main.tf @@ -43,7 +43,7 @@ locals { internal_boot_node_url = var.DEPLOY_INTERNAL_BOOTNODE ? "http://${var.RELEASE_PREFIX}-p2p-bootstrap-node.${var.NAMESPACE}.svc.cluster.local:8080" : "" - internal_rpc_url = "http://${var.RELEASE_PREFIX}-rpc-aztec-node-admin.${var.NAMESPACE}.svc.cluster.local:8080" + internal_rpc_url = "http://${var.RELEASE_PREFIX}-rpc-aztec-node.${var.NAMESPACE}.svc.cluster.local:8080" internal_rpc_admin_url = "http://${var.RELEASE_PREFIX}-rpc-aztec-node-admin.${var.NAMESPACE}.svc.cluster.local:8880" # Common settings for all releases diff --git a/yarn-project/aztec/src/cli/cmds/start_bot.ts b/yarn-project/aztec/src/cli/cmds/start_bot.ts index 9a3bba19f4db..7c5c5227c2d1 100644 --- a/yarn-project/aztec/src/cli/cmds/start_bot.ts +++ b/yarn-project/aztec/src/cli/cmds/start_bot.ts @@ -1,11 +1,16 @@ import { type BotConfig, BotRunner, botConfigMappings, getBotRunnerApiHandler } from '@aztec/bot'; import type { NamespacedApiHandlers } from '@aztec/foundation/json-rpc/server'; import type { LogFn } from '@aztec/foundation/log'; -import type { AztecNode, PXE } from '@aztec/stdlib/interfaces/client'; +import { type AztecNode, type PXE, createAztecNodeClient } from '@aztec/stdlib/interfaces/client'; import type { TelemetryClient } from '@aztec/telemetry-client'; -import { getConfigEnvVars as getTelemetryClientConfig, initTelemetryClient } from '@aztec/telemetry-client'; +import { + getConfigEnvVars as getTelemetryClientConfig, + initTelemetryClient, + makeTracedFetch, +} from '@aztec/telemetry-client'; import { extractRelevantOptions } from '../util.js'; +import { getVersions } from '../versioning.js'; export async function startBot( options: any, @@ -20,15 +25,19 @@ export async function startBot( ); process.exit(1); } + + const fetch = makeTracedFetch([1, 2, 3], true); + const node = createAztecNodeClient(options.nodeUrl, getVersions(), fetch); + // Start a PXE client that is used by the bot if required let pxe: PXE | undefined; if (options.pxe) { const { addPXE } = await import('./start_pxe.js'); - ({ pxe } = await addPXE(options, signalHandlers, services, userLog)); + ({ pxe } = await addPXE(options, signalHandlers, services, userLog, { node })); } const telemetry = initTelemetryClient(getTelemetryClientConfig()); - await addBot(options, signalHandlers, services, { pxe, telemetry }); + await addBot(options, signalHandlers, services, { pxe, telemetry, node }); } export function addBot( diff --git a/yarn-project/cli/src/config/chain_l2_config.ts b/yarn-project/cli/src/config/chain_l2_config.ts index 794b01325496..8ca3eee06eac 100644 --- a/yarn-project/cli/src/config/chain_l2_config.ts +++ b/yarn-project/cli/src/config/chain_l2_config.ts @@ -70,7 +70,7 @@ const DefaultSlashConfig = { export const stagingIgnitionL2ChainConfig: L2ChainConfig = { l1ChainId: 11155111, - testAccounts: true, + testAccounts: false, sponsoredFPC: false, p2pEnabled: true, p2pBootstrapNodes: [], diff --git a/yarn-project/ethereum/src/l1_tx_utils.test.ts b/yarn-project/ethereum/src/l1_tx_utils.test.ts index 9777b1658179..305e9ba236ae 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.test.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.test.ts @@ -94,6 +94,78 @@ describe('L1TxUtils', () => { }); }); + it('regression: speed-up of blob tx via L1TxUtils sets non-zero maxFeePerBlobGas', async () => { + await cheatCodes.setAutomine(false); + await cheatCodes.setIntervalMining(0); + + const baseUtils = createL1TxUtilsFromViemWallet(l1Client, logger, dateProvider, { + gasLimitBufferPercentage: 20, + maxGwei: 500n, + maxAttempts: 1, + checkIntervalMs: 50, + stallTimeMs: 300, + }); + + const blobData = new Uint8Array(131072).fill(1); + const kzg = Blob.getViemKzgInstance(); + + const request = { + to: '0x1234567890123456789012345678901234567890' as `0x${string}`, + data: '0x' as `0x${string}`, + value: 0n, + } as const; + + const estimatedGas = await l1Client.estimateGas(request); + + // Send initial blob tx with a valid maxFeePerBlobGas + const { txHash } = await baseUtils.sendTransaction(request, undefined, { + blobs: [blobData], + kzg, + maxFeePerBlobGas: 10n * WEI_CONST, + }); + + // Capture the replacement tx when it is being signed + const originalSign = l1Client.signTransaction; + const signedTxs: TransactionSerializable[] = []; + using _spy = jest.spyOn(l1Client, 'signTransaction').mockImplementation((arg: any) => { + signedTxs.push(arg); + return originalSign(arg); + }); + + // Trigger monitor with blob inputs but WITHOUT maxFeePerBlobGas so the bug manifests + const monitorPromise = baseUtils.monitorTransaction( + request, + txHash, + new Set(), + { gasLimit: estimatedGas }, + undefined, + { + blobs: [blobData], + kzg, + }, + ); + + // Wait until a speed-up is attempted + await retryUntil( + () => baseUtils['state'] === TxUtilsState.SPEED_UP || signedTxs.length > 0, + 'waiting for speed-up', + 40, + 0.05, + ); + + // Interrupt to stop the monitor loop and avoid hanging the test + baseUtils.interrupt(); + await expect(monitorPromise).rejects.toThrow(); + + // Ensure we captured a replacement tx being signed + expect(signedTxs.length).toBeGreaterThan(0); + const replacement = signedTxs[signedTxs.length - 1] as any; + + // Assert fix: maxFeePerBlobGas is populated and non-zero on replacement + expect(replacement.maxFeePerBlobGas).toBeDefined(); + expect(replacement.maxFeePerBlobGas!).toBeGreaterThan(0n); + }, 20_000); + it('sends and monitors a simple transaction', async () => { const { receipt } = await gasUtils.sendAndMonitorTransaction({ to: '0x1234567890123456789012345678901234567890', diff --git a/yarn-project/ethereum/src/l1_tx_utils.ts b/yarn-project/ethereum/src/l1_tx_utils.ts index 8a8cddd923f2..645cd91f039c 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.ts @@ -279,7 +279,13 @@ export class ReadOnlyL1TxUtils { let blobBaseFee = 0n; if (isBlobTx) { try { - blobBaseFee = await this.client.getBlobBaseFee(); + blobBaseFee = await retry( + () => this.client.getBlobBaseFee(), + 'Getting L1 blob base fee', + makeBackoff(times(2, () => 1)), + this.logger, + true, + ); this.logger?.debug('L1 Blob base fee:', { blobBaseFee: formatGwei(blobBaseFee) }); } catch { this.logger?.warn('Failed to get L1 blob base fee', attempt); @@ -864,7 +870,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils { }, ); - const txData = { + const txData: PrepareTransactionRequestRequest = { ...request, ...blobInputs, nonce, @@ -872,6 +878,9 @@ export class L1TxUtils extends ReadOnlyL1TxUtils { maxFeePerGas: newGasPrice.maxFeePerGas, maxPriorityFeePerGas: newGasPrice.maxPriorityFeePerGas, }; + if (isBlobTx && newGasPrice.maxFeePerBlobGas) { + (txData as any).maxFeePerBlobGas = newGasPrice.maxFeePerBlobGas; + } const signedRequest = await this.prepareSignedTransaction(txData); const newHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest }); if (!isCancelTx) { diff --git a/yarn-project/ethereum/src/utils.ts b/yarn-project/ethereum/src/utils.ts index 89d22e9d6ff8..d480b8f809fa 100644 --- a/yarn-project/ethereum/src/utils.ts +++ b/yarn-project/ethereum/src/utils.ts @@ -181,41 +181,19 @@ export function formatViemError(error: any, abi: Abi = ErrorsAbi): FormattedViem return new FormattedViemError(error.message, (error as any)?.metaMessages); } - // Extract the actual error message and highlight it for clarity - let formattedRes = extractAndFormatRequestBody(error?.message || String(error)); - - let errorDetail = ''; - // Look for specific details in known locations - if (error) { - // Check for details property which often has the most specific error message - if (typeof error.details === 'string' && error.details) { - errorDetail = error.details; - } - // Check for shortMessage which is often available in Viem errors - else if (typeof error.shortMessage === 'string' && error.shortMessage) { - errorDetail = error.shortMessage; - } - } - - // If we found a specific error detail, format it clearly - if (errorDetail) { - // Look for key sections of the formatted result to replace with highlighted error - let replaced = false; - - // Try to find the Details: section - const detailsMatch = formattedRes.match(/Details: ([^\n]+)/); - if (detailsMatch) { - formattedRes = formattedRes.replace(detailsMatch[0], `Details: *${errorDetail}*`); - replaced = true; - } - - // If we didn't find a Details section, add the error at the beginning - if (!replaced) { - formattedRes = `Error: *${errorDetail}*\n\n${formattedRes}`; - } - } - - return new FormattedViemError(formattedRes.replace(/\\n/g, '\n'), error?.metaMessages); + const body = String(error); + const length = body.length; + // LogExplorer can only render up to 2500 characters in it's summary view. Try to keep the whole message below this number + // Limit the error to 2000 chacaters in order to allow code higher up to decorate this error with extra details (up to 500 characters) + if (length > 2000) { + const chunk = 950; + const truncated = length - 2 * chunk; + return new FormattedViemError( + body.slice(0, chunk) + `...${truncated} characters truncated...` + body.slice(-1 * chunk), + ); + } + + return new FormattedViemError(body); } function stripAbis(obj: any) { @@ -241,156 +219,6 @@ function stripAbis(obj: any) { }); } -function extractAndFormatRequestBody(message: string): string { - // First check if message is extremely large and contains very large hex strings - if (message.length > 50000) { - message = replaceHexStrings(message, { minLength: 10000, truncateLength: 200 }); - } - - // Add a specific check for RPC calls with large params - if (message.includes('"method":"eth_sendRawTransaction"')) { - message = replaceHexStrings(message, { - pattern: /"params":\s*\[\s*"(0x[a-fA-F0-9]{1000,})"\s*\]/g, - transform: hex => `"params":["${truncateHex(hex, 200)}"]`, - }); - } - - // First handle Request body JSON - const requestBodyRegex = /Request body: ({[\s\S]*?})\n/g; - let result = message.replace(requestBodyRegex, (match, body) => { - return `Request body: ${formatRequestBody(body)}\n`; - }); - - // Then handle Arguments section - const argsRegex = /((?:Request |Estimate Gas )?Arguments:[\s\S]*?(?=\n\n|$))/g; - result = result.replace(argsRegex, section => { - const lines = section.split('\n'); - const processedLines = lines.map(line => { - // Check if line contains a colon followed by content - const colonIndex = line.indexOf(':'); - if (colonIndex !== -1) { - const [prefix, content] = [line.slice(0, colonIndex + 1), line.slice(colonIndex + 1).trim()]; - // If content contains a hex string, truncate it - if (content.includes('0x')) { - const processedContent = replaceHexStrings(content); - return `${prefix} ${processedContent}`; - } - } - return line; - }); - return processedLines.join('\n'); - }); - - // Finally, catch any remaining hex strings in the message - result = replaceHexStrings(result); - - return result; -} - -function truncateHex(hex: string, length = 100) { - if (!hex || typeof hex !== 'string') { - return hex; - } - if (!hex.startsWith('0x')) { - return hex; - } - if (hex.length <= length * 2) { - return hex; - } - // For extremely large hex strings, use more aggressive truncation - if (hex.length > 10000) { - return `${hex.slice(0, length)}...<${hex.length - length * 2} chars omitted>...${hex.slice(-length)}`; - } - return `${hex.slice(0, length)}...${hex.slice(-length)}`; -} - -function replaceHexStrings( - text: string, - options: { - minLength?: number; - maxLength?: number; - truncateLength?: number; - pattern?: RegExp; - transform?: (hex: string) => string; - } = {}, -): string { - const { - minLength = 10, - maxLength = Infinity, - truncateLength = 100, - pattern, - transform = hex => truncateHex(hex, truncateLength), - } = options; - - const hexRegex = pattern ?? new RegExp(`(0x[a-fA-F0-9]{${minLength},${maxLength}})`, 'g'); - return text.replace(hexRegex, match => transform(match)); -} - -function formatRequestBody(body: string) { - try { - // Special handling for eth_sendRawTransaction - if (body.includes('"method":"eth_sendRawTransaction"')) { - try { - const parsed = JSON.parse(body); - if (parsed.params && Array.isArray(parsed.params) && parsed.params.length > 0) { - // These are likely large transaction hex strings - parsed.params = parsed.params.map((param: any) => { - if (typeof param === 'string' && param.startsWith('0x') && param.length > 1000) { - return truncateHex(param, 200); - } - return param; - }); - } - return JSON.stringify(parsed, null, 2); - } catch { - // If specific parsing fails, fall back to regex-based truncation - return replaceHexStrings(body, { - pattern: /"params":\s*\[\s*"(0x[a-fA-F0-9]{1000,})"\s*\]/g, - transform: hex => `"params":["${truncateHex(hex, 200)}"]`, - }); - } - } - - // For extremely large request bodies, use simple truncation instead of parsing - if (body.length > 50000) { - const jsonStart = body.indexOf('{'); - const jsonEnd = body.lastIndexOf('}'); - if (jsonStart >= 0 && jsonEnd > jsonStart) { - return replaceHexStrings(body, { minLength: 10000, truncateLength: 200 }); - } - } - - const parsed = JSON.parse(body); - - // Process the entire request body - const processed = processParams(parsed); - return JSON.stringify(processed, null, 2); - } catch { - // If JSON parsing fails, do a simple truncation of any large hex strings - return replaceHexStrings(body, { minLength: 1000, truncateLength: 150 }); - } -} - -// Recursively process all parameters that might contain hex strings -function processParams(obj: any): any { - if (Array.isArray(obj)) { - return obj.map(item => processParams(item)); - } - if (typeof obj === 'object' && obj !== null) { - const result: any = {}; - for (const [key, value] of Object.entries(obj)) { - result[key] = processParams(value); - } - return result; - } - if (typeof obj === 'string') { - if (obj.startsWith('0x')) { - return truncateHex(obj); - } - } - return obj; -} - export function tryGetCustomErrorName(err: any) { try { // See https://viem.sh/docs/contract/simulateContract#handling-custom-errors diff --git a/yarn-project/foundation/src/string/index.test.ts b/yarn-project/foundation/src/string/index.test.ts index 77203424aa90..498e62e8d30a 100644 --- a/yarn-project/foundation/src/string/index.test.ts +++ b/yarn-project/foundation/src/string/index.test.ts @@ -6,8 +6,11 @@ describe('string', () => { expect(urlJoin('http://example.com', 'foo', 'bar')).toBe('http://example.com/foo/bar'); }); - it('removes duplicate slashes', () => { - expect(urlJoin('http://example.com/', '/foo/', '/bar/')).toBe('http://example.com/foo/bar'); + it.each([ + [['http://example.com/', '/foo/', '/bar/'], 'http://example.com/foo/bar'], + [['http://example.com/', '', '//', '///', '////foo//', '//bar////', 'baz'], 'http://example.com/foo/bar/baz'], + ])('removes duplicate slashes', (parts, url) => { + expect(urlJoin(...parts)).toBe(url); }); }); diff --git a/yarn-project/foundation/src/string/index.ts b/yarn-project/foundation/src/string/index.ts index 3395ceea0c5d..d4a6abb2a223 100644 --- a/yarn-project/foundation/src/string/index.ts +++ b/yarn-project/foundation/src/string/index.ts @@ -39,5 +39,25 @@ export function isoDate(date?: Date) { } export function urlJoin(...args: string[]): string { - return args.map(arg => arg.replace(/\/+$/, '').replace(/^\/+/, '')).join('/'); + const processed = []; + for (const arg of args) { + if (arg.length === 0) { + continue; + } + + let start = 0; + let end = arg.length - 1; + + while (start <= end && arg[start] === '/') { + start++; + } + while (end >= start && arg[end] === '/') { + end--; + } + + if (start < end) { + processed.push(arg.slice(start, end + 1)); + } + } + return processed.join('/'); }