From 4337f3f5193c7fd2110eef2a58a99dcf4be1fd52 Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Fri, 14 Nov 2025 18:58:29 -0300 Subject: [PATCH 1/3] PrecompileTester --- .../precompileTester/PrecompileTester.sol | 297 ++++++++++++++++++ tests/pos/evm/evm.bats | 60 ++++ 2 files changed, 357 insertions(+) create mode 100644 core/contracts/precompileTester/PrecompileTester.sol create mode 100644 tests/pos/evm/evm.bats diff --git a/core/contracts/precompileTester/PrecompileTester.sol b/core/contracts/precompileTester/PrecompileTester.sol new file mode 100644 index 000000000..77e54e5e7 --- /dev/null +++ b/core/contracts/precompileTester/PrecompileTester.sol @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/** + * Precompile probe harness. + * + * - Executes a fixed, deterministic set of calls to the given precompiles in the constructor. + * - Uses low-level .call for every precompile and NEVER reverts; it logs per-case success/failure. + * - Emits one standardized event per test case in deterministic order. + * - outputHash = keccak256(returndata) when success == true, else bytes32(0). + * + * IMPORTANT: + * - For complex precompiles (KZG, BLS12-381 family, P-256), placeholder inputs are included to ensure + * the call path is exercised and logged. Replace those with your real test vectors to validate correctness. + * - The precompile order matches exactly the list provided in the prompt. + */ +contract PrecompileTester { + event PrecompileTestResult( + uint256 id, + address precompile, + bool success, + bytes32 outputHash + ); + + // ---- Ordered precompile list (exact order preserved) ---- + address private constant PC_ECRECOVER = address(uint160(0x01)); + address private constant PC_SHA256 = address(uint160(0x02)); + address private constant PC_RIPEMD160 = address(uint160(0x03)); + address private constant PC_IDENTITY = address(uint160(0x04)); + address private constant PC_MODEXP = address(uint160(0x05)); + address private constant PC_BN256_ADD = address(uint160(0x06)); + address private constant PC_BN256_MUL = address(uint160(0x07)); + address private constant PC_BN256_PAIRING = address(uint160(0x08)); + address private constant PC_BLAKE2F = address(uint160(0x09)); + address private constant PC_KZG_POINT_EVAL = address(uint160(0x0a)); + address private constant PC_BLS12_G1_ADD = address(uint160(0x0b)); + address private constant PC_BLS12_G1_MULTIEXP = address(uint160(0x0c)); + address private constant PC_BLS12_G2_ADD = address(uint160(0x0d)); + address private constant PC_BLS12_G2_MULTIEXP = address(uint160(0x0e)); + address private constant PC_BLS12_PAIRING = address(uint160(0x0f)); + address private constant PC_BLS12_MAP_G1 = address(uint160(0x10)); + address private constant PC_BLS12_MAP_G2 = address(uint160(0x11)); + address private constant PC_P256_VERIFY = address(uint160(0x0100)); // 256 + + constructor() { + // We advance through precompiles in this exact sequence; IDs are deterministic. + uint256 p = 0; + + // 1) ecrecover (0x01) — 128-byte input: h(32) | v(32) | r(32) | s(32). + unchecked { + ++p; + } + _callAndLog( + _id(p, 1), + PC_ECRECOVER, + _encECRecover( + bytes32( + uint256( + 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + ) + ), + 27, + bytes32(uint256(1)), + bytes32(uint256(2)) + ) + ); + _callAndLog( + _id(p, 2), + PC_ECRECOVER, + _encECRecover( + bytes32( + uint256( + 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ) + ), + 28, + bytes32(uint256(3)), + bytes32(uint256(4)) + ) + ); + + // 2) sha256 (0x02) — returns SHA-256 digest of input. + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_SHA256, bytes("abc")); + _callAndLog(_id(p, 2), PC_SHA256, hex""); + + // 3) ripemd160 (0x03) — returns 20 bytes (we hash the return in logs). + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_RIPEMD160, bytes("abc")); + + // 4) identity / datacopy (0x04) — echoes the input. + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_IDENTITY, hex"deadbeef"); + _callAndLog(_id(p, 2), PC_IDENTITY, hex""); + + // 5) bigModExp (0x05) — EIP-198-compatible encoding: 32|32|32 len headers + base|exp|mod. + // Using small, deterministic values to keep gas low. + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_MODEXP, _encModExp(2, 5, 97)); // 2^5 mod 97 = 32 + _callAndLog(_id(p, 2), PC_MODEXP, _encModExp(1234567, 891011, 7919)); + + // 6) bn256Add (0x06) — EIP-196, input = (x1,y1,x2,y2) each 32 bytes, values < p. + // Use the classic on-curve point (1,2) twice. + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_BN256_ADD, _encU256x4(1, 2, 1, 2)); + + // 7) bn256ScalarMul (0x07) — input = (x,y,scalar). + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_BN256_MUL, _encU256x3(1, 2, 3)); + + // 8) bn256Pairing (0x08) — EIP-197. + // Empty input is a valid "0-pair" which evaluates to true on spec-compliant impls; cheap & deterministic. + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_BN256_PAIRING, hex""); + + // 9) blake2F (0x09) — EIP-152, 213 bytes input. We'll use 12 rounds, zero state/message, final=1. + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_BLAKE2F, _blake2f_12rounds_zero_final1()); + + // 10) KZG point evaluation (0x0a) — EIP-4844. Placeholder input. + // Replace with a real 192-byte vector (commitment/proof/eval) when available. + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_KZG_POINT_EVAL, _kzgVector()); + + // 11) BLS12-381 G1 add (0x0b) — Placeholder; replace with real EIP-2537 vector (compressed points). + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_BLS12_G1_ADD, _zeros(256)); // likely 2*48 compressed + + // 12) BLS12-381 G1 multiexp (0x0c) — Placeholder; replace with real (point,scalar) pairs. + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_BLS12_G1_MULTIEXP, _zeros(160)); // variable-length; 0 to trigger deterministic fail/success + + // 13) BLS12-381 G2 add (0x0d) — Placeholder; replace with 2*96 compressed. + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_BLS12_G2_ADD, _zeros(512)); + + // 14) BLS12-381 G2 multiexp (0x0e) — Placeholder. + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_BLS12_G2_MULTIEXP, _zeros(288)); + + // 15) BLS12-381 pairing (0x0f) — Placeholder; replace with k*(48 + 96) bytes for k pairs (compressed). + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_BLS12_PAIRING, _zeros(384)); + + // 16) BLS12-381 map-to-G1 (0x10) — Placeholder; replace with proper field encoding. + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_BLS12_MAP_G1, _zeros(64)); // typical field size placeholder + + // 17) BLS12-381 map-to-G2 (0x11) — Placeholder. + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_BLS12_MAP_G2, _zeros(128)); + + // 18) P-256 verify (0x0100) — Placeholder signature tuple; replace with real (msg, pk, sig) encoding per your spec. + unchecked { + ++p; + } + _callAndLog(_id(p, 1), PC_P256_VERIFY, _zeros(128)); + } + + // ------------------------------------------------------------------------ + // Internal helpers + // ------------------------------------------------------------------------ + + function _id( + uint256 precompileIndex, + uint256 caseIndex + ) private pure returns (uint256) { + unchecked { + return precompileIndex * 1000 + caseIndex; + } + } + + function _callAndLog(uint256 id, address pc, bytes memory input) private { + // NOTE: requirement asked for .call; we keep it exactly that. + (bool ok, bytes memory out) = pc.call(input); + emit PrecompileTestResult(id, pc, ok, ok ? keccak256(out) : bytes32(0)); + } + + // --- ecrecover encoding: 128 bytes = h(32) | v(32) | r(32) | s(32) --- + function _encECRecover( + bytes32 h, + uint8 v, + bytes32 r, + bytes32 s + ) private pure returns (bytes memory) { + // v is read from the lowest byte of a 32-byte word by the precompile. + return bytes.concat(h, bytes32(uint256(v)), r, s); + } + + // --- bigModExp (EIP-198): [lenB(32), lenE(32), lenM(32)] | base | exp | mod --- + // For simplicity we always use 32-byte limbs. + function _encModExp( + uint256 base, + uint256 exponent, + uint256 modulus + ) private pure returns (bytes memory) { + return + bytes.concat( + bytes32(uint256(32)), // base length + bytes32(uint256(32)), // exp length + bytes32(uint256(32)), // mod length + bytes32(base), + bytes32(exponent), + bytes32(modulus) + ); + } + + // --- bn256 helpers: pack uint256s to 32-byte big-endian words in sequence --- + function _encU256x4( + uint256 a, + uint256 b, + uint256 c, + uint256 d + ) private pure returns (bytes memory) { + return bytes.concat(bytes32(a), bytes32(b), bytes32(c), bytes32(d)); + } + + function _encU256x3( + uint256 a, + uint256 b, + uint256 c + ) private pure returns (bytes memory) { + return bytes.concat(bytes32(a), bytes32(b), bytes32(c)); + } + + // --- blake2F (EIP-152) 213-byte input builder: 12 rounds, zero state & message, final=1 --- + function _blake2f_12rounds_zero_final1() + private + pure + returns (bytes memory input) + { + input = new bytes(213); + // rounds: BIG-endian uint32 => 12 == 0x00 0x00 0x00 0x0c + input[0] = 0x00; + input[1] = 0x00; + input[2] = 0x00; + input[3] = 0x0c; + // h[0..7] 64-bit words (8*8 = 64 bytes) -> already zero + // m[0..15] 64-bit words (16*8 = 128 bytes) -> already zero + // t (offset) 16 bytes -> already zero + // final block flag (1 byte) at the end: + input[212] = 0x01; + } + + // --- simple zero-filled buffer --- + function _zeros(uint256 len) private pure returns (bytes memory b) { + b = new bytes(len); + // bytes are zero-initialized by default in Solidity, so nothing else to do + } + + function _kzgVector() private pure returns (bytes memory) { + return bytes.concat( + // versionedHash (32 bytes) + hex"013cb9810630b811b199f0e62870e6f5db2ace0b5645e436ad7092c3544fc30d", + // point (32 bytes) + hex"0000000000000000000000000000000000000000000000000000000000000005", + // claim (32 bytes) + hex"2d49f6b7e4749dbd3c95dc2674f80d988626b6d1c22bbd1ad56f9d6a3c306bb4", + // commitment (48 bytes) + hex"9869b5669003ce14283e97370073773f8f1d3821f0d27beaedfb310cacf08ccc84114065d20200475f7ee2606a777ea4", + // proof (48 bytes) + hex"8c9bd0478fc7e81c03dfa87160ede0188f7ae822aa4f93e684caeaa15cd1f1bbd22cf1275dabc37a78778f1dffd8647e" + ); +} +} diff --git a/tests/pos/evm/evm.bats b/tests/pos/evm/evm.bats new file mode 100644 index 000000000..cd7cca6b0 --- /dev/null +++ b/tests/pos/evm/evm.bats @@ -0,0 +1,60 @@ +#!/usr/bin/env bats +# bats file_tags=pos,evm + +setup() { + # Load libraries. + load "../../../core/helpers/pos-setup.bash" + pos_setup +} + +# bats test_tags=precompilers +@test "push and validate all available precompilers" { + # BIN_PATH="core/contracts/bin/precompiletester.bin" + # BYTECODE="0x$(tr -d '\n' < "$BIN_PATH")" + + # txhash=$(cast send --create "$BYTECODE" \ + # --rpc-url "$L2_RPC_URL" \ + # --private-key "$PRIVATE_KEY" \ + # --json | jq -r '.transactionHash') + txhash=$(forge create core/contracts/precompileTester/PrecompileTester.sol:PrecompileTester \ + --private-key "$PRIVATE_KEY" \ + --rpc-url "$L2_RPC_URL" \ + --broadcast \ + --priority-gas-price 35gwei \ + --gas-price 35gwei \ + --json | jq -r '.transactionHash') + + echo "${txhash}" + + SIG=$(cast keccak "PrecompileTestResult(uint256,address,bool,bytes32)") + RECEIPT=$(cast receipt "${txhash}" --rpc-url "$L2_RPC_URL" --json) + + FAIL=0 + + while read -r DATA; do + OUT=$(cast abi-decode -i --json \ + "PrecompileTestResult(uint256 id,address precompile,bool success,bytes32 outputHash)" \ + "$DATA") + + ID=$(echo "$OUT" | jq -r '.[0]') + PRE=$(echo "$OUT" | jq -r '.[1]') + SUCCESS=$(echo "$OUT" | jq -r '.[2]') + + if [ "$SUCCESS" = "true" ]; then + echo "✔ id=$ID precompile=$PRE success=true" + else + echo "❌ id=$ID precompile=$PRE success=false" + FAIL=1 + fi + done < <( + echo "$RECEIPT" \ + | jq -r --arg sig "$SIG" '.logs[] | select(.topics[0] == $sig) | .data' + ) + + if [ "$FAIL" -eq 0 ]; then + echo "🎉 All precompiles passed!" + else + echo "💥 Some precompiles failed!" + fi + +} \ No newline at end of file From 0175cafdb498071203e77b7c44dc2ec6dcc890cb Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Fri, 14 Nov 2025 18:59:06 -0300 Subject: [PATCH 2/3] test inventory --- TESTSINVENTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TESTSINVENTORY.md b/TESTSINVENTORY.md index c1f87a417..fde688c75 100644 --- a/TESTSINVENTORY.md +++ b/TESTSINVENTORY.md @@ -128,6 +128,7 @@ Table of tests currently implemented or being implemented in the E2E repository. | bridge some ERC20 tokens from L1 to L2 and confirm L2 ERC20 balance increased | [Link](./tests/pos/bridge.bats#L95) | | | delegate MATIC/POL to a validator | [Link](./tests/pos/validator.bats#L181) | | | prune TxIndexer | [Link](./tests/pos/heimdall-v2.bats#L86) | | +| push and validate all available precompilers | [Link](./tests/pos/evm/evm.bats#L11) | | | remove validator | [Link](./tests/pos/validator.bats#L363) | | | undelegate MATIC/POL from a validator | [Link](./tests/pos/validator.bats#L275) | | | update signer | [Link](./tests/pos/validator.bats#L147) | | From adbf505702a1e8ab2a9b7a9fdf5a6a074629c8fa Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Fri, 14 Nov 2025 19:03:17 -0300 Subject: [PATCH 3/3] missing test tag --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ad615acef..aa7e6b13c 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,7 @@ grep -hoR --include="*.bats" 'test_tags=[^ ]*' . | sed 's/.*test_tags=//' | tr ' - pos-delegate - pos-undelegate - pos-validator +- precompilers - prune - railgun - smooth-crypto-lib