diff --git a/packages/common/src/eips.ts b/packages/common/src/eips.ts index 42a2933f015..d2abacef501 100644 --- a/packages/common/src/eips.ts +++ b/packages/common/src/eips.ts @@ -521,4 +521,13 @@ export const eipsDict: EIPsDict = { minimumHardfork: Hardfork.Chainstart, requiredEIPs: [], }, + /** + * Description : Precompile for secp256r1 Curve Support + * URL : https://eips.ethereum.org/EIPS/eip-7951 + * Status : Draft + */ + 7951: { + minimumHardfork: Hardfork.Chainstart, + requiredEIPs: [], + }, } diff --git a/packages/common/src/hardforks.ts b/packages/common/src/hardforks.ts index c60a771829d..6651c6577ed 100644 --- a/packages/common/src/hardforks.ts +++ b/packages/common/src/hardforks.ts @@ -166,7 +166,7 @@ export const hardforksDict: HardforksDict = { * Status : Draft */ osaka: { - eips: [7594, 7823, 7825, 7883, 7939], + eips: [7594, 7823, 7825, 7883, 7939, 7951], }, /** * Description: Next feature hardfork after osaka, internally used for verkle testing/implementation (incomplete/experimental) diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index 25134bee3f2..915dc1b99bd 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -257,7 +257,7 @@ export class EVM implements EVMInterface { 663, 1153, 1559, 2537, 2565, 2718, 2929, 2930, 2935, 3198, 3529, 3540, 3541, 3607, 3651, 3670, 3855, 3860, 4200, 4399, 4750, 4788, 4844, 4895, 5133, 5450, 5656, 6110, 6206, 6780, 6800, 7002, 7069, 7251, 7480, 7516, 7594, 7620, 7685, 7691, 7692, 7698, 7702, 7709, 7823, 7825, - 7939, + 7939, 7951, ] for (const eip of this.common.eips()) { diff --git a/packages/evm/src/precompiles/100-p256verify.ts b/packages/evm/src/precompiles/100-p256verify.ts new file mode 100644 index 00000000000..0aaf7acc109 --- /dev/null +++ b/packages/evm/src/precompiles/100-p256verify.ts @@ -0,0 +1,149 @@ +import { bytesToBigInt, bytesToHex, setLengthLeft } from '@ethereumjs/util' +import { p256 } from '@noble/curves/p256' + +import { OOGResult } from '../evm.ts' + +import { getPrecompileName } from './index.ts' +import type { PrecompileInput } from './types.ts' +import { gasLimitCheck } from './util.ts' + +import type { ExecResult } from '../types.ts' + +const P256VERIFY_GAS_COST = BigInt(6900) +const SUCCESS_RETURN = new Uint8Array(32).fill(0).map((_, i) => (i === 31 ? 1 : 0)) + +// Curve parameters for secp256r1 +const P256_N = BigInt('0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551') // Subgroup order +const P256_P = BigInt('0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff') // Base field modulus +const P256_A = BigInt('0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc') // Curve coefficient a +const P256_B = BigInt('0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b') // Curve coefficient b + +export function precompile100(opts: PrecompileInput): ExecResult { + const pName = getPrecompileName('100') + const gasUsed = P256VERIFY_GAS_COST + + if (!gasLimitCheck(opts, gasUsed, pName)) { + return OOGResult(opts.gasLimit) + } + + const data = opts.data + + // 1. Input length: Input MUST be exactly 160 bytes + if (data.length !== 160) { + if (opts._debug !== undefined) { + opts._debug(`${pName} failed: invalid input length ${data.length}, expected 160`) + } + return { + executionGasUsed: gasUsed, + returnValue: new Uint8Array(), + } + } + + const msgHash = data.subarray(0, 32) + const r = data.subarray(32, 64) + const s = data.subarray(64, 96) + const qx = data.subarray(96, 128) + const qy = data.subarray(128, 160) + + const rBigInt = bytesToBigInt(r) + const sBigInt = bytesToBigInt(s) + const qxBigInt = bytesToBigInt(qx) + const qyBigInt = bytesToBigInt(qy) + + // 2. Signature component bounds: Both r and s MUST satisfy 0 < r < n and 0 < s < n + if (!(rBigInt > BigInt(0) && rBigInt < P256_N && sBigInt > BigInt(0) && sBigInt < P256_N)) { + if (opts._debug !== undefined) { + opts._debug( + `${pName} failed: signature component out of bounds: r=${bytesToHex(r)}, s=${bytesToHex(s)}`, + ) + } + return { + executionGasUsed: gasUsed, + returnValue: new Uint8Array(), + } + } + + // 3. Public key bounds: Both qx and qy MUST satisfy 0 ≤ qx < p and 0 ≤ qy < p + if (!(qxBigInt >= BigInt(0) && qxBigInt < P256_P && qyBigInt >= BigInt(0) && qyBigInt < P256_P)) { + if (opts._debug !== undefined) { + opts._debug( + `${pName} failed: public key component out of bounds: qx=${bytesToHex(qx)}, qy=${bytesToHex(qy)}`, + ) + } + return { + executionGasUsed: gasUsed, + returnValue: new Uint8Array(), + } + } + + // 4. Point validity: The point (qx, qy) MUST satisfy the curve equation qy^2 ≡ qx^3 + a*qx + b (mod p) + const leftSide = (qyBigInt * qyBigInt) % P256_P + const rightSide = (qxBigInt * qxBigInt * qxBigInt + P256_A * qxBigInt + P256_B) % P256_P + + if (leftSide !== rightSide) { + if (opts._debug !== undefined) { + opts._debug(`${pName} failed: point not on curve`) + } + return { + executionGasUsed: gasUsed, + returnValue: new Uint8Array(), + } + } + + // 5. Point not at infinity: The point (qx, qy) MUST NOT be the point at infinity (represented as (0, 0)) + if (qxBigInt === BigInt(0) && qyBigInt === BigInt(0)) { + if (opts._debug !== undefined) { + opts._debug(`${pName} failed: public key is point at infinity`) + } + return { + executionGasUsed: gasUsed, + returnValue: new Uint8Array(), + } + } + + try { + // Create public key point + const publicKey = p256.ProjectivePoint.fromAffine({ + x: qxBigInt, + y: qyBigInt, + }) + + // Create signature + const rBytes = setLengthLeft(r, 32) + const sBytes = setLengthLeft(s, 32) + const signatureBytes = new Uint8Array(64) + signatureBytes.set(rBytes, 0) + signatureBytes.set(sBytes, 32) + + const signature = p256.Signature.fromCompact(signatureBytes) + + // Verify signature + const isValid = p256.verify(signature, msgHash, publicKey.toRawBytes(false)) + + if (isValid) { + if (opts._debug !== undefined) { + opts._debug(`${pName} succeeded: signature verification passed`) + } + return { + executionGasUsed: gasUsed, + returnValue: SUCCESS_RETURN, + } + } else { + if (opts._debug !== undefined) { + opts._debug(`${pName} failed: signature verification failed`) + } + return { + executionGasUsed: gasUsed, + returnValue: new Uint8Array(), + } + } + } catch (error) { + if (opts._debug !== undefined) { + opts._debug(`${pName} failed: verification error: ${error}`) + } + return { + executionGasUsed: gasUsed, + returnValue: new Uint8Array(), + } + } +} diff --git a/packages/evm/src/precompiles/index.ts b/packages/evm/src/precompiles/index.ts index 1d2ac3605d6..4cc47f56616 100644 --- a/packages/evm/src/precompiles/index.ts +++ b/packages/evm/src/precompiles/index.ts @@ -18,6 +18,7 @@ import { precompile08 } from './08-bn254-pairing.ts' import { precompile09 } from './09-blake2f.ts' import { precompile10 } from './10-bls12-map-fp-to-g1.ts' import { precompile11 } from './11-bls12-map-fp2-to-g2.ts' +import { precompile100 } from './100-p256verify.ts' import { MCLBLS, NobleBLS } from './bls12_381/index.ts' import { NobleBN254, RustBN254 } from './bn254/index.ts' @@ -213,6 +214,15 @@ const precompileEntries: PrecompileEntry[] = [ precompile: precompile11, name: 'BLS12_MAP_FP_TO_G2 (0x11)', }, + { + address: '0000000000000000000000000000000000000100', + check: { + type: PrecompileAvailabilityCheck.EIP, + param: 7951, + }, + precompile: precompile100, + name: 'P256VERIFY (0x100)', + }, ] const precompiles: Precompiles = { @@ -233,6 +243,7 @@ const precompiles: Precompiles = { [BYTES_19 + '0f']: precompile0f, [BYTES_19 + '10']: precompile10, [BYTES_19 + '11']: precompile11, + '0000000000000000000000000000000000000100': precompile100, } type DeletePrecompile = { diff --git a/packages/evm/test/precompiles/100-p256verify.spec.ts b/packages/evm/test/precompiles/100-p256verify.spec.ts new file mode 100644 index 00000000000..88204caa529 --- /dev/null +++ b/packages/evm/test/precompiles/100-p256verify.spec.ts @@ -0,0 +1,164 @@ +import { Common, Mainnet } from '@ethereumjs/common' +import { Address, hexToBytes } from '@ethereumjs/util' +import { p256 } from '@noble/curves/p256' +import { assert, beforeAll, describe, it } from 'vitest' + +import { createEVM } from '../../src/index.ts' +import { precompile100 } from '../../src/precompiles/100-p256verify.ts' + +import type { PrecompileInput } from '../../src/precompiles/types.ts' + +const testCases = [ + { + name: 'valid signature verification', + input: (() => { + // Generate a test key pair and signature + const privateKey = p256.utils.randomPrivateKey() + const publicKey = p256.getPublicKey(privateKey, false) // Get uncompressed public key + const message = new Uint8Array(32) + message[31] = 1 // Simple test message + + const signature = p256.sign(message, privateKey) + const signatureBytes = signature.toCompactRawBytes() + + // Format input: msgHash (32) + r (32) + s (32) + qx (32) + qy (32) + const input = new Uint8Array(160) + input.set(message, 0) // msgHash + input.set(signatureBytes.slice(0, 32), 32) // r + input.set(signatureBytes.slice(32, 64), 64) // s + input.set(publicKey.slice(1, 33), 96) // qx (uncompressed public key) + input.set(publicKey.slice(33, 65), 128) // qy + + return input + })(), + expectedReturn: new Uint8Array(32).fill(0).map((_, i) => (i === 31 ? 1 : 0)), // Success + expectedGasUsed: BigInt(6900), + }, + { + name: 'invalid input length', + input: new Uint8Array(159), // Wrong length + expectedReturn: new Uint8Array(0), // Failure + expectedGasUsed: BigInt(6900), + }, + { + name: 'invalid signature - r out of bounds', + input: (() => { + const input = new Uint8Array(160) + // Set r to curve order (invalid) + const r = hexToBytes('0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551') + input.set(r, 32) + return input + })(), + expectedReturn: new Uint8Array(0), // Failure + expectedGasUsed: BigInt(6900), + }, + { + name: 'invalid signature - s out of bounds', + input: (() => { + const input = new Uint8Array(160) + // Set s to curve order (invalid) + const s = hexToBytes('0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551') + input.set(s, 64) + return input + })(), + expectedReturn: new Uint8Array(0), // Failure + expectedGasUsed: BigInt(6900), + }, + { + name: 'invalid public key - point not on curve', + input: (() => { + const input = new Uint8Array(160) + // Set invalid public key coordinates + input.set(new Uint8Array(32).fill(1), 96) // qx + input.set(new Uint8Array(32).fill(2), 128) // qy + return input + })(), + expectedReturn: new Uint8Array(0), // Failure + expectedGasUsed: BigInt(6900), + }, + { + name: 'invalid public key - point at infinity', + input: (() => { + const input = new Uint8Array(160) + // Set point at infinity (0, 0) + input.set(new Uint8Array(32), 96) // qx = 0 + input.set(new Uint8Array(32), 128) // qy = 0 + return input + })(), + expectedReturn: new Uint8Array(0), // Failure + expectedGasUsed: BigInt(6900), + }, + { + name: 'invalid signature verification', + input: (() => { + // Generate a valid key pair but wrong signature + const privateKey = p256.utils.randomPrivateKey() + const publicKey = p256.getPublicKey(privateKey, false) // Get uncompressed public key + const message = new Uint8Array(32) + message[31] = 1 + + // Use wrong message for signature + const wrongMessage = new Uint8Array(32) + wrongMessage[31] = 2 + const signature = p256.sign(wrongMessage, privateKey) + const signatureBytes = signature.toCompactRawBytes() + + const input = new Uint8Array(160) + input.set(message, 0) // msgHash (different from signed message) + input.set(signatureBytes.slice(0, 32), 32) // r + input.set(signatureBytes.slice(32, 64), 64) // s + input.set(publicKey.slice(1, 33), 96) // qx + input.set(publicKey.slice(33, 65), 128) // qy + + return input + })(), + expectedReturn: new Uint8Array(0), // Failure + expectedGasUsed: BigInt(6900), + }, +] + +describe('P256VERIFY precompile', () => { + let common: Common + let evm: any + + beforeAll(async () => { + common = new Common({ chain: Mainnet, eips: [7951] }) + evm = await createEVM({ common }) + }) + + describe('precompile100', () => { + for (const testCase of testCases) { + it(`should handle ${testCase.name}`, () => { + const opts: PrecompileInput = { + data: testCase.input, + gasLimit: BigInt(10000), + common, + _EVM: evm, + } + + const result = precompile100(opts) + + assert.equal(result.executionGasUsed, testCase.expectedGasUsed) + assert.deepEqual(result.returnValue, testCase.expectedReturn) + }) + } + }) + + describe('integration with EVM', () => { + it('should be callable from EVM', async () => { + // Create a simple contract that calls the P256VERIFY precompile + const code = hexToBytes( + '0x6101006000526001601f600060003660006000610100611af4f13d6001556000553d600060003e3d600020600255', + ) + + const result = await evm.runCall({ + to: undefined, // Contract creation + caller: new Address(hexToBytes('0x0000000000000000000000000000000000000000')), + data: code, + gasLimit: BigInt(100000), + }) + + assert.equal(result.execResult.exceptionError, undefined) + }) + }) +})