Why we picked Ed25519 + RFC 8785 for R+2
The cryptographic design choices behind the Open Provenance Standard. What we considered, what we rejected, and the migration path when (not if) one of these primitives breaks.
When you publish a cryptographic spec, you're committing every implementation to a set of design choices that get expensive to change. Every R+2 receipt that ever gets signed will be signed using the algorithms we picked. If we got those choices wrong, we lock millions of future agents into a bad design.
So when the spec landed at Ed25519 + RFC 8785 + SHA-256 + Ed25519-signed hash chain, those weren't defaults pulled from a tutorial. Each was a deliberate decision after considering the alternatives. This post walks through the reasoning.
If you're a builder using R+2, none of this affects you directly — the primitives are abstracted by the verifier library. But if you're proposing changes to R+2, considering rolling your own provenance scheme, or evaluating R+2 against alternatives for a procurement decision, this is the substance behind the spec.
The four primitives and the four choices
R+2 is held up by four cryptographic primitives. Each was a choice between viable alternatives:
| Primitive | What we picked | What we considered |
|---|---|---|
| Signature algorithm | Ed25519 | ECDSA-P256, ECDSA-secp256k1, Schnorr (secp256k1), BLS12-381, Falcon, Dilithium |
| Canonicalization | RFC 8785 (JCS) | CBOR, MessagePack, Protobuf canonical form, Roll-your-own JSON canonicalizer |
| Hash function | SHA-256 | SHA-512, SHA-3-256, BLAKE2b, BLAKE3 |
| Chain structure | Explicit linked list (prev_receipt_cid) | Merkle tree, Sparse Merkle tree, Verkle tree, Inclusion proofs via STARKs |
Let's go one at a time.
Why Ed25519 over the alternatives
The signature algorithm is the most consequential choice in the spec. It's the one that says "this agent did this thing" and we need it to be:
- Fast enough to sign 100,000 receipts per agent per day without becoming the bottleneck
- Short enough that signatures don't bloat the receipt unreasonably
- Implemented correctly in every major language so that two independent verifiers don't disagree
- Side-channel safe so that signing doesn't leak the private key through timing or power analysis
- Standardized so we're not betting on someone's clever-but-unreviewed scheme
Ed25519 satisfies all five. Specifically:
Ed25519 vs ECDSA-P256 (NIST curve)
ECDSA-P256 is the algorithm most "enterprise crypto" uses — TLS, PKI, JWTs. We rejected it for three reasons:
- ECDSA requires per-signature randomness that, if biased, leaks the private key. Sony's PS3 firmware was famously compromised this way. Ed25519 derives its nonce deterministically from the message hash, making this class of attack impossible.
- The P-256 curve constants are NIST-blessed but the seed values are unexplained. There's been a 20-year argument about whether the constants were chosen to allow backdoors. Ed25519 uses Curve25519, whose constants are explicitly chosen for performance with no unexplained magic numbers.
- ECDSA implementations are easier to get wrong. Ed25519's spec is mechanically simpler — fewer footguns.
Ed25519 vs secp256k1 (Bitcoin/Ethereum curve)
We use Base mainnet for the identity layer (TRDWorkerSBT contract). Why not use the same curve Ethereum uses?
Two reasons. First, secp256k1 with ECDSA has the same nonce-randomness problem as P-256. Second — and more importantly — using a different curve at the receipt layer than the chain layer is a feature, not a bug. Compromise of one doesn't immediately compromise the other. If a flaw is found in secp256k1, our SBT contract is in trouble but the receipt chain is fine. If a flaw is found in Ed25519, our receipts need re-signing but the on-chain identity is fine.
This is defense in depth. We separated the trust roots deliberately.
Ed25519 vs Schnorr signatures
Schnorr is widely considered the "correct" foundation for modern signing. It's been adopted by Bitcoin (BIP-340), and aggregation across multiple signers is genuinely cleaner than ECDSA.
We considered it. We rejected it because:
- Library support in 2026 is still narrower than Ed25519. Every Node, Python, Go, Rust, Java standard library ships a tested Ed25519 implementation. Schnorr support is mostly through purpose-built crypto libraries.
- Multi-party signing isn't a requirement in R+2 v0.1. Each agent signs its own actions. We'll revisit if R+2 v0.2 needs multi-signature receipts (federated agents, multi-party transactions).
Ed25519 vs post-quantum (Falcon, Dilithium, SPHINCS+)
This is the question we agonized over most. Post-quantum (PQ) signatures are coming — NIST has standardized them (FIPS 204 for Dilithium, FIPS 205 for SPHINCS+). Eventually, quantum computers will break Ed25519. The question is when, and what to do about it now.
We chose Ed25519 for v0.1 with a planned migration path because:
- PQ signatures are still 10-100x bigger than Ed25519. Dilithium signatures are ~2,400 bytes; Ed25519 is 64 bytes. For high-volume agent receipts, this matters.
- PQ verification is slower. The whole point of receipts is that verifying them should be cheap. Ed25519 verifies in microseconds on modern hardware; Dilithium is milliseconds.
- The PQ threat is real but not 2026. Cryptographically-relevant quantum computers are at least 5-10 years out. By that point, every R+2 receipt-issuer can re-sign with PQ keys, and the spec will have a
spec_versionbump (r2/v0.2 or r3) that includes a PQ algorithm identifier. - Receipts produced today are still verifiable tomorrow with PQ keys, because the chain structure doesn't depend on the signature algorithm — only the receipts themselves do.
"Choose the simpler, faster, more battle-tested primitive that meets today's threat model. Build the spec to support a clean migration. Don't pre-emptively pay the cost of a future migration that may or may not happen on the timeline you imagine."
Why RFC 8785 over the alternatives
Canonicalization is the part of the spec that most people don't think about. It's also the part where naive implementations silently disagree.
Here's the problem: you have a JSON object. You want to hash and sign it. But JSON allows different equivalent representations — same data, different bytes, different hashes, different signatures. Two implementations that both "parse and re-emit JSON" will produce different bytes for the same logical content, and you can't verify across them.
The fix is canonicalization: a single deterministic byte representation of the JSON.
RFC 8785 (JCS) vs roll-your-own
The temptation when designing a provenance system is to roll your own canonicalization: sort keys, strip whitespace, normalize numbers. We've seen this done badly enough times to know it's a trap.
Subtle issues you don't think of:
- What about
1.0vs1? Same number, different representation. - What about
1e10vs10000000000? - What about Unicode characters that can be encoded as
\uXXXXescapes vs raw UTF-8? - What about the BOM at the start of a UTF-8 file?
- What about
"A"vs"A"?
RFC 8785 (the JSON Canonicalization Scheme, also known as JCS) answers all of these. Specifically: keys sorted lexicographically by UTF-16 code units; numbers serialized using ECMAScript's Number.prototype.toString() algorithm; strings use minimum-length escaping with only quote, backslash, and control characters escaped; no insignificant whitespace; UTF-8 output. There's a published reference implementation. Any conforming JCS library will produce the same bytes.
RFC 8785 vs CBOR / MessagePack
CBOR (RFC 8949) and MessagePack are both more compact than JSON and have canonical forms. We considered them seriously.
We rejected them because:
- JSON is universally readable by every language, every regulator, every auditor, with zero setup. CBOR requires libraries. Agent receipts will be inspected by people who haven't installed anything.
- JSON canonicalization is more mature in 2026 than CBOR canonicalization. There are more implementations and more conformance tests.
- The compactness benefit doesn't matter. Our receipts are typically 800-1500 bytes. Going to CBOR saves maybe 200 bytes. Not worth the readability cost.
RFC 8785 vs Protobuf canonical form
Protobuf is great for typed schemas in microservices. It's a poor fit for receipts because:
- Protobuf requires schema distribution. R+2 receipts are self-describing — anyone can parse them with no prior agreement.
- Protobuf canonical form is less rigorously specified than JCS.
- The
extensionsfield needs to be open-ended; Protobuf'sAnytype makes that ugly.
Why SHA-256 over SHA-3 or BLAKE3
SHA-256 is the hash we use for the chain pointer (prev_receipt_cid) and for content addressing. The case for it is short: it's everywhere.
Every cryptocurrency uses it. Every TLS certificate uses it. Hardware-accelerated implementations exist for every CPU and most embedded chips. It's been studied for two decades and remains unbroken.
SHA-3 and BLAKE3 are arguably better in some dimensions (SHA-3 is theoretically more robust to length-extension; BLAKE3 is faster). We chose SHA-256 because:
- Length-extension doesn't matter for our use case. We use SHA-256 as a content hash, not as a MAC.
- BLAKE3 is fast but still less universally available. Standard library support is uneven.
- SHA-256 + IPFS CIDs already work together. CIDv1 with the SHA-256 multihash is the IPFS default, and it integrates cleanly into our optional pinning path.
If SHA-256 is ever broken (no current sign of it), R+2 v0.2 will support algorithm-identifier prefixes in the chain pointer (sha3-256: instead of sha256:) and verifiers will be expected to accept both during the transition window.
Why an explicit linked-list chain over Merkle trees
R+2 uses a simple linked list: each receipt's prev_receipt_cid field contains the hash of the previous receipt. Walk backwards from the head and you reconstruct the chain.
The alternative would be a Merkle tree (or Sparse Merkle tree, or Verkle tree) where membership in the chain is proven by an inclusion proof rather than chain-walking.
We picked the linked list because:
- Verifying a single receipt is constant-time with a linked list (you only need the receipt itself + the agent's pubkey). With a Merkle tree, you need the receipt + the inclusion proof + the current root.
- The full chain is verifiable end-to-end with a linked list — you can prove there are no hidden insertions because every receipt links to a specific predecessor. Merkle trees prove inclusion but not non-existence outside the proven set.
- Linked-list chains are conceptually simpler for auditors and regulators. "Did this receipt happen?" is an obvious question to answer when receipts link to each other in order. Merkle proofs require explaining what the root is and what the inclusion proof shows.
- Performance doesn't matter at our scale. Even with 100K receipts per agent per day, walking the chain to verify a specific receipt is fast. Merkle trees become important at millions-of-receipts scale, which is years out for a single agent.
Worth noting: if you want a Merkle tree summary of a chain for batch verification (useful for regulator audit exports), R+2 v0.1 supports it as an optional R+3 extension. The linked-list base + optional Merkle summary gives you both properties.
What could break and what we do about it
Every cryptographic choice eventually fails. The question isn't whether one of these primitives breaks — it's how the spec survives when one does.
R+2 v0.1 includes the spec_version field specifically so we can declare "this receipt uses crypto from r2/v0.1; later receipts may use crypto from r2/v0.2 or r3/v0.1." Old receipts remain verifiable forever with old primitives; new receipts use new primitives.
Concretely, our planned migration paths:
| If this breaks | Migration in spec version | Migration effort |
|---|---|---|
| Ed25519 (quantum or otherwise) | r2/v0.2 adds signature_algorithm field | Re-sign with PQ key; existing receipts remain verifiable with old code |
| SHA-256 (preimage attack) | r2/v0.2 prefixes chain CIDs with algorithm | Old chains untouched; new chains use SHA-3 or BLAKE3 |
| RFC 8785 (canonicalization ambiguity) | r3 — major version bump | Chain transition: pin all v0.1 receipts to IPFS, then re-issue under v0.2 canonicalization. Most painful migration scenario. |
| Base mainnet (chain shutdown) | Migrate identity layer to another L2 or to DNS | Replay SBT mints to new identity layer; existing R+2 receipts remain verifiable using historical pubkey records. |
The worst case in the table is RFC 8785 failing — that's a multi-year migration. We mitigate by picking RFC 8785 specifically because it's an IETF RFC with multiple independent implementations and extensive analysis. The chance of an unfixable canonicalization bug is much lower than for a roll-your-own scheme.
What we'd do differently if starting over
The hardest design decisions to defend are the ones you make under deadline pressure. R+2 v0.1 wasn't free of those. Three things I might revisit if R+2 v1.0 ratification happens at W3C-AI or IETF:
- Mandate algorithm identifiers in v0.1 itself rather than waiting for v0.2. Current receipts implicitly use Ed25519 + SHA-256; future receipts should explicitly say which algorithms they use, even if only one is allowed today. Future-proofing is cheaper than retrofitting.
- Add a Merkle accumulator from day one. Even if linked-list is the primary chain structure, having an optional Merkle root per N receipts would enable cleaner batch verification for very-high-volume agents.
- Pick a more rigorous nonce derivation. R+2 v0.1 uses 16 random bytes as the nonce. v0.2 should specify that the nonce derives deterministically from a counter + agent secret, eliminating randomness-source-quality concerns.
These are revisitable in v0.2 without breaking v0.1 verifiers. None are urgent. All three are on the editorial backlog.
The meta-point
Cryptographic specs fail more often from over-engineering than from under-engineering. The R+2 v0.1 design is intentionally simple: one signature algorithm, one canonicalization, one hash, one chain structure. Each chosen to be the most battle-tested, most-implemented, most-reviewed option that meets the requirements.
If we're proven wrong about any of them, the spec has a clean migration path. If we're proven right, R+2 becomes the substrate that thousands of agents and dozens of regulators rely on without thinking about the choices we made tonight.
Either way, the cryptographic foundation isn't where I expect R+2 to be challenged. It's a known-good combination of known-good primitives. The interesting work happens in the sectoral profiles and the standards-body adoption — that's where R+2 either becomes the default or becomes a footnote.
— Deepak
Want to push back?
If you've spotted a flaw in any of the design choices above, the editorial group genuinely wants to hear it before R+2 v0.2. Open an issue at github.com/DCS-LabsAI or email [email protected].