Inside the Solana DID Registry: One Identifier Primitive from Student to Consortium
A deep dive into the bio-did-registry program, and how two thousand lines of Rust on a global immutable chain give students, universities and pharmaceutical consortia an identifier they hold themselves, from the first day of the work.
The gap nobody designed, everybody inherits
Ask who is allowed to hold a persistent, resolvable identifier for research output today, and the answer is better than it used to be. Zenodo, Figshare, OSF and Harvard Dataverse will each mint a DOI for free, for anyone, for whatever they publish there. An undergraduate with a genuinely novel dataset can have a DOI by this afternoon. Getting an identifier is not the problem anymore.
What a free DOI gives you is a receipt for a deposit, and three things follow from that.
- It arrives with the deposit. The DOI belongs to a record in someone's repository and becomes resolvable when that record is published, so the raw captures, failed runs, intermediate datasets and calibration files that never become a record never get one.
- It is a name rather than a key. Resolving a DOI tells you where a landing page is. It cannot tell you that a file, a claim or a grant of access came from the person the record is about.
- It is held by the repository. You change the record through the repository's interface, under its rules and its metadata schema, and the right to mint under a prefix of your own belongs to organizations that join a registration agency such as DataCite.
This is not malice. It is architecture. A DOI is issued by an organization that holds a prefix, so the identifier is that organization's artifact, however generous the organization. The bio-did-registry program gives the researcher an identifier of their own instead, derived from a key they hold, usable from the first raw capture, and able to sign. It does not replace the DOI. When the work is published, the DOI goes into the DID document as a service and the two identifiers point at each other, which is what the platform's Dataverse bridge does.
This post walks through the program in detail, covering the account model, the authorization gate, and the invariants. It then works through what the program 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 two thousand lines of Rust, unchanged.
First principles: the identifier is the key
A did:bio identifier is the base58 encoding of a 32 byte subject, usually 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 valid key subject identifier resolves to a deterministic generative document derived from the key itself. Since release 0.1.2 there is a second kind, the owned DID, whose subject is an address the program derives for its creator from ["bio-did-owned", authority, nonce]. It has no key of its own, so it has no generative form and resolves as notFound until it is registered. The registry only enters the picture when you want more than the default, such as key rotation, service endpoints, additional verification methods, or deactivation.
The lifecycle above is the whole product in one picture. Three states, two 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, whose full specification is here, is a zero allocation Rust program of fourteen instructions whose lasting state is one account type. The program edits its bytes in place rather than deserializing a struct. After an 8 byte discriminator the layout reads like this.
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. It is not permanence of data, since the account is mutable by design. It is permanence of the rules by which the data 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
service type <= 64 chars other controller <= 128 chars
key material: exactly 32 (Ed25519, X25519), 33 (secp256k1) or 2592 (ML-DSA-87)The caps bound the account below ~52 KB in the worst case, which bounds three things at once, namely 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 rather than a fee
Accounts are exactly sized on every mutation via realloc. Growth is paid by the transaction's payer. Shrinkage refunds the difference to whoever pays for the shrinking transaction. Deactivating shrinks the account to a 74 byte tombstone and returns the excess, but only the excess. At today's rent of 5,080 lamports per byte, a freshly registered DID holds about 0.00128 SOL, and about 0.001 SOL of that stays with the tombstone for good, because the tombstone is never closed (the reason is below). The economics are a deposit on state you actually use, most of which you do not get back. It is still a shape institutional procurement rarely sees.
The authorization gate
Every mutation flows through one rule.
pub fn authorize(doc: &DidView, signer: &[u8; 32], controller: Option<&AccountView>) -> Result<(), ProgramError> {
require(!doc.is_deactivated(), DidError::DidDeactivated)?;
if doc.is_authority(signer) {
return Ok(());
}
let controller = controller.ok_or(DidError::Unauthorized)?;
let subject = load_did_account(controller)?;
require(doc.is_native_controller(&subject), DidError::Unauthorized)?;
let data = controller.try_borrow()?;
let parent = DidView::parse(&data)?;
require(!parent.is_deactivated() && parent.is_authority(signer), DidError::Unauthorized)
}A signer is an authority iff it matches an Ed25519 verification method carrying the capabilityInvocation flag, either on the DID itself or, since release 0.2.0, on one of the DID's native controllers. In the second case the transaction passes the controller's registry account along, and authority reaches exactly one level. Two consequences deserve emphasis.
The fee payer is not the authority. Every instruction that changes rent takes payer and authority as distinct signers, and the rest can have their fee paid by anyone. 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 rather than 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 footgun 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 coauthority such as a departing colleague or a compromised admin key can never remove the subject's own key. And a method can only be born protected if its key signs the transaction, so nobody can plant an unremovable key either. Since 0.2.0 a protected method must also keepcapabilityInvocation, so its own key can always sign the changes reserved to it. One power is not fenced off. Deactivation needs only an authority, so anycapabilityInvocationkey, including one held through a native controller, can tombstone the DID. Only add keys and controllers you would trust with that.
These two rules are why the multiparty 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 rather than 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, whether student or independent researcher
The entry cost is a keypair.
# The DID exists the moment this returns. It is the base58 of the raw
# 32-byte public key inside the PEM.
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, and any signature they make over their data verifies against the identifier itself. When they later join a lab, publish, graduate, and 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 publication. The system meets people where research actually begins. When the dataset is published, the student deposits it with Zenodo or Dataverse, registers the DID and adds the DOI as a service, so the identifier that named the work from its first day now points at the citable version.
The lab
A lab's needs are modest but real. It wants metadata endpoints, shared datasets, and 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, which are paying and providing continuity.
- Sponsored registration. The library or core facility funds
initializefor every researcher, and the payer gains no authority. A thousand DIDs cost the institution about 1.3 SOL of rent, against the membership and per DOI fees of running its own DOI prefix. Budget it as spent, since about 80% of it stays locked in tombstones if the DIDs are ever deactivated. - Escrow, honestly scoped. Researchers may add an institutional escrow key as a second
capabilityInvocationmethod, as recovery insurance against lost laptops. The researcher's#defaultmethod is protected, so escrow can never remove the researcher's key. Like any authority, though, it could deactivate the DID, so the escrow key deserves the care of a root key. The trust relationship is written into the program rather than 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 in one instruction, and its involvement ends. Compare the status quo.
Pharma and multisite research
Regulated research is where the registry's properties compound. A multisite trial has a sponsor, several sites, a CRO, and a regulator. Those are five parties who today reconcile provenance through contracts, portals, and PDFs.
The registry makes that graph public and versioned:
- Cross organisation controllers. The protocol DID lists the sponsor's own
did:bioinnative_controllersand, viaother_controllers, DIDs from other methods, such as a partner'sdid:web. Since release 0.2.0 the native entries carry authority. AcapabilityInvocationkey of the sponsor's registered DID can update the protocol DID directly, one level deep, so a key the sponsor rotates in reaches every protocol that names it. The other method entries stay declarative, because the program cannot check adid:web. 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, which is 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. A key that size does not fit in one Solana tx, so it is uploaded in chunks through a staging account and folded into the document in a final step, five txs behind one command. The design is honest about the 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, rather than 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, with no network opinions and 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"
// (notFound for an owned subject)
// Some(account) -> materialized document, or deactivatedThe same logic runs in the CLI resolver, the backend, and the test suite, with parity tests pinning the program ID, discriminators, layout and error codes against the on-chain program.
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. The Quorum reader in did-bio-core does the cross check, accepting an account only when a majority of the providers it asks return it identically.
On top of those two paths, the surrounding stack composes per audience. That stack is capability scoped sharing with UCAN tokens, encrypted IPFS storage, and Dataverse bridging. 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 rather than 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. Free repositories fixed access to identifiers for published work. They did not give researchers an identifier of their own, one that exists from the first raw capture and carries the keys to sign for it. Put that identifier under two thousand lines of Rust whose rules are enforced by global consensus, and issuance becomes free, instant and universal, while everything institutions and repositories genuinely provide, funding, continuity, governance, escrow and the citable DOI, 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, and the method itself in did:bio Identifiers. Source: ekayana-labs/bio-did-registry.
Ready to Get Started?
Explore our documentation to learn how to integrate Ekayana into your research workflow.