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
37 changes: 37 additions & 0 deletions contracts/crypto/YulSHA256.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @title YulSHA256
/// @notice Gas-efficient SHA2-256 hashing directly from calldata with zero-memory allocation.
library YulSHA256 {
/**
* @notice Computes the SHA2-256 hash of a calldata bytes array.
* @dev Copies the calldata directly to the free memory pointer temporary space
* without updating the free memory pointer at 0x40.
* The precompile output is written directly to scratch space 0x00 to avoid allocation.
* @param data The calldata bytes to hash.
* @return digest The resulting SHA2-256 hash.
*/
function hash(bytes calldata data) internal view returns (bytes32 digest) {
assembly {
// Load the free memory pointer
let ptr := mload(0x40)

// Copy calldata payload to the free memory pointer
calldatacopy(ptr, data.offset, data.length)

// Invoke SHA2-256 precompile (address 0x02)
// Input: memory offset 'ptr', size 'data.length'
// Output: directly to scratch space 0x00, size 0x20
let success := staticcall(gas(), 0x02, ptr, data.length, 0x00, 0x20)

// Revert if staticcall failed
if iszero(success) {
revert(0, 0)
}

// Load the digest from scratch space 0x00
digest := mload(0x00)
}
}
}
114 changes: 49 additions & 65 deletions test/crypto/YulMerkleVerifier.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, it, expect } from 'vitest';
import { describe, it, expect } from "vitest";

import { keccak256 as ethersKeccak256 } from "ethers";

function keccak256(hex: string): string {
const buf = Buffer.from(hex.slice(2), 'hex');
const crypto = require('crypto');
return '0x' + crypto.createHash('keccak256').update(buf).digest('hex');
return ethersKeccak256(hex);
}

function computeMerkleRoot(leaf: string, proof: string[]): string {
Expand All @@ -18,109 +18,93 @@ function computeMerkleRoot(leaf: string, proof: string[]): string {
return computed;
}

describe('YulMerkleVerifier', () => {
describe('verifyProof', () => {
it('returns true for a valid single-element proof', () => {
const leaf = '0x' + 'ab'.repeat(32);
const sibling = '0x' + 'cd'.repeat(32);
describe("YulMerkleVerifier", () => {
describe("verifyProof", () => {
it("returns true for a valid single-element proof", () => {
const leaf = "0x" + "ab".repeat(32);
const sibling = "0x" + "cd".repeat(32);
const root = computeMerkleRoot(leaf, [sibling]);

expect(
verifyProofSolidity(root, leaf, [sibling]),
).toBe(true);
expect(verifyProofSolidity(root, leaf, [sibling])).toBe(true);
});

it('returns false for an invalid leaf', () => {
const leaf = '0x' + 'ab'.repeat(32);
const wrongLeaf = '0x' + 'ef'.repeat(32);
const sibling = '0x' + 'cd'.repeat(32);
it("returns false for an invalid leaf", () => {
const leaf = "0x" + "ab".repeat(32);
const wrongLeaf = "0x" + "ef".repeat(32);
const sibling = "0x" + "cd".repeat(32);
const root = computeMerkleRoot(leaf, [sibling]);

expect(
verifyProofSolidity(root, wrongLeaf, [sibling]),
).toBe(false);
expect(verifyProofSolidity(root, wrongLeaf, [sibling])).toBe(false);
});

it('returns false for an invalid proof path', () => {
const leaf = '0x' + 'ab'.repeat(32);
const sibling1 = '0x' + 'cd'.repeat(32);
const sibling2 = '0x' + '12'.repeat(32);
it("returns false for an invalid proof path", () => {
const leaf = "0x" + "ab".repeat(32);
const sibling1 = "0x" + "cd".repeat(32);
const sibling2 = "0x" + "12".repeat(32);
const root = computeMerkleRoot(leaf, [sibling1]);

expect(
verifyProofSolidity(root, leaf, [sibling1, sibling2]),
).toBe(false);
expect(verifyProofSolidity(root, leaf, [sibling1, sibling2])).toBe(false);
});

it('returns true for a valid three-element proof', () => {
const leaf = '0x' + 'ab'.repeat(32);
const sibling1 = '0x' + 'cd'.repeat(32);
const sibling2 = '0x' + 'ef'.repeat(32);
const sibling3 = '0x' + '01'.repeat(32);
it("returns true for a valid three-element proof", () => {
const leaf = "0x" + "ab".repeat(32);
const sibling1 = "0x" + "cd".repeat(32);
const sibling2 = "0x" + "ef".repeat(32);
const sibling3 = "0x" + "01".repeat(32);
const root = computeMerkleRoot(leaf, [sibling1, sibling2, sibling3]);

expect(
verifyProofSolidity(root, leaf, [sibling1, sibling2, sibling3]),
).toBe(true);
});

it('returns true when leaf equals root (empty proof)', () => {
const leaf = '0x' + 'ab'.repeat(32);
it("returns true when leaf equals root (empty proof)", () => {
const leaf = "0x" + "ab".repeat(32);

expect(verifyProofSolidity(leaf, leaf, [])).toBe(true);
});

it('returns false for an empty proof with mismatched leaf and root', () => {
const leaf = '0x' + 'ab'.repeat(32);
const root = '0x' + 'cd'.repeat(32);
it("returns false for an empty proof with mismatched leaf and root", () => {
const leaf = "0x" + "ab".repeat(32);
const root = "0x" + "cd".repeat(32);

expect(verifyProofSolidity(root, leaf, [])).toBe(false);
});

it('handles proof elements in any order (canonical sorting)', () => {
const leaf = '0x' + 'ab'.repeat(32);
const sibling = '0x' + 'cd'.repeat(32);
it("handles proof elements in any order (canonical sorting)", () => {
const leaf = "0x" + "ab".repeat(32);
const sibling = "0x" + "cd".repeat(32);
const root = computeMerkleRoot(leaf, [sibling]);

expect(
verifyProofSolidity(root, leaf, [sibling]),
).toBe(true);
expect(verifyProofSolidity(root, leaf, [sibling])).toBe(true);
});
});

describe('verifyProofOrdered', () => {
it('returns true for a valid proof with correct ordering', () => {
const leaf = '0x' + 'ab'.repeat(32);
const sibling = '0x' + 'cd'.repeat(32);
describe("verifyProofOrdered", () => {
it("returns true for a valid proof with correct ordering", () => {
const leaf = "0x" + "ab".repeat(32);
const sibling = "0x" + "cd".repeat(32);
const root = computeMerkleRoot(leaf, [sibling]);

expect(
verifyProofOrderedSolidity(root, leaf, [sibling]),
).toBe(true);
expect(verifyProofOrderedSolidity(root, leaf, [sibling])).toBe(true);
});

it('returns false when proof elements are in wrong order', () => {
const leaf = '0x' + 'ab'.repeat(32);
const sibling = '0x' + 'cd'.repeat(32);
it("returns false when proof elements are in wrong order", () => {
const leaf = "0x" + "ab".repeat(32);
const sibling = "0x" + "cd".repeat(32);
const root = computeMerkleRoot(leaf, [sibling]);

expect(
verifyProofOrderedSolidity(root, leaf, [sibling]),
).toBe(true);
expect(verifyProofOrderedSolidity(root, leaf, [sibling])).toBe(true);
});
});

describe('gas efficiency', () => {
it('uses constant scratch memory (no allocation per proof step)', () => {
const leaf = '0x' + 'ab'.repeat(32);
const proof = Array.from({ length: 10 }, () =>
'0x' + 'cd'.repeat(32),
);
describe("gas efficiency", () => {
it("uses constant scratch memory (no allocation per proof step)", () => {
const leaf = "0x" + "ab".repeat(32);
const proof = Array.from({ length: 10 }, () => "0x" + "cd".repeat(32));
const root = computeMerkleRoot(leaf, proof);

expect(
verifyProofSolidity(root, leaf, proof),
).toBe(true);
expect(verifyProofSolidity(root, leaf, proof)).toBe(true);
});
});
});
Expand Down Expand Up @@ -151,4 +135,4 @@ function verifyProofOrderedSolidity(
computed = keccak256(computed + sibling.slice(2));
}
return computed.toLowerCase() === root.toLowerCase();
}
}
58 changes: 58 additions & 0 deletions test/crypto/YulSHA256.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, it, expect } from "vitest";
import * as crypto from "crypto";

function sha256(data: string | Buffer): string {
const buf =
typeof data === "string"
? data.startsWith("0x")
? Buffer.from(data.slice(2), "hex")
: Buffer.from(data, "utf-8")
: data;
return "0x" + crypto.createHash("sha256").update(buf).digest("hex");
}

describe("YulSHA256", () => {
describe("hash", () => {
it("computes sha256 hash for empty input", () => {
const input = "";
const expected = sha256(input);
// The expected empty hash is: 0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
expect(expected).toBe(
"0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
);
});

it("computes sha256 hash for short inputs", () => {
const input = "0x1234567890";
const expected = sha256(input);
expect(expected).toBe(
"0x6c450e037e79b76f231a71a22ff40403f7d9b74b15e014e52fe1156d3666c3e6",
);
});

it("computes sha256 hash for standard 32-byte hash inputs", () => {
const input = "0x" + "ab".repeat(32);
const expected = sha256(input);
expect(expected).toBe(
"0x9a2db2e23f1504cd056606553ac049c5e718e8f9ce9233876df1a7a1821af885",
);
});

it("computes sha256 hash for large inputs", () => {
const input = "0x" + "ff".repeat(1024);
const expected = sha256(input);
expect(expected).toBe(
"0x5f4ecdb7b71c3e403983fe405cddcdc2f2576b655fdb3e80d94a6f7c32e58bc2",
);
});

it("matches standard sha256 output across random input sizes", () => {
for (let size = 1; size <= 128; size++) {
const input = "0x" + crypto.randomBytes(size).toString("hex");
const expected = sha256(input);
expect(expected.startsWith("0x")).toBe(true);
expect(expected.length).toBe(66); // 0x + 64 hex chars
}
});
});
});
3 changes: 1 addition & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@
"@gas-evaluator/*": ["libs/gas-evaluator/src/*"]
}
},
"include": ["apps/**/*", "libs/**/*", "packages/**/*", "src/**/*"],
"include": ["apps/**/*", "libs/**/*", "packages/**/*", "rules/**/*", "src/rules/**/*"],
"include": ["apps/**/*", "libs/**/*", "packages/**/*", "src/**/*", "rules/**/*", "src/rules/**/*"],
"exclude": [
"node_modules",
"dist",
Expand Down
Loading