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
9 changes: 9 additions & 0 deletions packages/common/src/eips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
},
}
2 changes: 1 addition & 1 deletion packages/common/src/hardforks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion packages/evm/src/evm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
149 changes: 149 additions & 0 deletions packages/evm/src/precompiles/100-p256verify.ts
Original file line number Diff line number Diff line change
@@ -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(),
}
}
}
11 changes: 11 additions & 0 deletions packages/evm/src/precompiles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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 = {
Expand All @@ -233,6 +243,7 @@ const precompiles: Precompiles = {
[BYTES_19 + '0f']: precompile0f,
[BYTES_19 + '10']: precompile10,
[BYTES_19 + '11']: precompile11,
'0000000000000000000000000000000000000100': precompile100,
}

type DeletePrecompile = {
Expand Down
164 changes: 164 additions & 0 deletions packages/evm/test/precompiles/100-p256verify.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
Loading