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);
// On invalid signatures the precompile returns no data, which would
// make a high-level interface call revert when decoding the `bytes`
// return value. Use a low-level staticcall and treat empty return
// data as "invalid signature" instead.
(bool success, bytes memory output) = P256VERIFY_PRECOMPILE_ADDRESS.staticcall(
abi.encodeWithSelector(IP256VERIFY.verify.selector, input)
);
return success && output.length > 0;
}
}
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 (bytes memory response);
}
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
);
// Success returns a non-empty (ABI-encoded 32-byte) value; invalid
// signatures return no data, which would revert a high-level call.
(bool ok, bytes memory output) = address(P256_PRECOMPILE).staticcall(
abi.encodeWithSelector(IP256Verify.verify.selector, input)
);
return ok && output.length > 0;
}
}
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
);
(bool ok, bytes memory output) = address(P256_PRECOMPILE).staticcall(
abi.encodeWithSelector(IP256Verify.verify.selector, input)
);
return ok && output.length > 0;
}
}
Apple Secure Enclave Integration
contract SecureEnclaveAuth {
IP256Verify constant P256_PRECOMPILE = IP256Verify(0x0000000000000000000000000000000000001011);
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);
(bool ok, bytes memory output) = address(P256_PRECOMPILE).staticcall(
abi.encodeWithSelector(IP256Verify.verify.selector, input)
);
return ok && output.length > 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
);
(bool ok, bytes memory output) = address(P256_PRECOMPILE).staticcall(
abi.encodeWithSelector(IP256Verify.verify.selector, input)
);
if (ok && output.length > 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 (n / 2, where n is
// the P-256 group order). This prevents signature malleability.
return uint256(s) <= 0x7FFFFFFF800000007FFFFFFFFFFFFFFFDE737D56D38BCF4279DCE5617E3192A8;
}
JavaScript Integration
// Example: Preparing P256 verification input (ethers v6)
function prepareP256Input(messageHash, signature, publicKey) {
// Ensure all components are 32 bytes
const hash = ethers.zeroPadValue(messageHash, 32);
const r = ethers.zeroPadValue(signature.r, 32);
const s = ethers.zeroPadValue(signature.s, 32);
const x = ethers.zeroPadValue(publicKey.x, 32);
const y = ethers.zeroPadValue(publicKey.y, 32);
// Concatenate all components
return ethers.concat([hash, r, s, x, y]);
}
// Usage with ethers.js
const P256_VERIFY_ADDRESS = '0x0000000000000000000000000000000000001011';
const P256_VERIFY_ABI = ['function verify(bytes input) view returns (bytes response)'];
async function verifyP256Signature(provider, messageHash, signature, publicKey) {
const precompile = new ethers.Contract(P256_VERIFY_ADDRESS, P256_VERIFY_ABI, provider);
const input = prepareP256Input(messageHash, signature, publicKey);
try {
const result = await precompile.verify(input);
// Success returns 32 bytes ending in 0x01
return result === '0x0000000000000000000000000000000000000000000000000000000000000001';
} catch (err) {
// Invalid signatures return no data, which ethers cannot decode as `bytes`
// (BAD_DATA) — treat that as a failed verification.
return false;
}
}
Error Handling
The P256 precompile returns no data on failure. Because verify is declared as returning bytes, a high-level Solidity interface call reverts when it tries to decode that empty return data — always use a low-level staticcall (as in the examples above) so invalid signatures resolve to false instead of reverting. Common failure cases include:
- Invalid Input Length: Input must be exactly 160 bytes
- Invalid Public Key: Point not on the P256 curve
- Invalid Signature: r or s values out of valid range
- 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
);
(bool ok, bytes memory output) = address(P256_PRECOMPILE).staticcall(
abi.encodeWithSelector(IP256Verify.verify.selector, input)
);
bool verified = ok && output.length > 0;
return (verified, verified ? "" : "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);
(bool ok, bytes memory output) = address(P256_PRECOMPILE).staticcall(
abi.encodeWithSelector(IP256Verify.verify.selector, input)
);
assert(ok && output.length > 0);
}
}
- 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