Skip to content

Commit

Permalink
fix(cli): improve performance of linked library resolution during dep…
Browse files Browse the repository at this point in the history
…loyment (#3197)
  • Loading branch information
alvrs committed Sep 19, 2024
1 parent 20fac30 commit 22c37c3
Show file tree
Hide file tree
Showing 9 changed files with 86 additions and 37 deletions.
6 changes: 6 additions & 0 deletions .changeset/few-olives-judge.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion packages/cli/src/deploy/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand Down Expand Up @@ -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;
Expand Down
26 changes: 14 additions & 12 deletions packages/cli/src/deploy/createPrepareDeploy.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
14 changes: 8 additions & 6 deletions packages/cli/src/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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`,
})),
Expand Down Expand Up @@ -126,7 +128,7 @@ export async function deploy({
const systemTxs = await ensureSystems({
client,
deployerAddress,
libraries,
libraryMap,
worldDeploy,
systems,
});
Expand All @@ -146,7 +148,7 @@ export async function deploy({
const moduleTxs = await ensureModules({
client,
deployerAddress,
libraries,
libraryMap,
worldDeploy,
modules,
});
Expand Down Expand Up @@ -178,7 +180,7 @@ export async function deploy({
const tagTxs = await ensureResourceTags({
client,
deployerAddress,
libraries,
libraryMap,
worldDeploy,
tags: [...namespaceTags, ...tableTags, ...systemTags],
valueToHex: stringToHex,
Expand Down
11 changes: 6 additions & 5 deletions packages/cli/src/deploy/ensureModules.ts
Original file line number Diff line number Diff line change
@@ -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<Transport, Chain | undefined, Account>;
readonly deployerAddress: Hex;
readonly libraries: readonly Library[];
readonly libraryMap: LibraryMap;
readonly worldDeploy: WorldDeploy;
readonly modules: readonly Module[];
}): Promise<readonly Hex[]> {
Expand All @@ -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`,
})),
Expand All @@ -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)
Expand Down
9 changes: 5 additions & 4 deletions packages/cli/src/deploy/ensureResourceTags.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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);

Expand All @@ -23,14 +24,14 @@ export type ResourceTag<value> = {
export async function ensureResourceTags<const value>({
client,
deployerAddress,
libraries,
libraryMap,
worldDeploy,
tags,
valueToHex = identity,
}: {
readonly client: Client<Transport, Chain | undefined, Account>;
readonly deployerAddress: Hex;
readonly libraries: readonly Library[];
readonly libraryMap: LibraryMap;
readonly worldDeploy: WorldDeploy;
readonly tags: readonly ResourceTag<value>[];
} & (value extends Hex
Expand Down Expand Up @@ -59,7 +60,7 @@ export async function ensureResourceTags<const value>({
client,
deployerAddress,
worldDeploy,
libraries,
libraryMap,
modules: [
{
optional: true,
Expand Down
17 changes: 9 additions & 8 deletions packages/cli/src/deploy/ensureSystems.ts
Original file line number Diff line number Diff line change
@@ -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<Transport, Chain | undefined, Account>;
readonly deployerAddress: Hex;
readonly libraries: readonly Library[];
readonly libraryMap: LibraryMap;
readonly worldDeploy: WorldDeploy;
readonly systems: readonly System[];
}): Promise<readonly Hex[]> {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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`,
})),
Expand All @@ -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,
Expand All @@ -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),
),
Expand Down
35 changes: 35 additions & 0 deletions packages/cli/src/deploy/getLibraryMap.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion packages/cli/src/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down

0 comments on commit 22c37c3

Please sign in to comment.