Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(cli): improve performance of linked library resolution during deployment #3197

Merged
merged 8 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading