Back to Blog
Technical

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.

Suraj Kumar
January 12, 2026
22 min read

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.

DOI / institutionaldid:bioUndergraduatePhD studentPostdoc / labUniversityPharma R&Done keypair,no membership,no gatekeeperThe excluded rows are where most primary data is produced.
Who can hold a persistent identifier today, by actor

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.

GenerativeKeypair only · free, offlineinitialize~0.00175 SOLRegisteredPDA holds the documentauthorized updates · version++deactivateTombstonePermanent · irreversibleno path back - a closed account would resurrect the subject keyEvery state on this line resolves. Only the middle one costs anything.
The DID lifecycle: generative, registered, tombstone

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:

rust
#[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:

rust
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:

DOI - membership + per-DOIRequires institutional membership$1–5 each + annual feedid:bio - registeredRefunded on deactivation~0.00175 SOL rentdid:bio - generativeResolves without an accountfreeRent is a refundable deposit, not a fee. Bars are indicative, not to scale across currencies.
Cost per identifier, compared

The authorization gate

Every mutation flows through one rule:

rust
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+capabilityInvocation methods is rejected. You cannot strand a DID by accident; the only exit is explicit deactivation.
  • ProtectedVerificationMethod - a method with the PROTECTED bit can only be added, re-flagged or removed by its own key. The subject's #default method 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.

ONE PRIMITIVE, FOUR POSTURESIndividualStudent, independentGenerate keypairCite the DIDZero costLabPI + membersinitializeAdd servicesPer-dataset DIDsInstitutionUniversity, core facilitySponsor feesEscrow authorityBulk registrationConsortiumPharma, multi-siteCross-org controllersML-DSA-87 assertionsAudit exportsEach step adds capability. None invalidates the identifier minted at the step before.The student's DID and the consortium's DID resolve through the same algorithm.No migration, no re-issuance, no second identifier to reconcile.
The onboarding ladder from individual to consortium

The individual - student, independent researcher

The entry cost is a keypair.

bash
# The DID exists the moment this returns.
openssl genpkey -algorithm ed25519 -out my-dataset.pem

No 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 key

Per-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 initialize for 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 capabilityInvocation method - recovery insurance against lost laptops. But the researcher's #default method 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:
Institutional status quoIdentifier issued by an officeOnly for "finished" outputsData on a lab serverProvenance asserted in proseLeaving = losing the recordTrust the institutionRegistry-backedIdentifier derived from a keyFrom first raw capture onwardContent-addressed, replicatedProvenance is a resolvable chainThe DID travels with the authorVerify the mathematics
What changes for an institution

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.

controlsgovernsgovernsgovernsderivesverified independentlyTrial protocoldid:bio · versionedSponsorControllerRegulatorRead-only verifierSite ADataset DIDSite BDataset DIDSite CDataset DIDCRO analysisDerived, references inputsSubmission bundleEvery CID resolvable
A multi-site trial as a controller graph

The registry gives that graph teeth:

  • Cross-organisation controllers. The protocol DID lists sponsor keys as native_controllers and, via other_controllers, DIDs from other methods (a partner's did:web, say). Governance is public, machine-readable and versioned - every change increments version and lands in a DidModified event 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 Dilithium5 method 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:

rust
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 document

The 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.