From eab8f39f8814b70e432f3a4a84281360a0e27197 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 27 Jun 2024 17:59:57 -0400 Subject: [PATCH 01/21] Add Data.prototype.concat(...others: Data[]) --- packages/pointers/src/data.ts | 9 +++++++++ .../src/dereference/index.integration.test.ts | 11 +++-------- packages/pointers/src/evaluate.ts | 12 +++--------- packages/pointers/test/run.ts | 11 +++-------- 4 files changed, 18 insertions(+), 25 deletions(-) diff --git a/packages/pointers/src/data.ts b/packages/pointers/src/data.ts index 186a7c4b..ad7c7e78 100644 --- a/packages/pointers/src/data.ts +++ b/packages/pointers/src/data.ts @@ -84,4 +84,13 @@ export class Data extends Uint8Array { return Data.fromBytes(resized); } + + concat(...others: Data[]): Data { + // HACK concatenate via string representation + const concatenatedHex = [this, ...others] + .map(data => data.toHex().slice(2)) + .reduce((accumulator, hex) => `${accumulator}${hex}`, "0x"); + + return Data.fromHex(concatenatedHex); + } } diff --git a/packages/pointers/src/dereference/index.integration.test.ts b/packages/pointers/src/dereference/index.integration.test.ts index 49bd8e2d..4e4e2ff2 100644 --- a/packages/pointers/src/dereference/index.integration.test.ts +++ b/packages/pointers/src/dereference/index.integration.test.ts @@ -49,14 +49,9 @@ describe("dereference (integration)", () => { let lastObservedStringValue; for await (const state of machine.trace()) { const { regions, read } = await cursor.view(state); - const stringData = Data.fromHex( - await regions.named("string") - .map(read) - // HACK concatenate via string representation - .map(async data => (await data).toHex().slice(2)) - .reduce(async (accumulator, data) => { - return `${await accumulator}${await data}`; - }, Promise.resolve("0x")) + const strings = await regions.named("string"); + const stringData: Data = Data.zero().concat( + ...await Promise.all(strings.map(read)) ); const storedString = new TextDecoder().decode(stringData); diff --git a/packages/pointers/src/evaluate.ts b/packages/pointers/src/evaluate.ts index b3db2205..88db8bea 100644 --- a/packages/pointers/src/evaluate.ts +++ b/packages/pointers/src/evaluate.ts @@ -209,16 +209,10 @@ async function evaluateKeccak256( async expression => await evaluate(expression, options) )); - // HACK concatenate via string representation - const concatenatedData = operands.reduce( - (data, operand) => `${data}${operand.toHex().slice(2)}`, - "" - ); + const preimage = Data.zero().concat(...operands); + const hash = Data.fromBytes(keccak256(preimage)); - const buffer = Buffer.from(concatenatedData, "hex"); - const hash = keccak256(buffer); - - return Data.fromBytes(hash); + return hash; } async function evaluateResize( diff --git a/packages/pointers/test/run.ts b/packages/pointers/test/run.ts index 1ea1981f..7a2b872e 100644 --- a/packages/pointers/test/run.ts +++ b/packages/pointers/test/run.ts @@ -80,14 +80,9 @@ export async function run() { let currentStoredString; for await (const state of trace) { const { regions, read } = await cursor.view(state); - const stringData = Data.fromHex( - await regions.named("string") - .map(read) - // HACK concatenate via string representation - .map(async data => (await data).toHex().slice(2)) - .reduce(async (accumulator, data) => { - return `${await accumulator}${await data}`; - }, Promise.resolve("0x")) + const strings = await regions.named("string"); + const stringData: Data = Data.zero().concat( + ...await Promise.all(strings.map(read)) ); const storedString = new TextDecoder().decode(stringData); From eca36275a9c0c1f3abd6d1f889f883aacced999e Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 27 Jun 2024 19:33:02 -0400 Subject: [PATCH 02/21] Cleanup integration test - Define `observeTrace` function to do all the setup and watching - Structure integration test in mapping of test by name - Replace test/run.ts with bin/run-example.ts, also cleaned up - Restructure string storage schema example a bit --- packages/pointers/bin/run-example.ts | 90 ++++++++++++ packages/pointers/package.json | 1 + .../src/dereference/index.integration.test.ts | 131 ++++++++++-------- packages/pointers/test/examples.ts | 46 +++--- packages/pointers/test/index.ts | 3 +- packages/pointers/test/observe.ts | 65 +++++++++ packages/pointers/test/run.ts | 112 --------------- schemas/pointer.schema.yaml | 119 ++++++++-------- 8 files changed, 312 insertions(+), 255 deletions(-) create mode 100644 packages/pointers/bin/run-example.ts create mode 100644 packages/pointers/test/observe.ts delete mode 100644 packages/pointers/test/run.ts diff --git a/packages/pointers/bin/run-example.ts b/packages/pointers/bin/run-example.ts new file mode 100644 index 00000000..16d3ddeb --- /dev/null +++ b/packages/pointers/bin/run-example.ts @@ -0,0 +1,90 @@ +import chalk from "chalk"; +import { highlight } from "cli-highlight"; +import { describeSchema } from "@ethdebug/format"; + +import { + type Cursor, + Data +} from "../src/index.js"; + +import { + prepareCompileOptions, + findExamplePointer, + observeTrace, + type ObserveTraceOptions +} from "../test/index.js"; + +const pointer = findExamplePointer("string-storage-contract-variable-slot"); + +const compileOptions = prepareCompileOptions({ + path: "StringStorage.sol", + contractName: "StringStorage", + content: `contract StringStorage { + string storedString; + bool done; + + event Done(); + + constructor() { + storedString = "hello world"; + storedString = "solidity storage is a fun lesson in endianness"; + + done = true; + } + } + ` +}); + +const observe = async ({ regions, read }: Cursor.View): Promise => { + const strings = await regions.named("string"); + const stringData: Data = Data.zero().concat( + ...await Promise.all(strings.map(read)) + ); + + return new TextDecoder().decode(stringData); +}; + +export async function run() { + console.log( + chalk.bold(chalk.cyan( + "demo: run compiled solidity and watch a changing ethdebug/format pointer\n" + )) + ); + + console.group(chalk.bold("ethdebug/format pointer used by demo")); + console.log( + highlight( + describeSchema({ + schema: { id: "schema:ethdebug/format/pointer" }, + pointer: "#/examples/4" + }).yaml, + { language: "yaml" } + ).trim() + ); + console.groupEnd(); + console.log(""); + + console.group(chalk.bold("solidity source code used by demo")); + console.log( + compileOptions.sources["StringStorage.sol"].content.trim() + ); + console.groupEnd(); + console.log(""); + + console.group(chalk.bold("observing deployment trace")); + + const observedValues = + await observeTrace({ pointer, compileOptions, observe }); + + console.groupEnd(); + console.log(""); + + console.group(chalk.bold("observed values:")); + for (const value of observedValues) { + console.log("- %o", value); + } + console.groupEnd(); + console.log(""); +} + +await run(); diff --git a/packages/pointers/package.json b/packages/pointers/package.json index f6377da5..86bca002 100644 --- a/packages/pointers/package.json +++ b/packages/pointers/package.json @@ -6,6 +6,7 @@ "type": "module", "license": "MIT", "scripts": { + "run-example": "node ./dist/bin/run-example.js", "prepare": "tsc", "watch": "yarn prepare --watch", "test": "node --experimental-vm-modules $(yarn bin jest)" diff --git a/packages/pointers/src/dereference/index.integration.test.ts b/packages/pointers/src/dereference/index.integration.test.ts index 4e4e2ff2..9eff7815 100644 --- a/packages/pointers/src/dereference/index.integration.test.ts +++ b/packages/pointers/src/dereference/index.integration.test.ts @@ -1,71 +1,94 @@ import { jest, expect, describe, it, beforeEach } from "@jest/globals"; - -import { describeSchema } from "@ethdebug/format"; - import { - loadGanache, - machineForProvider, - compileCreateBytecode, - deployContract, - examples + prepareCompileOptions, + findExamplePointer, + observeTrace, + type ObserveTraceOptions } from "../../test/index.js"; +import { type Cursor, Data } from "../index.js"; -import { Machine, Data, type Pointer, dereference } from "../index.js"; - -const { schema: { examples: examplePointers } } = describeSchema({ - schema: { id: "schema:ethdebug/format/pointer" } -}) as { schema: { examples: Pointer[] } }; - -describe("dereference (integration)", () => { - describe("solidity string storage", () => { - it("allows dereferencing solidity string storage pointers", async () => { - const expectedStringValues = [ - "", - "hello world", - "solidity storage is a fun lesson in endianness" - ]; - const observedStringValues = []; - - const pointer: Pointer = examplePointers.find( - example => JSON.stringify(example).includes("long-string-length-data") - )!; +export interface ObserveTraceTest extends ObserveTraceOptions { + expectedValues: V[]; +} - // initialize local development blockchain - const provider = (await loadGanache()).provider({ - logging: { - quiet: true - } - }); +export type ObserveTraceTests = { + [K in keyof M]: ObserveTraceTest; +} - const bytecode = await compileCreateBytecode(examples.stringStorage); - const { - transactionHash, - contractAddress - } = await deployContract(bytecode, provider); +/** + * collection of descriptions of tests that compile+deploy Solidity code, + * then step through the machine trace of that code's execution, watching + * and recording a pointer's value over the course of that trace. + * + * tests are described in terms of an expected sequence of values which the + * list of observed values should contain by the end of the trace, allowing + * for additional unexpected values in between and around the expected values. + */ +export const observeTraceTests: ObserveTraceTests<{ + "storage string": string; +}> = { + "storage string": { + pointer: findExamplePointer("string-storage-contract-variable-slot"), - const machine = machineForProvider(provider, { transactionHash }); + compileOptions: prepareCompileOptions({ + path: "StringStorage.sol", + contractName: "StringStorage", + content: `contract StringStorage { + string storedString; + bool done; - let cursor = await dereference(pointer); - let lastObservedStringValue; - for await (const state of machine.trace()) { - const { regions, read } = await cursor.view(state); - const strings = await regions.named("string"); - const stringData: Data = Data.zero().concat( - ...await Promise.all(strings.map(read)) - ); + event Done(); - const storedString = new TextDecoder().decode(stringData); + constructor() { + storedString = "hello world"; + storedString = "solidity storage is a fun lesson in endianness"; - if (storedString !== lastObservedStringValue) { - observedStringValues.push(storedString); - lastObservedStringValue = storedString; + done = true; } } + ` + }), + + expectedValues: [ + "", + "hello world", + "solidity storage is a fun lesson in endianness" + ], - expect(observedStringValues).toEqual( - expect.arrayContaining(expectedStringValues) + async observe({ regions, read }: Cursor.View): Promise { + const strings = await regions.named("string"); + const stringData: Data = Data.zero().concat( + ...await Promise.all(strings.map(read)) ); - }); + return new TextDecoder().decode(stringData); + }, + } +}; + +describe("dereference (integration)", () => { + describe("changing pointer values over the course of a trace", () => { + for (const [name, test] of Object.entries(observeTraceTests)) { + const { + pointer, + compileOptions, + observe, + expectedValues + } = test; + + describe(`example pointer: ${name}`, () => { + it("resolves to values containing the expected sequence", async () => { + const observedValues = await observeTrace({ + pointer, + compileOptions, + observe + }); + + expect(observedValues).toEqual( + expect.arrayContaining(expectedValues) + ); + }); + }); + } }); }); diff --git a/packages/pointers/test/examples.ts b/packages/pointers/test/examples.ts index 7a5782f0..35e2f81f 100644 --- a/packages/pointers/test/examples.ts +++ b/packages/pointers/test/examples.ts @@ -1,39 +1,27 @@ -import { type CompileOptions } from "./solc.js"; +import { describeSchema } from "@ethdebug/format"; -export const examples = { - emptyContract: makeExample({ - path: "EmptyContract.sol", - contractName: "EmptyContract", - content: `contract EmptyContract { -} -`, - }), - - stringStorage: makeExample({ - path: "StringStorage.sol", - contractName: "StringStorage", - content: `contract StringStorage { - string storedString; - bool done; - - event Done(); +import type { Pointer } from "../src/pointer.js"; +import type { CompileOptions } from "./solc.js"; - constructor() { - storedString = "hello world"; - storedString = "solidity storage is a fun lesson in endianness"; +export const findExamplePointer = (() => { + const { + schema: { + examples: examplePointers + } + } = describeSchema({ + schema: { id: "schema:ethdebug/format/pointer" } + }) as { schema: { examples: Pointer[] } }; - done = true; - } -} -`, - }), -} as const; + return (text: string): Pointer => + examplePointers + .find(pointer => JSON.stringify(pointer).includes(text))!; +})(); -export function makeExample(example: { +export const prepareCompileOptions = (example: { path: string; contractName: string; content: string; -}): CompileOptions { +}): CompileOptions => { const { path, contractName, content: contentWithoutHeader } = example; const spdxLicenseIdentifier = "// SPDX-License-Identifier: UNLICENSED"; diff --git a/packages/pointers/test/index.ts b/packages/pointers/test/index.ts index 25427b5a..f5e8d712 100644 --- a/packages/pointers/test/index.ts +++ b/packages/pointers/test/index.ts @@ -1,4 +1,5 @@ export { loadGanache, machineForProvider } from "./ganache.js"; export { compileCreateBytecode, type CompileOptions } from "./solc.js"; export { deployContract, type DeployContractResult } from "./deploy.js"; -export { examples, makeExample } from "./examples.js"; +export { findExamplePointer, prepareCompileOptions } from "./examples.js"; +export { observeTrace, ObserveTraceOptions } from "./observe.js"; diff --git a/packages/pointers/test/observe.ts b/packages/pointers/test/observe.ts new file mode 100644 index 00000000..486d9f0d --- /dev/null +++ b/packages/pointers/test/observe.ts @@ -0,0 +1,65 @@ +import { type Pointer, type Cursor, dereference } from "../src/index.js"; + +import { loadGanache, machineForProvider } from "./ganache.js"; +import { compileCreateBytecode, type CompileOptions } from "./solc.js"; +import { deployContract } from "./deploy.js"; + +export interface ObserveTraceOptions { + pointer: Pointer; + compileOptions: CompileOptions; + observe({ regions, read }: Cursor.View): Promise; +} + +/** + * This function performs the steps necessary to setup and watch the code + * execution of the given contract's deployment. + * + * This function tracks the changes to the given pointer's dereferenced cursor + * by invoking the given `observe()` function to obtain a single primitive + * result of type `V`. + * + * Upon reaching the end of the trace for this code execution, this function + * then returns an ordered list of all the observed values, removing sequential + * duplicates (via `===`). + */ +export async function observeTrace({ + pointer, + compileOptions, + observe +}: ObserveTraceOptions): Promise { + const observedValues = []; + + // initialize local development blockchain + const provider = (await loadGanache()).provider({ + logging: { + quiet: true + } + }); + + // perform compilation + const bytecode = await compileCreateBytecode(compileOptions); + + // deploy contract + const { transactionHash } = await deployContract(bytecode, provider); + + // prepare to inspect the EVM for that deployment transaction + const machine = machineForProvider(provider, { transactionHash }); + + let cursor; // delay initialization until first state of trace + let lastObservedValue; + for await (const state of machine.trace()) { + if (!cursor) { + cursor = await dereference(pointer, { state }); + } + + const { regions, read } = await cursor.view(state); + const observedValue = await observe({ regions, read }); + + if (observedValue !== lastObservedValue) { + observedValues.push(observedValue); + lastObservedValue = observedValue; + } + } + + return observedValues; +} diff --git a/packages/pointers/test/run.ts b/packages/pointers/test/run.ts deleted file mode 100644 index 7a2b872e..00000000 --- a/packages/pointers/test/run.ts +++ /dev/null @@ -1,112 +0,0 @@ -import chalk from "chalk"; -import { highlight } from "cli-highlight"; -import { describeSchema } from "@ethdebug/format"; - -import { Data } from "../src/data.js"; -import type { Pointer } from "../src/pointer.js"; -import { dereference } from "../src/index.js"; - -import { loadGanache, machineForProvider } from "./ganache.js"; -import { deployContract } from "./deploy.js"; -import { compileCreateBytecode } from "./solc.js"; -import { examples } from "./examples.js"; - -const { - schema: pointerSchema -} = describeSchema({ - schema: { id: "schema:ethdebug/format/pointer" }, -}) as { schema: { examples: Pointer[] } }; - -const stringStoragePointer: Pointer = - pointerSchema.examples.find( - example => JSON.stringify(example).includes("long-string-length-data") - )!; - -export async function run() { - console.log( - chalk.bold(chalk.cyan( - "demo: run compiled solidity and watch a changing ethdebug/format pointer\n" - )) - ); - - console.group(chalk.bold("ethdebug/format pointer used by demo")); - console.log( - highlight( - describeSchema({ - schema: { id: "schema:ethdebug/format/pointer" }, - pointer: "#/examples/4" - }).yaml, - { language: "yaml" } - ).trim() - ); - console.groupEnd(); - console.log(""); - - console.group(chalk.bold("solidity source code used by demo")); - console.log( - examples.stringStorage.sources["StringStorage.sol"].content.trim() - ); - console.groupEnd(); - console.log(""); - - console.group(chalk.bold("preparing demo")); - - const provider = (await loadGanache()).provider({ - logging: { - quiet: true - } - }); - - const bytecode = await compileCreateBytecode(examples.stringStorage); - console.log("- compiled source code."); - - const { - transactionHash, - contractAddress - } = await deployContract(bytecode, provider); - console.log("- deployed contract."); - - const machine = machineForProvider(provider, { transactionHash }); - - const trace = machine.trace(); - console.log("- requested trace."); - - console.groupEnd(); - console.log(""); - - console.group(chalk.bold("watching trace for changing pointer values")); - - const cursor = await dereference(stringStoragePointer); - let currentStoredString; - for await (const state of trace) { - const { regions, read } = await cursor.view(state); - const strings = await regions.named("string"); - const stringData: Data = Data.zero().concat( - ...await Promise.all(strings.map(read)) - ); - - const storedString = new TextDecoder().decode(stringData); - - if (storedString !== currentStoredString) { - const pc = Number(await state.programCounter); - console.group(chalk.bold( - pc === 0 ? - "initial storedString" - : "storedString changed" - )); - console.log("pc: %o", pc); - console.log("new value: %o", storedString); - console.groupEnd(); - - currentStoredString = storedString; - } - } - - console.groupEnd(); - console.log(""); - - console.log(chalk.bold("thanks for reading!")); - -} - -await run(); diff --git a/schemas/pointer.schema.yaml b/schemas/pointer.schema.yaml index 789e23d0..38cc7243 100644 --- a/schemas/pointer.schema.yaml +++ b/schemas/pointer.schema.yaml @@ -143,24 +143,17 @@ examples: - # example `string storage` allocation define: - "contract-variable-slot": 0 + "string-storage-contract-variable-slot": 0 in: group: # for short strings, the length is stored as 2n in the last byte of slot - name: "length-flag" location: storage - slot: contract-variable-slot + slot: "string-storage-contract-variable-slot" offset: $difference: [$wordsize, 1] length: 1 - # long strings may use full word to describe length as 2n+1 - - name: "long-string-length-data" - location: storage - slot: contract-variable-slot - offset: 0 - length: $wordsize - # define the region representing the string data itself conditionally # based on odd or even length data - if: @@ -178,58 +171,66 @@ examples: in: name: "string" location: storage - slot: "contract-variable-slot" + slot: "string-storage-contract-variable-slot" offset: 0 length: "string-length" # long string case (flag is odd) else: - define: - "string-length": - $quotient: - - $difference: - - $read: "long-string-length-data" - - 1 - - 2 - - "start-slot": - $keccak256: - - $wordsized: "contract-variable-slot" - - "total-slots": - # account for both zero and nonzero slot remainders by adding - # $wordsize-1 to the length before dividing - $quotient: - - $sum: ["string-length", { $difference: [$wordsize, 1] }] - - $wordsize - in: - list: - count: "total-slots" - each: "i" - is: - define: - "current-slot": - $sum: ["start-slot", "i"] - "previous-length": - $product: ["i", $wordsize] - in: - # conditional based on whether this is the last slot: - # is the string length longer than the previous length - # plus this whole slot? - if: - $difference: - - "string-length" - - $sum: ["previous-length", "$wordsize"] - then: - # include the whole slot - name: "string" - location: storage - slot: "current-slot" - else: - # include only what's left in the string - name: "string" - location: storage - slot: "current-slot" - offset: 0 - length: - $difference: ["string-length", "previous-length"] + group: + # long strings may use full word to describe length as 2n+1 + - name: "long-string-length-data" + location: storage + slot: "string-storage-contract-variable-slot" + offset: 0 + length: $wordsize + + - define: + "string-length": + $quotient: + - $difference: + - $read: "long-string-length-data" + - 1 + - 2 + + "start-slot": + $keccak256: + - $wordsized: "string-storage-contract-variable-slot" + + "total-slots": + # account for both zero and nonzero slot remainders by adding + # $wordsize-1 to the length before dividing + $quotient: + - $sum: ["string-length", { $difference: [$wordsize, 1] }] + - $wordsize + in: + list: + count: "total-slots" + each: "i" + is: + define: + "current-slot": + $sum: ["start-slot", "i"] + "previous-length": + $product: ["i", $wordsize] + in: + # conditional based on whether this is the last slot: + # is the string length longer than the previous length + # plus this whole slot? + if: + $difference: + - "string-length" + - $sum: ["previous-length", "$wordsize"] + then: + # include the whole slot + name: "string" + location: storage + slot: "current-slot" + else: + # include only what's left in the string + name: "string" + location: storage + slot: "current-slot" + offset: 0 + length: + $difference: ["string-length", "previous-length"] From 47558f54dd629b7bc653b4f8e0b75021936ecc43 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Sun, 30 Jun 2024 16:44:56 -0400 Subject: [PATCH 03/21] Add test:debug script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 8a05f14d..8a4304b4 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ ], "scripts": { "test": "node --experimental-vm-modules ./node_modules/.bin/jest", + "test:debug": "open -a \"Brave Browser\" brave://inspect && node --experimental-vm-modules --nolazy --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --colors --verbose", "start": "./bin/start", "postinstall": "lerna run prepare" }, From 6d6bb8844ca216ea6a33d3c238e6a301914ec55f Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Sun, 30 Jun 2024 16:46:13 -0400 Subject: [PATCH 04/21] Add inspect functionality to Data --- packages/pointers/src/data.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/pointers/src/data.ts b/packages/pointers/src/data.ts index ad7c7e78..8ba0f992 100644 --- a/packages/pointers/src/data.ts +++ b/packages/pointers/src/data.ts @@ -1,5 +1,12 @@ import { toHex } from "ethereum-cryptography/utils"; +import type * as Util from "util"; + +let util: typeof Util | undefined; +try { + util = await import("util"); +} catch {} + export class Data extends Uint8Array { static zero(): Data { return new Data([]); @@ -93,4 +100,25 @@ export class Data extends Uint8Array { return Data.fromHex(concatenatedHex); } + + inspect( + depth: number, + options: Util.InspectOptionsStylized, + inspect: typeof Util.inspect + ): string { + return `Data[${options.stylize(this.toHex(), "number")}]`; + } + + [ + util + ? util.inspect.custom + : "_inspect" + ]( + depth: number, + options: Util.InspectOptionsStylized, + inspect: typeof Util.inspect + ): string { + return this.inspect(depth, options, inspect); + } + } From d141a4f3189e30e08541fc155ed45636ae6460f3 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Sun, 30 Jun 2024 16:46:32 -0400 Subject: [PATCH 05/21] Fix Data.prototype.fromHex --- packages/pointers/src/data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pointers/src/data.ts b/packages/pointers/src/data.ts index 8ba0f992..a0de0ba3 100644 --- a/packages/pointers/src/data.ts +++ b/packages/pointers/src/data.ts @@ -40,7 +40,7 @@ export class Data extends Uint8Array { if (!hex.startsWith('0x')) { throw new Error('Invalid hex string format. Expected "0x" prefix.'); } - const bytes = new Uint8Array(hex.length / 2 - 1); + const bytes = new Uint8Array((hex.length - 2) / 2 + 0.5); for (let i = 2; i < hex.length; i += 2) { bytes[i / 2 - 1] = parseInt(hex.slice(i, i + 2), 16); } From 6425ca83f1f02555f6ad91536915973cbae36627 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Sun, 30 Jun 2024 16:48:29 -0400 Subject: [PATCH 06/21] Fix stack adjustment expression --- packages/pointers/src/dereference/region.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pointers/src/dereference/region.ts b/packages/pointers/src/dereference/region.ts index 405521e7..3f8262cf 100644 --- a/packages/pointers/src/dereference/region.ts +++ b/packages/pointers/src/dereference/region.ts @@ -100,8 +100,8 @@ export function adjustStackLength( const slot: Pointer.Expression = stackLengthChange === 0n ? region.slot : stackLengthChange > 0n - ? { $sum: [region.slot, `"0x${stackLengthChange.toString(16)}"`] } - : { $difference: [region.slot, `"0x${-stackLengthChange.toString(16)}"`] }; + ? { $sum: [region.slot, `0x${stackLengthChange.toString(16)}`] } + : { $difference: [region.slot, `0x${-stackLengthChange.toString(16)}`] }; return { ...region, From 3957316a1d7904e7f7d1934a433a38b11f4c0308 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Sun, 30 Jun 2024 16:49:07 -0400 Subject: [PATCH 07/21] Fix Ganache integration --- packages/pointers/test/ganache.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pointers/test/ganache.ts b/packages/pointers/test/ganache.ts index 2babea9f..b97c5102 100644 --- a/packages/pointers/test/ganache.ts +++ b/packages/pointers/test/ganache.ts @@ -92,7 +92,7 @@ function toMachineState( const { index } = options; const constantUint = (value: number): Promise => - Promise.resolve(Data.fromNumber(index).asUint()); + Promise.resolve(Data.fromNumber(value).asUint()); const makeStack = ( stack: StructLog["stack"] @@ -125,7 +125,7 @@ function toMachineState( const makeBytes = ( words: StructLog["memory" /* | theoretically others */] ): Machine.State.Bytes => { - const data = Data.fromHex(`0x${words.map(word => word.slice(2)).join("")}`); + const data = Data.fromHex(`0x${words.join("")}`); return { length: constantUint(data.length), From 62125d2c0042b1a984e3a1f3a811b7e0ff004a12 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Sun, 30 Jun 2024 16:49:36 -0400 Subject: [PATCH 08/21] Update uint256[] memory pointer example --- schemas/pointer.schema.yaml | 77 +++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/schemas/pointer.schema.yaml b/schemas/pointer.schema.yaml index 38cc7243..9eb309c8 100644 --- a/schemas/pointer.schema.yaml +++ b/schemas/pointer.schema.yaml @@ -24,43 +24,46 @@ examples: slot: 2 - # example `uint256[] memory` allocation pointer - # this pointer composes an ordered list of other pointers - group: - # declare the first sub-pointer to be the "array-start" region of data - # corresponding to the first item in the stack (at time of observation) - - name: "array-start" - location: stack - slot: 0 - - # declare the "array-count" region to be at the offset indicated by - # the value at "array-start" - - name: "array-count" - location: memory - offset: - $read: "array-start" - length: $wordsize - - # thirdly, declare a sub-pointer that is a dynamic list whose size is - # indicated by the value at "array-count", where each "item-index" - # corresponds to a discrete "array-item" region - - list: - count: - $read: "array-count" - each: "item-index" - is: - name: "array-item" - location: "memory" - offset: - # array items are positioned so that the item with index 0 - # immediately follows "array-count", and each subsequent item - # immediately follows the previous. - $sum: - - .offset: "array-count" - - .length: "array-count" - - $product: - - "item-index" - - .length: "array-item" - length: $wordsize + define: + "uint256-array-memory-pointer-slot": 0 + in: + # this pointer composes an ordered list of other pointers + group: + # declare the first sub-pointer to be the "array-start" region of data + # corresponding to the first item in the stack (at time of observation) + - name: "array-start" + location: stack + slot: "uint256-array-memory-pointer-slot" + + # declare the "array-count" region to be at the offset indicated by + # the value at "array-start" + - name: "array-count" + location: memory + offset: + $read: "array-start" + length: $wordsize + + # thirdly, declare a sub-pointer that is a dynamic list whose size is + # indicated by the value at "array-count", where each "item-index" + # corresponds to a discrete "array-item" region + - list: + count: + $read: "array-count" + each: "item-index" + is: + name: "array-item" + location: "memory" + offset: + # array items are positioned so that the item with index 0 + # immediately follows "array-count", and each subsequent item + # immediately follows the previous. + $sum: + - .offset: "array-count" + - .length: "array-count" + - $product: + - "item-index" + - .length: $this + length: $wordsize - # example `struct Record { uint128 x; uint128 y }` in memory group: From 9dc6b813e67517e66b17affd8efa9e4d1de94f0a Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Sun, 30 Jun 2024 16:50:36 -0400 Subject: [PATCH 09/21] Include more info in error message --- packages/pointers/src/evaluate.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/pointers/src/evaluate.ts b/packages/pointers/src/evaluate.ts index 88db8bea..039d33d9 100644 --- a/packages/pointers/src/evaluate.ts +++ b/packages/pointers/src/evaluate.ts @@ -80,7 +80,11 @@ export async function evaluate( return evaluateRead(expression, options); } - throw new Error("Unexpected runtime failure to recognize kind of expression"); + throw new Error( + `Unexpected runtime failure to recognize kind of expression: ${ + JSON.stringify(expression) + }` + ); } async function evaluateLiteral( From 2200d0d1518701f7109f031b2e37bd0171863524 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Sun, 30 Jun 2024 16:52:44 -0400 Subject: [PATCH 10/21] Update observeTrace to be more flexible - Allow custom `equals` function to enable comparing nonprimitives - Allow specifying `shouldObserve` predicate to specify to tests when it is safe to observe the machine - Add `state` argument to `observe` function, just in case --- .../src/dereference/index.integration.test.ts | 19 ++++---------- packages/pointers/test/observe.ts | 25 +++++++++++++------ 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/pointers/src/dereference/index.integration.test.ts b/packages/pointers/src/dereference/index.integration.test.ts index 9eff7815..f0ef697a 100644 --- a/packages/pointers/src/dereference/index.integration.test.ts +++ b/packages/pointers/src/dereference/index.integration.test.ts @@ -69,24 +69,15 @@ export const observeTraceTests: ObserveTraceTests<{ describe("dereference (integration)", () => { describe("changing pointer values over the course of a trace", () => { for (const [name, test] of Object.entries(observeTraceTests)) { - const { - pointer, - compileOptions, - observe, - expectedValues - } = test; + const { expectedValues, ...options } = test; describe(`example pointer: ${name}`, () => { it("resolves to values containing the expected sequence", async () => { - const observedValues = await observeTrace({ - pointer, - compileOptions, - observe - }); + const observedValues = + await observeTrace(options as Parameters[0]); - expect(observedValues).toEqual( - expect.arrayContaining(expectedValues) - ); + expect(observedValues) + .toEqual(expect.arrayContaining(expectedValues)); }); }); } diff --git a/packages/pointers/test/observe.ts b/packages/pointers/test/observe.ts index 486d9f0d..f534cd5e 100644 --- a/packages/pointers/test/observe.ts +++ b/packages/pointers/test/observe.ts @@ -1,4 +1,4 @@ -import { type Pointer, type Cursor, dereference } from "../src/index.js"; +import { type Machine, type Pointer, type Cursor, dereference } from "../src/index.js"; import { loadGanache, machineForProvider } from "./ganache.js"; import { compileCreateBytecode, type CompileOptions } from "./solc.js"; @@ -7,7 +7,9 @@ import { deployContract } from "./deploy.js"; export interface ObserveTraceOptions { pointer: Pointer; compileOptions: CompileOptions; - observe({ regions, read }: Cursor.View): Promise; + observe({ regions, read }: Cursor.View, state: Machine.State): Promise; + equals?(a: V, b: V): boolean; + shouldObserve?(state: Machine.State): Promise; } /** @@ -20,14 +22,16 @@ export interface ObserveTraceOptions { * * Upon reaching the end of the trace for this code execution, this function * then returns an ordered list of all the observed values, removing sequential - * duplicates (via `===`). + * duplicates (using the defined `equals` function if it exists or just `===`). */ export async function observeTrace({ pointer, compileOptions, - observe + observe, + equals = (a, b) => a === b, + shouldObserve = () => Promise.resolve(true) }: ObserveTraceOptions): Promise { - const observedValues = []; + const observedValues: V[] = []; // initialize local development blockchain const provider = (await loadGanache()).provider({ @@ -48,14 +52,21 @@ export async function observeTrace({ let cursor; // delay initialization until first state of trace let lastObservedValue; for await (const state of machine.trace()) { + if (!await shouldObserve(state)) { + continue; + } + if (!cursor) { cursor = await dereference(pointer, { state }); } const { regions, read } = await cursor.view(state); - const observedValue = await observe({ regions, read }); + const observedValue = await observe({ regions, read }, state); - if (observedValue !== lastObservedValue) { + if ( + typeof lastObservedValue === "undefined" || + !equals(observedValue, lastObservedValue) + ) { observedValues.push(observedValue); lastObservedValue = observedValue; } From 2afe2541469cf07e94a767c159fc66fcd1c6cb14 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Sun, 30 Jun 2024 16:55:48 -0400 Subject: [PATCH 11/21] Add uint256[] memory test to integration tests --- .../src/dereference/index.integration.test.ts | 101 +++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/packages/pointers/src/dereference/index.integration.test.ts b/packages/pointers/src/dereference/index.integration.test.ts index f0ef697a..e01e1e67 100644 --- a/packages/pointers/src/dereference/index.integration.test.ts +++ b/packages/pointers/src/dereference/index.integration.test.ts @@ -25,9 +25,106 @@ export type ObserveTraceTests = { * for additional unexpected values in between and around the expected values. */ export const observeTraceTests: ObserveTraceTests<{ - "storage string": string; + "string storage": string; + "uint256[] memory": number[]; }> = { - "storage string": { + "uint256[] memory": { + pointer: findExamplePointer("uint256-array-memory-pointer-slot"), + compileOptions: prepareCompileOptions({ + path: "Uint256Arraymemory.sol", + contractName: "Uint256ArrayMemory", + content: `contract Uint256ArrayMemory { + constructor() { + uint256[] memory values = new uint256[](0); + values = appendToArray(values, 1); + values = appendToArray(values, 2); + values = appendToArray(values, 3); + } + + function appendToArray( + uint256[] memory arr, + uint256 value + ) + private + pure + returns (uint256[] memory) + { + uint256[] memory newArray = new uint256[](arr.length + 1); + + for (uint i = 0; i < arr.length; i++) { + newArray[i] = arr[i]; + } + + newArray[arr.length] = value; + return newArray; + } + } + ` + }), + + expectedValues: [ + [], + [1], + [1, 2], + [1, 2, 3] + ], + + async observe({ regions, read }, state): Promise { + const items = regions.named("array-item"); + + return (await Promise.all( + items.map(async (item) => { + const data = await read(item); + + return Number(data.asUint()); + }) + )); + }, + + equals(a, b) { + if (a.length !== b.length) { + return false; + } + + for (const [index, value] of a.entries()) { + if (b[index] !== value) { + return false; + } + } + + return true; + }, + + // this function uses observation of solc + viaIR behavior to determine + // that the memory array we're looking for is going to have a pointer at + // the bottom of the stack + // + // also include a check to exclude observation when that bottom stack value + // would have `cursor.view` produce a ton of regions. + async shouldObserve(state) { + const stackLength = await state.stack.length; + if (stackLength === 0n) { + return false; + } + + // only consider the bottom of the stack + const arrayOffset = await state.stack.peek({ depth: stackLength - 1n }); + + const arrayCount = await state.memory.read({ + slice: { + offset: arrayOffset.asUint(), + length: 32n + } + }) + + // return false; + const isObservable = arrayCount.asUint() < 4n; + if (isObservable) { + } + return isObservable; + } + }, + "string storage": { pointer: findExamplePointer("string-storage-contract-variable-slot"), compileOptions: prepareCompileOptions({ From 11f32d4ede8606168ac69c6bbc3c1a46f1fde834 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 1 Jul 2024 00:49:17 -0400 Subject: [PATCH 12/21] Fix data module import in browser --- packages/pointers/src/data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pointers/src/data.ts b/packages/pointers/src/data.ts index a0de0ba3..937a4de0 100644 --- a/packages/pointers/src/data.ts +++ b/packages/pointers/src/data.ts @@ -110,7 +110,7 @@ export class Data extends Uint8Array { } [ - util + util && "inspect" in util && typeof util.inspect === "object" ? util.inspect.custom : "_inspect" ]( From e80eef2befb92260733717b748f3b2479dd8f36c Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 1 Jul 2024 00:50:31 -0400 Subject: [PATCH 13/21] Cleanup test infrastructure code - Inline some unnecessary `interface` declarations - Add documentation to fields and code statements and such - Move some functions around - Remove some dead code logic - Make some functions less declared inside other functions - Don't let a solc import failure prevent loading a module --- packages/pointers/test/deploy.ts | 14 +-- packages/pointers/test/examples.ts | 29 ----- packages/pointers/test/ganache.ts | 187 ++++++++++++----------------- packages/pointers/test/index.ts | 24 +++- packages/pointers/test/observe.ts | 34 +++++- packages/pointers/test/solc.ts | 52 +++++++- 6 files changed, 182 insertions(+), 158 deletions(-) diff --git a/packages/pointers/test/deploy.ts b/packages/pointers/test/deploy.ts index 1c559b24..38c2472d 100644 --- a/packages/pointers/test/deploy.ts +++ b/packages/pointers/test/deploy.ts @@ -2,21 +2,20 @@ import type { EthereumProvider } from "ganache"; import { Data } from "../src/data.js"; -export interface DeployContractResult { - transactionHash: Data; - contractAddress: Data; -} - export async function deployContract( createBytecode: Data, provider: EthereumProvider -): Promise { - +): Promise<{ + transactionHash: Data; + contractAddress: Data; +}> { + // just use the first unlocked account const [account] = await provider.request({ method: "eth_accounts", params: [] }); + // issue a transaction that will be mined immediately const transactionHash = Data.fromHex(await provider.request({ method: "eth_sendTransaction", params: [{ @@ -26,6 +25,7 @@ export async function deployContract( }] })); + // read the receipt and extract the deployed contract address const contractAddress = Data.fromHex((await provider.request({ method: "eth_getTransactionReceipt", params: [transactionHash.toHex()] diff --git a/packages/pointers/test/examples.ts b/packages/pointers/test/examples.ts index 35e2f81f..1c4d7663 100644 --- a/packages/pointers/test/examples.ts +++ b/packages/pointers/test/examples.ts @@ -16,32 +16,3 @@ export const findExamplePointer = (() => { examplePointers .find(pointer => JSON.stringify(pointer).includes(text))!; })(); - -export const prepareCompileOptions = (example: { - path: string; - contractName: string; - content: string; -}): CompileOptions => { - const { path, contractName, content: contentWithoutHeader } = example; - - const spdxLicenseIdentifier = "// SPDX-License-Identifier: UNLICENSED"; - const pragma = "pragma solidity ^0.8.25;"; - const header = `${spdxLicenseIdentifier} -${pragma} -`; - - return { - sources: { - [path]: { - content: `${header} -${contentWithoutHeader} -` - } - }, - - target: { - path, - contractName - } - }; -} diff --git a/packages/pointers/test/ganache.ts b/packages/pointers/test/ganache.ts index b97c5102..5cf85251 100644 --- a/packages/pointers/test/ganache.ts +++ b/packages/pointers/test/ganache.ts @@ -23,13 +23,9 @@ export async function loadGanache() { return Ganache; } -export interface MachineForProviderOptions { - transactionHash: Data; -} - export function machineForProvider( provider: EthereumProvider, - { transactionHash }: MachineForProviderOptions + transactionHash: Data ): Machine { return { trace(): AsyncIterable { @@ -40,16 +36,8 @@ export function machineForProvider( provider ); - let previousOp; - for (const [index, step] of structLogs.entries()) { - const { state } = toMachineState( - step, - { index } - ); - - yield state; - - previousOp = step.op; + for (const [index, structLog] of structLogs.entries()) { + yield toMachineState(structLog, index); } } }; @@ -71,118 +59,93 @@ async function requestStructLogs( type StructLogs = Depromise>; type StructLog = Dearray; - -// helpers instead of digging through ganache's types type Depromise

= P extends Promise ? T : P; type Dearray = A extends Array ? T : A; -interface ToMachineStateOptions { - index: number; -} +function toMachineState(step: StructLog, index: number): Machine.State { + return { + traceIndex: constantUint(index), + programCounter: constantUint(step.pc), + opcode: Promise.resolve(step.op), -function toMachineState( - step: StructLog, - options: ToMachineStateOptions -): { - state: Machine.State; - storage: { - [slot: string]: Data - }; -} { - const { index } = options; - - const constantUint = (value: number): Promise => - Promise.resolve(Data.fromNumber(value).asUint()); - - const makeStack = ( - stack: StructLog["stack"] - ): Machine.State.Stack => { - const length = stack.length; - - return { - length: constantUint(length), - - async peek({ - depth, - slice: { - offset = 0n, - length = 32n - } = {} - }) { - const entry = stack.at(-Number(depth)); - const data = Data.fromHex(`0x${entry || ""}`); - - const sliced = new Uint8Array(data).slice( - Number(offset), - Number(offset + length) - ); - - return new Data(sliced); - } - }; - }; + stack: makeStack(step.stack), - const makeBytes = ( - words: StructLog["memory" /* | theoretically others */] - ): Machine.State.Bytes => { - const data = Data.fromHex(`0x${words.join("")}`); + memory: makeBytes(step.memory), - return { - length: constantUint(data.length), + storage: makeWords(step.storage), - async read({ slice: { offset, length } }) { - return new Data(data.slice( - Number(offset), - Number(offset + length) - )); - } - } - }; + calldata: undefined as unknown as Machine.State.Bytes, + returndata: undefined as unknown as Machine.State.Bytes, + code: undefined as unknown as Machine.State.Bytes, - const makeWords = ( - slots: StructLog["storage" /* | theoretically others */] - ): Machine.State.Words => { - return { - async read({ - slot, - slice: { - offset = 0n, - length = 32n - } = {} - }) { - const rawHex = slots[ - slot.resizeTo(32).toHex().slice(2) as keyof typeof slots - ]; - - const data = Data.fromHex(`0x${rawHex}`); - - return new Data(data.slice( - Number(offset), - Number(offset + length) - )); - } - }; + transient: undefined as unknown as Machine.State.Words, }; +} - return { - state: { - traceIndex: constantUint(index), - programCounter: constantUint(step.pc), - opcode: Promise.resolve(step.op), +function constantUint(value: number): Promise { + return Promise.resolve(Data.fromNumber(value).asUint()); +} - stack: makeStack(step.stack), +function makeStack(stack: StructLog["stack"]): Machine.State.Stack { + const length = stack.length; - memory: makeBytes(step.memory), + return { + length: constantUint(length), + + async peek({ + depth, + slice: { + offset = 0n, + length = 32n + } = {} + }) { + const entry = stack.at(-Number(depth)); + const data = Data.fromHex(`0x${entry || ""}`); + + const sliced = new Uint8Array(data).slice( + Number(offset), + Number(offset + length) + ); + + return new Data(sliced); + } + }; +} - storage: makeWords(step.storage), +function makeBytes(words: StructLog["memory"]): Machine.State.Bytes { + const data = Data.fromHex(`0x${words.join("")}`); - calldata: undefined as unknown as Machine.State.Bytes, - returndata: undefined as unknown as Machine.State.Bytes, - code: undefined as unknown as Machine.State.Bytes, + return { + length: constantUint(data.length), - transient: undefined as unknown as Machine.State.Words, - }, + async read({ slice: { offset, length } }) { + return new Data(data.slice( + Number(offset), + Number(offset + length) + )); + } + } +} - storage: {} +function makeWords(slots: StructLog["storage"]): Machine.State.Words { + return { + async read({ + slot, + slice: { + offset = 0n, + length = 32n + } = {} + }) { + const rawHex = slots[ + slot.resizeTo(32).toHex().slice(2) as keyof typeof slots + ]; + + const data = Data.fromHex(`0x${rawHex}`); + + return new Data(data.slice( + Number(offset), + Number(offset + length) + )); + } }; } diff --git a/packages/pointers/test/index.ts b/packages/pointers/test/index.ts index f5e8d712..96114464 100644 --- a/packages/pointers/test/index.ts +++ b/packages/pointers/test/index.ts @@ -1,5 +1,19 @@ -export { loadGanache, machineForProvider } from "./ganache.js"; -export { compileCreateBytecode, type CompileOptions } from "./solc.js"; -export { deployContract, type DeployContractResult } from "./deploy.js"; -export { findExamplePointer, prepareCompileOptions } from "./examples.js"; -export { observeTrace, ObserveTraceOptions } from "./observe.js"; +export { + loadGanache, + machineForProvider +} from "./ganache.js"; + +export { + compileCreateBytecode, + singleSourceCompilation, + type CompileOptions +} from "./solc.js"; + +export { deployContract, } from "./deploy.js"; + +export { findExamplePointer } from "./examples.js"; + +export { + observeTrace, + type ObserveTraceOptions +} from "./observe.js"; diff --git a/packages/pointers/test/observe.ts b/packages/pointers/test/observe.ts index f534cd5e..f9e36039 100644 --- a/packages/pointers/test/observe.ts +++ b/packages/pointers/test/observe.ts @@ -5,10 +5,42 @@ import { compileCreateBytecode, type CompileOptions } from "./solc.js"; import { deployContract } from "./deploy.js"; export interface ObserveTraceOptions { + /** + * Pointer that is used repeatedly over the course of a trace to view the + * machine at each step. + */ pointer: Pointer; + + /** + * The necessary metadata and the Solidity source code for a contract whose + * `constructor()` manages the lifecycle of the variable that the specified + * `pointer` corresponds to + */ compileOptions: CompileOptions; + + /** + * A function that understands the structure of the specified `pointer` and + * converts a particular `Cursor.View` for that pointer into a + * JavaScript-native value of type `V` + */ observe({ regions, read }: Cursor.View, state: Machine.State): Promise; + + /** + * Optional predicate that compares two `V` values for domain-specific + * equality. + * + * If not specified, this defaults to `(a, b) => a === b`. + */ equals?(a: V, b: V): boolean; + + /** + * Optional asynchronous predicate that specifies whether or not a particular + * step in the machine trace is a safe time to view the cursor for the + * specified `pointer`. + * + * If not specified, this defaults to `() => Promise.resolve(true)` (i.e., + * every step gets observed). + */ shouldObserve?(state: Machine.State): Promise; } @@ -47,7 +79,7 @@ export async function observeTrace({ const { transactionHash } = await deployContract(bytecode, provider); // prepare to inspect the EVM for that deployment transaction - const machine = machineForProvider(provider, { transactionHash }); + const machine = machineForProvider(provider, transactionHash); let cursor; // delay initialization until first state of trace let lastObservedValue; diff --git a/packages/pointers/test/solc.ts b/packages/pointers/test/solc.ts index 5f017fe7..bae2c9c1 100644 --- a/packages/pointers/test/solc.ts +++ b/packages/pointers/test/solc.ts @@ -1,7 +1,15 @@ -import * as util from "util"; import { Data } from "../src/data.js"; -import solc from "solc"; +import type * as Solc from "solc"; +let solc: typeof Solc | undefined; +try { + solc = (await import("solc")).default; +} catch {} + +/** + * Organizes the sources being compiled by their path identifier, as well + * as includes information about which contract's bytecode is desired + */ export interface CompileOptions { sources: { [path: string]: { @@ -15,11 +23,18 @@ export interface CompileOptions { }; } -// just compile and get something that can go into transaction data +/** + * Compile a collection of sources and return the create (deployment) bytecode + * for a particular target contract + */ export async function compileCreateBytecode({ sources, target }: CompileOptions): Promise { + if (!solc) { + throw new Error("Unable to load solc"); + } + const input = { language: "Solidity", sources, @@ -45,7 +60,7 @@ export async function compileCreateBytecode({ const { errors = [] } = output; if (errors.length > 0) { - throw new Error(util.inspect(errors)); + throw new Error(`Compilation error: ${JSON.stringify(errors, undefined, 2)}`); } const { @@ -56,3 +71,32 @@ export async function compileCreateBytecode({ return Data.fromHex(`0x${createBytecode.object}`); } + +/** + * "Syntactic sugar"-like helper function to initialize CompileOptions for + * compiling only a single source file. + */ +export function singleSourceCompilation(options: { + path: string; + contractName: string; + content: string; +}): CompileOptions { + const { path, contractName, content: contentWithoutHeader } = options; + + const spdxLicenseIdentifier = "// SPDX-License-Identifier: UNLICENSED"; + const pragma = "pragma solidity ^0.8.25;"; + const header = `${spdxLicenseIdentifier}\n${pragma}\n`; + + return { + sources: { + [path]: { + content: `${header}\n${contentWithoutHeader}\n` + } + }, + + target: { + path, + contractName + } + }; +} From 20bd2c97cb9d182fb6f4f371d6229492ad2c75ca Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 1 Jul 2024 00:54:47 -0400 Subject: [PATCH 14/21] Move test cases to jest-free module ... so that they can be imported properly inside, e.g., a browser or the ./bin/run-example script --- packages/pointers/bin/run-example.ts | 43 +---- .../src/dereference/index.integration.test.ts | 182 ------------------ packages/pointers/src/integration.test.ts | 22 +++ packages/pointers/src/test-cases.ts | 159 +++++++++++++++ 4 files changed, 189 insertions(+), 217 deletions(-) delete mode 100644 packages/pointers/src/dereference/index.integration.test.ts create mode 100644 packages/pointers/src/integration.test.ts create mode 100644 packages/pointers/src/test-cases.ts diff --git a/packages/pointers/bin/run-example.ts b/packages/pointers/bin/run-example.ts index 16d3ddeb..db9b7ed4 100644 --- a/packages/pointers/bin/run-example.ts +++ b/packages/pointers/bin/run-example.ts @@ -7,44 +7,17 @@ import { Data } from "../src/index.js"; -import { - prepareCompileOptions, - findExamplePointer, - observeTrace, - type ObserveTraceOptions -} from "../test/index.js"; - -const pointer = findExamplePointer("string-storage-contract-variable-slot"); - -const compileOptions = prepareCompileOptions({ - path: "StringStorage.sol", - contractName: "StringStorage", - content: `contract StringStorage { - string storedString; - bool done; - - event Done(); - - constructor() { - storedString = "hello world"; - storedString = "solidity storage is a fun lesson in endianness"; +import { observeTrace } from "../test/index.js"; - done = true; - } - } - ` -}); - -const observe = async ({ regions, read }: Cursor.View): Promise => { - const strings = await regions.named("string"); - const stringData: Data = Data.zero().concat( - ...await Promise.all(strings.map(read)) - ); - - return new TextDecoder().decode(stringData); -}; +import { observeTraceTests } from "../src/test-cases.js"; export async function run() { + const { + pointer, + compileOptions, + observe + } = observeTraceTests["string storage"]; + console.log( chalk.bold(chalk.cyan( "demo: run compiled solidity and watch a changing ethdebug/format pointer\n" diff --git a/packages/pointers/src/dereference/index.integration.test.ts b/packages/pointers/src/dereference/index.integration.test.ts deleted file mode 100644 index e01e1e67..00000000 --- a/packages/pointers/src/dereference/index.integration.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { jest, expect, describe, it, beforeEach } from "@jest/globals"; -import { - prepareCompileOptions, - findExamplePointer, - observeTrace, - type ObserveTraceOptions -} from "../../test/index.js"; -import { type Cursor, Data } from "../index.js"; - -export interface ObserveTraceTest extends ObserveTraceOptions { - expectedValues: V[]; -} - -export type ObserveTraceTests = { - [K in keyof M]: ObserveTraceTest; -} - -/** - * collection of descriptions of tests that compile+deploy Solidity code, - * then step through the machine trace of that code's execution, watching - * and recording a pointer's value over the course of that trace. - * - * tests are described in terms of an expected sequence of values which the - * list of observed values should contain by the end of the trace, allowing - * for additional unexpected values in between and around the expected values. - */ -export const observeTraceTests: ObserveTraceTests<{ - "string storage": string; - "uint256[] memory": number[]; -}> = { - "uint256[] memory": { - pointer: findExamplePointer("uint256-array-memory-pointer-slot"), - compileOptions: prepareCompileOptions({ - path: "Uint256Arraymemory.sol", - contractName: "Uint256ArrayMemory", - content: `contract Uint256ArrayMemory { - constructor() { - uint256[] memory values = new uint256[](0); - values = appendToArray(values, 1); - values = appendToArray(values, 2); - values = appendToArray(values, 3); - } - - function appendToArray( - uint256[] memory arr, - uint256 value - ) - private - pure - returns (uint256[] memory) - { - uint256[] memory newArray = new uint256[](arr.length + 1); - - for (uint i = 0; i < arr.length; i++) { - newArray[i] = arr[i]; - } - - newArray[arr.length] = value; - return newArray; - } - } - ` - }), - - expectedValues: [ - [], - [1], - [1, 2], - [1, 2, 3] - ], - - async observe({ regions, read }, state): Promise { - const items = regions.named("array-item"); - - return (await Promise.all( - items.map(async (item) => { - const data = await read(item); - - return Number(data.asUint()); - }) - )); - }, - - equals(a, b) { - if (a.length !== b.length) { - return false; - } - - for (const [index, value] of a.entries()) { - if (b[index] !== value) { - return false; - } - } - - return true; - }, - - // this function uses observation of solc + viaIR behavior to determine - // that the memory array we're looking for is going to have a pointer at - // the bottom of the stack - // - // also include a check to exclude observation when that bottom stack value - // would have `cursor.view` produce a ton of regions. - async shouldObserve(state) { - const stackLength = await state.stack.length; - if (stackLength === 0n) { - return false; - } - - // only consider the bottom of the stack - const arrayOffset = await state.stack.peek({ depth: stackLength - 1n }); - - const arrayCount = await state.memory.read({ - slice: { - offset: arrayOffset.asUint(), - length: 32n - } - }) - - // return false; - const isObservable = arrayCount.asUint() < 4n; - if (isObservable) { - } - return isObservable; - } - }, - "string storage": { - pointer: findExamplePointer("string-storage-contract-variable-slot"), - - compileOptions: prepareCompileOptions({ - path: "StringStorage.sol", - contractName: "StringStorage", - content: `contract StringStorage { - string storedString; - bool done; - - event Done(); - - constructor() { - storedString = "hello world"; - storedString = "solidity storage is a fun lesson in endianness"; - - done = true; - } - } - ` - }), - - expectedValues: [ - "", - "hello world", - "solidity storage is a fun lesson in endianness" - ], - - async observe({ regions, read }: Cursor.View): Promise { - const strings = await regions.named("string"); - const stringData: Data = Data.zero().concat( - ...await Promise.all(strings.map(read)) - ); - - return new TextDecoder().decode(stringData); - }, - } -}; - -describe("dereference (integration)", () => { - describe("changing pointer values over the course of a trace", () => { - for (const [name, test] of Object.entries(observeTraceTests)) { - const { expectedValues, ...options } = test; - - describe(`example pointer: ${name}`, () => { - it("resolves to values containing the expected sequence", async () => { - const observedValues = - await observeTrace(options as Parameters[0]); - - expect(observedValues) - .toEqual(expect.arrayContaining(expectedValues)); - }); - }); - } - }); -}); diff --git a/packages/pointers/src/integration.test.ts b/packages/pointers/src/integration.test.ts new file mode 100644 index 00000000..93e7fb11 --- /dev/null +++ b/packages/pointers/src/integration.test.ts @@ -0,0 +1,22 @@ +import { jest, expect, describe, it, beforeEach } from "@jest/globals"; + +import { observeTrace } from "../test/index.js"; +import { observeTraceTests } from "./test-cases.js"; + +describe("dereference (integration)", () => { + describe("changing pointer values over the course of a trace", () => { + for (const [name, test] of Object.entries(observeTraceTests)) { + const { expectedValues, ...options } = test; + + describe(`example pointer: ${name}`, () => { + it("resolves to values containing the expected sequence", async () => { + const observedValues = + await observeTrace(options as Parameters[0]); + + expect(observedValues) + .toEqual(expect.arrayContaining(expectedValues)); + }); + }); + } + }); +}); diff --git a/packages/pointers/src/test-cases.ts b/packages/pointers/src/test-cases.ts new file mode 100644 index 00000000..3ef68df4 --- /dev/null +++ b/packages/pointers/src/test-cases.ts @@ -0,0 +1,159 @@ +import { + singleSourceCompilation, + findExamplePointer, + type ObserveTraceOptions +} from "../test/index.js"; +import { type Cursor, Data } from "./index.js"; + +export interface ObserveTraceTest extends ObserveTraceOptions { + expectedValues: V[]; +} + +const stringStorageTest: ObserveTraceTest = { + pointer: findExamplePointer("string-storage-contract-variable-slot"), + + compileOptions: singleSourceCompilation({ + path: "StringStorage.sol", + contractName: "StringStorage", + content: `contract StringStorage { + string storedString; + bool done; + + event Done(); + + constructor() { + storedString = "hello world"; + storedString = "solidity storage is a fun lesson in endianness"; + + done = true; + } + } + ` + }), + + expectedValues: [ + "", + "hello world", + "solidity storage is a fun lesson in endianness" + ], + + async observe({ regions, read }: Cursor.View): Promise { + // collect all the regions corresponding to string contents + const strings = await regions.named("string"); + + // read each region and concatenate all the bytes + const stringData: Data = Data.zero() + .concat(...await Promise.all(strings.map(read))); + + // decode into JS string + return new TextDecoder().decode(stringData); + }, +}; + +const uint256ArrayMemoryTest: ObserveTraceTest = { + pointer: findExamplePointer("uint256-array-memory-pointer-slot"), + compileOptions: singleSourceCompilation({ + path: "Uint256Arraymemory.sol", + contractName: "Uint256ArrayMemory", + content: `contract Uint256ArrayMemory { + constructor() { + uint256[] memory values = new uint256[](0); + values = appendToArray(values, 1); + values = appendToArray(values, 2); + values = appendToArray(values, 3); + } + + function appendToArray( + uint256[] memory arr, + uint256 value + ) + private + pure + returns (uint256[] memory) + { + uint256[] memory newArray = new uint256[](arr.length + 1); + + for (uint i = 0; i < arr.length; i++) { + newArray[i] = arr[i]; + } + + newArray[arr.length] = value; + return newArray; + } + } + ` + }), + + expectedValues: [ + [], + [1], + [1, 2], + [1, 2, 3] + ], + + async observe({ regions, read }, state): Promise { + const items = regions.named("array-item"); + + return (await Promise.all( + items.map(async (item) => { + const data = await read(item); + + return Number(data.asUint()); + }) + )); + }, + + equals(a, b) { + if (a.length !== b.length) { + return false; + } + + for (const [index, value] of a.entries()) { + if (b[index] !== value) { + return false; + } + } + + return true; + }, + + // this function uses observation of solc + viaIR behavior to determine + // that the memory array we're looking for is going to have a pointer at + // the bottom of the stack + // + // also include a check to exclude observation when that bottom stack value + // would have `cursor.view()` yield more regions than expected + async shouldObserve(state) { + const stackLength = await state.stack.length; + if (stackLength === 0n) { + return false; + } + + // only consider the bottom of the stack + const arrayOffset = await state.stack.peek({ depth: stackLength - 1n }); + + const arrayCount = await state.memory.read({ + slice: { + offset: arrayOffset.asUint(), + length: 32n + } + }) + + // the example code only appends three times + return arrayCount.asUint() < 4n; + } +}; + +/** + * collection of descriptions of tests that compile+deploy Solidity code, + * then step through the machine trace of that code's execution, watching + * and recording a pointer's value over the course of that trace. + * + * tests are described in terms of an expected sequence of values which the + * list of observed values should contain by the end of the trace, allowing + * for additional unexpected values in between and around the expected values. + */ +export const observeTraceTests = { + "string storage": stringStorageTest, + "uint256[] memory": uint256ArrayMemoryTest, +}; From 6096c5c2942e9fce0a10562e85aed80693fba3ac Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 1 Jul 2024 00:56:26 -0400 Subject: [PATCH 15/21] Add @ethdebug/pointers dependency to web ... and make sure it won't explode when importing the test code --- packages/web/docusaurus.config.ts | 12 ++++++++++-- packages/web/package.json | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/web/docusaurus.config.ts b/packages/web/docusaurus.config.ts index ea9b8498..c7c4e8cd 100644 --- a/packages/web/docusaurus.config.ts +++ b/packages/web/docusaurus.config.ts @@ -48,8 +48,16 @@ const config: Config = { react: path.resolve('../../node_modules/react'), }, fallback: { - buffer: false - } + assert: false, + buffer: false, + fs: false, + http: false, + https: false, + path: false, + stream: false, + url: false, + util: false, + }, } }; } diff --git a/packages/web/package.json b/packages/web/package.json index 30569732..cfe0ee69 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -22,6 +22,7 @@ "@docusaurus/tsconfig": "^3.4.0", "@docusaurus/types": "^3.4.0", "@ethdebug/format": "^0.1.0-0", + "@ethdebug/pointers": "^0.1.0-0", "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-brands-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1", From a93223aea8d5da0223b400017800d538c06617af Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 1 Jul 2024 00:57:13 -0400 Subject: [PATCH 16/21] Add prettier-plugin-solidity to web --- packages/web/package.json | 2 ++ yarn.lock | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/web/package.json b/packages/web/package.json index cfe0ee69..fcd745fd 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -34,6 +34,8 @@ "ajv": "^8.12.0", "clsx": "^1.2.1", "docusaurus-json-schema-plugin": "^1.12.1", + "prettier": "^3.3.2", + "prettier-plugin-solidity": "^1.3.1", "prism-react-renderer": "^2.1.0", "raw-loader": "^4.0.2", "react": "^18.0.0", diff --git a/yarn.lock b/yarn.lock index a50e9130..e8e17406 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3499,6 +3499,11 @@ micromark-util-character "^1.1.0" micromark-util-symbol "^1.0.1" +"@solidity-parser/parser@^0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.17.0.tgz#52a2fcc97ff609f72011014e4c5b485ec52243ef" + integrity sha512-Nko8R0/kUo391jsEHHxrGM07QFdnPGvlmox4rmH0kNiNAashItAilhy4Mv4pK5gQmW5f4sXAF58fwJbmlkGcVw== + "@stoplight/json-ref-resolver@^3.1.5": version "3.1.6" resolved "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.6.tgz" @@ -11504,6 +11509,20 @@ postcss@^8.4.24, postcss@^8.4.38: picocolors "^1.0.0" source-map-js "^1.2.0" +prettier-plugin-solidity@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.3.1.tgz#59944d3155b249f7f234dee29f433524b9a4abcf" + integrity sha512-MN4OP5I2gHAzHZG1wcuJl0FsLS3c4Cc5494bbg+6oQWBPuEamjwDvmGfFMZ6NFzsh3Efd9UUxeT7ImgjNH4ozA== + dependencies: + "@solidity-parser/parser" "^0.17.0" + semver "^7.5.4" + solidity-comments-extractor "^0.0.8" + +prettier@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" + integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== + pretty-error@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz" @@ -12707,6 +12726,11 @@ solc@^0.8.26: semver "^5.5.0" tmp "0.0.33" +solidity-comments-extractor@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/solidity-comments-extractor/-/solidity-comments-extractor-0.0.8.tgz#f6e148ab0c49f30c1abcbecb8b8df01ed8e879f8" + integrity sha512-htM7Vn6LhHreR+EglVMd2s+sZhcXAirB1Zlyrv5zBuTxieCvjfnRpd7iZk75m/u6NOlEyQ94C6TWbBn2cY7w8g== + sort-css-media-queries@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz#aa33cf4a08e0225059448b6c40eddbf9f1c8334c" From 7a7d9f9617d0a11fdf8974fc884e55b6258b127f Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 1 Jul 2024 01:28:31 -0400 Subject: [PATCH 17/21] Document integration tests in implementation guide --- .../pointers/testing/TestCase.tsx | 79 ++++++++ .../pointers/testing/_category_.json | 8 + .../testing/blockchain-simulation.mdx | 174 ++++++++++++++++++ .../pointers/testing/compilation.mdx | 54 ++++++ .../pointers/testing/deployment.mdx | 17 ++ .../pointers/testing/example-pointers.mdx | 33 ++++ .../pointers/testing/machine-observation.mdx | 63 +++++++ .../pointers/testing/summary.mdx | 80 ++++++++ .../pointers/testing/test-cases.mdx | 26 +++ 9 files changed, 534 insertions(+) create mode 100644 packages/web/docs/implementation-guides/pointers/testing/TestCase.tsx create mode 100644 packages/web/docs/implementation-guides/pointers/testing/_category_.json create mode 100644 packages/web/docs/implementation-guides/pointers/testing/blockchain-simulation.mdx create mode 100644 packages/web/docs/implementation-guides/pointers/testing/compilation.mdx create mode 100644 packages/web/docs/implementation-guides/pointers/testing/deployment.mdx create mode 100644 packages/web/docs/implementation-guides/pointers/testing/example-pointers.mdx create mode 100644 packages/web/docs/implementation-guides/pointers/testing/machine-observation.mdx create mode 100644 packages/web/docs/implementation-guides/pointers/testing/summary.mdx create mode 100644 packages/web/docs/implementation-guides/pointers/testing/test-cases.mdx diff --git a/packages/web/docs/implementation-guides/pointers/testing/TestCase.tsx b/packages/web/docs/implementation-guides/pointers/testing/TestCase.tsx new file mode 100644 index 00000000..3c0f512d --- /dev/null +++ b/packages/web/docs/implementation-guides/pointers/testing/TestCase.tsx @@ -0,0 +1,79 @@ +import { useState, useEffect } from "react"; + +import prettier from "prettier/standalone"; + +import { describeSchema } from "@ethdebug/format"; +import { observeTraceTests } from "@ethdebug/pointers/dist/src/test-cases" + +import { Collapsible } from "@theme/JSONSchemaViewer/components"; +import CodeBlock from "@theme/CodeBlock"; +import CodeListing from "@site/src/components/CodeListing"; + +const solidityPlugin: any = require("prettier-plugin-solidity/standalone"); + +export interface TestCaseProps { + name: string; + variableName: string; +} + +export default function TestCase({ + name, + variableName +}: TestCaseProps): JSX.Element { + const { + pointer, + compileOptions, + expectedValues + } = observeTraceTests[name as keyof typeof observeTraceTests]; + + const [sourcePath, { content }] = + // use the first since all test cases use only one source file + Object.entries(compileOptions.sources)[0]; + + const [ + formattedContent, + setFormattedContent + ] = useState(); + + useEffect(() => { + prettier.format(content, { + parser: "solidity-parse", + plugins: [solidityPlugin] + }) + .then(setFormattedContent); + }, [setFormattedContent]); + + return <> +

Solidity code

+ + {typeof formattedContent === "undefined" + ? <>Loading Solidity code... + : {formattedContent}} + +

Expected value sequence

+ +
    + {expectedValues.map((expectedValue, index) =>
  1. { + JSON.stringify(expectedValue) + }
  2. )} +
+ +

Pointer

+ + { + JSON.stringify(pointer, undefined, 2) + } + + +

Full test case code listing

+ + sourceFile.getVariableStatement(variableName)} + /> + + + + ; +} diff --git a/packages/web/docs/implementation-guides/pointers/testing/_category_.json b/packages/web/docs/implementation-guides/pointers/testing/_category_.json new file mode 100644 index 00000000..f8c6ccd9 --- /dev/null +++ b/packages/web/docs/implementation-guides/pointers/testing/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "End-to-end testing", + "position": 6, + "link": { + "type": "generated-index", + "description": "Implementing integration tests" + } +} diff --git a/packages/web/docs/implementation-guides/pointers/testing/blockchain-simulation.mdx b/packages/web/docs/implementation-guides/pointers/testing/blockchain-simulation.mdx new file mode 100644 index 00000000..10dd29d6 --- /dev/null +++ b/packages/web/docs/implementation-guides/pointers/testing/blockchain-simulation.mdx @@ -0,0 +1,174 @@ +--- +sidebar_position: 4 +--- + +import CodeListing from "@site/src/components/CodeListing"; + +# Simulating a blockchain + +:::warning + +In case you missed the +[note on the Summary page](/docs/implementation-guides/pointers/testing/summary#ganache-warning), +the functionality described in this page uses the unmaintained +[Ganache](https://github.com/trufflesuite/ganache) software library for +simulating the EVM. See note for rationale and risk expectations. + +::: + +This reference implemention relies heavily on the +[`Machine`](/docs/implementation-guides/pointers/types/data-and-machines#Machine) +interface it defines for reading the state of a running EVM; this page describes +how this implementation's integration tests adapt an +[EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) JavaScript provider object +to this interface. + +Since the primary purpose of `Machine` is to represent a series of code +execution steps, the adapter described here simplifies the concept of an +execution trace by restricting it to mean that which happens within the course +of an Ethereum transaction. The tests thus define a `machineForProvider` +function to adapt a provider object for a particular transaction hash. + +As a result, this code only functions in the context of a provider to a +blockchain whose JSON-RPC exposes the original +[go-ethereum](https://github.com/ethereum/go-ethereum)'s +`"debug_traceTransaction"` method, which exposes the state of the EVM at each +step of code execution for a particular transaction. Other kinds of traces (such +as tracing the execution of an `"eth_call"` request) are left to remain +intentionally out-of-scope for the purposes of testing this implementation. +Other implementations of the `Machine` interface need not make this restriction. + +## Implementing `machineForProvider()` + +The `machineForProvider()` function takes two arguments and returns an object +adhering to the `Machine` interface. See the code listing for this function: + + sourceFile.getFunction("machineForProvider")} +/> + +This function is written to return an object whose `trace()` method matches that +which is defined by `Machine`: a method to asynchronously produce an iterable +list of `Machine.State`s. This function leverages two other helper functions as +part of the behavior of this method: `requestStructLogs()` and +`toMachineState()`. + +### Requesting "struct logs" + +The Geth-style `"debug_traceTransaction"` method returns a list of execution +steps and machine states inside the `"structLogs"` field of the response's +result object. + +The asynchronous `requestStructLogs` function is implemented as follows: + + sourceFile.getFunction("requestStructLogs")} +/> + +Since Ganache does not have a publicly-documented or easily-accessible exported +collection of types, but since it **does** use string literal types to infer the +specific type of provider request being made, this code can use TypeScript's +type interference to ensure type safety in the adapter: + + sourceFile.getTypeAlias("StructLogs")} +/> + + sourceFile.getTypeAlias("StructLog")} +/> + + sourceFile.getTypeAlias("Depromise")} +/> + + sourceFile.getTypeAlias("Dearray")} +/> + +These types are not exported by this module because they are internal to +`machineForProvider()` concerns. + +### Converting to `Machine.State` + +The `toMachineState()` function is implemented by leveraging the use of the +[addressing schemes](/spec/pointer/concepts#a-region-is-specified-in-terms-of-an-addressing-scheme) +defined by the **ethdebug/format/pointer** schema. Notice the use of the various +helper functions, listed below. + + sourceFile.getFunction("toMachineState")} +/> + +#### Helper function: `constantUint()` + +Since the interface defined by `Machine.State` is more asynchronous than likely +necessary (certainly it is more asynchronous than necessary for these testing +purposes), many properties defined within `Machine.State` must be converted from +a readily-available constant value into a `Promise` that resolves to that value: + + sourceFile.getFunction("constantUint")} +/> + +#### Helper function: `makeStack()` + +Although the specification defines the `"stack"` data location to use a regular +segment-based addressing scheme, this reference implementation distinguishes the +stack from the other segment-based locations because of the use of numeric, +unstable slot values. + + sourceFile.getFunction("makeStack")} +/> + +#### Helper function: `makeWords()` + +For other segment-based locations, the `makeWords()` function is used: + + sourceFile.getFunction("makeWords")} +/> + +#### Helper function: `makeBytes()` + +The `makeBytes()` function is used for plain bytes-based data locations, such as +`"memory"`: + + sourceFile.getFunction("makeBytes")} +/> + +## Note on loading Ganache + +To prevent Ganache's warnings from appearing in test console output, a custom +`loadGanache()` function is defined to suppress known warnings while importing +the module: + + sourceFile.getFunction("loadGanache")} +/> diff --git a/packages/web/docs/implementation-guides/pointers/testing/compilation.mdx b/packages/web/docs/implementation-guides/pointers/testing/compilation.mdx new file mode 100644 index 00000000..e146718b --- /dev/null +++ b/packages/web/docs/implementation-guides/pointers/testing/compilation.mdx @@ -0,0 +1,54 @@ +--- +sidebar_position: 3 +--- + +import CodeListing from "@site/src/components/CodeListing"; + +# Invoking the compiler + +In being able to test a pointer dereference implementation, it is necessary to +pair each tested pointer with associated EVM code that makes the pointer +meaningful. To avoid solutions such as pre-compiling Solidity or handwriting EVM +bytecode, the **@ethdebug/pointers** reference implementation's integration +tests are written so that each test case is described in terms of Solidity code +that the testing infrastructure compiles when executing the test. + +The strategy taken by these tests is to use Solidity's `constructor` mechanism +for allowing tests to specify variable assignment and mutation logic without +needing to manage deployed contract instances. All these integration test cases +thus observe pointers only via the trace of a contract creation transaction. + +## Integration logic + +This testing infrastructure includes the `compileCreateBytecode()` function, +which accepts input resembling Solidity's compiler input data as argument (i.e., +the collection of source contents by path and additional contract target +information) and asynchronously returns `Data` with the create (deployment) +bytecode for the target contract. + + sourceFile.getFunction("compileCreateBytecode")} +/> + +## The `CompileOptions` interface + + sourceFile.getInterface("CompileOptions")} +/> + +## "Syntactic sugar"-like helper function + +To avoid test cases' needing to describe their associated code samples in terms +of source content by path, test cases that require only a single source file can +use the `singleSourceCompilation()` helper function that provides a more +succinct method for generating `CompileOptions` objects: + + sourceFile.getFunction("singleSourceCompilation")} +/> diff --git a/packages/web/docs/implementation-guides/pointers/testing/deployment.mdx b/packages/web/docs/implementation-guides/pointers/testing/deployment.mdx new file mode 100644 index 00000000..de1a92cc --- /dev/null +++ b/packages/web/docs/implementation-guides/pointers/testing/deployment.mdx @@ -0,0 +1,17 @@ +--- +sidebar_position: 5 +--- + +import CodeListing from "@site/src/components/CodeListing"; + +# Deploying contracts + +Deploying a contract with some EVM bytecode is straightforward with +[EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) JavaScript provider objects, +although it does require making a few RPC requests: + + sourceFile.getFunction("deployContract")} +/> diff --git a/packages/web/docs/implementation-guides/pointers/testing/example-pointers.mdx b/packages/web/docs/implementation-guides/pointers/testing/example-pointers.mdx new file mode 100644 index 00000000..bd3712b8 --- /dev/null +++ b/packages/web/docs/implementation-guides/pointers/testing/example-pointers.mdx @@ -0,0 +1,33 @@ +--- +sidebar_position: 2 +--- + +import CodeListing from "@site/src/components/CodeListing"; + +# Finding example pointers + +These integration tests seek to minimize the use of bespoke data whose +representations exist solely within the testing-associated code modules. + +Instead of containing custom pointer objects defined inline, the integration +tests for this reference implementation use the official pointer examples +that are distributed as part of the **ethdebug/format/pointer** schema itself. + +Since JSON Schema does not offer a means by which examples can be named (it +only defines a way to represent an ordered list of unlabeled example values), +these tests rely on searching for particular examples by their use of uniquely +indicative string values (e.g., the "string storage" example pointer is the +only example to contain the string `"string-storage-contract-variable-slot"`). + +The logic for doing this search is captured by the `findExamplePointer()` +function: + + sourceFile.getVariableStatement("findExamplePointer") + } /> + +(This function is written as an immediately-invoked inline function so as to +avoid unnecessary redundant calls to `describeSchema()`.) diff --git a/packages/web/docs/implementation-guides/pointers/testing/machine-observation.mdx b/packages/web/docs/implementation-guides/pointers/testing/machine-observation.mdx new file mode 100644 index 00000000..199d3516 --- /dev/null +++ b/packages/web/docs/implementation-guides/pointers/testing/machine-observation.mdx @@ -0,0 +1,63 @@ +--- +sidebar_position: 6 +--- + +import CodeListing from "@site/src/components/CodeListing"; + +# Observing the machine + +These integration tests leverage the `observeTrace()` helper function to +consolidate the logic to setup and execute the testing of a particular example +pointer. This function is designed to simulate an EVM and repeatedly observe the +result of dererencing this pointer across each step in the machine trace. + +This function accepts a test case description in the form of an `options` +argument of type `ObserveTraceOptions`. In its simplest form, this object must +contain the following information: + +- The pointer to be dereferenced and viewed repeatedly +- Solidity code for a contract whose constructor manages a variable to which the + pointer corresponds +- An `observe({ regions, read }: Cursor.View): Promise` function that + converts a cursor view into a native JavaScript value of type `V` + +With this information, the `observeTrace()` function initializes an in-memory +EVM, compiles and deploys the Solidity contract, then steps through the code +execution of that contract's deployment. Over the course of this stepping, this +function first dereferences the given `pointer` and then repeatedly calls +`observe()` with each new machine state. It aggregates all the changing values +of type `V` it observes and finally returns the full `V[]` list of these values. + +This enables the integration tests to evaluate how a pointer gets dereferenced +in native JavaScript terms, rather than just in the terms of a particular +resolved collection of regions. For instance, this allows tests to specify that +observing a Solidity `string storage` pointer should yield a list of JavaScript +`string` values. + +Beyond the "simplest form" described above, `ObserveTraceOptions` defines a +number of optional properties for customizing observation behavior, including to +allow observing pointers to complex types (e.g. arrays) and to allow skipping +observation at times where it may be unsafe. See [below](#interface-definition) +for the full documented code listing for this type. + +## Function implementation + +The full implementation for `observeTrace` follows: + + sourceFile.getFunction("observeTrace")} +/> + +## `interface ObserveTraceOptions` {#interface-definition} + +This interface is generic to some type `V`: + + + sourceFile.getExportedDeclarations().get("ObserveTraceOptions")[0] + } +/> diff --git a/packages/web/docs/implementation-guides/pointers/testing/summary.mdx b/packages/web/docs/implementation-guides/pointers/testing/summary.mdx new file mode 100644 index 00000000..3da08d29 --- /dev/null +++ b/packages/web/docs/implementation-guides/pointers/testing/summary.mdx @@ -0,0 +1,80 @@ +--- +sidebar_position: 1 +--- + +import { SyntaxKind, VariableDeclarationKind } from "ts-morph"; +import CodeListing from "@site/src/components/CodeListing"; + +# Summary + +In addition to unit tests for each of the various modules, the +**@ethdebug/pointers** reference implementation includes automated integration +tests that watch the changes to a `dereference()`d pointer over the course of +the execution of some Solidity code, ensuring that all expected values appear in +their expected sequence. + +These tests are defined to use a developer-friendly programmatic EVM and +Ethereum JSON-RPC simulator that models an Ethereum blockchain for purposes of +transactions and state inspection. + +:::warning + +These tests are implemented using +[Ganache](https://github.com/trufflesuite/ganache), which has been unmaintained +since the end of 2023. + +See more details [below](#ganache-warning). + +::: + +## Test code + +The following code listing shows the top-level test implementation. These tests +are written to use the [Jest](https://jestjs.io/) testing framework. + + { + const [describe] = sourceFile.getStatements() + .filter(statement => { + const callExpressions = statement.getChildrenOfKind(SyntaxKind.CallExpression); + + if (callExpressions.length === 0) { + return false; + } + + const functionName = callExpressions[0].getChildren()[0].getText(); + + return functionName === "describe"; + }); + + return describe; + } + +} /> + +In this code listing, please see how tests are generated programmatically based +on data definitions of each test case. (Notice how the `observeTraceTests` +variable is used.) + +## Unmaintained dependencies warning {#ganache-warning} + +The testing implementation described in this section uses the unmaintained +[Ganache](https://github.com/trufflesuite/ganache), thus resulting in +**@ethdebug/pointers** having a developer dependency on this package. This +dependency poses **no concern** for **@ethdebug/pointers** _distributions_, but +may impact future developers who are looking to build/test this reference +implementatino locally. + +At the time of implementation, Ganache was selected for use in these tests +because it was uniquely suitable for use programmatically inside TypeScript. +Sadly, other options such as Hardhat's `npx hardhat node` and Foundry's Anvil +would have required additional setup in the form of operating system process +management and complex adapter setup. + +Due to Ganache's end-of-life timing, these tests are limited in that they cannot +test pointers to transient storage. Features from EVM versions before the Cancun +hardfork should continue to work so long as there are no breaking changes +introduced by Node.js. diff --git a/packages/web/docs/implementation-guides/pointers/testing/test-cases.mdx b/packages/web/docs/implementation-guides/pointers/testing/test-cases.mdx new file mode 100644 index 00000000..6c5449a6 --- /dev/null +++ b/packages/web/docs/implementation-guides/pointers/testing/test-cases.mdx @@ -0,0 +1,26 @@ +--- +sidebar_position: 7 +--- + +import CodeListing from "@site/src/components/CodeListing"; +import TestCase from "./TestCase"; + +# Test cases + +## Storage strings + +Representing a Solidity `string storage` using an **ethdebug/format/pointer** +requires the use of conditional logic to identify the one or more regions that +correspond to raw UTF-8 Solidity string data. The `dereference()` function +should behave as expected for such a pointer and observe the changing string +value. + + + +## Memory arrays of word-sized items + +Memory arrays are primarily referenced using stack-located memory offset values, +and so this test case ensures that stack slot indexes are properly adjusted over +the course of the transaction. + + From 4f1038f2d10cb41e1c30d2be52e4d96f33d9db05 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 1 Jul 2024 02:17:33 -0400 Subject: [PATCH 18/21] Fix build --- .../pointers/testing/TestCase.tsx | 79 ------------------- .../pointers/testing/TestedPointer.tsx | 30 +++++++ .../testing/blockchain-simulation.mdx | 2 +- .../pointers/testing/test-cases.mdx | 30 ++++++- packages/web/docusaurus.config.ts | 12 +-- packages/web/package.json | 3 - yarn.lock | 24 ------ 7 files changed, 60 insertions(+), 120 deletions(-) delete mode 100644 packages/web/docs/implementation-guides/pointers/testing/TestCase.tsx create mode 100644 packages/web/docs/implementation-guides/pointers/testing/TestedPointer.tsx diff --git a/packages/web/docs/implementation-guides/pointers/testing/TestCase.tsx b/packages/web/docs/implementation-guides/pointers/testing/TestCase.tsx deleted file mode 100644 index 3c0f512d..00000000 --- a/packages/web/docs/implementation-guides/pointers/testing/TestCase.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useState, useEffect } from "react"; - -import prettier from "prettier/standalone"; - -import { describeSchema } from "@ethdebug/format"; -import { observeTraceTests } from "@ethdebug/pointers/dist/src/test-cases" - -import { Collapsible } from "@theme/JSONSchemaViewer/components"; -import CodeBlock from "@theme/CodeBlock"; -import CodeListing from "@site/src/components/CodeListing"; - -const solidityPlugin: any = require("prettier-plugin-solidity/standalone"); - -export interface TestCaseProps { - name: string; - variableName: string; -} - -export default function TestCase({ - name, - variableName -}: TestCaseProps): JSX.Element { - const { - pointer, - compileOptions, - expectedValues - } = observeTraceTests[name as keyof typeof observeTraceTests]; - - const [sourcePath, { content }] = - // use the first since all test cases use only one source file - Object.entries(compileOptions.sources)[0]; - - const [ - formattedContent, - setFormattedContent - ] = useState(); - - useEffect(() => { - prettier.format(content, { - parser: "solidity-parse", - plugins: [solidityPlugin] - }) - .then(setFormattedContent); - }, [setFormattedContent]); - - return <> -

Solidity code

- - {typeof formattedContent === "undefined" - ? <>Loading Solidity code... - : {formattedContent}} - -

Expected value sequence

- -
    - {expectedValues.map((expectedValue, index) =>
  1. { - JSON.stringify(expectedValue) - }
  2. )} -
- -

Pointer

- - { - JSON.stringify(pointer, undefined, 2) - } - - -

Full test case code listing

- - sourceFile.getVariableStatement(variableName)} - /> - - - - ; -} diff --git a/packages/web/docs/implementation-guides/pointers/testing/TestedPointer.tsx b/packages/web/docs/implementation-guides/pointers/testing/TestedPointer.tsx new file mode 100644 index 00000000..e5450a15 --- /dev/null +++ b/packages/web/docs/implementation-guides/pointers/testing/TestedPointer.tsx @@ -0,0 +1,30 @@ +import { Collapsible } from "@theme/JSONSchemaViewer/components"; +import CodeBlock from "@theme/CodeBlock"; +import { describeSchema } from "@ethdebug/format"; + +export interface TestedPointerProps { + pointerQuery: string; +} + +export default function TestedPointer({ + pointerQuery +}: TestedPointerProps): JSX.Element { + const { schema } = describeSchema({ + schema: { id: "schema:ethdebug/format/pointer" } + }); + + const [exampleIndex] = [...(schema.examples?.entries() || [])] + .find(([_, example]) => JSON.stringify(example).includes(pointerQuery)) + || []; + + if (typeof exampleIndex === "undefined") { + throw new Error("Could not find example in pointer schema"); + } + + const { yaml: pointerYaml } = describeSchema({ + schema: { id: "schema:ethdebug/format/pointer" }, + pointer: `#/examples/${exampleIndex}` + }); + + return {pointerYaml}; +} diff --git a/packages/web/docs/implementation-guides/pointers/testing/blockchain-simulation.mdx b/packages/web/docs/implementation-guides/pointers/testing/blockchain-simulation.mdx index 10dd29d6..1f716288 100644 --- a/packages/web/docs/implementation-guides/pointers/testing/blockchain-simulation.mdx +++ b/packages/web/docs/implementation-guides/pointers/testing/blockchain-simulation.mdx @@ -17,7 +17,7 @@ simulating the EVM. See note for rationale and risk expectations. ::: This reference implemention relies heavily on the -[`Machine`](/docs/implementation-guides/pointers/types/data-and-machines#Machine) +[`Machine`](/docs/implementation-guides/pointers/types/data-and-machines#machine) interface it defines for reading the state of a running EVM; this page describes how this implementation's integration tests adapt an [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) JavaScript provider object diff --git a/packages/web/docs/implementation-guides/pointers/testing/test-cases.mdx b/packages/web/docs/implementation-guides/pointers/testing/test-cases.mdx index 6c5449a6..ee2a9cb6 100644 --- a/packages/web/docs/implementation-guides/pointers/testing/test-cases.mdx +++ b/packages/web/docs/implementation-guides/pointers/testing/test-cases.mdx @@ -3,7 +3,7 @@ sidebar_position: 7 --- import CodeListing from "@site/src/components/CodeListing"; -import TestCase from "./TestCase"; +import TestedPointer from "./TestedPointer"; # Test cases @@ -15,7 +15,19 @@ correspond to raw UTF-8 Solidity string data. The `dereference()` function should behave as expected for such a pointer and observe the changing string value. - +### Test source + + sourceFile.getVariableStatement("stringStorageTest")} + /> + +### Tested pointer + + ## Memory arrays of word-sized items @@ -23,4 +35,16 @@ Memory arrays are primarily referenced using stack-located memory offset values, and so this test case ensures that stack slot indexes are properly adjusted over the course of the transaction. - +### Test source + + sourceFile.getVariableStatement("uint256ArrayMemoryTest")} + /> + +### Tested pointer + + diff --git a/packages/web/docusaurus.config.ts b/packages/web/docusaurus.config.ts index c7c4e8cd..ea9b8498 100644 --- a/packages/web/docusaurus.config.ts +++ b/packages/web/docusaurus.config.ts @@ -48,16 +48,8 @@ const config: Config = { react: path.resolve('../../node_modules/react'), }, fallback: { - assert: false, - buffer: false, - fs: false, - http: false, - https: false, - path: false, - stream: false, - url: false, - util: false, - }, + buffer: false + } } }; } diff --git a/packages/web/package.json b/packages/web/package.json index fcd745fd..30569732 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -22,7 +22,6 @@ "@docusaurus/tsconfig": "^3.4.0", "@docusaurus/types": "^3.4.0", "@ethdebug/format": "^0.1.0-0", - "@ethdebug/pointers": "^0.1.0-0", "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-brands-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1", @@ -34,8 +33,6 @@ "ajv": "^8.12.0", "clsx": "^1.2.1", "docusaurus-json-schema-plugin": "^1.12.1", - "prettier": "^3.3.2", - "prettier-plugin-solidity": "^1.3.1", "prism-react-renderer": "^2.1.0", "raw-loader": "^4.0.2", "react": "^18.0.0", diff --git a/yarn.lock b/yarn.lock index e8e17406..a50e9130 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3499,11 +3499,6 @@ micromark-util-character "^1.1.0" micromark-util-symbol "^1.0.1" -"@solidity-parser/parser@^0.17.0": - version "0.17.0" - resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.17.0.tgz#52a2fcc97ff609f72011014e4c5b485ec52243ef" - integrity sha512-Nko8R0/kUo391jsEHHxrGM07QFdnPGvlmox4rmH0kNiNAashItAilhy4Mv4pK5gQmW5f4sXAF58fwJbmlkGcVw== - "@stoplight/json-ref-resolver@^3.1.5": version "3.1.6" resolved "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.6.tgz" @@ -11509,20 +11504,6 @@ postcss@^8.4.24, postcss@^8.4.38: picocolors "^1.0.0" source-map-js "^1.2.0" -prettier-plugin-solidity@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.3.1.tgz#59944d3155b249f7f234dee29f433524b9a4abcf" - integrity sha512-MN4OP5I2gHAzHZG1wcuJl0FsLS3c4Cc5494bbg+6oQWBPuEamjwDvmGfFMZ6NFzsh3Efd9UUxeT7ImgjNH4ozA== - dependencies: - "@solidity-parser/parser" "^0.17.0" - semver "^7.5.4" - solidity-comments-extractor "^0.0.8" - -prettier@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" - integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== - pretty-error@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz" @@ -12726,11 +12707,6 @@ solc@^0.8.26: semver "^5.5.0" tmp "0.0.33" -solidity-comments-extractor@^0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/solidity-comments-extractor/-/solidity-comments-extractor-0.0.8.tgz#f6e148ab0c49f30c1abcbecb8b8df01ed8e879f8" - integrity sha512-htM7Vn6LhHreR+EglVMd2s+sZhcXAirB1Zlyrv5zBuTxieCvjfnRpd7iZk75m/u6NOlEyQ94C6TWbBn2cY7w8g== - sort-css-media-queries@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz#aa33cf4a08e0225059448b6c40eddbf9f1c8334c" From 8c63e2605857218ba32e745be8be8e9c4c8d705c Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 1 Jul 2024 17:54:30 -0400 Subject: [PATCH 19/21] Fix extract prop for CodeListing --- packages/web/src/components/CodeListing.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/src/components/CodeListing.tsx b/packages/web/src/components/CodeListing.tsx index 0c5ac543..a32c1c08 100644 --- a/packages/web/src/components/CodeListing.tsx +++ b/packages/web/src/components/CodeListing.tsx @@ -8,10 +8,10 @@ export interface CodeListingProps { packageName: string; sourcePath: string; - extract?: ( + extract?: ( sourceFile: SourceFile, project: Project - ) => N; + ) => Pick; } export default function CodeListing({ From 6eff3f6227294f076928ba3b361cc241a82f5fd3 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 1 Jul 2024 17:58:56 -0400 Subject: [PATCH 20/21] Fix typo --- .../web/docs/implementation-guides/pointers/testing/summary.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/docs/implementation-guides/pointers/testing/summary.mdx b/packages/web/docs/implementation-guides/pointers/testing/summary.mdx index 3da08d29..f858c2ae 100644 --- a/packages/web/docs/implementation-guides/pointers/testing/summary.mdx +++ b/packages/web/docs/implementation-guides/pointers/testing/summary.mdx @@ -66,7 +66,7 @@ The testing implementation described in this section uses the unmaintained **@ethdebug/pointers** having a developer dependency on this package. This dependency poses **no concern** for **@ethdebug/pointers** _distributions_, but may impact future developers who are looking to build/test this reference -implementatino locally. +implementation locally. At the time of implementation, Ganache was selected for use in these tests because it was uniquely suitable for use programmatically inside TypeScript. From 9963be6b1971fd83684db04c85c3610d4f5f841a Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 1 Jul 2024 18:07:05 -0400 Subject: [PATCH 21/21] Include listing for observeTraceTests variable --- .../pointers/testing/test-cases.mdx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/web/docs/implementation-guides/pointers/testing/test-cases.mdx b/packages/web/docs/implementation-guides/pointers/testing/test-cases.mdx index ee2a9cb6..c7da154b 100644 --- a/packages/web/docs/implementation-guides/pointers/testing/test-cases.mdx +++ b/packages/web/docs/implementation-guides/pointers/testing/test-cases.mdx @@ -7,6 +7,21 @@ import TestedPointer from "./TestedPointer"; # Test cases +This reference implementation currently defines the following integration test +cases. + +Test cases are aggregated into the `observeTraceTests` variable: + + sourceFile.getVariableStatement("observeTraceTests")} + /> + +See the [Test code](/docs/implementation-guides/pointers/testing/summary#test-code) +heading on this section's summary page for how this variable is used by the +[Jest](https://jestjs.io) testing framework. + ## Storage strings Representing a Solidity `string storage` using an **ethdebug/format/pointer**