diff --git a/packages/format/src/types/pointer/pointer.ts b/packages/format/src/types/pointer/pointer.ts index 594f8319..76661f58 100644 --- a/packages/format/src/types/pointer/pointer.ts +++ b/packages/format/src/types/pointer/pointer.ts @@ -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 => @@ -236,6 +237,7 @@ export namespace Pointer { Expression.isLookup, Expression.isRead, Expression.isKeccak256, + Expression.isConcat, Expression.isResize ].some(guard => guard(value)); @@ -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 = | Resize.ToNumber | Resize.ToWordsize; diff --git a/packages/pointers/src/evaluate.test.ts b/packages/pointers/src/evaluate.test.ts index 6901ef23..57be5051 100644 --- a/packages/pointers/src/evaluate.test.ts +++ b/packages/pointers/src/evaluate.test.ts @@ -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 = { diff --git a/packages/pointers/src/evaluate.ts b/packages/pointers/src/evaluate.ts index 4d39b237..eb2519ca 100644 --- a/packages/pointers/src/evaluate.ts +++ b/packages/pointers/src/evaluate.ts @@ -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); } @@ -219,6 +223,17 @@ async function evaluateKeccak256( return hash; } +async function evaluateConcat( + expression: Pointer.Expression.Concat, + options: EvaluateOptions +): Promise { + 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 diff --git a/packages/web/docs/implementation-guides/pointers/evaluating-expressions.mdx b/packages/web/docs/implementation-guides/pointers/evaluating-expressions.mdx index 01d5698e..73beaf19 100644 --- a/packages/web/docs/implementation-guides/pointers/evaluating-expressions.mdx +++ b/packages/web/docs/implementation-guides/pointers/evaluating-expressions.mdx @@ -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. + + sourceFile.getFunction("evaluateConcat") + } /> + ## Evaluating property lookups Pointer expressions can compose values taken from the properties of other, diff --git a/packages/web/spec/pointer/expression.mdx b/packages/web/spec/pointer/expression.mdx index 05f90c4d..29a010b8 100644 --- a/packages/web/spec/pointer/expression.mdx +++ b/packages/web/spec/pointer/expression.mdx @@ -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. + + + ## Resize operations In certain situations, e.g. keccak256 hashes, it's crucially important to be diff --git a/packages/web/src/schemas.ts b/packages/web/src/schemas.ts index cdeff995..6ad81eba 100644 --- a/packages/web/src/schemas.ts +++ b/packages/web/src/schemas.ts @@ -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" diff --git a/schemas/pointer/expression.schema.yaml b/schemas/pointer/expression.schema.yaml index f2410fb4..88296249 100644 --- a/schemas/pointer/expression.schema.yaml +++ b/schemas/pointer/expression.schema.yaml @@ -13,6 +13,7 @@ oneOf: - $ref: "#/$defs/Lookup" - $ref: "#/$defs/Read" - $ref: "#/$defs/Keccak256" + - $ref: "#/$defs/Concat" - $ref: "#/$defs/Resize" $defs: @@ -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: |