Skip to Content

P256 Precompile

Address: 0x0000000000000000000000000000000000001011

This is the implementation of RIP-7212, which provides a precompiled contract for verifying signatures in the secp256r1 or P-256 elliptic curve.

Overview

The P256 precompile enables efficient signature verification for the secp256r1 curve, which is widely used in modern security systems including:

  • Apple’s Secure Enclave
  • WebAuthn/FIDO2
  • Android Keychain
  • Various hardware security modules (HSMs)
  • PassKeys

This precompile implementation is significantly more gas efficient (up to 60x) compared to Solidity-based implementations.

Interface

// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; address constant P256VERIFY_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000001011; IP256VERIFY constant P256VERIFY_CONTRACT = IP256VERIFY(P256VERIFY_PRECOMPILE_ADDRESS); interface IP256VERIFY { function verify( bytes memory signature ) external view returns (bytes memory response); }

Implementation Library

Here’s a complete implementation of the P256 library that provides a convenient wrapper around the precompile:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import "./ECDSA.sol"; import "./P256Verify.sol"; /// @title P256 /// @author klkvr <https://github.com/klkvr> /// @author jxom <https://github.com/jxom> /// @notice Wrapper function to abstract low level details of call to the P256 /// signature verification precompile as defined in EIP-7212, see /// <https://eips.ethereum.org/EIPS/eip-7212>. library P256 { /// @notice P256VERIFY operation /// @param digest 32 bytes of the signed data hash /// @param signature Signature of the signer /// @param publicKey Public key of the signer /// @return success Represents if the operation was successful function verify(bytes32 digest, ECDSA.Signature memory signature, ECDSA.PublicKey memory publicKey) internal view returns (bool) { bytes memory input = abi.encode(digest, signature.r, signature.s, publicKey.x, publicKey.y); bytes memory output = P256VERIFY_CONTRACT.verify(input); bool success = output.length == 32 && output[31] == 0x01; return success; } }

Basic Usage

Using the P256 Library

// SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import "./P256.sol"; import "./ECDSA.sol"; contract P256Example { using P256 for bytes32; function verifySignature( bytes32 messageHash, ECDSA.Signature memory signature, ECDSA.PublicKey memory publicKey ) public view returns (bool) { return P256.verify(messageHash, signature, publicKey); } }

Direct Precompile Usage

For more control, you can call the precompile directly:

pragma solidity ^0.8.0; interface IP256Verify { function verify(bytes calldata input) external view returns (bytes32); } contract P256Verifier { IP256Verify constant P256_PRECOMPILE = IP256Verify(0x0000000000000000000000000000000000001011); function verifySignature( bytes32 messageHash, bytes32 r, bytes32 s, bytes32 publicKeyX, bytes32 publicKeyY ) external view returns (bool) { bytes memory input = abi.encodePacked( messageHash, r, s, publicKeyX, publicKeyY ); bytes32 result = P256_PRECOMPILE.verify(input); return result != bytes32(0); } }

Signature Format

The signature verification requires the following components:

  • digest: 32 bytes of the signed data hash
  • signature: Contains the r and s components of the signature
  • publicKey: Contains the x and y coordinates of the public key

The precompile expects these components to be encoded in a specific format:

  • First 32 bytes: message hash
  • Next 32 bytes: r component of the signature
  • Next 32 bytes: s component of the signature
  • Next 32 bytes: x coordinate of the public key
  • Next 32 bytes: y coordinate of the public key

Total length: 160 bytes

Gas Costs

The precompile is highly gas efficient compared to Solidity implementations. The exact gas cost per byte of verified data is set to GasCostPerByte = 300, which gives us:

  • Total Cost: 300 × 160 = 48,000 gas per verification
  • Efficiency: Up to 60x more efficient than pure Solidity implementations

Real-World Use Cases

WebAuthn/PassKeys Authentication

pragma solidity ^0.8.0; contract WebAuthnAuth { IP256Verify constant P256_PRECOMPILE = IP256Verify(0x0000000000000000000000000000000000001011); struct PublicKey { bytes32 x; bytes32 y; } mapping(address => PublicKey) public userKeys; function registerPasskey( bytes32 keyX, bytes32 keyY ) external { userKeys[msg.sender] = PublicKey(keyX, keyY); } function authenticateWithPasskey( bytes32 challengeHash, bytes32 r, bytes32 s ) external view returns (bool) { PublicKey memory userKey = userKeys[msg.sender]; require(userKey.x != 0 && userKey.y != 0, "No passkey registered"); bytes memory input = abi.encodePacked( challengeHash, r, s, userKey.x, userKey.y ); bytes32 result = P256_PRECOMPILE.verify(input); return result != bytes32(0); } }

Apple Secure Enclave Integration

contract SecureEnclaveAuth { event SecureOperation(address indexed user, bytes32 indexed operationHash); function executeSecureOperation( bytes32 operationHash, bytes32 r, bytes32 s, bytes32 enclaveKeyX, bytes32 enclaveKeyY ) external { require( verifyEnclaveSignature(operationHash, r, s, enclaveKeyX, enclaveKeyY), "Invalid Secure Enclave signature" ); emit SecureOperation(msg.sender, operationHash); } function verifyEnclaveSignature( bytes32 hash, bytes32 r, bytes32 s, bytes32 x, bytes32 y ) internal view returns (bool) { bytes memory input = abi.encodePacked(hash, r, s, x, y); bytes32 result = P256_PRECOMPILE.verify(input); return result != bytes32(0); } }

Multi-Signature with Hardware Keys

contract HardwareMultiSig { IP256Verify constant P256_PRECOMPILE = IP256Verify(0x0000000000000000000000000000000000001011); struct Signature { bytes32 r; bytes32 s; } struct PublicKey { bytes32 x; bytes32 y; } function verifyMultipleHardwareKeys( bytes32 messageHash, Signature[] calldata signatures, PublicKey[] calldata publicKeys, uint256 threshold ) external view returns (bool) { require(signatures.length == publicKeys.length, "Mismatched arrays"); require(threshold <= signatures.length, "Invalid threshold"); uint256 validSignatures = 0; for (uint i = 0; i < signatures.length; i++) { bytes memory input = abi.encodePacked( messageHash, signatures[i].r, signatures[i].s, publicKeys[i].x, publicKeys[i].y ); bytes32 result = P256_PRECOMPILE.verify(input); if (result != bytes32(0)) { validSignatures++; } } return validSignatures >= threshold; } }

Security Considerations

⚠️

Important: Always validate public keys and signature components before verification to prevent invalid curve point attacks.

Public Key Validation

function isValidPublicKey(bytes32 x, bytes32 y) internal pure returns (bool) { // Ensure point is not at infinity if (x == 0 && y == 0) return false; // Additional validation should be performed for production use // The precompile will reject invalid curve points return true; }

Signature Malleability

P256 signatures can be malleable. If your application requires unique signatures, implement additional checks:

function isLowS(bytes32 s) internal pure returns (bool) { // Check if s is in the lower half of the curve order // This prevents signature malleability return uint256(s) <= 0x7FFFFFFF80000000FFFFFFFFFFFFFFFF; }

JavaScript Integration

Preparing Input Data

// Example: Preparing P256 verification input function prepareP256Input(messageHash, signature, publicKey) { // Ensure all components are 32 bytes const hash = ethers.utils.hexZeroPad(messageHash, 32); const r = ethers.utils.hexZeroPad(signature.r, 32); const s = ethers.utils.hexZeroPad(signature.s, 32); const x = ethers.utils.hexZeroPad(publicKey.x, 32); const y = ethers.utils.hexZeroPad(publicKey.y, 32); // Concatenate all components return ethers.utils.concat([hash, r, s, x, y]); } // Usage with ethers.js async function verifyP256Signature(contract, messageHash, signature, publicKey) { const input = prepareP256Input(messageHash, signature, publicKey); const result = await contract.P256_PRECOMPILE.verify(input); return result !== '0x0000000000000000000000000000000000000000000000000000000000000000'; }

Error Handling

The P256 precompile returns zero on failure. Common failure cases include:

  1. Invalid Input Length: Input must be exactly 160 bytes
  2. Invalid Public Key: Point not on the P256 curve
  3. Invalid Signature: r or s values out of valid range
  4. Verification Failure: Signature doesn’t match message and public key
function safeVerifyP256( bytes32 messageHash, bytes32 r, bytes32 s, bytes32 publicKeyX, bytes32 publicKeyY ) internal view returns (bool success, string memory error) { // Validate inputs if (!isValidPublicKey(publicKeyX, publicKeyY)) { return (false, "Invalid public key"); } if (r == 0 || s == 0) { return (false, "Invalid signature components"); } // Prepare input and verify bytes memory input = abi.encodePacked( messageHash, r, s, publicKeyX, publicKeyY ); bytes32 result = P256_PRECOMPILE.verify(input); return (result != bytes32(0), result == bytes32(0) ? "Verification failed" : ""); }

Testing

Unit Tests

contract P256Test { IP256Verify constant P256_PRECOMPILE = IP256Verify(0x0000000000000000000000000000000000001011); function testValidSignature() public { // Test with known valid P256 signature bytes32 messageHash = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef; // Use actual test vectors for production tests bytes32 r = 0x...; // Valid r component bytes32 s = 0x...; // Valid s component bytes32 x = 0x...; // Valid public key x bytes32 y = 0x...; // Valid public key y bytes memory input = abi.encodePacked(messageHash, r, s, x, y); bytes32 result = P256_PRECOMPILE.verify(input); assert(result != bytes32(0)); } }

Performance Considerations

  • Gas Efficiency: 48,000 gas per verification vs 2M+ gas for Solidity implementations
  • Batch Operations: Consider batching multiple verifications in a single transaction
  • Caching: Cache public keys on-chain to reduce calldata for repeated verifications
  • Hardware Integration: Particularly efficient for applications using hardware-backed keys
For more information about the P256 precompile implementation, visit the Sei Chain repository.
Last updated on