Skip to content

Commit

Permalink
Issue #266 & #325 - Changed create API body schema + refactored Reque…
Browse files Browse the repository at this point in the history
…stHandler tests.
  • Loading branch information
thehenrytsai committed Feb 13, 2020
1 parent d5a81f1 commit 743f5a0
Show file tree
Hide file tree
Showing 32 changed files with 746 additions and 575 deletions.
87 changes: 34 additions & 53 deletions docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@ Architecturally, a Sidetree network is a network consisting of multiple logical
| DID Document | A document containing metadata of a DID, see [DID specification](https://w3c-ccg.github.io/did-spec/). |
| DID unique suffix | The unique portion of a DID. e.g. The unique suffix of 'did:sidetree:abc' would be 'abc'. |
| Operation | A change to a DID Document. |
| Operation hash | The hash of the encoded payload of an _operation request_. |
| Operation request | A JWS formatted request sent to a Sidetree node to perform an _operation_. |
| Original DID Document | A DID Document that is used in create operation to generate the DID. |
| Recovery key | A key that is used to perform recovery or delete operation. |
| Sidetree node | A logical server executing Sidetree protocol rules. |
| Suffix data | Data required to deterministically generate a DID . |
| Transaction | A blockchain transaction representing a batch of Sidetree operations. |


Expand Down Expand Up @@ -59,23 +58,16 @@ An update operation to a DID Document contains only the changes from the previou

> NOTE: Create and recover operations require a complete DID Document as input.
### Sidetree Operation Hashes
## Sidetree DID Unique Suffix
A Sidetree _DID unique suffix_ is the globally unique portion of a DID. It is computed deterministically from the following data (_suffix data) supplied in a create operation request:

An _operation hash_ is the hash of the _encoded payload_ of a Sidetree operation request. The exact request schema for all operations are defined in [Sidetree REST API](#sidetree-rest-api) section.
1. Recovery key.
1. Document hash.
1. Hash of one-time password for recovery.

## Sidetree DID and Original DID Document
A Sidetree DID is intentionally the hash of the encoded DID Document given as the create operation payload (_original DID Document_), prefixed by the Sidetree method name. Given how _operation hash_ is computed, A DID is also the operation hash of the initial create operation.
A requester can deterministically compute the DID before the create operation is anchored on the blockchain.

Since the requester is in control of the _original DID Document_, the requester can deterministically calculate the DID before the create operation is anchored on the blockchain.

A valid _original DID Document_ must be a valid generic DID Document that adheres to the following additional Sidetree protocol specific rules:
1. The document must NOT have the `id` property.
1. The document must contain at least 1 entry in the `publicKey` array property.
1. The `id` property of a `publickey` element must be specified and be a fragment (e.g. `#key1`).
1. Can have `service` property.
1. If an Identity Hub `serviceEndpoint` is desired, an entry must exist in the `service` array that conforms to the Identity Hub Service Endpoint descriptor schema.

See [DID Create API](#original-did-document-example) section for an example of an original DID Document.
See [DID Create API](#DID-and-DID-Document-Creation) section for detail on how to construct a create operation request.


## Unpublished DID Resolution
Expand All @@ -84,7 +76,7 @@ DIDs may include attached values that are used in resolution and other activitie

Many DID Methods feature a period of time (which may be indefinite) between the generation of an ID and the ID being anchored/propagated throughout the underlying trust system (i.e. blockchain, ledger). The community has recognized the need for a mechanism to support resolution and use of identifiers during this period. As such, the community will introduce a _Generic DID Parameter_ `initial-values` that any DID method can use to signify initial state variables during this period.

Sidetree uses the `initial-values` DID parameter to enable unpublished DID resolution. After generating a new Sidetree DID, in order to use this DID immediately, the user will attach the `initial-values` DID Parameter to the DID, with the value being the encoded string of the _original DID Document_.
Sidetree uses the `initial-values` DID parameter to enable unpublished DID resolution. After generating a new Sidetree DID, in order to use this DID immediately, the user will attach the `initial-values` DID Parameter to the DID, with the value being the encoded string of the _suffix data_.

e.g. `did:sidetree:<unique-portion>;initial-values=<encoded-original-did-document>`.

Expand Down Expand Up @@ -262,34 +254,21 @@ A _Sidetree node_ exposes a set of REST API that enables the creation of new DID


### JSON Web Signature (JWS)
Every operation request sent to a Sidetree node __must__ be signed using the __flattened JWS JSON serialization__ scheme.
Sidetree API uses __flattened JWS JSON serialization__ scheme when content need to be protected.

The JWS operation request header must be protected and be encoded in the following schema:

#### Protected header schema
```json
{
"kid": "ID of the key used to sign the original DID Document.",
"kid": "ID of the signing key.",
"alg": "ES256K"
}
```

#### JWS operation Create Request Example
```http
POST / HTTP/1.1
{
"header": "ewogICJvcGVyYXRpb24iOiAiY3JlYXRlIiwKICAia2lkIjogImtleTEiLAogICJhbGciOiAiRVMyNTZLIgp9",
"payload": "eyJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvZGlkL3YxIiwicHVibGljS2V5IjpbeyJpZCI6IiNrZXkxIiwidHlwZSI6IlNlY3AyNTZrMVZlcmlmaWNhdGlvbktleTIwMTgiLCJwdWJsaWNLZXlIZXgiOiIwMmY0OTgwMmZiM2UwOWM2ZGQ0M2YxOWFhNDEyOTNkMWUwZGFkMDQ0YjY4Y2Y4MWNmNzA3OTQ5OWVkZmQwYWE5ZjEifSx7ImlkIjoiI2tleTIiLCJ0eXBlIjoiUnNhVmVyaWZpY2F0aW9uS2V5MjAxOCIsInB1YmxpY0tleVBlbSI6Ii0tLS0tQkVHSU4gUFVCTElDIEtFWS4yLkVORCBQVUJMSUMgS0VZLS0tLS0ifV0sInNlcnZpY2UiOlt7InR5cGUiOiJJZGVudGl0eUh1YiIsInB1YmxpY0tleSI6IiNrZXkxIiwic2VydmljZUVuZHBvaW50Ijp7IkBjb250ZXh0Ijoic2NoZW1hLmlkZW50aXR5LmZvdW5kYXRpb24vaHViIiwiQHR5cGUiOiJVc2VyU2VydmljZUVuZHBvaW50IiwiaW5zdGFuY2VzIjpbImRpZDpiYXI6NDU2IiwiZGlkOnphejo3ODkiXX19XX0",
"signature": "mAJp4ZHwY5UMA05OEKvoZreRo0XrYe77s3RLyGKArG85IoBULs4cLDBtdpOToCtSZhPvCC2xOUXMGyGXDmmEHg"
}
```

### DID and DID Document Creation
Use this API to create a Sidetree DID and its initial state.

An encoded _original DID Document_ must be supplied as the request payload, see [Original DID Document](#Sidetree-DID-and-Original-DID-Document) section for the requirements of a valid original DID Document.

#### Request path
```http
POST / HTTP/1.1
Expand All @@ -300,42 +279,44 @@ POST / HTTP/1.1
| --------------------- | ---------------------- |
| ```Content-Type``` | ```application/json``` |

#### Request body schema

#### Create operation request body schema
```json
{
"protected": "Encoded protected header.",
"payload": "Encoded create payload JSON object defined by the schema below.",
"signature": "Encoded signature."
"type": "create",
"suffixData": "Encoded JSON object containing data used to compute the unique DID suffix.",
"operationData": "Encoded JSON object containing create operation data."
}
```

#### Create operation payload schema
#### Suffix data schema
```json
{
"type": "create",
"didDocument": "Encoded original DID Document",
"nextRecoveryOtpHash": "Hash of the one-time password to be used for the next recovery.",
"nextUpdateOtpHash": "Hash of the one-time password to be used for the next update.",
"operationDataHash": "Hash of the encoded operation data string.",
"recoveryKey": {
"publicKeyHex": "The recovery public key as a HEX string."
},
"nextRecoveryOtpHash": "Hash of the one-time password to be used for the next recovery."
}
```

#### Original DID Document example
#### Create operation data schema
```json
{
"nextUpdateOtpHash": "Hash of the one-time password to be used for the next update.",
"document": "Opaque content."
}
```

#### Opaque document example
```json
{
"id": "someIdasdf",
"@context": "https://w3id.org/did/v1",
"publicKey": [
{
"id": "#key1",
"type": "Secp256k1VerificationKey2018",
"publicKeyHex": "02f49802fb3e09c6dd43f19aa41293d1e0dad044b68cf81cf7079499edfd0aa9f1",
"usage": "signing"
},
{
"id": "#key2",
"type": "RsaVerificationKey2018",
"publicKeyPem": "-----BEGIN PUBLIC KEY.2.END PUBLIC KEY-----",
"usage": "recovery"
}
],
"service": [
Expand Down Expand Up @@ -401,7 +382,7 @@ Two forms of string can be passed in the URI:
e.g.
```did:sidetree:exKwW0HjS5y4zBtJ7vYDwglYhtckdO15JDt1j5F5Q0A;initial-values=ewogICAgICAiQGNvbnRleHQiOiAiaHR0cHM6Ly93M2lkLm9yZy9kaWQvdjEiLAogICAgICAicHVibGljS2V5IjogWwogICAgICAgIHsKICAgICAgICAgICAgImlkIjogIiNrZXkxIiwKICAgICAgICAgICAgInR5cGUiOiAiU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOCIsCiAgICAgICAgICAgICJwdWJsaWNLZXlIZXgiOiAiMDM0ZWUwZjY3MGZjOTZiYjc1ZThiODljMDY4YTE2NjUwMDdhNDFjOTg1MTNkNmE5MTFiNjEzN2UyZDE2ZjFkMzAwIgogICAgICAgIH0KICAgICAgXQogICAgfQ```

Standard resolution is performed if the DID is found to registered on the blockchain. If the DID Document cannot be found, the encoded DID Document given in the `initial-values` DID parameter is used directly to generate and return as the resolved DID Document, in which case the supplied encoded DID Document is subject to the same validation as an _original DID Document_ in a create operation.
Standard resolution is performed if the DID is found to registered on the blockchain. If the DID cannot be found, the data given in the `initial-values` DID parameter is used directly to generate and resolve the DID.

#### Request path
```http
Expand All @@ -414,12 +395,12 @@ None.
#### Request body schema
None.

#### Request example - DID
#### Request example
```http
GET /did:sidetree:exKwW0HjS5y4zBtJ7vYDwglYhtckdO15JDt1j5F5Q0A HTTP/1.1
```

#### Request example - Method name prefixed, encoded original DID Document
#### Request example - Resolution request with initial values.
```http
GET /did:sidetree:exKwW0HjS5y4zBtJ7vYDwglYhtckdO15JDt1j5F5Q0A;initial-values=ewogICAgICAiQGNvbnRleHQiOiAiaHR0cHM6Ly93M2lkLm9yZy9kaWQvdjEiLAogICAgICAicHVibGljS2V5IjogWwogICAgICAgIHsKICAgICAgICAgICAgImlkIjogIiNrZXkxIiwKICAgICAgICAgICAgInR5cGUiOiAiU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOCIsCiAgICAgICAgICAgICJwdWJsaWNLZXlIZXgiOiAiMDM0ZWUwZjY3MGZjOTZiYjc1ZThiODljMDY4YTE2NjUwMDdhNDFjOTg1MTNkNmE5MTFiNjEzN2UyZDE2ZjFkMzAwIgogICAgICAgIH0KICAgICAgXQogICAgfQ HTTP/1.1
```
Expand Down
2 changes: 1 addition & 1 deletion lib/core/Resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default class Resolver {
// NOTE: We are passing the DID resolution model into apply method so that both:
// 1. `didDocument` can be `undefined` initially; and
// 2. `didDocument` can be modified directly in-place in subsequent applying of operations.
let didResolutionModel: DidResolutionModel = {};
const didResolutionModel: DidResolutionModel = {};

const operations = await this.operationStore.get(didUniqueSuffix);
const createAndRecoverAndRevokeOperations = operations.filter(
Expand Down
4 changes: 2 additions & 2 deletions lib/core/interfaces/IOperationProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import AnchoredOperationModel from '../models/AnchoredOperationModel';
import NamedAnchoredOperationModel from '../models/NamedAnchoredOperationModel';
import DidResolutionModel from '../models/DidResolutionModel';

/**
Expand All @@ -18,7 +18,7 @@ export default interface IOperationProcessor {
* The container object that contains the metadata needed for applying the operation and the reference to the DID document to be modified.
*/
apply (
operation: AnchoredOperationModel,
operation: NamedAnchoredOperationModel,
didResolutionModel: DidResolutionModel
): Promise<ApplyResult>;
}
Expand Down
12 changes: 6 additions & 6 deletions lib/core/versions/latest/BatchFile.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import AnchoredOperation from './AnchoredOperation';
import AnchoredOperationModel from '../../models/AnchoredOperationModel';
import AnchorFileModel from './models/AnchorFileModel';
import BatchFileModel from './models/BatchFileModel';
import Compressor from './util/Compressor';
import Encoder from './Encoder';
import ErrorCode from './ErrorCode';
import JsonAsync from './util/JsonAsync';
import NamedAnchoredOperationModel from '../../models/NamedAnchoredOperationModel';
import Operation from './Operation';
import ProtocolParameters from './ProtocolParameters';
import SidetreeError from '../../SidetreeError';
import timeSpan = require('time-span');
Expand Down Expand Up @@ -87,15 +86,16 @@ export default class BatchFile {
);
}

const anchoredOperationModel: AnchoredOperationModel = {
const operation = await Operation.parse(operationBuffer);
const namedAnchoredOperationModel: NamedAnchoredOperationModel = {
didUniqueSuffix: operation.didUniqueSuffix,
type: operation.type,
operationBuffer,
operationIndex,
transactionNumber,
transactionTime
};

const operation = AnchoredOperation.createAnchoredOperation(anchoredOperationModel);

const didUniqueSuffixesInAnchorFile = anchorFile.didUniqueSuffixes[operationIndex];
if (operation.didUniqueSuffix !== didUniqueSuffixesInAnchorFile) {
throw new SidetreeError(
Expand All @@ -104,7 +104,7 @@ export default class BatchFile {
`is not the same as '${didUniqueSuffixesInAnchorFile}' seen in anchor file.`);
}

namedAnchoredOperationModels.push(operation);
namedAnchoredOperationModels.push(namedAnchoredOperationModel);
}
console.info(`Decoded ${batchSize} operations in batch file. Time taken: ${endTimer.rounded()} ms.`);

Expand Down
21 changes: 9 additions & 12 deletions lib/core/versions/latest/BatchWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import IBatchWriter from '../../interfaces/IBatchWriter';
import IBlockchain from '../../interfaces/IBlockchain';
import IOperationQueue from './interfaces/IOperationQueue';
import MapFile from './MapFile';
import Operation from './Operation';
import ProtocolParameters from './ProtocolParameters';

/**
Expand All @@ -24,20 +23,18 @@ export default class BatchWriter implements IBatchWriter {

public async write () {
// Get the batch of operations to be anchored on the blockchain.
const operationBuffers = await this.operationQueue.peek(ProtocolParameters.maxOperationsPerBatch);
const queuedOperations = await this.operationQueue.peek(ProtocolParameters.maxOperationsPerBatch);

console.info('Batch size = ' + operationBuffers.length);
console.info('Batch size = ' + queuedOperations.length);

// Do nothing if there is nothing to batch together.
if (operationBuffers.length === 0) {
if (queuedOperations.length === 0) {
return;
}

const batch = operationBuffers.map(
(buffer) => Operation.create(buffer)
);
const operationBuffers = queuedOperations.map(queuedOperations => queuedOperations.operationBuffer);

// Create the batch file buffer from the operation batch.
// Create the batch file buffer from the operation buffers.
const batchFileBuffer = await BatchFile.fromOperationBuffers(operationBuffers);

// Write the batch file to content addressable store.
Expand All @@ -50,15 +47,15 @@ export default class BatchWriter implements IBatchWriter {
console.info(`Wrote map file ${mapFileHash} to content addressable store.`);

// Construct the DID unique suffixes of each operation to be included in the anchor file.
const didUniqueSuffixes = batch.map(operation => operation.didUniqueSuffix);
const didUniqueSuffixes = queuedOperations.map(queuedOperations => queuedOperations.didUniqueSuffix);

// Construct the 'anchor file'.
const anchorFileModel: AnchorFileModel = {
mapFileHash,
didUniqueSuffixes
};

// Make the 'anchor file' available in content addressable store.
// Write the anchor file to content addressable store.
const anchorFileJsonBuffer = await AnchorFile.createBufferFromAnchorFileModel(anchorFileModel);
const anchorFileAddress = await this.cas.write(anchorFileJsonBuffer);
console.info(`Wrote anchor file ${anchorFileAddress} to content addressable store.`);
Expand All @@ -76,7 +73,7 @@ export default class BatchWriter implements IBatchWriter {

await this.blockchain.write(stringToWriteToBlockchain, fee);

// Remove written operations from queue if batch writing is successful.
await this.operationQueue.dequeue(batch.length);
// Remove written operations from queue after batch writing has completed successfully.
await this.operationQueue.dequeue(queuedOperations.length);
}
}

0 comments on commit 743f5a0

Please sign in to comment.