Skip to content

Commit 6f4b790

Browse files
authored
fix: batch deployer transactions and bump gas buffers (#8583)
1 parent 054b178 commit 6f4b790

13 files changed

Lines changed: 371 additions & 61 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperlane-xyz/sdk': patch
3+
---
4+
5+
Added batched transaction submission for hook, IGP, and routing ISM deployments to avoid hitting gas limits on chains with lower block gas caps. Chain-specific batch size overrides were added (e.g. citrea). Routing ISM deployment was refactored to deploy with an initial batch of domains and enroll the remainder individually, with per-chain initialization sizes; the final `transferOwnership` call is skipped when the deployer is already the configured owner. The gas buffer multiplier was increased for ISM factory deployments. A configurable `minConfirmationTimeoutMs` option was added to `MultiProvider`. The `defaultEthersV5ProviderBuilder` `retryOverride` parameter was widened from `ProviderRetryOptions` to `SmartProviderOptions` so callers can pass `fallbackStaggerMs`.

typescript/infra/config/environments/mainnet3/chains.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,16 @@ export const chainMetadataOverrides: ChainMap<Partial<ChainMetadata>> = {
140140
// confirmations: 3,
141141
// },
142142
// },
143+
// arbitrum: {
144+
// blocks: {
145+
// confirmations: 3,
146+
// },
147+
// },
148+
// unichain: {
149+
// blocks: {
150+
// confirmations: 5,
151+
// },
152+
// },
143153
};
144154

145155
export const getRegistry = async (

typescript/infra/scripts/agent-utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,9 @@ export async function getMultiProviderForRole(
631631
): Promise<MultiProvider> {
632632
const chainMetadata = await registry.getMetadata();
633633
logger.debug(`Getting multiprovider for ${role} role`);
634-
const multiProvider = new MultiProvider(chainMetadata);
634+
const multiProvider = new MultiProvider(chainMetadata, {
635+
minConfirmationTimeoutMs: 300_000,
636+
});
635637
if (inCIMode()) {
636638
logger.debug('Running in CI, returning multiprovider without secret keys');
637639
return multiProvider;

typescript/infra/scripts/check/check-utils.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { providers, Signer } from 'ethers';
12
import { Registry } from 'prom-client';
23

34
import {
45
CheckerViolation,
6+
defaultEthersV5ProviderBuilder,
57
HyperlaneCoreChecker,
68
HyperlaneIgp,
79
HyperlaneIgpChecker,
@@ -14,7 +16,7 @@ import {
1416
IsmType,
1517
MultiProvider,
1618
} from '@hyperlane-xyz/sdk';
17-
import { objFilter } from '@hyperlane-xyz/utils';
19+
import { objFilter, rootLogger } from '@hyperlane-xyz/utils';
1820

1921
import { Contexts } from '../../config/contexts.js';
2022
import { DEPLOYER } from '../../config/environments/mainnet3/owners.js';
@@ -59,6 +61,80 @@ export function getCheckDeployArgs() {
5961

6062
const ICA_ENABLED_MODULES = [Modules.INTERCHAIN_ACCOUNTS, Modules.HAAS];
6163

64+
const HAAS_SMART_PROVIDER_OPTIONS = {
65+
maxRetries: 4,
66+
baseRetryDelayMs: 100,
67+
fallbackStaggerMs: 2_000,
68+
};
69+
70+
const logger = rootLogger.child({ module: 'check-utils' });
71+
72+
function reconnectSigner(
73+
signer: Signer,
74+
provider: providers.Provider,
75+
chain: string,
76+
): Signer {
77+
try {
78+
return signer.connect(provider);
79+
} catch (error: unknown) {
80+
const code =
81+
typeof error === 'object' && error !== null && 'code' in error
82+
? String((error as Record<string, unknown>).code)
83+
: undefined;
84+
if (code === 'UNSUPPORTED_OPERATION') {
85+
logger.warn(
86+
{ chain },
87+
'Signer could not be reconnected to HAAS provider; smart-provider options not active',
88+
);
89+
return signer;
90+
}
91+
throw error;
92+
}
93+
}
94+
95+
function getHaasMultiProvider(baseMultiProvider: MultiProvider): MultiProvider {
96+
const haasMultiProvider = new MultiProvider(baseMultiProvider.metadata, {
97+
...baseMultiProvider.options,
98+
providerBuilder: (rpcUrls, network) =>
99+
defaultEthersV5ProviderBuilder(
100+
rpcUrls,
101+
network,
102+
HAAS_SMART_PROVIDER_OPTIONS,
103+
).provider,
104+
});
105+
106+
if (baseMultiProvider.useSharedSigner) {
107+
const sharedSigner = Object.values(baseMultiProvider.signers)[0];
108+
if (sharedSigner) {
109+
haasMultiProvider.setSharedSigner(sharedSigner);
110+
// Rebind each chain's signer to its HAAS provider so signer-backed
111+
// calls (estimateGas, sendTransaction) benefit from the smart-provider
112+
// options. We mutate the map directly because MultiProvider.setSigner()
113+
// is blocked once useSharedSigner is true.
114+
for (const chain of Object.keys(haasMultiProvider.signers)) {
115+
const provider = haasMultiProvider.tryGetProvider(chain);
116+
if (!provider) continue;
117+
haasMultiProvider.signers[chain] = reconnectSigner(
118+
sharedSigner,
119+
provider,
120+
chain,
121+
);
122+
}
123+
}
124+
return haasMultiProvider;
125+
}
126+
127+
for (const [chain, signer] of Object.entries(baseMultiProvider.signers)) {
128+
const provider = haasMultiProvider.tryGetProvider(chain);
129+
haasMultiProvider.setSigner(
130+
chain,
131+
provider ? reconnectSigner(signer, provider, chain) : signer,
132+
);
133+
}
134+
135+
return haasMultiProvider;
136+
}
137+
62138
export async function getGovernor(
63139
module: Modules,
64140
context: Contexts,
@@ -75,6 +151,10 @@ export async function getGovernor(
75151
multiProvider = await envConfig.getMultiProvider();
76152
}
77153

154+
if (module === Modules.HAAS) {
155+
multiProvider = getHaasMultiProvider(multiProvider);
156+
}
157+
78158
// must rotate to forked provider before building core contracts
79159
if (fork) {
80160
await useLocalProvider(multiProvider, fork);

typescript/infra/src/config/chain.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
getRegistryWithOverrides,
2727
} from '../../config/registry.js';
2828
import { getSecretRpcEndpoints } from '../agents/index.js';
29+
import { fetchExplorerApiKeys } from '../deployment/verify.js';
2930
import { getSafeApiKey } from '../utils/safe.js';
3031

3132
import { DeployEnvironment } from './environment.js';
@@ -65,6 +66,8 @@ export function getDisabledChains(): ChainName[] {
6566
export const chainsToSkip: ChainName[] = [
6667
// downtime
6768
'molten',
69+
'fluence',
70+
'tangle',
6871

6972
// not AW owned
7073
'forma',
@@ -160,7 +163,32 @@ export async function getRegistryForEnvironment(
160163
* @param chains The chains to get metadata overrides for.
161164
* @returns A partial chain metadata map with the secret overrides.
162165
*/
163-
export async function getSecretMetadataOverrides(
166+
// Process-level cache for secret metadata overrides. Explorer/Safe keys and
167+
// secret RPC URLs don't change within a single process run, so re-fetching
168+
// them on every MultiProvider construction is wasteful.
169+
const secretMetadataOverridesCache = new Map<
170+
string,
171+
Promise<ChainMap<Partial<ChainMetadata>>>
172+
>();
173+
174+
export function getSecretMetadataOverrides(
175+
deployEnv: DeployEnvironment,
176+
chains: string[],
177+
): Promise<ChainMap<Partial<ChainMetadata>>> {
178+
const cacheKey = `${deployEnv}:${[...chains].sort().join(',')}`;
179+
const cached = secretMetadataOverridesCache.get(cacheKey);
180+
if (cached) return cached;
181+
const promise = fetchSecretMetadataOverrides(deployEnv, chains).catch(
182+
(error) => {
183+
secretMetadataOverridesCache.delete(cacheKey);
184+
throw error;
185+
},
186+
);
187+
secretMetadataOverridesCache.set(cacheKey, promise);
188+
return promise;
189+
}
190+
191+
async function fetchSecretMetadataOverrides(
164192
deployEnv: DeployEnvironment,
165193
chains: string[],
166194
): Promise<ChainMap<Partial<ChainMetadata>>> {
@@ -208,6 +236,26 @@ export async function getSecretMetadataOverrides(
208236
}
209237
}
210238

239+
// Merge explorer API keys into chain metadata so that block explorer
240+
// verification checks (e.g. warp check) use authenticated requests.
241+
// These overrides live only in the PartialRegistry layer and are never
242+
// persisted back to the on-disk registry.
243+
const explorerApiKeys = await fetchExplorerApiKeys();
244+
for (const chain of chains) {
245+
const apiKey = explorerApiKeys[chain];
246+
if (!apiKey) continue;
247+
248+
const chainMetadata = getChain(chain);
249+
if (!chainMetadata.blockExplorers?.length) continue;
250+
251+
chainMetadataOverrides[chain] ??= {};
252+
chainMetadataOverrides[chain].blockExplorers =
253+
chainMetadata.blockExplorers.map((explorer) => ({
254+
...explorer,
255+
apiKey,
256+
}));
257+
}
258+
211259
return chainMetadataOverrides;
212260
}
213261

typescript/infra/src/govern/HyperlaneHaasGovernor.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export class HyperlaneHaasGovernor extends HyperlaneAppGovernor<
3131
HyperlaneCore,
3232
CoreConfig
3333
> {
34+
static readonly CHAIN_CHECK_CONCURRENCY = 10;
3435
protected readonly icaGovernor: ProxiedRouterGovernor<any, any>;
3536
protected readonly coreGovernor: HyperlaneCoreGovernor;
3637

@@ -109,22 +110,33 @@ export class HyperlaneHaasGovernor extends HyperlaneAppGovernor<
109110
chalk.yellow('Skipping chains:', chainsToSkip.join(', ')),
110111
);
111112
}
113+
const evmChainsToCheck = chains.filter(
114+
(chain) =>
115+
isEVMLike(
116+
this.coreChecker.multiProvider.getChainMetadata(chain).protocol,
117+
) && !chainsToSkip.includes(chain),
118+
);
119+
120+
let nextChainIndex = 0;
121+
const workerCount = Math.min(
122+
HyperlaneHaasGovernor.CHAIN_CHECK_CONCURRENCY,
123+
evmChainsToCheck.length,
124+
);
125+
112126
await Promise.allSettled(
113-
chains
114-
.filter(
115-
(chain) =>
116-
isEVMLike(
117-
this.coreChecker.multiProvider.getChainMetadata(chain).protocol,
118-
) && !chainsToSkip.includes(chain),
119-
)
120-
.map(async (chain) => {
127+
Array.from({ length: workerCount }, async () => {
128+
while (nextChainIndex < evmChainsToCheck.length) {
129+
const chain = evmChainsToCheck[nextChainIndex];
130+
nextChainIndex += 1;
131+
121132
try {
122133
await this.checkChain(chain);
123134
} catch (err) {
124135
rootLogger.error(chalk.red(`Failed to check chain ${chain}:`, err));
125136
failedChains.push(chain);
126137
}
127-
}),
138+
}
139+
}),
128140
);
129141

130142
if (failedChains.length > 0) {

typescript/sdk/src/deploy/utils.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Logger } from 'pino';
2+
3+
import { ChainName } from '../types.js';
4+
5+
const DEFAULT_MAX_BATCH_SIZE = 64;
6+
7+
const CHAIN_BATCH_SIZE_OVERRIDES: Partial<Record<ChainName, number>> = {
8+
citrea: 16,
9+
};
10+
11+
export function getTxConfigBatchSize(chain: ChainName): number {
12+
return CHAIN_BATCH_SIZE_OVERRIDES[chain] ?? DEFAULT_MAX_BATCH_SIZE;
13+
}
14+
15+
/**
16+
* Submits `items` to `fn` in sequential batches sized per `chain`.
17+
*
18+
* NOTE: Non-atomic. If batch N succeeds and batch N+1 fails, on-chain state
19+
* is partially mutated and a naive retry will re-submit the already-applied
20+
* batches. Callers must either (a) pre-filter `items` by comparing against
21+
* on-chain state before each run (the IGP/oracle path) or (b) accept that a
22+
* retry may redundantly re-submit already-applied entries (the hook routing
23+
* path).
24+
*/
25+
export async function submitBatched<T>(
26+
chain: ChainName,
27+
items: T[],
28+
fn: (batch: T[]) => Promise<void>,
29+
logger: Logger,
30+
label: string,
31+
): Promise<void> {
32+
const batchSize = getTxConfigBatchSize(chain);
33+
const batches: T[][] = [];
34+
for (let i = 0; i < items.length; i += batchSize) {
35+
batches.push(items.slice(i, i + batchSize));
36+
}
37+
38+
logger.info(
39+
`Splitting ${items.length} ${label} into ${batches.length} transaction(s)`,
40+
);
41+
42+
for (let i = 0; i < batches.length; i++) {
43+
const batch = batches[i];
44+
logger.info(
45+
`Sending batch ${i + 1}/${batches.length} with ${batch.length} config(s)`,
46+
);
47+
await fn(batch);
48+
}
49+
}

typescript/sdk/src/gas/HyperlaneIgpDeployer.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { TOKEN_EXCHANGE_RATE_SCALE_ETHEREUM } from '../consts/igp.js';
1515
import { HyperlaneContracts } from '../contracts/types.js';
1616
import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js';
17+
import { submitBatched } from '../deploy/utils.js';
1718
import { ContractVerifier } from '../deploy/verify/ContractVerifier.js';
1819
import { MultiProvider } from '../providers/MultiProvider.js';
1920
import { ChainName } from '../types.js';
@@ -88,14 +89,22 @@ export class HyperlaneIgpDeployer extends HyperlaneDeployer<
8889

8990
if (gasParamsToSet.length > 0) {
9091
await this.runIfOwner(chain, igp, async () => {
91-
const estimatedGas =
92-
await igp.estimateGas.setDestinationGasConfigs(gasParamsToSet);
93-
return this.multiProvider.handleTx(
92+
await submitBatched(
9493
chain,
95-
igp.setDestinationGasConfigs(gasParamsToSet, {
96-
gasLimit: addBufferToGasLimit(estimatedGas),
97-
...this.multiProvider.getTransactionOverrides(chain),
98-
}),
94+
gasParamsToSet,
95+
async (batch) => {
96+
const estimatedGas =
97+
await igp.estimateGas.setDestinationGasConfigs(batch);
98+
await this.multiProvider.handleTx(
99+
chain,
100+
igp.setDestinationGasConfigs(batch, {
101+
gasLimit: addBufferToGasLimit(estimatedGas),
102+
...this.multiProvider.getTransactionOverrides(chain),
103+
}),
104+
);
105+
},
106+
this.logger,
107+
'gas configs',
99108
);
100109
});
101110
}
@@ -177,14 +186,22 @@ export class HyperlaneIgpDeployer extends HyperlaneDeployer<
177186

178187
if (configsToSet.length > 0) {
179188
await this.runIfOwner(chain, gasOracle, async () => {
180-
const estimatedGas =
181-
await gasOracle.estimateGas.setRemoteGasDataConfigs(configsToSet);
182-
return this.multiProvider.handleTx(
189+
await submitBatched(
183190
chain,
184-
gasOracle.setRemoteGasDataConfigs(configsToSet, {
185-
gasLimit: addBufferToGasLimit(estimatedGas),
186-
...this.multiProvider.getTransactionOverrides(chain),
187-
}),
191+
configsToSet,
192+
async (batch) => {
193+
const estimatedGas =
194+
await gasOracle.estimateGas.setRemoteGasDataConfigs(batch);
195+
await this.multiProvider.handleTx(
196+
chain,
197+
gasOracle.setRemoteGasDataConfigs(batch, {
198+
gasLimit: addBufferToGasLimit(estimatedGas),
199+
...this.multiProvider.getTransactionOverrides(chain),
200+
}),
201+
);
202+
},
203+
this.logger,
204+
'gas oracle configs',
188205
);
189206
});
190207
}

0 commit comments

Comments
 (0)