diff --git a/packages/bugc-react/CHANGELOG.md b/packages/bugc-react/CHANGELOG.md index 4881e8c18d..87e9211520 100644 --- a/packages/bugc-react/CHANGELOG.md +++ b/packages/bugc-react/CHANGELOG.md @@ -7,6 +7,11 @@ specification itself are tracked in the root ## Unreleased +### Changed + +- The IR and CFG views show each operand of a `hash` instruction, which + now has a list of `values` ([#365]). + ## 0.1.0-preview.1 — 2026-10-03 Updated `@ethdebug/bugc` to `0.1.0-preview.1`. @@ -34,3 +39,4 @@ First publication. [#299]: https://github.com/ethdebug/format/pull/299 [#300]: https://github.com/ethdebug/format/pull/300 +[#365]: https://github.com/ethdebug/format/pull/365 diff --git a/packages/bugc-react/src/components/CfgView.tsx b/packages/bugc-react/src/components/CfgView.tsx index dba9b592a7..7bca09fb1e 100644 --- a/packages/bugc-react/src/components/CfgView.tsx +++ b/packages/bugc-react/src/components/CfgView.tsx @@ -354,7 +354,7 @@ function CfgViewContent({ ir }: CfgViewProps): JSX.Element { } } case "hash": - return `${inst.dest} = keccak256(${formatValue(inst.value)})`; + return `${inst.dest} = keccak256(${inst.values.map(formatValue).join(", ")})`; case "cast": return `${inst.dest} = cast ${formatValue(inst.value)} to ${inst.targetType.kind}`; case "compute_slot": { diff --git a/packages/bugc-react/src/components/IrView.tsx b/packages/bugc-react/src/components/IrView.tsx index fd38f1e1bc..725498f644 100644 --- a/packages/bugc-react/src/components/IrView.tsx +++ b/packages/bugc-react/src/components/IrView.tsx @@ -160,7 +160,10 @@ function InstructionRenderer({ case "hash": add(`${formatDest(instruction.dest)} = hash `); - addOperand("value", formatValue(instruction.value)); + instruction.values.forEach((value, index) => { + if (index > 0) add(", "); + addOperand("value", formatValue(value)); + }); break; case "cast": diff --git a/packages/bugc-react/src/utils/irDebugUtils.ts b/packages/bugc-react/src/utils/irDebugUtils.ts index 11bdcc7678..2d04dff01c 100644 --- a/packages/bugc-react/src/utils/irDebugUtils.ts +++ b/packages/bugc-react/src/utils/irDebugUtils.ts @@ -90,10 +90,6 @@ export function extractInstructionDebug( operands.push({ label: "operand", debug: instruction.operandDebug }); break; - case "hash": - operands.push({ label: "value", debug: instruction.valueDebug }); - break; - case "cast": operands.push({ label: "value", debug: instruction.valueDebug }); break; diff --git a/packages/bugc/CHANGELOG.md b/packages/bugc/CHANGELOG.md index f7b5d6634e..6596df1860 100644 --- a/packages/bugc/CHANGELOG.md +++ b/packages/bugc/CHANGELOG.md @@ -9,6 +9,14 @@ support. Changes to the specification itself are tracked in the root ### Added +- `keccak256` over value types: `keccak256(a, b, ...)` hashes the 32-byte + words of one or more integers, `address`, `bool` or `bytesN` values, in + order. For integers, `address` and `bool` this equals Solidity's + `keccak256(abi.encode(a, b, ...))`. A `bytesN` narrower than 32 bytes + hashes its word with its bytes at the right end, unlike `abi.encode`; + cast it to `bytes32` to match Solidity. A single dynamic `bytes` or + `string` argument still hashes its data, and must be the only argument. + The optimizer folds a hash of constant words at levels 1 to 3 ([#365]). - The `%` operator, with the precedence of `*` and `/`. It compiles to the EVM's `MOD` (`SMOD` for signed operands), so `x % 0` is `0`, as `x / 0` is ([#321]). @@ -28,6 +36,8 @@ support. Changes to the specification itself are tracked in the root ### Changed +- The IR `hash` instruction now has `values`, a list, in place of + `value` and `valueDebug` ([#365]). - An integer literal operand of an arithmetic or comparison operator now takes the type of the other operand when its value fits, so with `x: int8`, `x < 0`, `x == 1` and `-1 < x` compare as `int8`. A literal @@ -300,4 +310,5 @@ First publication. [#349]: https://github.com/ethdebug/format/pull/349 [#351]: https://github.com/ethdebug/format/pull/351 [#352]: https://github.com/ethdebug/format/pull/352 +[#365]: https://github.com/ethdebug/format/pull/365 [#356]: https://github.com/ethdebug/format/pull/356 diff --git a/packages/bugc/examples/README.md b/packages/bugc/examples/README.md index 197a50dbc9..f43aacb30a 100644 --- a/packages/bugc/examples/README.md +++ b/packages/bugc/examples/README.md @@ -143,3 +143,28 @@ account.isActive = true; accounts[user].balance = accounts[user].balance + 100; accounts[user].isActive = true; ``` + +## Hashing + +`keccak256` has two forms. The compiler tells them apart by the type of +the argument: + +- **One dynamic `bytes` or `string`** hashes its data: + `keccak256("transfer(address,uint256)")`. +- **One or more value types** (integers, `address`, `bool`, `bytesN`) + hashes their 32-byte words, concatenated in order. This equals + Solidity's `keccak256(abi.encode(a, b, ...))` for integers, `address` + and `bool`: + +```bug +// a toy roll: 2 in 3 is a hit +let hit = (keccak256(block.number, msg.sender) as uint256) % 3 != 0; +``` + +A `bytes` or `string` argument must be the only one. + +A `bytesN` narrower than 32 bytes hashes its full word, with its bytes +at the low-order (right) end, as BUG holds it. Solidity's `abi.encode` +puts a `bytesN` value's bytes at the high-order (left) end, so for +`bytes4` and the like the hashes differ from Solidity's. Cast to +`bytes32` first (which puts the bytes at the left end) to match it. diff --git a/packages/bugc/src/evmgen/analysis/liveness.ts b/packages/bugc/src/evmgen/analysis/liveness.ts index 3a92c67542..8d56bbe90f 100644 --- a/packages/bugc/src/evmgen/analysis/liveness.ts +++ b/packages/bugc/src/evmgen/analysis/liveness.ts @@ -275,7 +275,7 @@ function getUsedValues(inst: Ir.Instruction): Set { } break; case "hash": - addValue(inst.value); + inst.values.forEach(addValue); break; case "cast": addValue(inst.value); diff --git a/packages/bugc/src/evmgen/analysis/memory.ts b/packages/bugc/src/evmgen/analysis/memory.ts index 194fe13dfa..24c9b43198 100644 --- a/packages/bugc/src/evmgen/analysis/memory.ts +++ b/packages/bugc/src/evmgen/analysis/memory.ts @@ -248,10 +248,14 @@ function simulateInstruction(stack: string[], inst: Ir.Instruction): string[] { // Pop consumed values based on instruction type switch (inst.kind) { case "binary": - case "hash": newStack.pop(); // Two operands newStack.pop(); break; + case "hash": + for (const _ of inst.values) { + newStack.pop(); + } + break; case "compute_slot": // Depends on kind newStack.pop(); // base @@ -355,7 +359,7 @@ function getUsedValues(inst: Ir.Instruction): Set { addValue(inst.object); break; case "hash": - addValue(inst.value); + inst.values.forEach(addValue); break; case "assert": addValue(inst.condition); diff --git a/packages/bugc/src/evmgen/behavioral.test.ts b/packages/bugc/src/evmgen/behavioral.test.ts index 66913f8e72..dd8e9c7faa 100644 --- a/packages/bugc/src/evmgen/behavioral.test.ts +++ b/packages/bugc/src/evmgen/behavioral.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from "vitest"; import { keccak256 } from "ethereum-cryptography/keccak"; import { bytesToHex } from "ethereum-cryptography/utils"; +import { compile } from "#compiler"; import { executeProgram } from "#test/evm/behavioral"; describe("behavioral tests", () => { @@ -1289,6 +1290,128 @@ code { } }); + describe("keccak256 over words", () => { + // Solidity's keccak256(abi.encode(...)) for value types: each value + // is one 32-byte word, in order + const word = (value: bigint) => + BigInt.asUintN(256, value).toString(16).padStart(64, "0"); + const hashOfWords = (...values: bigint[]) => + BigInt( + "0x" + + bytesToHex( + keccak256( + Uint8Array.from(Buffer.from(values.map(word).join(""), "hex")), + ), + ), + ); + + const sender = 1n; // @ethdebug/evm calls from address 0x00..01 + + const hashes: Record = { + "two uint256 constants": [ + `out = keccak256(7, 9) as uint256;`, + hashOfWords(7n, 9n), + ], + "a uint256 and an address": [ + `let n: uint256 = 42; + out = keccak256(n, msg.sender) as uint256;`, + hashOfWords(42n, sender), + ], + "storage values": [ + `out = keccak256(a, b) as uint256;`, + hashOfWords(1071n, 462n), + ], + "narrow integers and a bool": [ + `let x: uint8 = 200; + let t = true; + out = keccak256(x, t) as uint256;`, + hashOfWords(200n, 1n), + ], + "a negative int8": [ + `let x = (0 as int8) - 3; + out = keccak256(x, a) as uint256;`, + hashOfWords(-3n, 1071n), + ], + "a bytes32 and three words": [ + `let h: bytes32 = + 0x1122334400000000000000000000000000000000000000000000000000000000; + out = keccak256(h, a, b) as uint256;`, + hashOfWords( + 0x1122334400000000000000000000000000000000000000000000000000000000n, + 1071n, + 462n, + ), + ], + // Not as in Solidity, which left-aligns bytesN in abi.encode + "a bytes4, by its right-aligned word": [ + `let s: bytes4 = 0xaabbccdd; + out = keccak256(s, a) as uint256;`, + hashOfWords(0xaabbccddn, 1071n), + ], + "a bytes4 cast to bytes32, as Solidity encodes it": [ + `let s: bytes4 = 0xaabbccdd; + out = keccak256(s as bytes32, a) as uint256;`, + hashOfWords(0xaabbccddn << 224n, 1071n), + ], + "one uint256": [`out = keccak256(a) as uint256;`, hashOfWords(1071n)], + "a roll": [ + `if ((keccak256(a, msg.sender) as uint256) % 3 != 0) { out = 1; }`, + hashOfWords(1071n, sender) % 3n !== 0n ? 1n : 0n, + ], + }; + + for (const [name, [body, expected]] of Object.entries(hashes)) { + for (const level of [0, 1, 2, 3] as const) { + it(`should hash ${name} (level ${level})`, async () => { + const source = `name HashWords; +storage { + [0] a: uint256; + [1] b: uint256; + [2] out: uint256; +} +create { a = 1071; b = 462; } +code { + ${body} +}`; + const result = await executeProgram(source, { + calldata: "", + optimizationLevel: level, + }); + + expect(result.callSuccess).toBe(true); + expect(await result.getStorage(2n)).toBe(expected); + }); + } + } + + for (const level of [1, 2, 3] as const) { + it(`should fold a hash of constant words (level ${level})`, async () => { + const result = await compile({ + to: "ir", + source: `name Fold; +storage { [0] out: uint256; } +code { out = keccak256(7, 9) as uint256; }`, + optimizer: { level }, + }); + if (!result.success) throw new Error("compile failed"); + + const instructions = [...result.value.ir.main.blocks.values()].flatMap( + (block) => block.instructions, + ); + expect(instructions.some(({ kind }) => kind === "hash")).toBe(false); + expect(instructions).toContainEqual( + expect.objectContaining({ + kind: "write", + value: expect.objectContaining({ + kind: "const", + value: hashOfWords(7n, 9n), + }), + }), + ); + }); + } + }); + describe("modulo", () => { const program = (expr: string) => `name Modulo; diff --git a/packages/bugc/src/evmgen/generation/instructions/hash.ts b/packages/bugc/src/evmgen/generation/instructions/hash.ts index ce5fe7835d..d1cbcb1a68 100644 --- a/packages/bugc/src/evmgen/generation/instructions/hash.ts +++ b/packages/bugc/src/evmgen/generation/instructions/hash.ts @@ -2,26 +2,64 @@ import type * as Ir from "#ir"; import type { Stack } from "#evm"; import { type Transition, pipe, operations } from "#evmgen/operations"; +import { Memory } from "#evmgen/analysis"; import { loadValue, storeValueIfNeeded } from "../values/index.js"; -const { PUSHn, MSTORE, KECCAK256 } = operations; +const { PUSHn, MSTORE, MLOAD, ADD, KECCAK256 } = operations; + +const freeMemoryPointer = BigInt(Memory.regions.FREE_MEMORY_POINTER); /** - * Generate code for hash operations + * Generate code for hash operations: keccak256 of the values' 32-byte + * words, in order. One or two words go in the scratch space at 0x00; + * more go in the free memory, which they do not allocate. */ export function generateHashOp( inst: Ir.Instruction.Hash, ): Transition { const debug = inst.operationDebug; + const size = BigInt(inst.values.length * 32); + + if (inst.values.length <= 2) { + const stores = inst.values.map((value, index) => + pipe() + .then(loadValue(value, { debug })) + .then(PUSHn(BigInt(index * 32), { debug }), { as: "offset" }) + .then(MSTORE({ debug })) + .done(), + ); + return pipe() + .then(sequence(stores)) + .then(PUSHn(size, { debug }), { as: "size" }) + .then(PUSHn(0n, { debug }), { as: "offset" }) + .then(KECCAK256({ debug }), { as: "value" }) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done(); + } + const stores = inst.values.map((value, index) => + pipe() + .then(loadValue(value, { debug })) + .then(PUSHn(BigInt(index * 32), { debug }), { as: "b" }) + .then(PUSHn(freeMemoryPointer, { debug }), { as: "offset" }) + .then(MLOAD({ debug }), { as: "a" }) + .then(ADD({ debug }), { as: "offset" }) + .then(MSTORE({ debug })) + .done(), + ); return pipe() - .then(loadValue(inst.value, { debug })) - .then(PUSHn(0n, { debug }), { as: "offset" }) - .then(MSTORE({ debug })) - .then(PUSHn(32n, { debug }), { as: "size" }) - .then(PUSHn(0n, { debug }), { as: "offset" }) + .then(sequence(stores)) + .then(PUSHn(size, { debug }), { as: "size" }) + .then(PUSHn(freeMemoryPointer, { debug }), { as: "offset" }) + .then(MLOAD({ debug }), { as: "offset" }) .then(KECCAK256({ debug }), { as: "value" }) .then(storeValueIfNeeded(inst.dest, { debug })) .done(); } + +function sequence( + steps: Transition[], +): Transition { + return (state) => steps.reduce((current, step) => step(current), state); +} diff --git a/packages/bugc/src/ir/analysis/formatter.ts b/packages/bugc/src/ir/analysis/formatter.ts index 0fea3d5570..3b848ac872 100644 --- a/packages/bugc/src/ir/analysis/formatter.ts +++ b/packages/bugc/src/ir/analysis/formatter.ts @@ -165,7 +165,7 @@ export class Formatter { return `${destWithType(inst.dest)} = env ${inst.op}`; case "hash": - return `${destWithType(inst.dest)} = hash ${this.formatValue(inst.value)}`; + return `${destWithType(inst.dest)} = hash ${inst.values.map((value) => this.formatValue(value)).join(", ")}`; case "cast": return `${destWithType(inst.dest, inst.targetType)} = cast ${this.formatValue(inst.value)} to ${this.formatType(inst.targetType)}`; diff --git a/packages/bugc/src/ir/analysis/validator.ts b/packages/bugc/src/ir/analysis/validator.ts index 51e819f043..9ca9804359 100644 --- a/packages/bugc/src/ir/analysis/validator.ts +++ b/packages/bugc/src/ir/analysis/validator.ts @@ -268,10 +268,10 @@ export class Validator { this.tempDefs.add(inst.dest); } - if (!inst.value) { + if (!inst.values?.length) { this.error("Hash instruction must have a value"); } else { - this.validateValue(inst.value); + inst.values.forEach((value) => this.validateValue(value)); } } diff --git a/packages/bugc/src/ir/spec/instruction.ts b/packages/bugc/src/ir/spec/instruction.ts index ddc5b6b6ee..f012bc433f 100644 --- a/packages/bugc/src/ir/spec/instruction.ts +++ b/packages/bugc/src/ir/spec/instruction.ts @@ -388,10 +388,14 @@ export namespace Instruction { dest: string; } + /** + * keccak256 of the 32-byte words of `values`, in order. A single + * memory reference (dynamic `bytes` or a `string`) instead hashes + * the data it refers to. + */ export interface Hash extends Instruction.Base { kind: "hash"; - value: Value; - valueDebug?: Instruction.Debug; + values: Value[]; dest: string; } diff --git a/packages/bugc/src/ir/utils/debug.ts b/packages/bugc/src/ir/utils/debug.ts index 0f171abb83..3b7ea32895 100644 --- a/packages/bugc/src/ir/utils/debug.ts +++ b/packages/bugc/src/ir/utils/debug.ts @@ -294,7 +294,6 @@ export function extractSubInstructionContexts( } break; - case "hash": case "cast": if (instruction.valueDebug?.context) { contexts.push(instruction.valueDebug.context); @@ -364,6 +363,8 @@ export function extractSubInstructionContexts( addValueDebug(instruction.size); break; case "hash": + instruction.values.forEach(addValueDebug); + break; case "cast": addValueDebug(instruction.value); break; diff --git a/packages/bugc/src/irgen/generate/expressions/call.ts b/packages/bugc/src/irgen/generate/expressions/call.ts index c1c0e4214e..2e6db74663 100644 --- a/packages/bugc/src/irgen/generate/expressions/call.ts +++ b/packages/bugc/src/irgen/generate/expressions/call.ts @@ -27,10 +27,10 @@ export const makeBuildCall = ( (expr.callee as Ast.Expression.Identifier).name === "keccak256" ) { // keccak256 built-in function - if (expr.arguments.length !== 1) { + if (expr.arguments.length === 0) { yield* Process.Errors.report( new IrgenError( - "keccak256 expects exactly 1 argument", + "keccak256 expects at least 1 argument", expr.loc ?? undefined, Severity.Error, ), @@ -38,10 +38,11 @@ export const makeBuildCall = ( return Ir.Value.constant(0n, Ir.Type.Scalar.bytes32); } - // Evaluate the argument - const argValue = yield* buildExpression(expr.arguments[0], { - kind: "rvalue", - }); + // Evaluate the arguments, in order + const values: Ir.Value[] = []; + for (const argument of expr.arguments) { + values.push(yield* buildExpression(argument, { kind: "rvalue" })); + } // Generate hash instruction const resultType: Ir.Type = Ir.Type.Scalar.bytes32; @@ -49,7 +50,7 @@ export const makeBuildCall = ( yield* Process.Instructions.emit({ kind: "hash", - value: argValue, + values, dest: resultTemp, operationDebug: yield* Process.Debug.forAstNode(expr), } as Ir.Instruction); diff --git a/packages/bugc/src/optimizer/steps/common-subexpression-elimination.ts b/packages/bugc/src/optimizer/steps/common-subexpression-elimination.ts index b3fc8311f3..bc66fdf5f4 100644 --- a/packages/bugc/src/optimizer/steps/common-subexpression-elimination.ts +++ b/packages/bugc/src/optimizer/steps/common-subexpression-elimination.ts @@ -236,6 +236,8 @@ export class CommonSubexpressionEliminationStep extends BaseOptimizationStep { result.size = replaceValue(result.size); break; case "hash": + result.values = result.values.map(replaceValue); + break; case "cast": result.value = replaceValue(result.value); break; diff --git a/packages/bugc/src/optimizer/steps/constant-folding.test.ts b/packages/bugc/src/optimizer/steps/constant-folding.test.ts index 3486c037fd..e3226fd968 100644 --- a/packages/bugc/src/optimizer/steps/constant-folding.test.ts +++ b/packages/bugc/src/optimizer/steps/constant-folding.test.ts @@ -42,7 +42,7 @@ describe("ConstantFoldingStep", () => { }, { kind: "hash", - value: { kind: "temp", id: "t0", type: Ir.Type.Scalar.uint256 }, + values: [{ kind: "temp", id: "t0", type: Ir.Type.Scalar.uint256 }], dest: "t1", operationDebug: {}, }, @@ -72,15 +72,14 @@ describe("ConstantFoldingStep", () => { it("should not fold keccak256 on non-constant values", () => { const module = createTestModule([ { - kind: "const", - value: 123n, - type: Ir.Type.Scalar.uint256, + kind: "env", + op: "msg_sender", dest: "t0", operationDebug: {}, }, { kind: "hash", - value: { kind: "temp", id: "t0", type: Ir.Type.Scalar.uint256 }, + values: [{ kind: "temp", id: "t0", type: Ir.Type.Scalar.uint256 }], dest: "t1", operationDebug: {}, }, @@ -99,7 +98,7 @@ describe("ConstantFoldingStep", () => { expect(block.instructions).toHaveLength(2); expect(block.instructions[1]).toMatchObject({ kind: "hash", - value: { kind: "temp", id: "t0" }, + values: [{ kind: "temp", id: "t0" }], dest: "t1", }); }); @@ -115,7 +114,7 @@ describe("ConstantFoldingStep", () => { }, { kind: "hash", - value: { kind: "temp", id: "t0", type: Ir.Type.Scalar.uint256 }, + values: [{ kind: "temp", id: "t0", type: Ir.Type.Scalar.uint256 }], dest: "t1", operationDebug: {}, }, @@ -128,7 +127,7 @@ describe("ConstantFoldingStep", () => { }, { kind: "hash", - value: { kind: "temp", id: "t2", type: Ir.Type.Scalar.uint256 }, + values: [{ kind: "temp", id: "t2", type: Ir.Type.Scalar.uint256 }], dest: "t3", operationDebug: {}, }, diff --git a/packages/bugc/src/optimizer/steps/constant-folding.ts b/packages/bugc/src/optimizer/steps/constant-folding.ts index e70dcc885b..28e617cc99 100644 --- a/packages/bugc/src/optimizer/steps/constant-folding.ts +++ b/packages/bugc/src/optimizer/steps/constant-folding.ts @@ -1,4 +1,5 @@ import { keccak256 } from "ethereum-cryptography/keccak"; +import { concatBytes, hexToBytes } from "ethereum-cryptography/utils"; import * as Ir from "#ir"; @@ -304,23 +305,44 @@ export class ConstantFoldingStep extends BaseOptimizationStep { constants: Map, ): boolean { if (inst.kind !== "hash") return false; + return this.hashInput(inst, constants) !== undefined; + } + + /** + * The bytes a hash of constants hashes: a single string's bytes, or + * the 32-byte words of its values, as code generation hashes them + */ + private hashInput( + inst: Ir.Instruction.Hash, + constants: Map, + ): Uint8Array | undefined { + const values = inst.values.map((value) => + this.getConstantValue(value, constants), + ); - const inputValue = this.getConstantValue(inst.value, constants); - // We can only fold if the input is a constant string - return typeof inputValue === "string"; + if (values.length === 1 && typeof values[0] === "string") { + return new TextEncoder().encode(values[0]); + } + + const words: Uint8Array[] = []; + for (const value of values) { + if (typeof value !== "bigint" && typeof value !== "boolean") { + return undefined; + } + const word = Ir.Utils.toWord( + typeof value === "boolean" ? (value ? 1n : 0n) : value, + ); + words.push(hexToBytes(word.toString(16).padStart(64, "0"))); + } + return concatBytes(...words); } private foldHash( inst: Ir.Instruction & { kind: "hash" }, constants: Map, ): Ir.Instruction | null { - const inputValue = this.getConstantValue(inst.value, constants); - - if (typeof inputValue !== "string") return null; - - // Convert string to bytes - const encoder = new TextEncoder(); - const inputBytes = encoder.encode(inputValue); + const inputBytes = this.hashInput(inst, constants); + if (!inputBytes) return null; // Compute keccak256 hash const hashBytes = keccak256(inputBytes); diff --git a/packages/bugc/src/optimizer/steps/constant-propagation.ts b/packages/bugc/src/optimizer/steps/constant-propagation.ts index 8f4c000f08..4367a689cb 100644 --- a/packages/bugc/src/optimizer/steps/constant-propagation.ts +++ b/packages/bugc/src/optimizer/steps/constant-propagation.ts @@ -129,7 +129,7 @@ export class ConstantPropagationStep extends BaseOptimizationStep { } break; case "hash": - result.value = propagateValue(result.value); + result.values = result.values.map(propagateValue); break; case "cast": result.value = propagateValue(result.value); diff --git a/packages/bugc/src/optimizer/steps/dead-code-elimination.ts b/packages/bugc/src/optimizer/steps/dead-code-elimination.ts index da5a55d6e7..53cafaa086 100644 --- a/packages/bugc/src/optimizer/steps/dead-code-elimination.ts +++ b/packages/bugc/src/optimizer/steps/dead-code-elimination.ts @@ -127,7 +127,9 @@ export class DeadCodeEliminationStep extends BaseOptimizationStep { } break; case "hash": - this.collectValueUse(inst.value, used); + for (const value of inst.values) { + this.collectValueUse(value, used); + } break; case "cast": this.collectValueUse(inst.value, used); diff --git a/packages/bugc/src/typechecker/checker.test.ts b/packages/bugc/src/typechecker/checker.test.ts index 106fb2f5b7..ed7b6eff74 100644 --- a/packages/bugc/src/typechecker/checker.test.ts +++ b/packages/bugc/src/typechecker/checker.test.ts @@ -640,4 +640,49 @@ describe("checkProgram", () => { expect(Result.hasMessages(result)).toBe(false); }); }); + describe("keccak256", () => { + const program = (expression: string) => ` + name Test; + storage { [0] h: bytes32; } + code { + let n: uint256 = 1; + let s = "x"; + h = ${expression}; + } + `; + + const accepted = { + "one string": `keccak256("x")`, + "one dynamic bytes": `keccak256(msg.data)`, + "one value type": `keccak256(n)`, + "several value types": `keccak256(n, msg.sender, true, h, 0x01)`, + }; + for (const [name, expression] of Object.entries(accepted)) { + it(`should accept ${name}`, () => { + const result = check(program(expression)); + expect(result.success).toBe(true); + }); + } + + const rejected = { + "no arguments": [`keccak256()`, "keccak256 expects at least 1 argument"], + "a string among several arguments": [ + `keccak256(n, s)`, + "keccak256 of several arguments takes only value types; " + + "bytes or string must be its only argument", + ], + "dynamic bytes among several arguments": [ + `keccak256(msg.data, n)`, + "keccak256 of several arguments takes only value types; " + + "bytes or string must be its only argument", + ], + }; + for (const [name, [expression, message]] of Object.entries(rejected)) { + it(`should reject ${name}`, () => { + const result = check(program(expression)); + expect(result.success).toBe(false); + expect(result).toHaveMessage({ severity: Severity.Error, message }); + }); + } + }); }); diff --git a/packages/bugc/src/typechecker/expressions.ts b/packages/bugc/src/typechecker/expressions.ts index d01fa9c74b..61cea3afb2 100644 --- a/packages/bugc/src/typechecker/expressions.ts +++ b/packages/bugc/src/typechecker/expressions.ts @@ -569,9 +569,12 @@ export const expressionChecker: Pick, "expression"> = { // Handle keccak256 built-in function if (functionName === "keccak256") { - if (node.arguments.length !== 1) { + // keccak256 takes one dynamic `bytes` or `string`, and hashes + // its data; or one or more value types, and hashes their + // words in order, as Solidity's keccak256(abi.encode(...)) + if (node.arguments.length === 0) { const error = new TypeError( - "keccak256 expects exactly 1 argument", + "keccak256 expects at least 1 argument", node.loc || undefined, undefined, undefined, @@ -581,39 +584,51 @@ export const expressionChecker: Pick, "expression"> = { return { symbols, nodeTypes, bindings, errors }; } - const argContext: Context = { - ...context, - nodeTypes, - symbols, - bindings, - }; - const argResult = Ast.visit( - context.visitor, - node.arguments[0], - argContext, - ); - nodeTypes = argResult.nodeTypes; - symbols = argResult.symbols; - bindings = argResult.bindings; - errors.push(...argResult.errors); + const argTypes: (Type | undefined)[] = []; + for (const argument of node.arguments) { + const argContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const argResult = Ast.visit(context.visitor, argument, argContext); + nodeTypes = argResult.nodeTypes; + symbols = argResult.symbols; + bindings = argResult.bindings; + errors.push(...argResult.errors); + argTypes.push(argResult.type); + } - if (!argResult.type) { + if (argTypes.some((type) => !type)) { return { symbols, nodeTypes, bindings, errors }; } - // keccak256 accepts bytes types and strings - if ( - !Type.Elementary.isBytes(argResult.type) && - !Type.Elementary.isString(argResult.type) - ) { - const error = new TypeError( - "keccak256 argument must be bytes or string type", - node.arguments[0].loc || undefined, - undefined, - undefined, - ErrorCode.TYPE_MISMATCH, - ); - errors.push(error); + const isData = (type: Type) => + Type.isElementary(type) && + ((Type.Elementary.isBytes(type) && type.size === undefined) || + Type.Elementary.isString(type)); + const isWord = (type: Type) => + Type.isElementary(type) && !isData(type); + + const single = argTypes.length === 1 && isData(argTypes[0]!); + const invalid = single + ? [] + : node.arguments.filter((_, index) => !isWord(argTypes[index]!)); + if (invalid.length > 0) { + for (const argument of invalid) { + const error = new TypeError( + argTypes.length === 1 + ? "keccak256 argument must be a value type, bytes or string" + : "keccak256 of several arguments takes only value types; " + + "bytes or string must be its only argument", + argument.loc || undefined, + undefined, + undefined, + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + } return { symbols, nodeTypes, bindings, errors }; }