Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/format/src/types/pointer/pointer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export namespace Pointer {
| Expression.Lookup
| Expression.Read
| Expression.Keccak256
| Expression.Concat
| Expression.Resize;

export const isExpression = (value: unknown): value is Expression =>
Expand All @@ -236,6 +237,7 @@ export namespace Pointer {
Expression.isLookup,
Expression.isRead,
Expression.isKeccak256,
Expression.isConcat,
Expression.isResize
].some(guard => guard(value));

Expand Down Expand Up @@ -394,6 +396,12 @@ export namespace Pointer {
export const isKeccak256 =
makeIsOperation<"$keccak256", Keccak256>("$keccak256", isOperands);

export interface Concat {
$concat: Expression[];
}
export const isConcat =
makeIsOperation<"$concat", Concat>("$concat", isOperands);

export type Resize<N extends number = number> =
| Resize.ToNumber<N>
| Resize.ToWordsize;
Expand Down
63 changes: 63 additions & 0 deletions packages/pointers/src/evaluate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,69 @@ describe("evaluate", () => {
.toEqual(Data.fromUint(42n % 0x1fn));
});

describe("evaluates concat expressions", () => {
it("concatenates hex literals", async () => {
const expression: Pointer.Expression = {
$concat: ["0x00", "0x00"]
};
expect(await evaluate(expression, options))
.toEqual(Data.fromHex("0x0000"));
});

it("concatenates multiple values preserving byte widths", async () => {
const expression: Pointer.Expression = {
$concat: ["0xdead", "0xbeef"]
};
expect(await evaluate(expression, options))
.toEqual(Data.fromHex("0xdeadbeef"));
});

it("returns empty data for empty operand list", async () => {
const expression: Pointer.Expression = {
$concat: []
};
expect(await evaluate(expression, options))
.toEqual(Data.zero());
});

it("preserves single operand unchanged", async () => {
const expression: Pointer.Expression = {
$concat: ["0xabcdef"]
};
expect(await evaluate(expression, options))
.toEqual(Data.fromHex("0xabcdef"));
});

it("concatenates variables", async () => {
const expression: Pointer.Expression = {
$concat: ["foo", "bar"]
};
// foo = 0x2a (42), bar = 0x1f
expect(await evaluate(expression, options))
.toEqual(Data.fromHex("0x2a1f"));
});

it("concatenates nested expressions", async () => {
const expression: Pointer.Expression = {
$concat: [
{ $sum: [1, 2] }, // 3 = 0x03
"0xff"
]
};
expect(await evaluate(expression, options))
.toEqual(Data.fromHex("0x03ff"));
});

it("preserves leading zeros in hex literals", async () => {
const expression: Pointer.Expression = {
$concat: ["0x0001", "0x0002"]
};
const result = await evaluate(expression, options);
expect(result).toEqual(Data.fromHex("0x00010002"));
expect(result.length).toBe(4);
});
});

// skipped because test does not perform proper padding
it.skip("evaluates keccak256 expressions", async () => {
const expression: Pointer.Expression = {
Expand Down
15 changes: 15 additions & 0 deletions packages/pointers/src/evaluate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ export async function evaluate(
return evaluateKeccak256(expression, options);
}

if (Pointer.Expression.isConcat(expression)) {
return evaluateConcat(expression, options);
}

if (Pointer.Expression.isResize(expression)) {
return evaluateResize(expression, options);
}
Expand Down Expand Up @@ -219,6 +223,17 @@ async function evaluateKeccak256(
return hash;
}

async function evaluateConcat(
expression: Pointer.Expression.Concat,
options: EvaluateOptions
): Promise<Data> {
const operands = await Promise.all(expression.$concat.map(
async expression => await evaluate(expression, options)
));

return Data.zero().concat(...operands);
}

async function evaluateResize(
expression: Pointer.Expression.Resize,
options: EvaluateOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,18 @@ use of hashing to allocate persistent data.
sourceFile => sourceFile.getFunction("evaluateKeccak256")
} />

## Evaluating concatenation

Byte concatenation is straightforward: recursively evaluate the operands and
join them together, preserving the byte width of each operand.

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="src/evaluate.ts"
extract={
sourceFile => sourceFile.getFunction("evaluateConcat")
} />

## Evaluating property lookups

Pointer expressions can compose values taken from the properties of other,
Expand Down
15 changes: 15 additions & 0 deletions packages/web/spec/pointer/expression.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,21 @@ hash of the concatenation of bytes specified by the list.
pointer="#/$defs/Keccak256"
/>

## Bytes concatenation

An expression can be an object of form `{ "$concat": [...] }`, indicating
that the value of the expression is the concatenation of bytes from each
value in the list. The byte width of each operand is preserved; no padding
is added or removed between operands.

This is useful for building composite byte sequences, for example when
constructing storage slot keys or preparing data for hashing.

<SchemaViewer
schema={{ id: "schema:ethdebug/format/pointer/expression" }}
pointer="#/$defs/Concat"
/>

## Resize operations

In certain situations, e.g. keccak256 hashes, it's crucially important to be
Expand Down
4 changes: 4 additions & 0 deletions packages/web/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ const pointerSchemaIndex: SchemaIndex = {
title: "Keccak256 hash expression schema",
anchor: "#keccak256-hashes"
},
Concat: {
title: "Bytes concatenation schema",
anchor: "#bytes-concatenation"
},
Resize: {
title: "Resize operation schema",
anchor: "#resize-operations"
Expand Down
27 changes: 27 additions & 0 deletions schemas/pointer/expression.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ oneOf:
- $ref: "#/$defs/Lookup"
- $ref: "#/$defs/Read"
- $ref: "#/$defs/Keccak256"
- $ref: "#/$defs/Concat"
- $ref: "#/$defs/Resize"

$defs:
Expand Down Expand Up @@ -188,6 +189,32 @@ $defs:
- 0
- "0x00"

Concat:
title: Concatenate values
description: |
An object of the form `{ "$concat": [...values] }`, indicating that this
expression evaluates to the concatenation of bytes from each value.
The byte width of each operand is preserved; no padding is added or
removed between operands.
type: object
properties:
$concat:
title: Array of values to concatenate
type: array
items:
$ref: "schema:ethdebug/format/pointer/expression"
additionalProperties: false
required:
- $concat
examples:
- $concat:
- "0x00"
- "0x00"
- $concat:
- "0xdead"
- "0xbeef"
- $concat: []

Resize:
title: Resize data
description: |
Expand Down