Inside the Solana DID Registry: One Identifier Primitive from Student to Consortium
A deep dive into the bio-did-registry program - how 674 lines of Rust on a global immutable chain close the identifier gap between individuals, universities, and pharmaceutical research.
The gap nobody designed, everybody inherits
Ask who is allowed to hold a persistent, resolvable identifier for research output today, and the answer is an org chart. DOIs are minted through registrar memberships that institutions hold and individuals do not. An undergraduate with a genuinely novel dataset, a PhD student mid-project, a two-person lab between grants - none of them can mint an identifier that outlives their current affiliation. The identifier system starts where institutional membership starts, which is after most primary data has already been produced.
This is not malice; it is architecture. When issuing an identifier requires a registrar, someone must pay the registrar, and the natural payer is an institution - so the identifier becomes an institutional artifact. Change the architecture and the gap closes on its own. That is what the bio-did-registry program is: a replacement for the registrar, not for the institution.
This post walks through the program in detail - the account model, the authorization gate, the invariants - and then through what it means in practice for three very different adopters: a lone student, a university, and a pharmaceutical research consortium. The remarkable property is that all three use the same 674 lines of Rust, unchanged.
First principles: the identifier is the key
A did:bio identifier is the base58 encoding of an Ed25519 public key:
did:bio:devnet:2T6zLFvMx7NJac5qQtiKTaPhMwHLkwKETWjUK1yKv4tc
^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
network segment base58btc(32-byte Ed25519
(absent = mainnet) public key)Nothing is assigned. Nothing is looked up. Generate a keypair on a laptop with no network connection, and the DID exists - and resolves, because every syntactically valid identifier resolves to a deterministic generative document derived from the key itself. The registry only enters the picture when you want more than the default: key rotation, service endpoints, additional verification methods, deactivation.
The lifecycle above is the whole product in one picture. Three states, two paid transitions, and one deliberately missing edge - there is no path out of the tombstone, for reasons we will get to.
The on-chain anatomy
The program - full specification here - is an Anchor program of eight instructions whose entire state is one account type:
#[account]
pub struct DidAccount {
pub version: u64, // monotonic update counter
pub bump: u8, // PDA bump seed
pub subject: Pubkey, // the key that IS the identifier
pub deactivated: bool, // tombstone flag
pub updated_at: i64,
pub native_controllers: Vec<Pubkey>,
pub other_controllers: Vec<String>,
pub verification_methods: Vec<VerificationMethod>,
pub services: Vec<Service>,
}Each DID maps to exactly one program-derived address:
find_program_address(["bio-did", subject], PROGRAM_ID)A PDA has no private key, and the Solana runtime only lets the owning program write its data. Together those two facts are the security model: state at that address can only ever have been produced by this program's instructions, and consensus replicates that guarantee across every honest node in the world. That is what "global immutable chain" buys concretely - not permanence of data (the account is mutable by design), but permanence of the rules by which it may change.
Why every list has a cap
verification methods ≤ 16 services ≤ 16
native controllers ≤ 8 other controllers ≤ 8
fragment ≤ 32 chars endpoint ≤ 512 chars
key material ≤ 2592 bytes (fits ML-DSA-87 exactly)The caps bound the account below ~52 KB in the worst case, which bounds three things at once: rent, resolver work, and grief. There is no instruction an attacker can spam to make someone else's DID expensive to read.
Rent as a deposit, not a fee
Accounts are exactly sized on every mutation via realloc. Growth is paid by the transaction's payer; shrinkage refunds the difference. Deactivating shrinks the account to a 74-byte tombstone and returns the excess. The economics are a refundable deposit on state you actually use - a shape institutional procurement rarely sees:
The authorization gate
Every mutation flows through one rule:
pub fn require_authority(did: &DidAccount, signer: &Pubkey) -> Result<()> {
require!(!did.deactivated, ErrorCode::DidDeactivated);
require!(did.is_authority(signer), ErrorCode::Unauthorized);
Ok(())
}A signer is an authority iff it matches an Ed25519 verification method carrying the capabilityInvocation flag. Two consequences deserve emphasis.
The fee payer is not the authority. payer and authority are distinct signers on every mutating instruction. A university can pay for ten thousand student transactions while holding authority over none of them. Sponsorship without custody is not a feature bolted on top; it falls out of the account structure.
Authorization is stateful, not credential-based. The gate checks the DID's current methods. Rotate a key out and its authority ends at that slot - nothing to revoke, no CRL to distribute, no cached token that lingers.
Two invariants stand guard
The program refuses two whole classes of foot-gun and betrayal:
LastAuthority- any operation that would leave zero Ed25519+capabilityInvocationmethods is rejected. You cannot strand a DID by accident; the only exit is explicit deactivation.ProtectedVerificationMethod- a method with thePROTECTEDbit can only be added, re-flagged or removed by its own key. The subject's#defaultmethod is born protected, so a co-authority - a departing colleague, a compromised admin key - can never evict the subject from their own identifier. And a method can only be born protected if its key signs the transaction, so nobody can plant an unremovable key either.
These two rules are why the multi-party arrangements later in this post are safe to build at all.
The tombstone, and the missing edge
deactivate erases all methods, services and controllers, sets a permanent flag, and shrinks to the tombstone. It does not close the account - because a closed account would make the DID resolve generatively again, silently resurrecting the original subject key's authority. If the reason for deactivation was key compromise, that would be the worst possible outcome. The tombstone guarantees every honest node forever answers "deactivated". The missing edge in the lifecycle diagram is a security property, not an omission.
Onboarding, rung by rung
Now the practical half. The same primitive serves four postures, and each rung of the ladder is reached by adding to the previous one - never by migrating.
The individual - student, independent researcher
The entry cost is a keypair.
# The DID exists the moment this returns.
openssl genpkey -algorithm ed25519 -out my-dataset.pemNo transaction, no account, no permission. The student cites did:bio:devnet:… in their thesis; any resolver in the world returns the generative document; any signature they make over their data verifies against the identifier itself. When they later join a lab, publish, graduate, change institutions twice - the identifier does not care. It was never affiliated with anything but a key.
This is the gap-closing move: participation before membership. The system meets people where research actually begins.
The lab
A lab's needs are modest but real: metadata endpoints, shared datasets, more than one person who can act. That is initialize plus a handful of updates:
initialize(subject) -> account exists, version 1
add_service("metadata", "BioMetadata", "ipfs://bafy…")
add_verification_method(pi_escrow_key) -> second capabilityInvocation keyPer-dataset DIDs keep correlation surface small (one keypair per dataset), and the LastAuthority invariant quietly protects the lab from its own turnover: nobody can remove the final working key while removing a departed member's.
The institution
A university onboards by doing the two things institutions are actually good at: paying and providing continuity.
- Sponsored registration. The library or core facility funds
initializefor every researcher - remember, the payer gains no authority. A thousand DIDs cost the institution under 2 SOL of refundable rent, against per-identifier fees plus membership under the DOI regime. - Escrow, honestly scoped. Researchers may add an institutional escrow key as a second
capabilityInvocationmethod - recovery insurance against lost laptops. But the researcher's#defaultmethod is protected, so escrow can help and can never evict. The trust relationship is written into the program, not into a policy PDF. - No lock-in, structurally. When the researcher leaves, the DID goes with them, because it was always theirs. The institution removes its escrow key - one instruction - and its involvement ends. Compare the status quo:
Pharma and multi-site research
Regulated research is where the registry's properties compound. A multi-site trial has a sponsor, several sites, a CRO, and a regulator - five parties who today reconcile provenance through contracts, portals, and PDFs.
The registry gives that graph teeth:
- Cross-organisation controllers. The protocol DID lists sponsor keys as
native_controllersand, viaother_controllers, DIDs from other methods (a partner'sdid:web, say). Governance is public, machine-readable and versioned - every change incrementsversionand lands in aDidModifiedevent that any auditor can index. - Verification without access. A regulator resolving the submission bundle checks every referenced dataset's DID and every content hash without asking the sponsor for anything. GxP audit trails are usually reconstructed; this one is simply read.
- Post-quantum assertions where they matter. Trials generate data whose confidentiality horizon is measured in decades - exactly the "harvest now, decrypt later" window. The registry's
Dilithium5method type carries a final FIPS 204 ML-DSA-87 public key (2592 bytes, the largest thing the account admits) for assertions verified off-chain, while on-chain control remains Ed25519 until Solana itself goes post-quantum. The design is honest about that boundary - see Post-Quantum Security. - Deactivation with a paper trail. When a program ends or a key is compromised, the tombstone is a permanent, public record that the identifier was retired - not a 404 that an auditor must interpret.
Building applications across the gap
For builders, the registry is deliberately boring to integrate: it is a read path you can trust and a write path you can sponsor.
Resolution is a pure function. The did-bio-core crate implements the resolution algorithm over an optional fetched account - no network opinions, no SDK lock-in:
let did: BioDid = "did:bio:devnet:2T6z…".parse()?;
let resolution = resolve_from_account(&did, fetched.as_ref());
// None | wrong owner | empty -> generative document, versionId "0"
// Some(account) -> verified, materialized documentThe same logic runs in the CLI resolver, the backend, and the test suite, with parity tests pinning it against the on-chain program byte for byte.
One caution for verifiers. Consensus authenticates state; your RPC connection authenticates the response. The read-path risk that matters is withholding - a node claiming the account does not exist so resolution falls back to the generative document, hiding a rotation or a deactivation. Production verifiers use finalized commitment, cross-check independent providers, and treat a generative answer for a DID previously seen at versionId > 0 as the alarm it is.
On top of those two paths, the surrounding stack - capability-scoped sharing with UCAN tokens, encrypted IPFS storage, Dataverse bridging
- composes per audience: a student notebook plugin needs nothing but a keypair
and the resolver; an institutional dashboard adds sponsored initialize and escrow management; a consortium adds controller governance and audit indexing over the three events. None of these applications needs permission from us or from each other, which is the entire point of putting the registrar on a public chain: the registry is infrastructure, not a product you integrate with - and infrastructure is what closes gaps.
The shape of the argument
Strip the details and the post says one thing. Identifier systems gate participation at the point of issuance, and issuance is gated because a registrar must be paid and trusted. Replace the registrar with 674 lines of Rust whose rules are enforced by global consensus, and issuance becomes free, instant and universal - while everything institutions genuinely provide (funding, continuity, governance, escrow) attaches on top, scoped by invariants that make betrayal a compile error rather than a breach of contract.
A student's thesis dataset and a consortium's trial protocol resolve through the same nine steps. That symmetry is not a demo simplification. It is the design.
---
The program specification lives in the Solana Registry Program docs; the method itself in Creating a did:bio Identity. Source: ekayana-labs/bio-did-registry.
Ready to Get Started?
Explore our documentation to learn how to integrate Ekayana into your research workflow.