TypeScript reference implementation of the did:btcr2 DID Method: a censorship-resistant Decentralized Identifier method using Bitcoin as a verifiable data registry.
This package is the core of the did-btcr2-js monorepo. It implements DID create/resolve/update operations, the three beacon types (Singleton, CAS, SMT), multi-party aggregation over MuSig2, and a pluggable transport layer for peer-to-peer coordination.
pnpm add @did-btcr2/method
| Feature | Entry point |
|---|---|
| Create a DID (offline, deterministic or external) | DidBtcr2.create() |
| Resolve a DID (sans-I/O state machine) | DidBtcr2.resolve() |
| Update a DID (sans-I/O state machine) | DidBtcr2.update() returns an Updater |
| Update utilities (construct, sign, announce) | Updater.construct(), Updater.sign(), Updater.announce() |
| Beacon types (Singleton, CAS, SMT) | SingletonBeacon, CASBeacon, SMTBeacon |
| Fee estimation (pluggable) | FeeEstimator, StaticFeeEstimator |
| Multi-party aggregation (MuSig2) | AggregationServiceRunner, AggregationParticipantRunner |
| Transport abstraction (Nostr, HTTP/REST, DIDComm stub) | Transport, NostrTransport, HttpClientTransport, HttpServerTransport |
| Signer abstraction (local key, KMS, custom) | Signer / LocalSigner from @did-btcr2/keypair; KeyManagerSigner from @did-btcr2/key-manager |
| DID document types and builders | Btcr2DidDocument, DidDocumentBuilder |
import { DidBtcr2 } from '@did-btcr2/method';
import { SchnorrKeyPair } from '@did-btcr2/keypair';
// Deterministic (k-type): the identifier IS the public key
const keys = SchnorrKeyPair.generate();
const did = DidBtcr2.create(keys.publicKey.compressed, {
idType : 'KEY',
network : 'mutinynet',
});
// did:btcr2:k1q5p...
DidBtcr2.resolve() returns a sans-I/O state machine. The caller drives resolution by fulfilling typed data needs (beacon signals, CAS announcements, signed updates).
import { DidBtcr2 } from '@did-btcr2/method';
const resolver = DidBtcr2.resolve(did, { sidecar });
let state = resolver.resolve();
while (state.status === 'action-required') {
for (const need of state.needs) {
const data = await fetchData(need); // your I/O goes here
resolver.provide(need, data);
}
state = resolver.resolve();
}
const { didDocument, metadata } = state.result;
See src/core/resolver.ts for the full DataNeed union and phase transitions.
DidBtcr2.update() returns a sans-I/O state machine: the counterpart to the Resolver. The caller drives the update by fulfilling typed data needs (signing key, funding confirmation, broadcast).
import { DidBtcr2, Updater } from '@did-btcr2/method';
import { LocalSigner } from '@did-btcr2/keypair';
import { BitcoinConnection } from '@did-btcr2/bitcoin';
// Build a Signer from a raw secret key. For KMS-backed keys use KeyManagerSigner
// from '@did-btcr2/key-manager' instead.
const signer = new LocalSigner(secretKeyBytes);
const bitcoin = BitcoinConnection.forNetwork('mutinynet');
const updater = DidBtcr2.update({
sourceDocument,
patches : [{ op: 'add', path: '/service/-', value: newService }],
sourceVersionId : 1,
verificationMethodId : '#initialKey',
beaconId : '#beacon-0',
});
let state = updater.advance();
while (state.status === 'action-required') {
for (const need of state.needs) {
switch (need.kind) {
case 'NeedSigningKey':
updater.provide(need, signer);
break;
case 'NeedFunding':
// Check UTXOs at need.beaconAddress, fund if needed
updater.provide(need);
break;
case 'NeedBroadcast':
await Updater.announce(need.beaconService, need.signedUpdate, signer, bitcoin);
updater.provide(need);
break;
}
}
state = updater.advance();
}
const { signedUpdate } = state.result;
See src/core/updater.ts for the full UpdaterDataNeed union and phase transitions.
Aggregation lets multiple DID controllers coordinate a single Bitcoin transaction that announces all of their updates at once, signed n-of-n with MuSig2. The high-level Runner API hides the message routing and decision plumbing:
import { AggregationServiceRunner, TransportFactory } from '@did-btcr2/method';
// Pick a transport. Nostr (relay-based) or HTTP/REST (operator-hosted).
const transport = TransportFactory.establish({
type : 'nostr',
relays : ['wss://relay.damus.io'],
});
// Or for HTTP: { type: 'http', role: 'server' } on the operator side,
// { type: 'http', role: 'client', baseUrl: '...' } on the participant side.
transport.registerActor(serviceDid, serviceKeys);
transport.start();
const runner = new AggregationServiceRunner({
transport,
did : serviceDid,
keys : serviceKeys,
config : { minParticipants: 2, network: 'mutinynet', beaconType: 'CASBeacon' },
onProvideTxData: async ({ beaconAddress, signalBytes }) => buildBeaconTx(beaconAddress, signalBytes),
});
runner.on('signing-complete', (result) => console.log('done'));
const result = await runner.run();
The full step-by-step protocol walkthrough (service flow, participant flow, decision callbacks, events, the low-level state machine API, and production deployment notes) is in docs/aggregation.md. The HTTP/REST transport has its own walkthrough in docs/http-transport.md.
AggregationServiceRunner) encapsulate boilerplate; low-level state machines stay available for tests, custom transports, and fine-grained control.Transport interface decouples protocol logic from the wire format. Ships with NostrTransport (relay-based) and HttpClientTransport + HttpServerTransport (HTTP/REST, framework-agnostic, browser-compatible). Add your own for DIDComm, libp2p, or anything else.# From packages/method/
pnpm build # Compile ESM + CJS + browser bundle
pnpm build:tests # Compile tests to tests/compiled/
pnpm test # Run the test suite with coverage
pnpm lint # ESLint (zero warnings tolerated)
Tests run from compiled JS, so run pnpm build:tests before pnpm test after any test changes.
docs/beacon-system-overview.md Beacon architecture, Singleton / CAS / SMT behavior, signal discoverydocs/aggregation.md Multi-party aggregation protocol, Runner and state machine APIs, e2e examplesdocs/http-transport.md HTTP/REST transport: wire protocol, signed envelopes, SSE subscriptions, Hono/Node framework mount example, permissive CORSdocs/test-vectors.md CLI tool for generating did:btcr2 test vectors via a stepped workflowDidBtcr2 (facade), Resolver (read path), Updater (write path), and the aggregation runners.