Abstract
This WIP introduces a precompiled contract that verifies an Ed25519 signature over an arbitrary-length message, using the cofactored verification equation standardized by ZIP-215.
The precompile takes a concatenated (publicKey, signature, message) input and returns a 32-byte big-endian 1 on a valid signature, or 0 on any failure mode (malformed input, non-canonical encoding, or invalid signature). It never reverts on signature failure; it reverts only on out-of-gas.
Motivation
Ed25519 is the dominant non-secp256k1 signature scheme in the broader ecosystem: it is the default in XMTP, NaCl/libsodium, OpenSSH, age, TLS 1.3, Solana, Cosmos / Tendermint consensus, Stellar, NEAR, Aptos, and Sui. Native verification on World Chain unlocks several first-class use cases:
Off-chain attestation services (orb signatures, OPRF transcripts, oracle quorums) can sign with Ed25519 — the format most existing key-management hardware already produces — without forcing on-chain consumers onto secp256k1.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Constants
| Name | Type | Value | Meaning |
|---|---|---|---|
EDDSA_PRECOMPILE_ADDRESS | address | 0x0000000000000000000000000000000000000100 | Precompile entry-point address. |
EDDSA_BASE_GAS | uint64 | 2000 | Flat per-call cost. |
EDDSA_WORD_GAS | uint64 | 12 | Gas charged per 32-byte word of message. |
ED25519_PUBKEY_BYTES | uint32 | 32 | Length of an Ed25519 public key. |
ED25519_SIGNATURE_BYTES | uint32 | 64 | Length of an Ed25519 signature. |
MAX_MESSAGE_BYTES | uint32 | 2^16 | Maximum message length the precompile will hash. |
EDDSA_PRECOMPILE_ADDRESS is the activation value for the EDDSA_PRECOMPILE parameter declared by WIP-1001.
Scheme
The precompile implements pure Ed25519 verification as specified in RFC 8032 §5.1.7, with the following pinned choices:
- Curve: edwards25519 (Ed25519). Ed25519ph and Ed448 are out of scope; a separate WIP MAY introduce additional variants at distinct addresses.
- Context: empty. Ed25519ctx is not supported.
- Verification equation: cofactored, per ZIP-215. Verification accepts the signature iff
[8]SB = [8]R + [8]([hash(R, A, M) mod L])A, whereS,R,A,M, andLare as defined in RFC 8032 §5.1. - Public key validation:
AMUST decode as a valid edwards25519 point per RFC 8032 §5.1.3. Non-canonical y-coordinate encodings (y >= p) MUST be rejected. Small-order public keys are NOT rejected — application-level policy is responsible for additional restrictions. - Signature
Rvalidation:RMUST decode as a valid edwards25519 point. Non-canonical y-coordinate encodings ofRMUST be rejected. - Signature
Svalidation:Sis interpreted as little-endian and MUST satisfy0 <= S < L, whereL = 2^252 + 27742317777372353535851937790883648493. Non-canonicalSMUST be rejected.
Input Encoding
The precompile input is the concatenation:
input = publicKey (32 bytes) || signature (64 bytes) || message (variable)
publicKeyis the 32-byte encoded edwards25519 pointA, per RFC 8032 §5.1.2.signatureisR (32 bytes) || S (32 bytes), per RFC 8032 §5.1.6.messageis the byte stringMpassed verbatim to the hash function.
The total input length MUST satisfy 96 <= len(input) <= 96 + MAX_MESSAGE_BYTES. Any other length is treated as a failure (the precompile consumes gas as specified in Gas Cost and returns the zero word).
Output Encoding
The precompile output is exactly 32 bytes:
0x0000…0001(big-endian) if the signature is valid under the rules above.0x0000…0000(big-endian) otherwise, including for all malformed input, non-canonical encoding, point-decoding failure, scalar-range failure, and equation failure.
The precompile MUST NOT revert on a failed verification. It MUST revert only when the caller supplies insufficient gas, in which case standard out-of-gas semantics apply.
Gas Cost
For an input of length N bytes:
gasCost = EDDSA_BASE_GAS + EDDSA_WORD_GAS * ceil(max(N, 96) - 96 / 32)
= EDDSA_BASE_GAS + EDDSA_WORD_GAS * ceil(messageLength / 32)
messageLength = max(N, 96) - 96. The per-word charge covers SHA-512 absorption of R || A || M and is intentionally aligned with the per-word cost of the sha256 precompile (EIP-2).
Gas is charged in full before execution. Inputs shorter than 96 bytes are charged EDDSA_BASE_GAS and return the zero word.
Rationale
Ed25519 only. Ed25519 dominates the non-secp256k1 deployed base by orders of magnitude. Supporting Ed448 doubles the surface area for marginal real-world demand; it can be added in a follow-on WIP at a separate address if needed. Pre-hashed variants (Ed25519ph, Ed25519ctx) are likewise excluded — callers that need them can hash off-chain and use the pure verifier, or a future WIP can allocate a distinct address.
ZIP-215 / cofactored verification. RFC 8032 leaves verification underspecified on edge cases involving small-order points and non-canonical encodings, and historical libraries have disagreed. ZIP-215 pins these choices: cofactored equation, canonical encoding required, small-order keys accepted. This is the modern consensus-rule choice (Zcash, Tendermint/CometBFT) and admits straightforward batch verification by re-deriving the cofactored form from a random linear combination. Cofactorless RFC 8032 verification would reject a strict subset of signatures that cofactored accepts; pinning the more permissive rule means a signature accepted by the precompile is also accepted by any conformant ZIP-215 verifier, which is the property bridges and light clients depend on.
Failure returns zero, never reverts. This mirrors ecrecover and EIP-7951 (secp256r1 verify): callers branch on the boolean result. Reverting on bad signatures would force every caller to wrap the call in a try/catch and would make the precompile incompatible with WIP-1001 restricted validation frames, which forbid revert-based control flow inside STATICCALLs.
Concatenated, variable-length input. A length prefix on the message would add a decoding branch without reducing ambiguity — the EVM already provides the total calldata length. The 32 + 64 prefix layout matches the wire layout produced by libsodium’s crypto_sign_open and Solana / Cosmos transaction signatures, so callers can forward bytes verbatim.
MAX_MESSAGE_BYTES bound. Caps worst-case SHA-512 work per call to a predictable upper bound, which simplifies gas-cost validation and matches the bounded-input style of other consensus-critical precompiles.
Gas cost. The base cost is calibrated against benchmark data for ed25519-dalek and curve25519-dalek (single-signature verify on modern x86_64 completes in roughly 50–100 µs), giving Ed25519 verification a per-call cost comparable to ecrecover (3000 gas) and modestly cheaper than RIP-7212 (3450 gas). The per-word charge matches sha256 because the dominant variable-cost component is the SHA-512 over R || A || M. Concrete values SHOULD be refined against client benchmarks before this WIP advances to Review.
Small-order public keys are not rejected. Rejecting them at the precompile boundary would deviate from ZIP-215 and break compatibility with signatures produced by certain hardware modules. Applications that care (e.g., signers used as identity commitments) MUST reject small-order keys themselves.
Address 0x0100. Reserves a precompile slot above the Ethereum-reserved 0x01–0x0A range and aligns with RIP-7212’s convention of placing non-mainnet precompiles starting at 0x0100. The concrete address MAY be revised by maintainers before activation; the activation value flows through EDDSA_PRECOMPILE in WIP-1001.
Backwards Compatibility
This WIP adds a precompile at a previously unused address. No existing transaction type, opcode, gas cost, or precompile behavior is modified. Contracts that previously called EDDSA_PRECOMPILE_ADDRESS would have received an empty return; after activation they receive a 32-byte response. Such pre-activation calls have no on-chain precedent on World Chain.
Test Cases
Conforming implementations MUST pass:
- RFC 8032 §7.1 vectors: Test 1–4 (empty, 1-byte, 2-byte, and 1023-byte messages); all MUST return
1. - ZIP-215 §“Tests” vectors: every signature listed as ZIP-215-valid MUST return
1; every signature listed as ZIP-215-invalid MUST return0. This includes the small-order public key cases that diverge from strict RFC 8032 cofactorless verification. - Non-canonical encodings:
Awithy >= p,Rwithy >= p, andSwithS >= LMUST each return0. - Mutated signature: every RFC 8032 §7.1 vector with the last byte of
Sincremented by 1 MUST return0. - Mutated message: every RFC 8032 §7.1 vector with the message extended by one zero byte MUST return
0. - Truncated input: inputs of length
0,31,32,95,96 - 1MUST return0and charge exactlyEDDSA_BASE_GAS. - Maximum-length message: input of length
96 + MAX_MESSAGE_BYTESMUST be accepted (modulo signature validity) and chargeEDDSA_BASE_GAS + EDDSA_WORD_GAS * (MAX_MESSAGE_BYTES / 32). - Oversize input: input of length
96 + MAX_MESSAGE_BYTES + 1MUST return0. - Identity / zero inputs: 32-byte zero public key, 64-byte zero signature MUST return
0. - Gas-charge boundary: a caller supplying exactly
gasCost - 1gas MUST observe an out-of-gas revert; a caller supplying exactlygasCostMUST observe a successful return.
Golden vectors with hex-encoded (publicKey, signature, message, expected) tuples covering each row above MUST be published in assets/wip-1004/ before this WIP advances to Review.
Reference Implementation
The precompile is implementable in a few lines on top of any conformant Ed25519 library that exposes a ZIP-215 verification mode. For Rust clients (reth, OP-reth derivatives) the recommended primitive is ed25519-dalek v2 with the zebra / cofactored verification feature, or the ed25519-zebra crate directly:
#![allow(unused)]
fn main() {
fn run_eddsa_verify(input: &[u8], gas_limit: u64) -> Result<(Vec<u8>, u64), PrecompileError> {
let msg_len = input.len().saturating_sub(96);
let gas_used = EDDSA_BASE_GAS + EDDSA_WORD_GAS * msg_len.div_ceil(32) as u64;
if gas_used > gas_limit {
return Err(PrecompileError::OutOfGas);
}
let mut output = [0u8; 32]; // default: invalid
if input.len() < 96 || msg_len > MAX_MESSAGE_BYTES as usize {
return Ok((output.to_vec(), gas_used));
}
let pk_bytes: [u8; 32] = input[0..32].try_into().unwrap();
let sig_bytes: [u8; 64] = input[32..96].try_into().unwrap();
let message = &input[96..];
let ok = ed25519_zebra::VerificationKey::try_from(pk_bytes)
.and_then(|vk| {
let sig = ed25519_zebra::Signature::from(sig_bytes);
vk.verify(&sig, message)
})
.is_ok();
if ok {
output[31] = 1;
}
Ok((output.to_vec(), gas_used))
}
}
ed25519-zebra implements ZIP-215 directly. Other clients SHOULD select an Ed25519 implementation that explicitly documents ZIP-215 compatibility; libraries that implement only strict RFC 8032 cofactorless verification are non-conforming.
Security Considerations
Verification-rule monoculture. All World Chain clients MUST implement identical edge-case behavior. A client that accepts a signature another client rejects causes a consensus split. Selecting ZIP-215 — a written, testable, and widely-deployed specification — is the primary mitigation. Implementers MUST pass the ZIP-215 test corpus, not just RFC 8032 §7.1.
Signature malleability. Ed25519 is non-malleable only when S is canonical (S < L). The precompile enforces this; callers MUST NOT assume malleability resistance from any other property of the encoding. In particular, the 32-byte encoding of R is canonicalized but the underlying point is not: ZIP-215 accepts certain low-order R values that strict RFC 8032 rejects.
Small-order public keys. Per ZIP-215, the precompile accepts public keys of small order. A small-order A makes the discrete log of A trivial, so any party can forge signatures under such a key. Identity systems and authentication contracts MUST reject small-order keys at registration time, not at verification time. WIP-1001 session verifiers that bind a public key into account state SHOULD perform this check during install.
Batch verification not exposed. This precompile verifies one signature per call. Consumers that need batch verification (e.g., light clients ingesting a consensus quorum) MUST batch at the application layer or rely on a future batched-verify WIP. Single-signature verification under ZIP-215 is, however, batch-equivalent: any signature accepted singly is accepted in a batch.
Side channels. Verification handles only public data; constant-time implementation is not required for correctness. Implementations SHOULD still use constant-time field arithmetic to remain consistent with their underlying curve library and to avoid surprising callers who later use the same library on secret data.
Restricted validation frames. When called from a WIP-1001 session verifier inside a restricted frame, the precompile’s STATICCALL MUST be the only externally-visible effect; it has no storage, no logs, and no nested calls, so it satisfies the WIP-1001 tracer rules by construction. The precompile MUST NOT be modified to read block context, account state, or any external mutable input.
Gas-cost calibration. Underpriced verification is a DoS vector. The recommended values in Constants MUST be re-benchmarked on representative consensus-client hardware before activation, and adjusted upward if Ed25519 verification proves slower than the priced cost on the slowest supported client.