go get github.com/Overclock-Validator/narya-ed25519
Narya is a no-cgo Ed25519 verification library for Go built to make
consensus-exact verification fast. Its opt-in AVX-512 IFMA backend delivers
substantial cold-key speedups over Go's crypto/ed25519 when independent
signatures can be processed together; see Performance for the
measured results, hardware, checkpoints, and limitations.
The speedup comes from wide-lane SIMD: Narya maps unrelated signatures onto independent AVX-512 lanes, keeping a separate verification equation and verdict for every input. It is aimed at replicated systems, high-throughput services, archival verification, and any other application where two properties matter at once: verification must be fast, and every implementation must agree on exactly which byte strings are valid.
Narya provides explicit DalekStrict and Go-standard-library-compatible
profiles instead of treating "Ed25519 verification" as one universal
predicate. Its cold path handles arbitrary public keys without persistent
state. Its optional cache adds a decoded-key tier for broad recurrence and a
larger precomputed tier for genuinely hot keys. Batch APIs never replace
per-signature verification with a randomized aggregate equation: every input
still receives its own deterministic accept/reject result.
On supported AMD64 processors, the forced r51 backend runs independent
signatures across AVX-512 IFMA lanes. Measured AMD CPUs use native x8 groups,
x4 tails, and a coordinate-packed singleton. Zen 5 additionally places two
complete verification equations in the independent low and high halves of one
ZMM register for strict two-signature tails. Portable callers retain the
pure-Go generic backend, and automatic backend selection remains
conservative.
Security status: experimental and unaudited. Narya has extensive differential, vector, fuzz, range, aliasing, and hardware tests, but it has not received an independent cryptographic or assembly audit. Do not treat it as a drop-in production security boundary without your own review and validation. The software is provided as is, without warranty; the authors and contributors accept no liability for its use. See LICENSE for the governing terms.
Narya was one of the Three Elven Rings in Tolkien's legendarium. Known as the Ring of Fire and carried by Gandalf, it had the power to inspire hope, withstand the weariness of time, and kindle resistance against tyranny and despair.
Ed25519 verification is not one predicate. RFC 8032 leaves several choices open, and production implementations resolve them differently:
| cofactorless | small-order A |
small-order R |
non-canonical A |
non-canonical R |
|
|---|---|---|---|---|---|
Go crypto/ed25519 |
yes | accept | accept | accept | reject |
ed25519-dalek verify_strict |
yes | reject | reject | accept | reject |
| ZIP 215 | no (cofactored) | accept | accept | accept | accept |
Any two rows disagree on a nonempty, adversarially constructible set of inputs. In a replicated or long-lived system, that difference can become a remotely triggerable consistency failure: an attacker chooses an input in the symmetric difference and two otherwise-correct implementations return different verdicts.
The subtlety that makes this worth a library: verify_strict is not
"strict about everything." It rejects non-canonical R but accepts
non-canonical A. Hand-rolling the predicate and getting that one case
backwards is a silent fork.
Narya makes the choice explicit, versioned, and differentially tested rather than incidental.
Verification enforces one of two versioned profiles, because the consensus-correct acceptance predicate is itself versioned. An accept/reject flip is a fork, so it is a deliberate choice, not an implementation detail:
DalekStrict(default, and the zero value) matchesed25519-dalek2.xverify_strict. This iscrypto/ed25519.Verifyplus rejection of small-order public keys A and small-order signature points R. The standard library accepts those (it never decodes R); a verifying node that used it unmodified could be forked off the network by a crafted block, so this is the default.StdlibCompatis exactlycrypto/ed25519.Verify, for differential testing and callers who explicitly want standard-library behavior.
Small-order rejection is a byte-level classifier over the seven low-255-bit
small-order y encodings, sign-bit-insensitive: fourteen byte strings, with
no point decode required.
For every input (non-canonical encodings, small-order points, malformed signatures), every backend, cached or not, batched or single, returns exactly what the active profile's predicate returns. This is enforced by differential tests, the CCTV and Wycheproof corpora, a cross-library differential against curve25519-voi, and fuzzing rather than assumed; the 165 CCTV vectors where the two profiles differ are precisely the small-order set, cross-checked against Firedancer's independent verdict.
Narya never uses random-coefficient aggregate verification: an ordinary
cofactored aggregate can accept adversarial signatures that cofactorless
per-signature verification rejects. A strict-compatible Monte Carlo hybrid can
first apply an exact per-item torsion-membership gate; it still returns one
probabilistic batch assertion rather than N independent, zero-error verdicts.
The proof boundary and the one justified point-halving experiment are recorded
in
docs/STRICT_AGGREGATE_BATCHING.md.
Narya's public "Batch" APIs instead preserve an independent verdict for every
signature. The default generic backend processes signatures independently.
The explicitly selected r51 backend hashes and decodes several independent
signatures in SIMD lanes while retaining a separate verdict for every input.
A future ZIP215 profile may expose the cofactored predicate explicitly. It
will not silently change either existing profile.
The security meaning of these checks is subtler than a simple
"strict-versus-permissive" ordering. Brendel, Cremers, Jackson, and Zhao prove
strong unforgeability from the canonical-S check for their standardized
Ed25519 variant, while their additional malicious-key and message-binding
results rely on rejecting small-order elements. Narya's profiles are not
identical to the paper's named variants, so the precise correspondence and the
limits of what can be inferred are recorded in
docs/PROVABLE_SECURITY.md.
The word is overloaded, and the two meanings have different guarantees. This
is worth being precise about, because it explains both what VerifyBatch does
here and why Narya's throughput curve has a different shape from other
libraries'.
| Lane-parallel (what Narya does) | Aggregate (VerifyBatch in most libraries) |
|
|---|---|---|
| equations evaluated | N, one per signature | 1, all signatures combined |
| verdicts returned | N | 1 |
| identical to verifying one at a time? | yes, bit for bit | no |
| valid under a cofactorless predicate? | yes | ordinary form: no; gated hybrid: probabilistic |
| source of the speedup | filling SIMD lanes | fewer group operations |
| scaling | flattens at the lane count | keeps improving with N |
Lane-parallel batching is a hardware optimization. Eight independent signatures occupy eight AVX-512 lanes, so one instruction stream performs eight signatures' worth of field arithmetic, but each signature still gets its own complete verification equation and its own answer. Nothing about the mathematics changes; the machine is merely busier. That is why every verdict is bit-for-bit what you would get from a loop, and why the curve flattens once the lanes are full.
Aggregate batching is a mathematical optimization. It folds N signatures into a single equation with random weights, replacing N double-scalar multiplications with one multi-scalar multiplication. It is genuinely faster where it applies, and it keeps getting faster as N grows. What it returns is one probabilistic answer for the whole set. The ordinary construction is sound for a cofactored predicate; applying it directly to a cofactorless predicate lets individual torsion errors cancel. A hybrid can restore cofactorless predicate compatibility by proving each residual torsion-free first, but that per-item cost and the aggregate's nonzero soundness error remain.
That constraint is not Narya's opinion. curve25519-voi implements ordinary
aggregate batch verification and refuses to apply it to cofactorless entries,
returning false rather than a possibly-unsound accept
(primitives/ed25519/batch_verify.go). Under DalekStrict, Narya does not use
aggregate verification because its contract requires deterministic,
independent verdicts. The more expensive strict-compatible hybrid is retained
only as a documented research question.
Choosing between them. If one answer for the whole set is enough (you reject an entire block on any failure, say), aggregate batching under a cofactored predicate is likely the better tool, and Narya is not it. If you need to know which input failed, or you must match a cofactorless predicate exactly, aggregate batching cannot give you that at any speed, and lane-parallel is what remains. Admission control is the clearest case: an aggregate verdict over a batch of user-submitted transactions cannot tell you which one to drop, and discarding the whole batch hands an attacker a very cheap denial of service.
Narya has no signing API, deliberately. The mismatched private/public-key signing-oracle failures catalogued by ed25519-unsafe-libs are therefore outside its API: Narya never accepts a caller-supplied private/public key pair. Verification still has its own consensus and input validation risks, which are addressed by the profile contract and differential test corpus below. If signing is ever added, it must derive or validate the public key from the secret as required by the RFC 8032 signing procedure; merely verifying the emitted signature is not a sufficient defense against the mixed-order substitution described by the Mysten audit.
func Verify(pub *[32]byte, message, sig []byte) bool // uses DefaultProfile()
func VerifyStrict(pub, message, sig []byte) bool // always DalekStrict; byte-slice pub,
// drop-in for crypto/ed25519.Verify
func VerifyBatch(pubs []*[32]byte, msgs, sigs [][]byte, ok []bool) bool
func VerifyBatchStrict(pubs []*[32]byte, msgs, sigs [][]byte, ok []bool) bool
func Precompute(pub *[32]byte) (*PrecomputedKey, error)
func (k *PrecomputedKey) Verify(message, sig []byte) bool
type Cache struct{ MaxTableBytes int64 } // 0 means DefaultMaxTableBytes (128 MiB)
func (c *Cache) Verify(pub *[32]byte, message, sig []byte) bool
func (c *Cache) VerifyStrict(pub *[32]byte, message, sig []byte) bool
func (c *Cache) VerifyBatch(pubs []*[32]byte, msgs, sigs [][]byte, ok []bool) bool
func (c *Cache) VerifyBatchStrict(pubs []*[32]byte, msgs, sigs [][]byte, ok []bool) bool
func (c *Cache) Stats() CacheStats
type Profile uint8
const (DalekStrict Profile = iota; StdlibCompat)
func SetDefaultProfile(p Profile)
func DefaultProfile() Profile
func ActiveBackend() string
func SetBackend(name string) error // must precede first verificationVerifyBatch writes per-signature verdicts into ok and panics if the slice
lengths differ. Precompute returns a non-nil error when pub does not decode.
ed25519is the public API above. Exactly one backend is active per process, latched on first use. Resolution order: the name passed toSetBackend, thenOVERCLOCK_ED25519_BACKEND, then the default.sha512mbis multi-buffer SHA-512.cmd/sigverifytracebenchis offline exact-input schema-v3 replay for a measured stdlib/generic/generic-cache diagnostic. It never promotes the generic table result to the pending r51 production cache gate.
| backend | status | notes |
|---|---|---|
generic |
default | Pure Go over the vendored edwards25519 internals, with per-key fixed-base comb tables for recurring signers. |
stdlib |
available | Routes to crypto/ed25519. The rollback proof point. |
ifma |
opt-in, in development | AVX-512 IFMA point arithmetic after Firedancer's r43x6 representation. Requires AVX512F/VL/DQ/BW/IFMA/VBMI, detected at runtime via x/sys/cpu, never via GOAMD64, since x86-64-v4 does not imply IFMA. |
r51 |
registered, forced-only | AMD lane-per-signature r51 backend. Strict singletons use paired A/R decode and a coordinate-packed projective finalizer. Zen 5 strict two-signature tails put one complete equation in each 256-bit half of a ZMM register; other measured CPUs call the packed singleton twice. Wider batches use a radix-32 A table, A-only decode, and cross-group batch encoding of Q. Measured AMD family 19h+ IFMA parts, including Zen 4 and Zen 5, use x8/ZMM for complete eight-signature groups and x4 curve arithmetic for the tail. Zen 5 injects width-10 generator digits into A's x8 doubling chain; Zen 4, unknown IFMA CPUs, and x4 tails retain the process-shared radix-256 generator comb. Zen 5 hashes four-to-seven-item cold tails through the x8 SHA/reduction path; Zen 4 and unknown IFMA CPUs retain x4 hashing. The x8 doubler uses the symmetry-aware raw-square schedule on both measured families; Zen 5 additionally keeps intermediate doubling results in a three-coordinate P2 type and computes extended T only at addition boundaries. From 16 signatures, Zen 5 also widens literal-Q batch encoding to x8; other measured widths retain x4. Its opt-in Cache first admits an exact-byte-bound decoded-A entry and promotes recurring valid strict keys to an immutable A6/r9 warm comb. Warm x4 groups are consumed in aligned pairs on the measured AMD set, except a final four-item tail, so a half-warm x8 group stays on the faster native-wide cold path. StdlibCompat singleton calls retain the generic literal-encoding path. This backend is never selected automatically. |
Selection is deliberately non-degrading. ifma requires AVX512F/VL/DQ/BW,
IFMA, and VBMI. r51 requires that same IFMA feature set plus AVX2 for its
native x4 SHA-512 path. SetBackend performs the complete activation check
synchronously; an unknown or unsupported forced name returns an error there
and panics on the environment-variable path. A forced name represents explicit
operator intent and must not silently fall back.
sha512mb's public Lanes() and Sum512Batch surface remains the portable
scalar implementation. Its AVX2 and AVX-512 kernels are hardware-gated behind
the Experimental* entry points; the forced r51 backend calls the x8 native
entry for complete groups on measured AMD family 19h+ IFMA parts. Zen 5 also
uses x8 hashing for four-to-seven-item cold x4 curve tails; Zen 4, smaller
tails, and unknown IFMA CPUs retain x4 hashing. Automatic
backend selection never reaches either kernel.
Unclaimed AVX2 path. The x4 kernel gates on AVX2 alone, with no AVX-512
term. Since the point arithmetic in r51 needs IFMA and cannot run without it,
an AVX2-only host falls to generic, which hashes through the scalar
Sum512Batch and so never reaches that kernel. Routing the default batch entry
through the native kernels would therefore speed up generic on any host with
AVX2, bounded by SHA-512's share of one verification. Not wired, not
measured, and no AVX2-only machine has been benchmarked — the size of the win
is an estimate, not a result.
The x8 fixed-three-segment entry recognizes full
groups of the exact R[32] || A[32] || message shapes at message sizes
64/200/1232, ingesting their first and final blocks without generic segmented
staging. Design details and historical measurements are kept in
docs/SHA512_MULTIBUFFER.md.
Narya's accelerated path is measured through the exported
SetBackend("r51"), VerifyBatchStrict, and Cache.VerifyBatchStrict APIs.
The release snapshot uses cold Zen 5 measurements only for its headline:
an AMD Ryzen 7 9700X, Go 1.26.4, one pinned physical core, the performance
governor, and GOMAXPROCS=1. Every single-core table below was rerun from
exact live-path implementation commit
bbc6c2194438090b0c48ac9bd95eab6b92602d6f; raw output, commands, host
details, and checksums are under
docs/results/zen5-packed-pair-whole-window-2026-07-29/.
Every timed Narya row reported 0 B/op, 0 allocs/op, and zero internal-fault
fallbacks. Displayed single-core values are medians of three two-second
samples. The unchanged multicore table retains its separately identified
six-sample checkpoint.
Units: every numeric timing cell in the tables below is microseconds per
signature (µs/signature, lower is better). These are per-signature costs,
not per-batch latencies.
Ryzen 7 9700X · arbitrary keys with no retained key state · µs/signature, lower is better
| batch size | 200-byte message | 1,232-byte message | 4,096-byte message |
|---|---|---|---|
| 1 | 13.040 | 13.590 | 15.780 |
| 2 | 11.110 | 11.620 | 13.730 |
| 4 | 7.408 | 7.872 | 9.223 |
| 8 | 3.585 | 3.850 | 4.548 |
| 64 | 3.373 | 3.623 | 4.330 |
At 1,232 bytes, the n=8 and n=64 rows correspond to approximately 259,700 and 276,100 signatures/second/core. Batch width matters because n=1 uses the coordinate-packed x4 verifier, Zen 5 n=2 fills both independent 256-bit halves of a ZMM register, n=4 fills one x4 curve group and uses x8 hashing on Zen 5, and n=8 or larger can fill native x8 groups. Zen 4 retains two independently measured singleton calls for n=2.
Ryzen 7 9700X · 1,232-byte messages · 64 promoted keys · µs/signature, lower is better
| batch size | cold µs/signature | warm µs/signature | warm speedup |
|---|---|---|---|
| 1 | 13.590 | 13.800 | 0.98x |
| 2 | 11.620 | 11.960 | 0.97x |
| 4 | 7.872 | 4.057 | 1.94x |
| 8 | 3.850 | 3.819 | 1.01x |
| 64 | 3.623 | 3.655 | 0.99x |
The cache fixture promotes 64 keys and occupies 1,243,136 table bytes. The cache deliberately bypasses prepared tables below n=4, so lookup overhead can make the singleton row marginally slower. Its wider-batch result depends on key population and locality; this small hot fixture is a reference, not a universal hit-rate claim. Current cold x8 now approximately matches the hot-fixture warm path at n=8 and exceeds it at n=64, so this cross-check is not a claim that a cache hit necessarily helps every current width.
The warm path is also not unconditionally faster for every message size. The complete measured matrix is:
Ryzen 7 9700X · 64 promoted keys · µs/signature, lower is better
| batch size | 200-byte message | 1,232-byte message | 4,096-byte message |
|---|---|---|---|
| 1 | 12.940 | 13.800 | 15.760 |
| 2 | 11.060 | 11.960 | 14.110 |
| 4 | 3.307 | 4.057 | 6.105 |
| 8 | 3.065 | 3.819 | 5.868 |
| 64 | 2.906 | 3.655 | 5.730 |
At 4,096 bytes, cold x8 is faster than the current warm x4-oriented path at n=8 and n=64. The two paths schedule hashing differently, and hashing is a larger fraction of a long-message warm verification. Cache population, message size, and width must therefore be reported together.
These numbers describe the explicitly forced backend, not automatic dispatch;
the portable generic backend remains the default.
Ryzen 7 9700X · 1,232-byte messages · µs/signature, lower is better
The comparison below uses one binary, the same Zen 5 host, and 1,232-byte messages. Every candidate runs ordinary per-signature verification and returns one verdict per input; no aggregate batch equation is used. Voi's expanded-key row excludes expansion cost and is included as a warm-key reference.
| implementation | n=1 µs/sig | n=2 µs/sig | n=4 µs/sig | n=8 µs/sig | n=64 µs/sig |
|---|---|---|---|---|---|
| Narya r51, cold strict | 13.860 | 12.200 | 7.851 | 3.789 | 3.601 |
Go crypto/ed25519 |
27.690 | 27.340 | 27.430 | 27.370 | 27.370 |
| curve25519-voi, cold strict | 21.980 | 21.690 | 21.780 | 21.770 | 21.890 |
| curve25519-voi, expanded key | 19.160 | 18.880 | 18.960 | 18.960 | 19.040 |
Within that comparison binary, Narya is 2.00x faster than Go at n=1, 2.24x at n=2, 3.49x at n=4, 7.22x at n=8, and 7.60x at n=64. The comparison binary includes the opt-in Voi dependency and has a different link layout from the release binary, so use this table for library ratios and the cold table above for Narya's release latency.
Ryzen 7 9700X · 1,232-byte messages · aggregate signatures/second, higher is better
These are aggregate signatures per second over 1,232-byte messages, not individual request latency. Each row pins only distinct physical cores; SMT siblings are excluded.
| physical cores | n=4 signatures/s | n=4 scaling | n=8 signatures/s | n=8 scaling |
|---|---|---|---|---|
| 1 | 125,100 | 1.00x | 260,104 | 1.00x |
| 2 | 248,807 | 1.99x | 524,739 | 2.02x |
| 4 | 488,958 | 3.91x | 1,029,362 | 3.96x |
| 6 | 702,317 | 5.61x | 1,476,064 | 5.68x |
| 8 | 891,736 | 7.13x | 1,821,228 | 7.00x |
The eight-core rows correspond to aggregate throughput costs of 1.121
and 0.549 microseconds per signature. Each worker still verifies complete,
independent equations; this table measures concurrent callers, not aggregate
cryptographic batch verification. Raw output is in the earlier, unchanged
docs/results/zen5-final-review-2026-07-29/
multicore checkpoint; it was not inferred from the newer serial run.
Hardware scope: AMD only so far. Every displayed timing above was captured
on an AMD Ryzen 7 9700X (Zen 5); historical bundles in docs/results/ also
include a Ryzen 7 PRO 8700GE (Zen 4). Narya dispatches on the AVX512-IFMA
feature set rather than on
vendor, so the same kernels are expected to run on Intel Ice Lake Server and
newer, and CI exercises them under Intel SDE emulating Ice Lake Server. But
emulation establishes function, not speed: no Intel silicon has been
benchmarked, and no server part of either vendor has. Treat the numbers as
characterizing consumer Zen 5 and nothing else. Intel and EPYC
measurement is outstanding work, not a completed check.
Historical measurements and their exact environments remain in
docs/results/; they are intentionally not stacked into the
current tables because code, CPU generation, and cache population materially
change the result. Current cold, warm, and cross-library outputs are in
docs/results/zen5-packed-pair-whole-window-2026-07-29/.
The unchanged multicore output remains in
docs/results/zen5-final-review-2026-07-29/.
Cold and warm are two execution conditions with the same acceptance predicate, not different security modes:
| path | public entry point | intended workload | trade-off |
|---|---|---|---|
| cold | VerifyStrict / VerifyBatchStrict |
arbitrary, first-seen, or low-recurrence public keys | no persistent key state; decodes A and builds its small variable-base table for each verification |
| warm | Cache.VerifyStrict / Cache.VerifyBatchStrict |
public keys that recur enough to repay preparation | exact-byte-bound decoded-A and precomputed-comb tiers reduce curve work in exchange for memory, lookup, and admission overhead |
Both paths hash the original public-key and signature bytes, retain independent per-signature verdicts, and enforce the selected profile. Invalid inputs and invalid equations never earn cache promotion. Warm results depend on the number and recurrence of keys: a tiny permanently hot fixture can overstate performance compared with a populated cache, so Narya reports cache population alongside every release measurement.
The cache is opt-in. Raw VerifyBatchStrict remains cold and stateless, and
automatic backend selection remains generic.
The public cold/warm benchmark is isolated behind a build tag so it cannot accidentally measure a private implementation seam:
taskset -c 2 env GOMAXPROCS=1 go test -tags r51_release_bench \
-run '^$' \
-bench '^BenchmarkPublicR51(VerifyBatchStrict|CacheVerifyBatchStrict)$/^msg=(200|1232|4096)$/^n=(1|2|4|8|64)$' \
-benchmem -benchtime=2s -count=10 ./ed25519The 1232-byte comparison table comes from the isolated Voi module:
taskset -c 2 env GOMAXPROCS=1 go test \
-modfile=go.oasis.mod -tags oasis_compare -run '^$' \
-bench '^BenchmarkEd25519CrossLibrary$/^mode=independent$/^impl=(narya-r51-dispatch|go-stdlib-loop|oasis-strict-cold-loop|oasis-strict-expanded-loop)$/^n=(1|2|4|8|64)$/^msg=1232$' \
-benchmem -benchtime=2s -count=6 ./ed25519The accelerated backends require AVX512-IFMA and must be selected explicitly
with SetBackend("r51") or OVERCLOCK_ED25519_BACKEND=r51. Unsupported
forced activation fails synchronously instead of silently measuring the
portable path.
For reproducible CPU measurements, pin a physical core, use the same Go
toolchain, record the CPU governor, and use repeated samples with
benchstat. Do not infer sustained frequency from an idle
/proc/cpuinfo sample; use hardware counters when cycle-level comparisons
matter.
- All five plain Ed25519 known-answer vectors from
RFC 8032 section 7.1,
914 CCTV
ed25519vectors, and 133 Project Wycheproofeddsa_testvectors, plus pinned Firedancer regression vectors and a generated edge-point corpus. - Differential tests anchoring every backend, cached or not, batched or single,
to
crypto/ed25519and to the generic backend, per profile. - A cross-library differential against
curve25519-voi
version
v0.0.0-20230904125328-1f23a7beb09a, configured to the equivalent strict option set. It is isolated from the library module graph ingo.oasis.mod; run it withmake test-oasis. - Fuzz targets comparing backends three ways.
Fuzz soak status: short rounds only. The differential fuzzing run so far is
smoke-scale — roughly 4.6 million executions across about 25 minutes, split
between the r51 pipeline, the public verifier, and multi-buffer SHA-512, all
passing. The bar this project sets for enabling automatic SIMD dispatch is a
prolonged soak on the order of 10^9 executions, and that has not been run.
The two facts are consistent rather than contradictory: automatic selection is
still generic, and r51 is reachable only by explicit SetBackend. But the
soak is a precondition for changing that, not a formality already satisfied.
The runner and its evidence format are described in
docs/FUZZ_SOAK.md.
No external review of the assembly. Everything above is self-consistency:
the vector kernels are checked against scalar models, the models against the
vector corpora, and the corpora against crypto/ed25519. That is a strong
structure and it is not the same thing as an independent audit. Roughly 4,500
lines of assembly have had no third-party review.
CI runs portable tests on ubuntu-latest and macos-latest, the isolated
Oasis differential on Linux, plus a pinned
Intel SDE
10.8 job that emulates Ice Lake Server and executes focused r51x5 IFMA,
native-SHA, and public forced-r51 differentials. Dedicated sde_gate tests
fail rather than skip when the emulated feature set is missing. SDE is
functional coverage only. Native release gates have run on the Ryzen 7 PRO
8700GE (Zen 4), while the displayed release-performance snapshots were taken
on the Ryzen 7 9700X (Zen 5); zero-allocation and differential gates are
required on the native hardware used for each release measurement. Automatic
backend selection remains generic.
Alpha. The generic backend, the profile contract, and the per-key comb cache
are functional and differential-tested. The r51 throughput backend is
registered for explicit selection on supported hardware but remains outside
automatic dispatch. It uses native x8 groups, x4 tails, and packed strict
singletons on measured AMD family 19h+ IFMA processors; Zen 5 also has the
two-equation packed-ZMM n=2 path described above. Its opt-in two-tier Cache
and width-aware A6/r9 warm promotion are implemented and hardware-tested; the
traffic-specific admission and eviction policy remains integration work. The
ifma reference backend and alternate arithmetic experiments remain test-only
evidence. Performance work is frozen at this checkpoint while the supported
path receives safety review.
The radix-51 backend's analytic range evidence, exact test coverage, and
remaining machine-certificate boundary are summarized in
docs/R51_ARITHMETIC_ASSURANCE.md.
Proof-oriented artifacts are indexed separately in
docs/formal/README.md; each artifact states whether
it is a Lean theorem, executable certificate, differential test, or native
hardware gate and names the remaining refinement boundary.
The exact supported/experimental boundary and pre-audit checklist are frozen in
docs/SAFETY_FREEZE_2026-07-29.md.
These are open, not pending paperwork. Each is described where it belongs above.
| item | state |
|---|---|
| 10^9-execution differential fuzz soak | not run; ~4.6M executions so far |
| Intel silicon benchmarks | not run; SDE gives function, not speed |
| Server-part benchmarks (EPYC, Xeon) | not run; consumer Zen 4/5 only |
| Independent review of the assembly | not done |
| Native SHA-512 under the default batch entry (helps AVX2-only hosts) | not wired, not measured |
| Traffic-specific cache admission and eviction policy | integration work |
Until these close, generic remains the automatic choice and r51 remains
opt-in. Published r51 figures describe an explicitly forced backend.
Narya was developed with extensive assistance from OpenAI Codex and ChatGPT Pro, together with Anthropic Claude. These systems contributed to code exploration, profiling analysis, mathematical review, hypothesis generation, test design, documentation, and implementation work, including assembly.
Humans selected the supported design and remain responsible for every change. AI output was treated as an untrusted proposal: retained work had to pass independent reference vectors, differential tests, range and aliasing checks, native-hardware correctness gates, and repeated performance measurements. The AI systems are development collaborators, not cryptographic auditors or endorsers of the library.
Apache-2.0. See NOTICE for the full attribution list.
Vendored code. internal/edwards25519 derives from the Go standard
library's crypto/internal/edwards25519 and from
filippo.io/edwards25519 v1.0.0;
its field subpackage is synchronized to
filippo.io/edwards25519 v1.2.0
(BSD-3-Clause). The upstream LICENSE files and BSD headers are preserved.
Modified vendored files retain those headers and are enumerated in
NOTICE; standalone Narya-authored files carry Apache-2.0 headers.
Derived work. The r43x6 AVX-512 IFMA design and constants in
internal/r43x6 follow
Firedancer at 3ed37488372b7e50bb03ca30477be48508ee7022,
Copyright 2022 Firedancer Contributors, Apache-2.0. Firedancer records that its
Ed25519 implementation was originally based on OpenSSL's circa-October-2022
implementation; the inherited notice and license text are carried in
NOTICE.
Prior work this library descends from. The generic backend, the per-key
comb cache, and internal/edwards25519/comb.go originate in pkg/ed25519fast
in Mithril, authored by
palmer.
Papers and design influences. The sources below influenced different parts of Narya; inclusion here does not mean their code was copied or that an experimental idea is part of the supported backend.
| Source | Influence on Narya |
|---|---|
| ENG25519: Faster TLS 1.3 Handshake Using Optimized X25519 and Ed25519 (Zhang, Huang, Zhao, Chen, and Koç, USENIX Security 2024) | The strongest academic influence on the shipping r51 backend. It provided independent published evidence that five radix-2^51 limbs are a good fit for AVX-512 IFMA. Narya's lane-per-signature kernels and implementation are its own. |
| Taming the many EdDSAs | Shaped the acceptance-predicate analysis: canonical encodings, cofactored versus cofactorless equations, mixed-order behavior, and the edge-case corpus used to prove cross-implementation agreement. |
| The Provable Security of Ed25519: Theory and Practice (Brendel, Cremers, Jackson, and Zhao, IEEE S&P 2021) | Supplies the formal security rationale for treating canonical scalar encodings, key-prefix hashing, and small-order rejection as distinct protocol choices. Narya does not claim that its profiles are identical to the paper's variants; the exact mapping and proof boundary are documented in docs/PROVABLE_SECURITY.md. |
| Accelerating EdDSA Signature Verification with Faster Scalar Size Halving (ElSheikh, Keskinkurt Paksoy, Cenk, and Hasan, TCHES 2025) | Basis of Narya's test-only HEEA/scalar-halving track. Narya adds the modulo-8L relation and unit-multiplier requirement needed to preserve its cofactorless full-group equation; the published speedup is not treated as a prediction for Narya. |
| Optimized Lattice Basis Reduction in Dimension 2, and Fast Schnorr and EdDSA Signature Verification (Pornin, 2020) | Earlier ABGLSV–Pornin lineage behind scalar-size-halving verification and Voi's cofactored fast path. It informed the research comparison, not the production DalekStrict backend. |
| Point-Halving and Subgroup Membership in Twisted Edwards Curves (Pornin, 2022) | Supplies the nonlinear prime-subgroup-membership method behind Narya's documented strict-compatible aggregate-batching research gate. No aggregate verifier or point-halving kernel is currently shipped. See docs/STRICT_AGGREGATE_BATCHING.md. |
| High-speed high-security signatures and RFC 8032 | Foundational Ed25519 scheme, encoding, hashing, and known-answer vectors. Narya deliberately exposes explicit named verification predicates where real implementations resolve edge cases differently. |
The largest non-paper engineering influences are Firedancer's AVX-512 Ed25519 and SHA-512 work, curve25519-dalek and curve25519-voi's vectorized Edwards implementations and predicate behavior, and BoringSSL's square-root-ratio derivation. Eclipse Labs' Breaking 10 Million TPS was useful directional confirmation of the x8/radix-51 IFMA design space, but is not treated as a formal result or performance claim. Exact code provenance, pinned revisions, and license obligations are recorded in NOTICE.
Specifications and reference implementations. The acceptance predicate is
that of ed25519-dalek 2.2.0 at 8016d6d
verify_strict, as reached by
Agave audit snapshot 7e51da9
(Anza) through the solana-signature crate; Agave is the reference Narya's
verdict must match. The reserved ZIP215 profile name is from Zcash's
ZIP 215. sha512mb implements FIPS 180-4. The
square-root-ratio derivation used in field/fe.go and internal/r43x6 follows
BoringSSL commit 0fc57bef1821c163ac023a0aa96e4fb2a67c0d82.
Test corpora. The CCTV ed25519vectors corpus is redistributed under
BSD-3-Clause, Copyright 2019 Google LLC and Copyright 2022 Filippo Valsorda;
its license text is reproduced in full in NOTICE as that license
requires. Project Wycheproof vectors are redistributed under Apache-2.0,
Copyright Google LLC.
Comparison and prior art.
curve25519-voi at 1f23a7beb09a
(Oasis Protocol, BSD-3-Clause) serves as a cross-library differential oracle
and performance baseline. Narya does not use voi's shipped cofactored
ABGLSV–Pornin verification path for DalekStrict; multiplying the error point
by a non-injective cofactor can change that predicate. Narya's separate
torsion-safe modulo-8L HEEA work remains experimental.
voi is an opt-in test dependency only. The main go.mod retains only
golang.org/x/sys; go.oasis.mod pins the independent comparator used by
make test-oasis and the separately recorded comparison benchmarks.
voi is itself largely derived from
curve25519-dalek 3.2.0 at 09a726c, and
the vectorized Edwards backend that produces its uncached single-signature
timings is a Go port of dalek's AVX2 backend (Copyright isis agora lovecruft,
Henry de Valence, and Oasis Labs), selected whenever AVX2 is present. That intra-signature
orientation, one point's coordinates across vector lanes, is prior art that
Narya did not originate: dalek's AVX2 backend is a documented implementation,
and Firedancer's r43x6 QUAD packing, which internal/r43x6 credits, is the
same idea at AVX-512 width. Narya's experimental
coordinate-parallel work uses that orientation at radix 2^51. See
NOTICE.