Skip to main content

Data and machines

The @ethdebug/pointers package includes two abstractions that it uses for representing low-level concerns:

  • class Data extends JavaScript's Uint8Array to represent sequences of bytes and allow conversion to/from different formats (like hex strings)
  • interface Machine defines an asynchronous API to model a running EVM. It includes the sole method trace(): AsyncIterable<Machine.State>, where the Machine.State type represents point-in-time access to a running EVM at a particular program counter in a particular execution context, etc.

Data

tip

Note how this implementation handles the conversion to/from JavaScript's number and bigint types. The EVM orders numerical values' bytes by starting with the most significant byte first (i.e., they are big-endian).

When implementing this functionality, be careful not to get this wrong.

Code listing for src/data.ts
src/data.ts
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([]);
}

static fromUint(value: bigint): Data {
if (value === 0n) {
return this.zero();
}

const byteCount = Math.ceil(Number(value.toString(2).length) / 8);
const bytes = new Uint8Array(byteCount);
for (let i = byteCount - 1; i >= 0; i--) {
bytes[i] = Number(value & 0xffn);
value >>= 8n;
}
return new Data(bytes);
}

static fromNumber(value: number): Data {
const byteCount = Math.ceil(Math.log2(value + 1) / 8);
const bytes = new Uint8Array(byteCount);
for (let i = byteCount - 1; i >= 0; i--) {
bytes[i] = value & 0xff;
value >>= 8;
}
return new Data(bytes);
}

static fromHex(hex: string): Data {
if (!hex.startsWith('0x')) {
throw new Error('Invalid hex string format. Expected "0x" prefix.');
}
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);
}
return new Data(bytes);
}

static fromBytes(bytes: Uint8Array): Data {
return new Data(bytes);
}

asUint(): bigint {
const bits = 8n;

let value = 0n;
for (const byte of this.values()) {
const byteValue = BigInt(byte)
value = (value << bits) + byteValue
}
return value;
}

toHex(): string {
return `0x${toHex(this)}`;
}

padUntilAtLeast(length: number): Data {
if (this.length >= length) {
return this;
}

const padded = new Uint8Array(length);
padded.set(this, length - this.length);
return Data.fromBytes(padded);
}

resizeTo(length: number): Data {
if (this.length === length) {
return this;
}

const resized = new Uint8Array(length);

if (this.length < length) {
resized.set(this, length - this.length);
} else {
resized.set(this.slice(this.length - length));
}

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);
}

inspect(
depth: number,
options: Util.InspectOptionsStylized,
inspect: typeof Util.inspect
): string {
return `Data[${options.stylize(this.toHex(), "number")}]`;
}

[
util && "inspect" in util && typeof util.inspect === "object"
? util.inspect.custom
: "_inspect"
](
depth: number,
options: Util.InspectOptionsStylized,
inspect: typeof Util.inspect
): string {
return this.inspect(depth, options, inspect);
}

}

Machine

Code listing for src/machine.ts
src/machine.ts
import type { Data } from "./data.js";

export interface Machine {
trace(): AsyncIterable<Machine.State>;
}

export namespace Machine {

export interface State {
get traceIndex(): Promise<bigint>;
get programCounter(): Promise<bigint>;
get opcode(): Promise<string>;

get stack(): State.Stack;
get memory(): State.Bytes;
get storage(): State.Words;
get calldata(): State.Bytes;
get returndata(): State.Bytes;
get transient(): State.Words;
get code(): State.Bytes;
}

export namespace State {
export interface Slice {
offset: bigint;
length: bigint;
}

export interface Stack {
get length(): Promise<bigint>;

/** read element at position from top of stack */
peek(options: {
depth: bigint;
slice?: Slice;
}): Promise<Data>;
}

export interface Bytes {
get length(): Promise<bigint>;

read(options: { slice: Slice }): Promise<Data>;
}

export interface Words {
read(options: { slot: Data; slice?: Slice }): Promise<Data>;
}
}
}