Skip to main content

Documentation Index

Fetch the complete documentation index at: https://seilabs.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

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.