diff --git a/.changeset/few-olives-judge.md b/.changeset/few-olives-judge.md new file mode 100644 index 0000000000..09489132e6 --- /dev/null +++ b/.changeset/few-olives-judge.md @@ -0,0 +1,6 @@ +--- +"@latticexyz/cli": patch +--- + +Significantly improved the deployment performance for large projects with public libraries by implementing a more efficient algorithm to resolve public libraries during deployment. +The local deployment time on a large reference project was reduced from over 10 minutes to 4 seconds. diff --git a/packages/cli/src/deploy/common.ts b/packages/cli/src/deploy/common.ts index 30c762843c..f88db9f65f 100644 --- a/packages/cli/src/deploy/common.ts +++ b/packages/cli/src/deploy/common.ts @@ -2,6 +2,7 @@ import { Abi, Address, Hex, padHex } from "viem"; import IBaseWorldAbi from "@latticexyz/world/out/IBaseWorld.sol/IBaseWorld.abi.json" assert { type: "json" }; import { helloStoreEvent } from "@latticexyz/store"; import { helloWorldEvent } from "@latticexyz/world"; +import { LibraryMap } from "./getLibraryMap"; export const salt = padHex("0x", { size: 32 }); @@ -61,7 +62,7 @@ export type LibraryPlaceholder = { export type DeterministicContract = { readonly prepareDeploy: ( deployer: Address, - libraries: readonly Library[], + libraryMap?: LibraryMap, ) => { readonly address: Address; readonly bytecode: Hex; diff --git a/packages/cli/src/deploy/createPrepareDeploy.ts b/packages/cli/src/deploy/createPrepareDeploy.ts index a1846de350..065ade5e0b 100644 --- a/packages/cli/src/deploy/createPrepareDeploy.ts +++ b/packages/cli/src/deploy/createPrepareDeploy.ts @@ -1,24 +1,26 @@ -import { DeterministicContract, Library, LibraryPlaceholder, salt } from "./common"; +import { DeterministicContract, LibraryPlaceholder, salt } from "./common"; import { spliceHex } from "@latticexyz/common"; import { Hex, getCreate2Address, Address } from "viem"; +import { LibraryMap } from "./getLibraryMap"; export function createPrepareDeploy( bytecodeWithPlaceholders: Hex, placeholders: readonly LibraryPlaceholder[], ): DeterministicContract["prepareDeploy"] { - return function prepareDeploy(deployer: Address, libraries: readonly Library[]) { + return function prepareDeploy(deployer: Address, libraryMap?: LibraryMap) { let bytecode = bytecodeWithPlaceholders; + + if (placeholders.length === 0) { + return { bytecode, address: getCreate2Address({ from: deployer, bytecode, salt }) }; + } + + if (!libraryMap) { + throw new Error("Libraries must be provided if there are placeholders"); + } + for (const placeholder of placeholders) { - const library = libraries.find((lib) => lib.path === placeholder.path && lib.name === placeholder.name); - if (!library) { - throw new Error(`Could not find library for bytecode placeholder ${placeholder.path}:${placeholder.name}`); - } - bytecode = spliceHex( - bytecode, - placeholder.start, - placeholder.length, - library.prepareDeploy(deployer, libraries).address, - ); + const address = libraryMap.getAddress({ name: placeholder.name, path: placeholder.path, deployer }); + bytecode = spliceHex(bytecode, placeholder.start, placeholder.length, address); } return { bytecode, diff --git a/packages/cli/src/deploy/deploy.ts b/packages/cli/src/deploy/deploy.ts index bbd13d5beb..e0a7e3688e 100644 --- a/packages/cli/src/deploy/deploy.ts +++ b/packages/cli/src/deploy/deploy.ts @@ -20,6 +20,7 @@ import { ContractArtifact } from "@latticexyz/world/node"; import { World } from "@latticexyz/world"; import { deployCustomWorld } from "./deployCustomWorld"; import { uniqueBy } from "@latticexyz/common/utils"; +import { getLibraryMap } from "./getLibraryMap"; type DeployOptions = { config: World; @@ -64,22 +65,23 @@ export async function deploy({ await ensureWorldFactory(client, deployerAddress, config.deploy.upgradeableWorldImplementation); // deploy all dependent contracts, because system registration, module install, etc. all expect these contracts to be callable. + const libraryMap = getLibraryMap(libraries); await ensureContractsDeployed({ client, deployerAddress, contracts: [ ...libraries.map((library) => ({ - bytecode: library.prepareDeploy(deployerAddress, libraries).bytecode, + bytecode: library.prepareDeploy(deployerAddress, libraryMap).bytecode, deployedBytecodeSize: library.deployedBytecodeSize, debugLabel: `${library.path}:${library.name} library`, })), ...systems.map((system) => ({ - bytecode: system.prepareDeploy(deployerAddress, libraries).bytecode, + bytecode: system.prepareDeploy(deployerAddress, libraryMap).bytecode, deployedBytecodeSize: system.deployedBytecodeSize, debugLabel: `${resourceToLabel(system)} system`, })), ...modules.map((mod) => ({ - bytecode: mod.prepareDeploy(deployerAddress, libraries).bytecode, + bytecode: mod.prepareDeploy(deployerAddress, libraryMap).bytecode, deployedBytecodeSize: mod.deployedBytecodeSize, debugLabel: `${mod.name} module`, })), @@ -126,7 +128,7 @@ export async function deploy({ const systemTxs = await ensureSystems({ client, deployerAddress, - libraries, + libraryMap, worldDeploy, systems, }); @@ -146,7 +148,7 @@ export async function deploy({ const moduleTxs = await ensureModules({ client, deployerAddress, - libraries, + libraryMap, worldDeploy, modules, }); @@ -178,7 +180,7 @@ export async function deploy({ const tagTxs = await ensureResourceTags({ client, deployerAddress, - libraries, + libraryMap, worldDeploy, tags: [...namespaceTags, ...tableTags, ...systemTags], valueToHex: stringToHex, diff --git a/packages/cli/src/deploy/ensureModules.ts b/packages/cli/src/deploy/ensureModules.ts index 04b20c2433..5dc2128514 100644 --- a/packages/cli/src/deploy/ensureModules.ts +++ b/packages/cli/src/deploy/ensureModules.ts @@ -1,21 +1,22 @@ import { Client, Transport, Chain, Account, Hex, BaseError } from "viem"; import { writeContract } from "@latticexyz/common"; -import { Library, Module, WorldDeploy, worldAbi } from "./common"; +import { Module, WorldDeploy, worldAbi } from "./common"; import { debug } from "./debug"; import { isDefined } from "@latticexyz/common/utils"; import pRetry from "p-retry"; import { ensureContractsDeployed } from "./ensureContractsDeployed"; +import { LibraryMap } from "./getLibraryMap"; export async function ensureModules({ client, deployerAddress, - libraries, + libraryMap, worldDeploy, modules, }: { readonly client: Client; readonly deployerAddress: Hex; - readonly libraries: readonly Library[]; + readonly libraryMap: LibraryMap; readonly worldDeploy: WorldDeploy; readonly modules: readonly Module[]; }): Promise { @@ -25,7 +26,7 @@ export async function ensureModules({ client, deployerAddress, contracts: modules.map((mod) => ({ - bytecode: mod.prepareDeploy(deployerAddress, libraries).bytecode, + bytecode: mod.prepareDeploy(deployerAddress, libraryMap).bytecode, deployedBytecodeSize: mod.deployedBytecodeSize, debugLabel: `${mod.name} module`, })), @@ -40,7 +41,7 @@ export async function ensureModules({ try { // append module's ABI so that we can decode any custom errors const abi = [...worldAbi, ...mod.abi]; - const moduleAddress = mod.prepareDeploy(deployerAddress, libraries).address; + const moduleAddress = mod.prepareDeploy(deployerAddress, libraryMap).address; // TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645) const params = mod.installAsRoot ? ({ functionName: "installRootModule", args: [moduleAddress, mod.installData] } as const) diff --git a/packages/cli/src/deploy/ensureResourceTags.ts b/packages/cli/src/deploy/ensureResourceTags.ts index c279649132..5d4ae14185 100644 --- a/packages/cli/src/deploy/ensureResourceTags.ts +++ b/packages/cli/src/deploy/ensureResourceTags.ts @@ -1,5 +1,5 @@ import { Hex, Client, Transport, Chain, Account, stringToHex, BaseError } from "viem"; -import { Library, WorldDeploy } from "./common"; +import { WorldDeploy } from "./common"; import { debug } from "./debug"; import { hexToResource, writeContract } from "@latticexyz/common"; import { identity, isDefined } from "@latticexyz/common/utils"; @@ -11,6 +11,7 @@ import metadataModule from "@latticexyz/world-module-metadata/out/MetadataModule import { getContractArtifact } from "../utils/getContractArtifact"; import { createPrepareDeploy } from "./createPrepareDeploy"; import { waitForTransactions } from "./waitForTransactions"; +import { LibraryMap } from "./getLibraryMap"; const metadataModuleArtifact = getContractArtifact(metadataModule); @@ -23,14 +24,14 @@ export type ResourceTag = { export async function ensureResourceTags({ client, deployerAddress, - libraries, + libraryMap, worldDeploy, tags, valueToHex = identity, }: { readonly client: Client; readonly deployerAddress: Hex; - readonly libraries: readonly Library[]; + readonly libraryMap: LibraryMap; readonly worldDeploy: WorldDeploy; readonly tags: readonly ResourceTag[]; } & (value extends Hex @@ -59,7 +60,7 @@ export async function ensureResourceTags({ client, deployerAddress, worldDeploy, - libraries, + libraryMap, modules: [ { optional: true, diff --git a/packages/cli/src/deploy/ensureSystems.ts b/packages/cli/src/deploy/ensureSystems.ts index c018db1043..48da5611c3 100644 --- a/packages/cli/src/deploy/ensureSystems.ts +++ b/packages/cli/src/deploy/ensureSystems.ts @@ -1,24 +1,25 @@ import { Client, Transport, Chain, Account, Hex, getAddress, Address } from "viem"; import { writeContract, resourceToLabel } from "@latticexyz/common"; -import { Library, System, WorldDeploy, worldAbi } from "./common"; +import { System, WorldDeploy, worldAbi } from "./common"; import { debug } from "./debug"; import { getSystems } from "./getSystems"; import { getResourceAccess } from "./getResourceAccess"; import pRetry from "p-retry"; import { ensureContractsDeployed } from "./ensureContractsDeployed"; +import { LibraryMap } from "./getLibraryMap"; // TODO: move each system registration+access to batch call to be atomic export async function ensureSystems({ client, deployerAddress, - libraries, + libraryMap, worldDeploy, systems, }: { readonly client: Client; readonly deployerAddress: Hex; - readonly libraries: readonly Library[]; + readonly libraryMap: LibraryMap; readonly worldDeploy: WorldDeploy; readonly systems: readonly System[]; }): Promise { @@ -33,7 +34,7 @@ export async function ensureSystems({ worldSystems.some( (worldSystem) => worldSystem.systemId === system.systemId && - getAddress(worldSystem.address) === getAddress(system.prepareDeploy(deployerAddress, libraries).address), + getAddress(worldSystem.address) === getAddress(system.prepareDeploy(deployerAddress, libraryMap).address), ), ); if (existingSystems.length) { @@ -48,7 +49,7 @@ export async function ensureSystems({ worldSystems.some( (worldSystem) => worldSystem.systemId === system.systemId && - getAddress(worldSystem.address) !== getAddress(system.prepareDeploy(deployerAddress, libraries).address), + getAddress(worldSystem.address) !== getAddress(system.prepareDeploy(deployerAddress, libraryMap).address), ), ); if (systemsToUpgrade.length) { @@ -66,7 +67,7 @@ export async function ensureSystems({ client, deployerAddress, contracts: missingSystems.map((system) => ({ - bytecode: system.prepareDeploy(deployerAddress, libraries).bytecode, + bytecode: system.prepareDeploy(deployerAddress, libraryMap).bytecode, deployedBytecodeSize: system.deployedBytecodeSize, debugLabel: `${resourceToLabel(system)} system`, })), @@ -82,7 +83,7 @@ export async function ensureSystems({ abi: worldAbi, // TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645) functionName: "registerSystem", - args: [system.systemId, system.prepareDeploy(deployerAddress, libraries).address, system.allowAll], + args: [system.systemId, system.prepareDeploy(deployerAddress, libraryMap).address, system.allowAll], }), { retries: 3, @@ -106,7 +107,7 @@ export async function ensureSystems({ resourceId: system.systemId, address: worldSystems.find((s) => s.systemId === systemId)?.address ?? - systems.find((s) => s.systemId === systemId)?.prepareDeploy(deployerAddress, libraries).address, + systems.find((s) => s.systemId === systemId)?.prepareDeploy(deployerAddress, libraryMap).address, })) .filter((access): access is typeof access & { address: Address } => access.address != null), ), diff --git a/packages/cli/src/deploy/getLibraryMap.ts b/packages/cli/src/deploy/getLibraryMap.ts new file mode 100644 index 0000000000..49153b7671 --- /dev/null +++ b/packages/cli/src/deploy/getLibraryMap.ts @@ -0,0 +1,35 @@ +import { Address } from "viem"; +import { Library } from "./common"; + +export type LibraryMap = { + getAddress: (opts: { path: string; name: string; deployer: Address }) => Address; +}; + +function getLibraryKey({ path, name }: { path: string; name: string }): string { + return `${path}:${name}`; +} + +type LibraryCache = { + [key: string]: Library & { + address?: { + [deployer: Address]: Address; + }; + }; +}; + +export function getLibraryMap(libraries: readonly Library[]): LibraryMap { + const cache: LibraryCache = Object.fromEntries(libraries.map((library) => [getLibraryKey(library), library])); + const libraryMap = { + getAddress: ({ path, name, deployer }) => { + const library = cache[getLibraryKey({ path, name })]; + if (!library) { + throw new Error(`Could not find library for bytecode placeholder ${path}:${name}`); + } + library.address ??= {}; + // Store the prepared address in the library cache to avoid preparing the same library twice + library.address[deployer] ??= library.prepareDeploy(deployer, libraryMap).address; + return library.address[deployer]; + }, + } satisfies LibraryMap; + return libraryMap; +} diff --git a/packages/cli/src/verify.ts b/packages/cli/src/verify.ts index b5acaad720..c6e20663a0 100644 --- a/packages/cli/src/verify.ts +++ b/packages/cli/src/verify.ts @@ -105,7 +105,7 @@ export async function verify({ ); modules.map(({ name, prepareDeploy }) => { - const { address } = prepareDeploy(deployerAddress, []); + const { address } = prepareDeploy(deployerAddress); return verifyQueue.add(() => verifyContract({ // TODO: figure out dir from artifactPath via import.meta.resolve?