slotdiff 0.0.0-stage → 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +10 -0
  3. package/README.md +213 -2
  4. package/dist/bytecode.d.ts +46 -0
  5. package/dist/bytecode.d.ts.map +1 -0
  6. package/dist/bytecode.js +101 -0
  7. package/dist/bytecode.js.map +1 -0
  8. package/dist/chain.d.ts +20 -0
  9. package/dist/chain.d.ts.map +1 -0
  10. package/dist/chain.js +52 -0
  11. package/dist/chain.js.map +1 -0
  12. package/dist/diff.d.ts +15 -0
  13. package/dist/diff.d.ts.map +1 -0
  14. package/dist/diff.js +11 -0
  15. package/dist/diff.js.map +1 -0
  16. package/dist/errors.d.ts +18 -0
  17. package/dist/errors.d.ts.map +1 -0
  18. package/dist/errors.js +24 -0
  19. package/dist/errors.js.map +1 -0
  20. package/dist/explorer.d.ts +42 -0
  21. package/dist/explorer.d.ts.map +1 -0
  22. package/dist/explorer.js +131 -0
  23. package/dist/explorer.js.map +1 -0
  24. package/dist/index.d.ts +11 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +10 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/reconstruct.d.ts +27 -0
  29. package/dist/reconstruct.d.ts.map +1 -0
  30. package/dist/reconstruct.js +140 -0
  31. package/dist/reconstruct.js.map +1 -0
  32. package/dist/record.d.ts +48 -0
  33. package/dist/record.d.ts.map +1 -0
  34. package/dist/record.js +91 -0
  35. package/dist/record.js.map +1 -0
  36. package/dist/resolve.d.ts +54 -0
  37. package/dist/resolve.d.ts.map +1 -0
  38. package/dist/resolve.js +80 -0
  39. package/dist/resolve.js.map +1 -0
  40. package/dist/solc.d.ts +38 -0
  41. package/dist/solc.d.ts.map +1 -0
  42. package/dist/solc.js +194 -0
  43. package/dist/solc.js.map +1 -0
  44. package/package.json +70 -4
  45. package/src/bytecode.ts +141 -0
  46. package/src/chain.ts +75 -0
  47. package/src/diff.ts +31 -0
  48. package/src/errors.ts +24 -0
  49. package/src/explorer.ts +190 -0
  50. package/src/index.ts +36 -0
  51. package/src/reconstruct.ts +228 -0
  52. package/src/record.ts +127 -0
  53. package/src/resolve.ts +138 -0
  54. package/src/solc.ts +268 -0
package/src/resolve.ts ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Proven layouts of deployed code, as `LayoutRecord`s. Nothing here writes to
3
+ * disk; see `record.ts` for saving and loading records.
4
+ *
5
+ * Under lazy or queued (multisig) upgrades, a project's files describe its
6
+ * latest build while proxies keep running older implementations. Reading the
7
+ * implementation from the chain and proving its source against the deployed
8
+ * code gives the layout of what actually runs.
9
+ */
10
+
11
+ import type { StorageLayout } from "@openzeppelin/upgrades-core";
12
+ import {
13
+ compareDeployedBytecode,
14
+ isProvingMatch,
15
+ type DeployedBytecodeMatch,
16
+ type ImmutableReferences,
17
+ } from "./bytecode.js";
18
+ import { codeSha256, readChainId, readCode, readProxy, type EthProvider } from "./chain.js";
19
+ import { LayoutUnavailableError } from "./errors.js";
20
+ import { canQueryExplorer, fetchVerifiedSource, type ExplorerConfig } from "./explorer.js";
21
+ import { reconstructLayout } from "./reconstruct.js";
22
+ import { parseRecord, type LayoutRecord } from "./record.js";
23
+ import { getSolc, type SolcOptions } from "./solc.js";
24
+
25
+ export interface ResolveOptions {
26
+ explorer: ExplorerConfig;
27
+ solc?: SolcOptions;
28
+ }
29
+
30
+ /**
31
+ * Rebuilds the layout of the code at `address` from its verified source and
32
+ * proves that source is the deployed code. Always fetches and compiles; check
33
+ * your own saved record first (`verifyRecord`) to skip that.
34
+ */
35
+ export async function resolveLayout(
36
+ provider: EthProvider,
37
+ address: string,
38
+ options: ResolveOptions,
39
+ ): Promise<LayoutRecord> {
40
+ const lower = address.toLowerCase();
41
+ if (!canQueryExplorer(options.explorer)) {
42
+ throw new LayoutUnavailableError(
43
+ `Cannot rebuild the layout of ${lower}: the explorer needs an apiKey or an apiUrl.`,
44
+ );
45
+ }
46
+ const code = await readCode(provider, lower);
47
+ const chainId = await readChainId(provider);
48
+ const source = await fetchVerifiedSource(chainId, lower, options.explorer);
49
+ const solc = await getSolc(source.solcLongVersion, options.solc);
50
+ const rebuilt = await reconstructLayout(source, solc, code);
51
+
52
+ return {
53
+ format: 1,
54
+ address: lower,
55
+ chainId,
56
+ codeSha256: codeSha256(code),
57
+ contract: rebuilt.contract,
58
+ compiler: rebuilt.compiler,
59
+ bytecodeMatch: rebuilt.bytecodeMatch,
60
+ source: "explorer",
61
+ recordedAt: new Date().toISOString(),
62
+ layout: rebuilt.layout,
63
+ };
64
+ }
65
+
66
+ export interface ProxyLayout {
67
+ /** The implementation the proxy runs now. */
68
+ implementation: string;
69
+ /** Set for a beacon proxy. */
70
+ beacon?: string;
71
+ record: LayoutRecord;
72
+ }
73
+
74
+ /** `resolveLayout` for whatever implementation `proxy` runs right now (ERC-1967 or beacon). */
75
+ export async function resolveProxyLayout(
76
+ provider: EthProvider,
77
+ proxy: string,
78
+ options: ResolveOptions,
79
+ ): Promise<ProxyLayout> {
80
+ const state = await readProxy(provider, proxy);
81
+ if (state === undefined) {
82
+ throw new LayoutUnavailableError(
83
+ `${proxy} has no ERC-1967 implementation or beacon slot set on this chain.`,
84
+ );
85
+ }
86
+ const record = await resolveLayout(provider, state.implementation, options);
87
+ return { ...state, record };
88
+ }
89
+
90
+ export interface LocalBuild {
91
+ /** Fully qualified name, `source.sol:Contract`. */
92
+ contract: string;
93
+ compiler?: string;
94
+ layout: StorageLayout;
95
+ deployedBytecode: string;
96
+ immutableReferences?: ImmutableReferences;
97
+ }
98
+
99
+ export interface LocalBuildResult {
100
+ bytecodeMatch: DeployedBytecodeMatch;
101
+ /** Set only when the match proves the layout (`exact` or `immutables-only`). */
102
+ record?: LayoutRecord;
103
+ }
104
+
105
+ /**
106
+ * Makes a record from a local build of the code at `address`, but only after
107
+ * proving the chain runs that build. A `metadata-only` or `none` match gives
108
+ * no record, so a layout is never attributed to code that is not there.
109
+ */
110
+ export async function proveLocalBuild(
111
+ provider: EthProvider,
112
+ address: string,
113
+ build: LocalBuild,
114
+ ): Promise<LocalBuildResult> {
115
+ const lower = address.toLowerCase();
116
+ const code = await readCode(provider, lower);
117
+ const bytecodeMatch = compareDeployedBytecode(
118
+ code,
119
+ build.deployedBytecode,
120
+ build.immutableReferences,
121
+ );
122
+ if (!isProvingMatch(bytecodeMatch)) return { bytecodeMatch };
123
+
124
+ const record: LayoutRecord = {
125
+ format: 1,
126
+ address: lower,
127
+ chainId: await readChainId(provider),
128
+ codeSha256: codeSha256(code),
129
+ contract: build.contract,
130
+ ...(build.compiler !== undefined ? { compiler: build.compiler } : {}),
131
+ bytecodeMatch,
132
+ source: "local-compile",
133
+ recordedAt: new Date().toISOString(),
134
+ layout: build.layout,
135
+ };
136
+ // A record this returns must load back through parseRecord, so check it now.
137
+ return { bytecodeMatch, record: parseRecord(JSON.parse(JSON.stringify(record)), lower) };
138
+ }
package/src/solc.ts ADDED
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Native solc acquisition and invocation, independent of Hardhat's compiler
3
+ * manager so the core stays usable outside a Hardhat runtime.
4
+ *
5
+ * Lookup order: explicit override, our cache, Hardhat's compiler caches (v3
6
+ * then v2, so a project that already compiled with that version pays nothing),
7
+ * then a download from binaries.soliditylang.org verified against the
8
+ * published sha256.
9
+ *
10
+ * Takes a long version (`0.8.24+commit.e11b9ed9`) or a release version
11
+ * (`0.8.24`, which is what Foundry's build-info records); a release version
12
+ * resolves to that release's build, never a nightly.
13
+ */
14
+
15
+ import { spawn } from "node:child_process";
16
+ import { createHash, randomUUID } from "node:crypto";
17
+ import { access, chmod, mkdir, readdir, rename, writeFile } from "node:fs/promises";
18
+ import { constants } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { join } from "node:path";
21
+ import type { SolcInput, SolcOutput } from "@openzeppelin/upgrades-core";
22
+ import { LayoutIntegrityError, LayoutUnavailableError } from "./errors.js";
23
+
24
+ const BINARIES_URL = "https://binaries.soliditylang.org";
25
+
26
+ const RELEASE_VERSION = /^\d+\.\d+\.\d+$/;
27
+ // Validated before use: the version becomes part of a path that gets executed.
28
+ const SOLC_VERSION =
29
+ /^\d+\.\d+\.\d+(?:-nightly\.\d+\.\d+\.\d+|-pre\.\d+)?(?:\+commit\.[0-9a-f]+)?$/;
30
+ const RELEASE_LONG_VERSION = /^\d+\.\d+\.\d+\+commit\.[0-9a-f]+$/;
31
+
32
+ export interface SolcRunner {
33
+ /**
34
+ * Resolved, e.g. `0.8.24+commit.e11b9ed9` even when asked for `0.8.24`. With
35
+ * `resolvePath`, the version as passed.
36
+ */
37
+ longVersion: string;
38
+ path: string;
39
+ compile(input: SolcInput): Promise<SolcOutput>;
40
+ }
41
+
42
+ export interface SolcOptions {
43
+ /** Where downloaded compilers are kept. Defaults to `<os cache>/slotdiff/compilers`. */
44
+ cacheDir?: string;
45
+ /** Explicit binary for a version (as passed to `getSolc`), e.g. on platforms with no official native build. */
46
+ resolvePath?: (version: string) => string | undefined;
47
+ }
48
+
49
+ type FetchBinary = (url: string) => Promise<{
50
+ ok: boolean;
51
+ status: number;
52
+ json(): Promise<unknown>;
53
+ arrayBuffer(): Promise<ArrayBuffer>;
54
+ }>;
55
+
56
+ export async function getSolc(
57
+ version: string,
58
+ options: SolcOptions = {},
59
+ fetchImpl: FetchBinary = fetch,
60
+ ): Promise<SolcRunner> {
61
+ const { path, longVersion } = await locateSolc(version, options, fetchImpl);
62
+ return { longVersion, path, compile: (input) => runSolc(path, input) };
63
+ }
64
+
65
+ const downloads = new Map<string, Promise<void>>();
66
+
67
+ interface LocatedSolc {
68
+ path: string;
69
+ longVersion: string;
70
+ }
71
+
72
+ interface SolcBuild {
73
+ path: string;
74
+ longVersion: string;
75
+ sha256: string;
76
+ }
77
+
78
+ async function locateSolc(
79
+ version: string,
80
+ options: SolcOptions,
81
+ fetchImpl: FetchBinary,
82
+ ): Promise<LocatedSolc> {
83
+ if (!SOLC_VERSION.test(version)) {
84
+ throw new LayoutUnavailableError(`"${version}" is not a solc version.`);
85
+ }
86
+ const explicit = options.resolvePath?.(version);
87
+ if (explicit !== undefined) return { path: explicit, longVersion: version };
88
+
89
+ const platform = nativePlatform();
90
+ const cacheDir = options.cacheDir ?? join(osCacheDir(), "slotdiff", "compilers");
91
+ // Hardhat's cache follows env-paths, which adds a `Cache` folder on Windows.
92
+ const hardhatCache =
93
+ process.platform === "win32"
94
+ ? join(osCacheDir(), "hardhat-nodejs", "Cache")
95
+ : join(osCacheDir(), "hardhat-nodejs");
96
+ const dirs = [
97
+ join(cacheDir, platform),
98
+ join(hardhatCache, "compilers-v3", platform),
99
+ join(hardhatCache, "compilers-v2", platform),
100
+ ];
101
+ for (const dir of dirs) {
102
+ const cached = await findCached(dir, platform, version);
103
+ if (cached !== undefined) return cached;
104
+ }
105
+
106
+ const build = await findBuild(platform, version, fetchImpl);
107
+ const dest = join(cacheDir, platform, binaryName(platform, build.longVersion));
108
+ // Concurrent callers (one per proxy, say) share one download per binary.
109
+ let pending = downloads.get(dest);
110
+ if (pending === undefined) {
111
+ pending = download(platform, build, dest, fetchImpl).finally(() => downloads.delete(dest));
112
+ downloads.set(dest, pending);
113
+ }
114
+ await pending;
115
+ return { path: dest, longVersion: build.longVersion };
116
+ }
117
+
118
+ function binaryName(platform: string, longVersion: string): string {
119
+ return `solc-${platform}-v${longVersion}${platform === "windows-amd64" ? ".exe" : ""}`;
120
+ }
121
+
122
+ async function findCached(
123
+ dir: string,
124
+ platform: string,
125
+ version: string,
126
+ ): Promise<LocatedSolc | undefined> {
127
+ if (!RELEASE_VERSION.test(version)) {
128
+ const path = join(dir, binaryName(platform, version));
129
+ return (await isExecutable(path)) ? { path, longVersion: version } : undefined;
130
+ }
131
+ let names: string[];
132
+ try {
133
+ names = await readdir(dir);
134
+ } catch {
135
+ return undefined;
136
+ }
137
+ // Only finished release binaries of this version: no nightlies (`0.8.24-nightly...`),
138
+ // no longer versions (`0.8.240`), no in-progress `.tmp` downloads.
139
+ const prefix = `solc-${platform}-v`;
140
+ const ext = platform === "windows-amd64" ? ".exe" : "";
141
+ for (const name of names.sort()) {
142
+ if (!name.startsWith(prefix) || !name.endsWith(ext)) continue;
143
+ const longVersion = name.slice(prefix.length, name.length - ext.length);
144
+ if (!RELEASE_LONG_VERSION.test(longVersion) || !longVersion.startsWith(`${version}+`)) continue;
145
+ const path = join(dir, name);
146
+ if (await isExecutable(path)) return { path, longVersion };
147
+ }
148
+ return undefined;
149
+ }
150
+
151
+ async function findBuild(
152
+ platform: string,
153
+ version: string,
154
+ fetchImpl: FetchBinary,
155
+ ): Promise<SolcBuild> {
156
+ // Failing to obtain a compiler means the layout cannot be rebuilt, not
157
+ // that it is wrong; only a checksum mismatch is an integrity problem.
158
+ const listRes = await fetchOrUnavailable(fetchImpl, `${BINARIES_URL}/${platform}/list.json`);
159
+ if (!listRes.ok) {
160
+ throw new LayoutUnavailableError(
161
+ `Could not fetch the solc build list (HTTP ${listRes.status}).`,
162
+ );
163
+ }
164
+ const list = (await listRes.json()) as { builds: SolcBuild[] };
165
+ const build = RELEASE_VERSION.test(version)
166
+ ? // A nightly reads `0.8.24-nightly...`, so only the release starts with `0.8.24+`.
167
+ list.builds.find((b) => b.longVersion.startsWith(`${version}+`))
168
+ : list.builds.find((b) => b.longVersion === version);
169
+ if (build === undefined) {
170
+ throw new LayoutUnavailableError(`solc ${version} has no native ${platform} build.`);
171
+ }
172
+ // Old Windows releases (up to 0.7.1) ship as zip archives, not executables.
173
+ if (build.path.endsWith(".zip")) {
174
+ throw new LayoutUnavailableError(
175
+ `solc ${build.longVersion} for ${platform} ships as a zip archive; pass a compiler via solc.resolvePath.`,
176
+ );
177
+ }
178
+ return build;
179
+ }
180
+
181
+ async function download(
182
+ platform: string,
183
+ build: SolcBuild,
184
+ dest: string,
185
+ fetchImpl: FetchBinary,
186
+ ): Promise<void> {
187
+ const binRes = await fetchOrUnavailable(fetchImpl, `${BINARIES_URL}/${platform}/${build.path}`);
188
+ if (!binRes.ok) {
189
+ throw new LayoutUnavailableError(
190
+ `Could not download solc ${build.longVersion} (HTTP ${binRes.status}).`,
191
+ );
192
+ }
193
+ const bytes = Buffer.from(await binRes.arrayBuffer());
194
+
195
+ const actual = createHash("sha256").update(bytes).digest("hex");
196
+ if (`0x${actual}` !== build.sha256.toLowerCase()) {
197
+ throw new LayoutIntegrityError(
198
+ `solc ${build.longVersion} download failed its sha256 check; refusing to run it.`,
199
+ );
200
+ }
201
+
202
+ await mkdir(join(dest, ".."), { recursive: true });
203
+ const tmp = `${dest}.${randomUUID()}.tmp`;
204
+ await writeFile(tmp, bytes);
205
+ await chmod(tmp, 0o755);
206
+ await rename(tmp, dest);
207
+ }
208
+
209
+ function runSolc(path: string, input: SolcInput): Promise<SolcOutput> {
210
+ return new Promise((resolve, reject) => {
211
+ const child = spawn(path, ["--standard-json"], { stdio: ["pipe", "pipe", "pipe"] });
212
+ const out: Buffer[] = [];
213
+ const err: Buffer[] = [];
214
+ child.stdout.on("data", (d: Buffer) => out.push(d));
215
+ child.stderr.on("data", (d: Buffer) => err.push(d));
216
+ child.on("error", reject);
217
+ // A compiler that exits before reading all input closes the pipe; reject
218
+ // instead of crashing the process with an unhandled EPIPE.
219
+ child.stdin.on("error", reject);
220
+ child.on("close", (code) => {
221
+ if (code !== 0) {
222
+ reject(new Error(`solc exited with code ${code}: ${Buffer.concat(err).toString()}`));
223
+ return;
224
+ }
225
+ try {
226
+ resolve(JSON.parse(Buffer.concat(out).toString()) as SolcOutput);
227
+ } catch (e) {
228
+ reject(e);
229
+ }
230
+ });
231
+ child.stdin.end(JSON.stringify(input));
232
+ });
233
+ }
234
+
235
+ function nativePlatform(): string {
236
+ const { platform, arch } = process;
237
+ if (platform === "darwin") return "macosx-amd64"; // universal binaries for recent versions
238
+ if (platform === "linux" && arch === "x64") return "linux-amd64";
239
+ if (platform === "win32" && arch === "x64") return "windows-amd64";
240
+ throw new LayoutUnavailableError(
241
+ `No official native solc build for ${platform}/${arch}; pass a compiler path via solc.resolvePath.`,
242
+ );
243
+ }
244
+
245
+ async function fetchOrUnavailable(fetchImpl: FetchBinary, url: string) {
246
+ try {
247
+ return await fetchImpl(url);
248
+ } catch (e) {
249
+ throw new LayoutUnavailableError(`Could not reach ${BINARIES_URL}: ${(e as Error).message}`);
250
+ }
251
+ }
252
+
253
+ function osCacheDir(): string {
254
+ if (process.platform === "darwin") return join(homedir(), "Library", "Caches");
255
+ if (process.platform === "win32") {
256
+ return process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local");
257
+ }
258
+ return process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
259
+ }
260
+
261
+ async function isExecutable(path: string): Promise<boolean> {
262
+ try {
263
+ await access(path, constants.X_OK);
264
+ return true;
265
+ } catch {
266
+ return false;
267
+ }
268
+ }