# DID PLC Registry on CKB (RFC)
## Metadata
**Status**:: #x
**Zettel**:: #zettel/fleeting
**Created**:: [[2025-04-29]]
## Synopsis
DID PLC is a self-authenticating DID which is strongly-consistent, recoverable, and allows for key rotation. DID PLC relies on a central directory server to resolve the mutation conflicts and determine the latest state for any identity. Centralized services are prone to censorship. Additionally, centralized service providers may collude with the early owners of an identity to rewrite history, thereby altering the latest state of the identity. This RFC proposes an alternative registry service running on CKB, which stores operations log on CKB chain and relies on the CKB consensus to resolve the mutation conflicts. DKD PLC identities hosted on this new registry service can achieve the same level of decentralization and censorship-resistant ability as the CKB consensus.
This RFC will introduce DID PLC briefly first. Then the following chapters are organized by features that each chapter will propose the design for a new feature based on the prior chapter. To avoid interrupting the reading flow, the design decision choices will be explained in the appendix and is referenced by link like `(a1)`.
## Introduction to DID PLC
DID PLC identity is derived from the hash of its initial state. Following update operations reference a prior version of the identity state by hash. For any identity, the initial state and the chained update operations constitute a tree. Given an identity and an operations tree, users can verify that the operations tree is for the identity and any operation is authorized by a prior owner listed in the prior state. However, users cannot tell which leaf of the tree is the latest state of the identity. There's no safe and deterministic algorithm to determine the latest state. If users want to reach consensus, they have to trust a third-party service, such as the central directory server `https://plc.directory/`.
![[DID PLC Registry on CKB (RFC) - Drawing Operations Tree.excalidraw.svg]]
%%[[DID PLC Registry on CKB (RFC) - Drawing Operations Tree.excalidraw|π Edit in Excalidraw]]%%
More details about DID PLC can be found in [its specification](https://web.plc.directory/spec/v0.1/did-plc).
## Operations Chain
The basic version of the DID PLC Registry on CKB stores signed operations in cells. A script R is responsible to verify the operations.
Here is how a DID PLC operation cell looks like:
- data: signed operation encoded with DAG-CBOR. An alternative design is putting unsigned operations in data and signatures in transaction witness. However, saving signatures in cell data can make it easy to fetch the signed operations from the CKB chain.
- type script: R with args `OP | did`
- OP: an integer tag to indicate that the script is used as the type script of an operation cell. It is encoded as a single byte and the value is 1.
- did: the full string in utf-8 of the did identity, example: `did:plc:ewvi7nxzyoun6zhxrhs64oiz`.
- lock script: R without args.
To register a new identity on CKB, create a transaction with an output for the genesis operation.
![[DID PLC Registry on CKB (RFC) - Drawing Creation Tx.excalidraw.svg]]
%%[[DID PLC Registry on CKB (RFC) - Drawing Creation Tx.excalidraw|π Edit in Excalidraw]]%%
To update or deactivate an identity, first get the latest operation via the CKB indexer RPC by searching the live cell with the type script `R(op | did)`. Create a transaction to include the cell as an input, and add a output operation cell to store the update operation or deactivation operation.
![[DID PLC Registry on CKB (RFC) - Drawing Update Tx.excalidraw.svg]]
%%[[DID PLC Registry on CKB (RFC) - Drawing Update Tx.excalidraw|π Edit in Excalidraw]]%%
When the script is given the empty args, it exists with a success status immediately. Thus `R()` acts as a always success script when used as the lock script.
When the script is used as the type script `R(OP | did)`, it must verify that:
1. There's at most one input, and exactly one output in the script group.
2. All cells in the script group must use `R(OP | did)` as the type script, and `R()` as the lock script.
3. When there is no inputs in the cell group:
1. The output is a valid genesis operation serialized in DAG-CBOR.
2. The field `did` in args matches the genesis operation.
3. The field `signature`in the genesis operation MUST be a valid signature of the unsigned genesis operation signed by a key listed in the field `rotationKeys` of the genesis operation.
4. When there is an input in the cell group:
1. The output is either a well-formatted update or deactivation operation in DAG-CBOR.
2. The field `prev` of the output operation is the CID of the input operation.
3. The field `signature`in the output operation MUST be a valid signature of the unsigned output operation signed by a key listed in the field `rotationKeys` of the input operation.
When the script is given args other then `R()` nor `R(OP | did)`, it must fail.
## Recovery Operations
The version of the DID PLC Registry in the previous chapter does not support Recovery Operations.
> The PLC server provides a 72hr window during which a higher authority rotation key can βrewriteβ history, clobbering any operations (or chain of operations) signed by a lower-authority rotation key.
The priority depends on the sequence that a key appears in the `rotationKeys`. Higher priority key comes first.
CKB recovery operations can be implemented in multiple ways, each with distinct trade-offs. The specification must carefully weigh these pros and cons before selecting an approach.
### Providing Proof of The Fork Point
The recovery operation has to replace the last valid operation cell for the identity. The transaction must provide proof so the script can verify the transaction against the recovery rule.
The following figure illustrates the recovery operation. The solid line arrow indicates the DID PLC relationship where the arrow points from the prior operation. The dotted line arrow indicates the CKB transaction relationship where the connected operations appears in a CKB transaction as input and output respectively, and the input points to the output.
![[DID PLC Registry on CKB (RFC) - Drawing Recovery Operation.excalidraw.svg]]
%%[[DID PLC Registry on CKB (RFC) - Drawing Recovery Operation.excalidraw|π Edit in Excalidraw]]%%
To submit the recovery operation:
- Get the latest operation for the identity via the CKB indexer RPC by searching type script `R(OP | did)`.
- Get all active operations from the fork operation until the latest operation by tracing through CKB transactions backward. An active operation is an operation that has not been invalided by a recovery operation yet.
- Get the block hash of the block which committed the transaction that creates the latest operation as an output cell.
- Get the block hash of the block which committed the transaction that creates the reference operation as an output cell, and the Merkle tree existence proof for the transaction.
- Create a CKB transaction that:
- Use the latest operation cell as an input.
- Create an output operation cell for the recover op.
- Add the found active operations and Merkle proof into the witness for the input operation cell.
- Add the found block hash as a dep header.
Extend the verification rule 4 of `R(OP | did)`
- 4. When there is an input in the cell group:
1. The output is either a well-formatted update or deactivation operation in DAG-CBOR.
2. If the field `prev` of the output operation is the CID of the input operation.
1. The field `signature`in the output operation MUST be a valid signature of the unsigned output operation signed by a key listed in the field `rotationKeys` of the input operation.
3. Otherwise:
1. There is a list of operations op 1, op 2, ..., and op n in the input cell witness. The list length is at least 2.
2. The field `prev` of the output operation is the CID of op n
3. The field `prev` of the op k + 1 is the CID of op k for k from 1 to n - 1.
4. The field `prev` of the output operation equals to the `prev` of the op 2.
5. There is a transaction in the input cell witness and its Merkle proof.
6. The transaction is in one of the block found in the dep headers according to the Merkle proof.
7. Op 2 is an output of the transaction found in the witness.
8. The header deps for the output operation and op 1 can be found. The timestamp t0 of the block for the output operation and the timestamp t2 of the block for op 2 must satisfy that `0 < t0 - t2 <= 72hr`
9. The field `signature`in the output operation MUST be a valid signature of the unsigned output operation signed by a key listed in the field `rotationKeys` of op 1.
10. The signed key index of the output operation is less than the signed key index of op 2.
There are two issues in this solution:
1. It requires a large size of witness. For frequently updated identity, the transaction fee is high or the size may exceed the block size limit.
2. The time window is between the reference operation and the latest operation, but not between the reference operation and the output operation because CKB script cannot access the current block header information.
The issue 2 can be resolved by using 2-step commit. Create the recovery proposal first, then accept the proposal.
1. Create a recovery operation proposal cell with the type script `R(RECOVERY | did)` where RECOVERY is a 8-bit integer tag whose value is 2.
2. The transaction creating the recovery operation cell must include the proposal cell and the data of the output cell and the proposal cell are identical. Add the header dep for the proposal cell instead of the latest operation cell.
This leads to following changes to the verification rules:
- 4.3.8. There exists an recovery proposal cell with the type script `R(RECOVERY | id)` in inputs. The data of the proposal cell is identical to the output operation cell. The header deps for op 1 and the proposal cell can be found. The timestamp t0 of the block for the proposal cell and the timestamp t2 of the block for op 2 must satisfy that `0 < t0 - t2 <= 72hr`
The type script `R(RECOVERY | did)` must verify that:
1. It is used as the type script.
2. There's either exactly one input of exactly one output in the script group.
### Deferred Deletion
An alternative solution is deferring the deletion of the operation cells. Any operation cell must be alive for at least 72hrs so the recovery operation can reference them in a CKB transaction. This can be achieved by using the `since` field of the CKB transaction.
While deleting the prior operation, the update transaction now will create a pending-deletion cell as well.
![[DID PLC Registry on CKB (RFC) - Drawing Update Tx 2.excalidraw.svg]]
%%[[DID PLC Registry on CKB (RFC) - Drawing Update Tx 2.excalidraw|π Edit in Excalidraw]]%%
The pending deletion cell has the type script `R(PENDING_DELETION, did)` and the lock script `R(DELEGATION, lock_script_hash)`. The tag `PENDING_DELETION` has the value 3, and `DELEGATION` has the value 4. The pending deletion cell has the same data as the input operation cell. The transaction creator can choose arbitrary the args `lock_script_hash`. The lock script `R(DELEGATION, lock_script_hash)` ensures the cell will be transferred to the user who has the lock script matches the `lock_script_hash`.
The type script `R(PENDING_DELETION, did)` must verify that:
- It is used as the type script.
- All the cells in the script group must use a lock script `R(DELEGATION, lock_script_hash)` where `lock_script_hash` can be arbitrary hash.
- There is either exactly one input or exactly one output in the script group, but not both.
- When there is exactly one output:
- There is exactly one input having the type script `R(OP, did)`
- The output and the input with type script `R(OP, did)` have the same data.
- When there is exactly one input:
- Assume that the input has the CKB capacity C, there is at least C capacity in outputs locked by the lock script with hash `lock_script_hash` with empty type script.
- The transaction must satisfy one of the two following conditions:
- There's a transaction output with type script `R(OP, did)` with the same did,
- or the input `since` field is set to relative timestamp with a value 72hrs.
Extend the verification rule 4 of `R(OP | did)` described in the basic version, and add a new rule 5.
- 4. When there is an input in the cell group:
1. The output is either a well-formatted update or deactivation operation in DAG-CBOR.
2. If the field `prev` of the output operation is the CID of the input operation.
1. The field `signature`in the output operation MUST be a valid signature of the unsigned output operation signed by a key listed in the field `rotationKeys` of the input operation.
2. There's no cells in inputs which have the type script `R(PENDING_DELETION, did)`.
3. Otherwise:
1. There is a list of input cells having the type script `R(PENDING_DELETION, did)`, named as op 2, ..., and op n. The list length is at least 1.
2. There is an operation op 1 can be found in the input operation cell witness.
3. The field `prev` of the output operation is the CID of op n
4. The field `prev` of the op k + 1 is the CID of op k for k from 1 to n - 1.
5. The field `prev` of the output operation equals to the `prev` of the op 2.
6. The header deps for the output operation and op 2 can be found. The timestamp t0 of the block for the output operation and the timestamp t2 of the block for op 2 must satisfy that `0 < t0 - t2 <= 72hr`
7. The field `signature`in the output operation MUST be a valid signature of the unsigned output operation signed by a key listed in the field `rotationKeys` of op 1.
8. The signed key index of the output operation is less than the signed key index of op 2.
- 5. There is a output cell which have the type script `R(PENDING_DELETION, did)`.
### Saving Only Keys, CIDs, and Timestamps
The recovery operations requires an active rotation key of higher priority, and the CID and timestamp of the operation the key belongs to. This proposal saves these information in the operation cell lock args to verify the recovery options.
The cell lock script must use `R(ROTATION_KEYS, keys)` as the lock script, where `ROTATION_KEYS` is an integer flag 6, and `keys` is a list of tuple `(key, cid, timestamp)`.
The lock script `R(ROTATION_KEYS, keys)` exits with success status immediately. The verification is delegated to the type script.
The type script `R(OP, did)` must verify that the output cell lock script is `R(ROTATION_KEYS, keys)`. When the output operation cell is a genesis operation, `keys` must be empty. Otherwise, the `keys` in the output operation cell must be constructed as:
- First copy the `keys` in the input operation cell lock script args.
- Then append the unused rotation keys in the input operation cell data. The field `cid` is the CID of the input operation cell. The field `timestamp` is the timestamp of the block where the input operation cell is created. So the transaction must include that block header in the header deps.
- Remove all tuples which `timestamp` is 72hrs older than the timestamp of the block creating the input operation cell.
The recovery operation is similar to the proposal "Providing Proof of The Fork Point". Instead of providing history operations, the verification only needs the input operation cell block header as a header dep.
- The recovery operation is signed by a key listed in the input operation cell lock script, where:
- The field `cid` is identical to the output recovery operation `prev` field.
- The field `timestamp` is NOT 72hrs than the timestamp of the block creating the input operation cell.
- The output recovery operation cell lock script must be `R(ROTATION_KEYS, keys)`, and `keys` is constructed as:
- Copy `keys` from the input operation cell.
- Remove the key used to sign the output recovery operation cell, and all the keys after it in the `keys` list.
- Remove all tuples which `timestamp` is 72hrs older than the timestamp of the block creating the input operation cell.
This solution also requires 2-step commitment to allow script accessing the timestamp of when the recovery operation is created just like in "Providing Proof of The Fork Point".
## Operations Chain Uniqueness
Another issue of the basic version is that anyone can submit the genesis operation and start duplicated chains for an identity. This chapter proposes a solution to ensure there's only one chain for any identity.
The proposal in this chapter requires a new types script pattern `R(VACANT_RANGE, from, to)`. `VACANT_RANGE` is an 8-bit integer tag with value 5. Both `from` and `to` are 32-byte integer encoded in big-endian.
The type script `R(VACANT_RANGE, from, to)` must verify:
- When it only apears in outputs with the args `from` being all ones, and `to` being all zeros.
- It must be the only cell in its script group.
- There must be one input cell which is created in the same transaction as the script `R`. In other words, any output created along with the script `R` acts as a token to create the first `VACANT_RANGE` cell for the script.
- Otherwise there's only one input cell which type script match the pattern `R(VACANT_RANGE, from, to)`, and a genesis operation output with the type script `R(OP, did)`. When `did` is converted into integer as big-endian format, it must in the range `[from, to]` (inclusively). If `did` is larger than `from`, there must be an output cell with the type script `R(VACANT_RANGE, from, did-1)`. If `did` is smaller than `to`, there must be an output cell with the type script `R(VACANT_RANGE, did+1, to)`. No other output cell should have the type script `R` and starts args with `VACANT_RANGE`.
The proposal here is an implementation of the distributed map described in Bartoletti, M., Marchesin, R., & Zunino, R. (2025). _Scalable UTXO Smart Contracts via Fine-Grained Distributed State_ (No. arXiv:2406.07700). arXiv. [https://doi.org/10.48550/arXiv.2406.07700](https://doi.org/10.48550/arXiv.2406.07700)
%% (**Reference**:: [[Bartoletti et al. - Scalable UTXO Smart Contracts via Fine-Grained Distributed State]]) %%
## API Gateway
A new API to get the meta data about the service
- `GET /meta`: returns the information about the script `R`.
Compatible with the [DID PLC Directory Server API](https://web.plc.directory/api/redoc)
- `GET /:did`
- `GET /:did/log`
- `GET /:did/log/audit`
- `GET /:did/data`
Optional extension:
- `GET /:did/log/ckb`, `GET /:did/log/audit/ckb`
- Return the CKB UTXO out point for each operation
- `POST /:did/import?from=https://plc.directory/`
- Synchronize an identity from another registry.
## Identity Services
Use API Gateway like `plc.directory` since the API interface is fully compatible. Example:
```javascript
{
"service": [
{
"id": "#atproto_pds",
"type": "AtprotoPersonalDataServer",
"serviceEndpoint": "https://ckb-did.directory",
}
]
}
```