Skip to content

Commit e2c6a51

Browse files
committed
feat(sdk): use routing ISM setBatch/removeBatch when available
Adds a PACKAGE_VERSION gate on the target routing ISM. When it reports >= 11.4.0 (where setBatch/removeBatch were introduced), consolidate enrollments and unenrollments into chunked setBatch/removeBatch txs sized by domainRoutingInitializationSize(destination). Older ISMs fall back to per-domain set()/remove() loops. Applies to three paths: - Reconfigure-existing-ISM enrollments and unenrollments - Post-initializer follow-on enrollments on new DomainRoutingIsm / IncrementalDomainRoutingIsm deploys - DefaultFallbackRoutingIsm new deploys (initialize with initial batch + setBatch remainder, matching the sharding already done on the proxy-factory path)
1 parent 38d81ab commit e2c6a51

2 files changed

Lines changed: 210 additions & 32 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': minor
3+
---
4+
5+
Wired `HyperlaneIsmFactory` to use the routing ISM `setBatch` and `removeBatch` functions added in `@hyperlane-xyz/core` 11.4.0. Enrollments and unenrollments are now consolidated into chunked batched transactions sized by the per-chain `domainRoutingInitializationSize`, avoiding one tx per domain on low-capacity chains (citrea, shibarium, tempo, etc.). Version-gated via `PACKAGE_VERSION()` on the target routing ISM, with a per-domain fallback for older deployed ISMs.

typescript/sdk/src/ism/HyperlaneIsmFactory.ts

Lines changed: 205 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
TrustedRelayerIsm__factory,
3232
ZKSyncArtifact,
3333
} from '@hyperlane-xyz/core';
34+
3435
import {
3536
Address,
3637
Domain,
@@ -84,6 +85,31 @@ const ismFactories = {
8485
[IsmType.CCIP]: new CCIPIsm__factory(),
8586
};
8687

88+
// First @hyperlane-xyz/core version with setBatch/removeBatch on
89+
// DomainRoutingIsm. Routing ISMs at or above this version let the SDK
90+
// consolidate per-domain txs into chunked setBatch/removeBatch calls;
91+
// older ISMs fall back to the per-domain loop.
92+
const ROUTING_ISM_BATCH_MIN_VERSION = { major: 11, minor: 4 };
93+
94+
const routingIsmSupportsBatch = async (
95+
address: Address,
96+
provider: ethers.providers.Provider,
97+
): Promise<boolean> => {
98+
try {
99+
const version = await DomainRoutingIsm__factory.connect(
100+
address,
101+
provider,
102+
).PACKAGE_VERSION();
103+
const [major, minor] = version.split('.').map((n) => Number.parseInt(n));
104+
if (!Number.isFinite(major) || !Number.isFinite(minor)) return false;
105+
if (major > ROUTING_ISM_BATCH_MIN_VERSION.major) return true;
106+
if (major < ROUTING_ISM_BATCH_MIN_VERSION.major) return false;
107+
return minor >= ROUTING_ISM_BATCH_MIN_VERSION.minor;
108+
} catch {
109+
return false;
110+
}
111+
};
112+
87113
const domainRoutingInitializationSize = (destination: ChainName) => {
88114
if (destination === 'tempo') {
89115
return 30;
@@ -541,7 +567,14 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
541567
existingIsmAddress,
542568
this.multiProvider.getSigner(destination),
543569
);
570+
const batchSize = domainRoutingInitializationSize(destination);
571+
const supportsBatch = await routingIsmSupportsBatch(
572+
existingIsmAddress,
573+
provider,
574+
);
575+
544576
// deploying all the ISMs which have to be updated
577+
const enrollAddresses: Address[] = [];
545578
for (const originDomain of delta.domainsToEnroll) {
546579
const origin = this.multiProvider.getChainName(originDomain); // already filtered to only include domains in the multiprovider
547580
logger.debug(
@@ -554,20 +587,46 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
554587
mailbox,
555588
});
556589
isms[originDomain] = ism.address;
557-
const tx = await routingIsm.set(
558-
originDomain,
559-
isms[originDomain],
590+
enrollAddresses.push(ism.address);
591+
}
592+
if (supportsBatch) {
593+
await this.setDomainsBatched(
594+
routingIsm,
595+
delta.domainsToEnroll,
596+
enrollAddresses,
597+
batchSize,
560598
overrides,
599+
destination,
600+
logger,
561601
);
562-
await this.multiProvider.handleTx(destination, tx);
602+
} else {
603+
for (let i = 0; i < delta.domainsToEnroll.length; i++) {
604+
const tx = await routingIsm.set(
605+
delta.domainsToEnroll[i],
606+
enrollAddresses[i],
607+
overrides,
608+
);
609+
await this.multiProvider.handleTx(destination, tx);
610+
}
563611
}
564612
// unenrolling domains if needed
565-
for (const originDomain of delta.domainsToUnenroll) {
566-
logger.debug(
567-
`Unenrolling originDomain ${originDomain} from preexisting routing ISM at ${existingIsmAddress}...`,
613+
if (supportsBatch && delta.domainsToUnenroll.length > 0) {
614+
await this.removeDomainsBatched(
615+
routingIsm,
616+
delta.domainsToUnenroll,
617+
batchSize,
618+
overrides,
619+
destination,
620+
logger,
568621
);
569-
const tx = await routingIsm.remove(originDomain, overrides);
570-
await this.multiProvider.handleTx(destination, tx);
622+
} else {
623+
for (const originDomain of delta.domainsToUnenroll) {
624+
logger.debug(
625+
`Unenrolling originDomain ${originDomain} from preexisting routing ISM at ${existingIsmAddress}...`,
626+
);
627+
const tx = await routingIsm.remove(originDomain, overrides);
628+
await this.multiProvider.handleTx(destination, tx);
629+
}
571630
}
572631
// transfer ownership if needed
573632
if (delta.owner) {
@@ -603,16 +662,50 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
603662
await getZKSyncArtifactByContractName(config.type),
604663
);
605664
// TODO: Should verify contract here
606-
logger.debug('Initialising fallback routing ISM ...');
665+
const batchSize = domainRoutingInitializationSize(destination);
666+
const initialBatchSize = Math.min(batchSize, safeConfigDomains.length);
667+
const initialDomains = safeConfigDomains.slice(0, initialBatchSize);
668+
const initialAddresses = submoduleAddresses.slice(0, initialBatchSize);
669+
logger.debug(
670+
`Initialising fallback routing ISM with ${initialBatchSize} domains on ${destination}`,
671+
);
607672
receipt = await this.multiProvider.handleTx(
608673
destination,
609674
routingIsm['initialize(address,uint32[],address[])'](
610675
config.owner,
611-
safeConfigDomains,
612-
submoduleAddresses,
676+
initialDomains,
677+
initialAddresses,
613678
overrides,
614679
),
615680
);
681+
const remainingDomains = safeConfigDomains.slice(initialBatchSize);
682+
const remainingAddresses = submoduleAddresses.slice(initialBatchSize);
683+
if (remainingDomains.length > 0) {
684+
const supportsBatch = await routingIsmSupportsBatch(
685+
routingIsm.address,
686+
provider,
687+
);
688+
if (supportsBatch) {
689+
await this.setDomainsBatched(
690+
routingIsm,
691+
remainingDomains,
692+
remainingAddresses,
693+
batchSize,
694+
overrides,
695+
destination,
696+
logger,
697+
);
698+
} else {
699+
for (let i = 0; i < remainingDomains.length; i++) {
700+
const tx = await routingIsm.set(
701+
remainingDomains[i],
702+
remainingAddresses[i],
703+
overrides,
704+
);
705+
await this.multiProvider.handleTx(destination, tx);
706+
}
707+
}
708+
}
616709
} else {
617710
// deploying new domain routing ISM
618711
const owner = config.owner;
@@ -704,27 +797,47 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
704797

705798
// Enroll remaining domains and addresses
706799
// If all domains are enrolled already, this is a no-op
707-
for (let i = initialBatchSize; i < safeConfigDomains.length; i++) {
708-
const estimatedGas = await routingIsm.estimateGas.set(
709-
safeConfigDomains[i],
710-
submoduleAddresses[i],
711-
overrides,
712-
);
713-
const chainName = this.multiProvider.getChainName(
714-
safeConfigDomains[i],
715-
);
716-
this.logger.debug(
717-
`Enrolling ${chainName} (${safeConfigDomains[i]}) ISM at ${submoduleAddresses[i]} on Domain Routing ISM ${moduleAddress}`,
718-
);
719-
const enrollTx = await routingIsm.set(
720-
safeConfigDomains[i],
721-
submoduleAddresses[i],
722-
{
723-
gasLimit: addBufferToGasLimit(estimatedGas, 15),
724-
...overrides,
725-
},
800+
const remainingDomains = safeConfigDomains.slice(initialBatchSize);
801+
const remainingAddresses = submoduleAddresses.slice(initialBatchSize);
802+
if (remainingDomains.length > 0) {
803+
const supportsBatch = await routingIsmSupportsBatch(
804+
moduleAddress,
805+
provider,
726806
);
727-
await this.multiProvider.handleTx(destination, enrollTx);
807+
if (supportsBatch) {
808+
await this.setDomainsBatched(
809+
routingIsm,
810+
remainingDomains,
811+
remainingAddresses,
812+
batchSize,
813+
overrides,
814+
destination,
815+
logger,
816+
);
817+
} else {
818+
for (let i = 0; i < remainingDomains.length; i++) {
819+
const estimatedGas = await routingIsm.estimateGas.set(
820+
remainingDomains[i],
821+
remainingAddresses[i],
822+
overrides,
823+
);
824+
const chainName = this.multiProvider.getChainName(
825+
remainingDomains[i],
826+
);
827+
this.logger.debug(
828+
`Enrolling ${chainName} (${remainingDomains[i]}) ISM at ${remainingAddresses[i]} on Domain Routing ISM ${moduleAddress}`,
829+
);
830+
const enrollTx = await routingIsm.set(
831+
remainingDomains[i],
832+
remainingAddresses[i],
833+
{
834+
gasLimit: addBufferToGasLimit(estimatedGas, 15),
835+
...overrides,
836+
},
837+
);
838+
await this.multiProvider.handleTx(destination, enrollTx);
839+
}
840+
}
728841
}
729842

730843
// Transfer ownership after all enrollments are complete, unless the
@@ -746,6 +859,66 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
746859
return routingIsm;
747860
}
748861

862+
private async setDomainsBatched(
863+
routingIsm:
864+
| DomainRoutingIsm
865+
| IncrementalDomainRoutingIsm
866+
| DefaultFallbackRoutingIsm,
867+
domains: number[],
868+
addresses: Address[],
869+
batchSize: number,
870+
overrides: ethers.Overrides,
871+
destination: ChainName,
872+
logger: Logger,
873+
): Promise<void> {
874+
for (let i = 0; i < domains.length; i += batchSize) {
875+
const chunk = domains.slice(i, i + batchSize).map((domain, j) => ({
876+
domain,
877+
module: addresses[i + j],
878+
}));
879+
logger.debug(
880+
`setBatch enrolling ${chunk.length} domains on routing ISM ${routingIsm.address} (${destination})`,
881+
);
882+
const estimatedGas = await routingIsm.estimateGas.setBatch(
883+
chunk,
884+
overrides,
885+
);
886+
const tx = await routingIsm.setBatch(chunk, {
887+
gasLimit: addBufferToGasLimit(estimatedGas, 15),
888+
...overrides,
889+
});
890+
await this.multiProvider.handleTx(destination, tx);
891+
}
892+
}
893+
894+
private async removeDomainsBatched(
895+
routingIsm:
896+
| DomainRoutingIsm
897+
| IncrementalDomainRoutingIsm
898+
| DefaultFallbackRoutingIsm,
899+
domains: number[],
900+
batchSize: number,
901+
overrides: ethers.Overrides,
902+
destination: ChainName,
903+
logger: Logger,
904+
): Promise<void> {
905+
for (let i = 0; i < domains.length; i += batchSize) {
906+
const chunk = domains.slice(i, i + batchSize);
907+
logger.debug(
908+
`removeBatch unenrolling ${chunk.length} domains on routing ISM ${routingIsm.address} (${destination})`,
909+
);
910+
const estimatedGas = await routingIsm.estimateGas.removeBatch(
911+
chunk,
912+
overrides,
913+
);
914+
const tx = await routingIsm.removeBatch(chunk, {
915+
gasLimit: addBufferToGasLimit(estimatedGas, 15),
916+
...overrides,
917+
});
918+
await this.multiProvider.handleTx(destination, tx);
919+
}
920+
}
921+
749922
protected async deployAggregationIsm(params: {
750923
destination: ChainName;
751924
config: AggregationIsmConfig;

0 commit comments

Comments
 (0)