Data and machines
The @ethdebug/pointers package includes two abstractions that it uses for representing low-level concerns:
class Data
extends JavaScript'sUint8Array
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 methodtrace(): AsyncIterable<Machine.State>
, where theMachine.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>;
}
}
}