From f3f8318031ef66ccb7ec12428f4aa000683037c9 Mon Sep 17 00:00:00 2001 From: Henry Tsai <17891086+thehenrytsai@users.noreply.github.com> Date: Fri, 14 Aug 2020 16:15:56 -0700 Subject: [PATCH] feat(ref-imp): #336 - added published field in method metadata + fixed issue #833 * feat(ref-imp): #336 - added published field in method metadata * fix(ref-imp): #833 - fixed bug where long-form DID is used in document of a published DID --- lib/core/models/AnchoredOperationModel.ts | 2 +- lib/core/versions/0.9.0/Did.ts | 3 ++ lib/core/versions/0.9.0/DocumentComposer.ts | 3 +- lib/core/versions/0.9.0/OperationProcessor.ts | 4 --- lib/core/versions/0.9.0/RequestHandler.ts | 26 ++++++++++---- lib/core/versions/latest/Did.ts | 3 ++ lib/core/versions/latest/DocumentComposer.ts | 3 +- .../versions/latest/OperationProcessor.ts | 4 --- lib/core/versions/latest/RequestHandler.ts | 26 ++++++++++---- tests/core/DocumentComposer.spec.ts | 35 ++++++++++++++++--- tests/core/RequestHandler.spec.ts | 5 +-- tests/core/Resolver.spec.ts | 18 ++++++---- tests/fixtures/create/resultingDocument.json | 1 + tests/fixtures/recover/resultingDocument.json | 1 + tests/fixtures/update/resultingDocument.json | 1 + 15 files changed, 96 insertions(+), 39 deletions(-) diff --git a/lib/core/models/AnchoredOperationModel.ts b/lib/core/models/AnchoredOperationModel.ts index e9fec16f7..a513eeee4 100644 --- a/lib/core/models/AnchoredOperationModel.ts +++ b/lib/core/models/AnchoredOperationModel.ts @@ -10,7 +10,7 @@ export default interface AnchoredOperationModel { didUniqueSuffix: string; /** The type of operation. */ type: OperationType; - /** The logical blockchain time that this opeartion was anchored on the blockchain */ + /** The logical blockchain time that this operation was anchored on the blockchain */ transactionTime: number; /** The transaction number of the transaction this operation was batched within. */ transactionNumber: number; diff --git a/lib/core/versions/0.9.0/Did.ts b/lib/core/versions/0.9.0/Did.ts index d32d10207..3b36277e3 100644 --- a/lib/core/versions/0.9.0/Did.ts +++ b/lib/core/versions/0.9.0/Did.ts @@ -23,6 +23,8 @@ export default class Did { public createOperation?: CreateOperation; /** The short form. */ public shortForm: string; + /** The long form. */ + public longForm: string | undefined; /** * Parses the input string as Sidetree DID. @@ -51,6 +53,7 @@ export default class Did { } else { // This is long-form. this.uniqueSuffix = did.substring(didPrefix.length, indexOfQuestionMarkChar); + this.longForm = did; } if (this.uniqueSuffix.length === 0) { diff --git a/lib/core/versions/0.9.0/DocumentComposer.ts b/lib/core/versions/0.9.0/DocumentComposer.ts index 23279935f..370418fa3 100644 --- a/lib/core/versions/0.9.0/DocumentComposer.ts +++ b/lib/core/versions/0.9.0/DocumentComposer.ts @@ -14,7 +14,7 @@ export default class DocumentComposer { /** * Transforms the given DID state into a DID Document. */ - public static transformToExternalDocument (didState: DidState, did: string): any { + public static transformToExternalDocument (didState: DidState, did: string, published: boolean): any { // If the DID is deactivated. if (didState.nextRecoveryCommitmentHash === undefined) { return { status: 'deactivated' }; @@ -83,6 +83,7 @@ export default class DocumentComposer { '@context': 'https://www.w3.org/ns/did-resolution/v1', didDocument: didDocument, methodMetadata: { + published, recoveryCommitment: didState.nextRecoveryCommitmentHash, updateCommitment: didState.nextUpdateCommitmentHash } diff --git a/lib/core/versions/0.9.0/OperationProcessor.ts b/lib/core/versions/0.9.0/OperationProcessor.ts index 140c5ee06..3a89d0bc2 100644 --- a/lib/core/versions/0.9.0/OperationProcessor.ts +++ b/lib/core/versions/0.9.0/OperationProcessor.ts @@ -123,7 +123,6 @@ export default class OperationProcessor implements IOperationProcessor { } const newDidState = { - didUniqueSuffix: operation.didUniqueSuffix, document, nextRecoveryCommitmentHash: operation.suffixData.recoveryCommitment, nextUpdateCommitmentHash: delta ? delta.updateCommitment : undefined, @@ -229,9 +228,7 @@ export default class OperationProcessor implements IOperationProcessor { } const newDidState = { - didUniqueSuffix: operation.didUniqueSuffix, document, - recoveryKey: operation.signedData.recoveryKey, nextRecoveryCommitmentHash: operation.signedData.recoveryCommitment, nextUpdateCommitmentHash: delta ? delta.updateCommitment : undefined, lastOperationTransactionNumber: anchoredOperationModel.transactionNumber @@ -266,7 +263,6 @@ export default class OperationProcessor implements IOperationProcessor { const newDidState = { document: didState.document, // New values below. - recoveryKey: undefined, nextRecoveryCommitmentHash: undefined, nextUpdateCommitmentHash: undefined, lastOperationTransactionNumber: anchoredOperationModel.transactionNumber diff --git a/lib/core/versions/0.9.0/RequestHandler.ts b/lib/core/versions/0.9.0/RequestHandler.ts index 994f5bda3..cbc4bdf1e 100644 --- a/lib/core/versions/0.9.0/RequestHandler.ts +++ b/lib/core/versions/0.9.0/RequestHandler.ts @@ -132,7 +132,8 @@ export default class RequestHandler implements IRequestHandler { } const did = `did:${this.didMethodName}:${operationModel.didUniqueSuffix}`; - const document = DocumentComposer.transformToExternalDocument(didState, did); + const published = false; + const document = DocumentComposer.transformToExternalDocument(didState, did, published); return { status: ResponseStatus.Succeeded, @@ -153,10 +154,15 @@ export default class RequestHandler implements IRequestHandler { const did = await Did.create(shortOrLongFormDid, this.didMethodName); let didState: DidState | undefined; + let published = false; if (did.isShortForm) { didState = await this.resolver.resolve(did.uniqueSuffix); + + if (didState !== undefined) { + published = true; + } } else { - didState = await this.resolveLongFormDid(did); + [didState, published] = await this.resolveLongFormDid(did); } if (didState === undefined) { @@ -166,7 +172,11 @@ export default class RequestHandler implements IRequestHandler { }; } - const document = DocumentComposer.transformToExternalDocument(didState, shortOrLongFormDid); + // We reach here it means there is a DID Document to return. + + // If DID is published, use the short-form DID; else use long-form DID in document. + const didStringToUseInDidDocument = published ? did.shortForm : did.longForm!; + const document = DocumentComposer.transformToExternalDocument(didState, didStringToUseInDidDocument, published); return { status: ResponseStatus.Succeeded, @@ -190,22 +200,24 @@ export default class RequestHandler implements IRequestHandler { /** * Resolves the given long-form DID by resolving using operations found over the network first; - * if no operations found, the given create operation will is used to construct the DID state. + * if no operations found, the given create operation will be used to construct the DID state. + * + * @returns [DID state, published] */ - private async resolveLongFormDid (did: Did): Promise { + private async resolveLongFormDid (did: Did): Promise<[DidState | undefined, boolean]> { // Attempt to resolve the DID by using operations found from the network first. let didState = await this.resolver.resolve(did.uniqueSuffix); // If DID state found then return it. if (didState !== undefined) { - return didState; + return [didState, true]; } // The code reaches here if this DID is not registered on the ledger. didState = await this.applyCreateOperation(did.createOperation!); - return didState; + return [didState, false]; } private async applyCreateOperation (createOperation: OperationModel): Promise { diff --git a/lib/core/versions/latest/Did.ts b/lib/core/versions/latest/Did.ts index d32d10207..3b36277e3 100644 --- a/lib/core/versions/latest/Did.ts +++ b/lib/core/versions/latest/Did.ts @@ -23,6 +23,8 @@ export default class Did { public createOperation?: CreateOperation; /** The short form. */ public shortForm: string; + /** The long form. */ + public longForm: string | undefined; /** * Parses the input string as Sidetree DID. @@ -51,6 +53,7 @@ export default class Did { } else { // This is long-form. this.uniqueSuffix = did.substring(didPrefix.length, indexOfQuestionMarkChar); + this.longForm = did; } if (this.uniqueSuffix.length === 0) { diff --git a/lib/core/versions/latest/DocumentComposer.ts b/lib/core/versions/latest/DocumentComposer.ts index 23279935f..370418fa3 100644 --- a/lib/core/versions/latest/DocumentComposer.ts +++ b/lib/core/versions/latest/DocumentComposer.ts @@ -14,7 +14,7 @@ export default class DocumentComposer { /** * Transforms the given DID state into a DID Document. */ - public static transformToExternalDocument (didState: DidState, did: string): any { + public static transformToExternalDocument (didState: DidState, did: string, published: boolean): any { // If the DID is deactivated. if (didState.nextRecoveryCommitmentHash === undefined) { return { status: 'deactivated' }; @@ -83,6 +83,7 @@ export default class DocumentComposer { '@context': 'https://www.w3.org/ns/did-resolution/v1', didDocument: didDocument, methodMetadata: { + published, recoveryCommitment: didState.nextRecoveryCommitmentHash, updateCommitment: didState.nextUpdateCommitmentHash } diff --git a/lib/core/versions/latest/OperationProcessor.ts b/lib/core/versions/latest/OperationProcessor.ts index 140c5ee06..3a89d0bc2 100644 --- a/lib/core/versions/latest/OperationProcessor.ts +++ b/lib/core/versions/latest/OperationProcessor.ts @@ -123,7 +123,6 @@ export default class OperationProcessor implements IOperationProcessor { } const newDidState = { - didUniqueSuffix: operation.didUniqueSuffix, document, nextRecoveryCommitmentHash: operation.suffixData.recoveryCommitment, nextUpdateCommitmentHash: delta ? delta.updateCommitment : undefined, @@ -229,9 +228,7 @@ export default class OperationProcessor implements IOperationProcessor { } const newDidState = { - didUniqueSuffix: operation.didUniqueSuffix, document, - recoveryKey: operation.signedData.recoveryKey, nextRecoveryCommitmentHash: operation.signedData.recoveryCommitment, nextUpdateCommitmentHash: delta ? delta.updateCommitment : undefined, lastOperationTransactionNumber: anchoredOperationModel.transactionNumber @@ -266,7 +263,6 @@ export default class OperationProcessor implements IOperationProcessor { const newDidState = { document: didState.document, // New values below. - recoveryKey: undefined, nextRecoveryCommitmentHash: undefined, nextUpdateCommitmentHash: undefined, lastOperationTransactionNumber: anchoredOperationModel.transactionNumber diff --git a/lib/core/versions/latest/RequestHandler.ts b/lib/core/versions/latest/RequestHandler.ts index 994f5bda3..cbc4bdf1e 100644 --- a/lib/core/versions/latest/RequestHandler.ts +++ b/lib/core/versions/latest/RequestHandler.ts @@ -132,7 +132,8 @@ export default class RequestHandler implements IRequestHandler { } const did = `did:${this.didMethodName}:${operationModel.didUniqueSuffix}`; - const document = DocumentComposer.transformToExternalDocument(didState, did); + const published = false; + const document = DocumentComposer.transformToExternalDocument(didState, did, published); return { status: ResponseStatus.Succeeded, @@ -153,10 +154,15 @@ export default class RequestHandler implements IRequestHandler { const did = await Did.create(shortOrLongFormDid, this.didMethodName); let didState: DidState | undefined; + let published = false; if (did.isShortForm) { didState = await this.resolver.resolve(did.uniqueSuffix); + + if (didState !== undefined) { + published = true; + } } else { - didState = await this.resolveLongFormDid(did); + [didState, published] = await this.resolveLongFormDid(did); } if (didState === undefined) { @@ -166,7 +172,11 @@ export default class RequestHandler implements IRequestHandler { }; } - const document = DocumentComposer.transformToExternalDocument(didState, shortOrLongFormDid); + // We reach here it means there is a DID Document to return. + + // If DID is published, use the short-form DID; else use long-form DID in document. + const didStringToUseInDidDocument = published ? did.shortForm : did.longForm!; + const document = DocumentComposer.transformToExternalDocument(didState, didStringToUseInDidDocument, published); return { status: ResponseStatus.Succeeded, @@ -190,22 +200,24 @@ export default class RequestHandler implements IRequestHandler { /** * Resolves the given long-form DID by resolving using operations found over the network first; - * if no operations found, the given create operation will is used to construct the DID state. + * if no operations found, the given create operation will be used to construct the DID state. + * + * @returns [DID state, published] */ - private async resolveLongFormDid (did: Did): Promise { + private async resolveLongFormDid (did: Did): Promise<[DidState | undefined, boolean]> { // Attempt to resolve the DID by using operations found from the network first. let didState = await this.resolver.resolve(did.uniqueSuffix); // If DID state found then return it. if (didState !== undefined) { - return didState; + return [didState, true]; } // The code reaches here if this DID is not registered on the ledger. didState = await this.applyCreateOperation(did.createOperation!); - return didState; + return [didState, false]; } private async applyCreateOperation (createOperation: OperationModel): Promise { diff --git a/tests/core/DocumentComposer.spec.ts b/tests/core/DocumentComposer.spec.ts index 9cfcec9e0..cc6c5156a 100644 --- a/tests/core/DocumentComposer.spec.ts +++ b/tests/core/DocumentComposer.spec.ts @@ -12,7 +12,7 @@ describe('DocumentComposer', async () => { describe('transformToExternalDocument', () => { it('should output the expected resolution result given key(s) across all purpose types.', async () => { const [anySigningPublicKey] = await OperationGenerator.generateKeyPair('anySigningKey'); // All purposes will be included by default. - const [authPublicKey] = await OperationGenerator.generateKeyPair('authePbulicKey', [PublicKeyPurpose.Auth]); + const [authPublicKey] = await OperationGenerator.generateKeyPair('authPublicKey', [PublicKeyPurpose.Auth]); const document = { public_keys: [anySigningPublicKey, authPublicKey] }; @@ -23,10 +23,12 @@ describe('DocumentComposer', async () => { nextUpdateCommitmentHash: 'anyCommitmentHash' }; - const result = DocumentComposer.transformToExternalDocument(didState, 'did:method:suffix'); + const published = true; + const result = DocumentComposer.transformToExternalDocument(didState, 'did:method:suffix', published); expect(result['@context']).toEqual('https://www.w3.org/ns/did-resolution/v1'); expect(result.methodMetadata).toEqual({ + published: true, recoveryCommitment: 'anyCommitmentHash', updateCommitment: 'anyCommitmentHash' }); @@ -43,7 +45,7 @@ describe('DocumentComposer', async () => { authentication: [ '#anySigningKey', // reference because it is a general purpose key { - id: '#authePbulicKey', // object here because it is an auth purpose only key + id: '#authPublicKey', // object here because it is an auth purpose only key controller: '', type: 'EcdsaSecp256k1VerificationKey2019', publicKeyJwk: { @@ -54,6 +56,28 @@ describe('DocumentComposer', async () => { }); }); + it('should output method metadata with the given `published` value.', async () => { + const [anySigningPublicKey] = await OperationGenerator.generateKeyPair('anySigningKey'); // All purposes will be included by default. + const [authPublicKey] = await OperationGenerator.generateKeyPair('authPublicKey', [PublicKeyPurpose.Auth]); + const document = { + public_keys: [anySigningPublicKey, authPublicKey] + }; + const didState: DidState = { + document, + lastOperationTransactionNumber: 123, + nextRecoveryCommitmentHash: 'anyCommitmentHash', + nextUpdateCommitmentHash: 'anyCommitmentHash' + }; + + let published = false; + let result = DocumentComposer.transformToExternalDocument(didState, 'did:method:suffix', published); + expect(result.methodMetadata.published).toEqual(published); + + published = true; + result = DocumentComposer.transformToExternalDocument(didState, 'did:method:suffix', published); + expect(result.methodMetadata.published).toEqual(published); + }); + it('should return status deactivated if next recovery commit hash is undefined', async () => { const [anySigningPublicKey] = await OperationGenerator.generateKeyPair('anySigningKey'); const [authPublicKey] = await OperationGenerator.generateKeyPair('authPublicKey', [PublicKeyPurpose.Auth]); @@ -67,7 +91,8 @@ describe('DocumentComposer', async () => { nextUpdateCommitmentHash: 'anyCommitmentHash' }; - const result = DocumentComposer.transformToExternalDocument(didState, 'did:method:suffix'); + const published = true; + const result = DocumentComposer.transformToExternalDocument(didState, 'did:method:suffix', published); expect(result).toEqual({ status: 'deactivated' }); }); }); @@ -483,7 +508,7 @@ describe('DocumentComposer', async () => { describe('validateId()', async () => { it('should throw if ID given is not using characters from Base64URL character set.', async () => { - const invalidId = 'AnInavlidIdWith#'; + const invalidId = 'AnInvalidIdWith#'; const expectedError = new SidetreeError(ErrorCode.DocumentComposerIdNotUsingBase64UrlCharacterSet); expect(() => { (DocumentComposer as any).validateId(invalidId); }).toThrow(expectedError); diff --git a/tests/core/RequestHandler.spec.ts b/tests/core/RequestHandler.spec.ts index 8a08787f4..6cf7a7191 100644 --- a/tests/core/RequestHandler.spec.ts +++ b/tests/core/RequestHandler.spec.ts @@ -360,7 +360,7 @@ describe('RequestHandler', () => { }); describe('resolveLongFormDid()', async () => { - it('should return the resolved DID document if it is resolvable as a registered DID.', async () => { + it('should return the resolved DID document, and `published` value as `true` if it is resolvable as a registered DID.', async () => { const [anySigningPublicKey] = await OperationGenerator.generateKeyPair('anySigningKey'); const document = { publicKeys: [anySigningPublicKey] @@ -373,8 +373,9 @@ describe('RequestHandler', () => { }; spyOn((requestHandler as any).resolver, 'resolve').and.returnValue(Promise.resolve(mockedResolverReturnedDidState)); - const didState = await (requestHandler as any).resolveLongFormDid('unused'); + const [didState, published] = await (requestHandler as any).resolveLongFormDid('unused'); + expect(published).toEqual(true); expect(didState.document.publicKeys.length).toEqual(1); expect(didState.document.publicKeys[0].jwk).toEqual(anySigningPublicKey.jwk); }); diff --git a/tests/core/Resolver.spec.ts b/tests/core/Resolver.spec.ts index ebcbafd68..df1fed0b9 100644 --- a/tests/core/Resolver.spec.ts +++ b/tests/core/Resolver.spec.ts @@ -58,12 +58,13 @@ describe('Resolver', () => { }; await operationStore.put([anchoredOperationModel]); + const published = true; const didState = await resolver.resolve(didUniqueSuffix) as DidState; - const resultingDocument = DocumentComposer.transformToExternalDocument(didState, `did:sidetree:${didUniqueSuffix}`); + const resultingDocument = DocumentComposer.transformToExternalDocument(didState, `did:sidetree:${didUniqueSuffix}`, published); expect(resultingDocument).toEqual(createResultingDocument); }); - it('should resolve update operation', async () => { + it('should resolve DID that has an update operation', async () => { const operationBuffer = Buffer.from(JSON.stringify(updateFixtureCreate)); const createOperation = await CreateOperation.parse(operationBuffer); const didUniqueSuffix = createOperation.didUniqueSuffix; @@ -88,12 +89,13 @@ describe('Resolver', () => { }; await operationStore.put([anchoredUpdateOperation]); + const published = true; const didState = await resolver.resolve(didUniqueSuffix) as DidState; - const resultingDocument = DocumentComposer.transformToExternalDocument(didState, `did:sidetree:${didUniqueSuffix}`); + const resultingDocument = DocumentComposer.transformToExternalDocument(didState, `did:sidetree:${didUniqueSuffix}`, published); expect(resultingDocument).toEqual(updateResultingDocument); }); - it('should resolve recover operation', async () => { + it('should resolve DID that has a recover operation', async () => { const operationBuffer = Buffer.from(JSON.stringify(recoverFixtureCreate)); const createOperation = await CreateOperation.parse(operationBuffer); const didUniqueSuffix = createOperation.didUniqueSuffix; @@ -112,12 +114,13 @@ describe('Resolver', () => { const anchoredRecoverOperation = OperationGenerator.createAnchoredOperationModelFromOperationModel(recoverOperation, 2, 2, 2); await operationStore.put([anchoredRecoverOperation]); + const published = true; const didState = await resolver.resolve(didUniqueSuffix) as DidState; - const resultingDocument = DocumentComposer.transformToExternalDocument(didState, `did:sidetree:${didUniqueSuffix}`); + const resultingDocument = DocumentComposer.transformToExternalDocument(didState, `did:sidetree:${didUniqueSuffix}`, published); expect(resultingDocument).toEqual(recoverResultingDocument); }); - it('should resolve deactivate operation', async () => { + it('should resolve DID that has a deactivate operation', async () => { const operationBuffer = Buffer.from(JSON.stringify(deactivateFixtureCreate)); const createOperation = await CreateOperation.parse(operationBuffer); const didUniqueSuffix = createOperation.didUniqueSuffix; @@ -137,7 +140,8 @@ describe('Resolver', () => { await operationStore.put([anchoredRecoverOperation]); const didState = await resolver.resolve(didUniqueSuffix) as DidState; - const resultingDocument = DocumentComposer.transformToExternalDocument(didState, `did:sidetree:${didUniqueSuffix}`); + const published = true; + const resultingDocument = DocumentComposer.transformToExternalDocument(didState, `did:sidetree:${didUniqueSuffix}`, published); expect(resultingDocument).toEqual(deactivateResultingDocument); }); }); diff --git a/tests/fixtures/create/resultingDocument.json b/tests/fixtures/create/resultingDocument.json index a3c1207fa..a453028b2 100644 --- a/tests/fixtures/create/resultingDocument.json +++ b/tests/fixtures/create/resultingDocument.json @@ -33,6 +33,7 @@ ] }, "methodMetadata": { + "published": true, "recoveryCommitment": "EiC8G4IdbD7D4Co57GjLNKhmDEabrzkO1wsKE9MQeUvOgw", "updateCommitment": "EiCIPcXBzjjQaJUIcR52euI0rIXzhNZ_MljsKK9zxXTyqQ" } diff --git a/tests/fixtures/recover/resultingDocument.json b/tests/fixtures/recover/resultingDocument.json index 06e626979..2d5755a69 100644 --- a/tests/fixtures/recover/resultingDocument.json +++ b/tests/fixtures/recover/resultingDocument.json @@ -33,6 +33,7 @@ ] }, "methodMetadata": { + "published": true, "recoveryCommitment": "EiDmNGrlFH0fe8uGzlyCyF1uIF1eZtSUb5LdhKzR_8T-5Q", "updateCommitment": "EiB-KmuNII4gZeHWHuUsjH8W_jhWd_Ig4QU80Lgb-LiLPQ" } diff --git a/tests/fixtures/update/resultingDocument.json b/tests/fixtures/update/resultingDocument.json index 9a269e96f..1d5e437a9 100644 --- a/tests/fixtures/update/resultingDocument.json +++ b/tests/fixtures/update/resultingDocument.json @@ -45,6 +45,7 @@ ] }, "methodMetadata": { + "published": true, "recoveryCommitment": "EiC_iix1uVmVminsKXR60qN7BndYGVzqQPjylR22GwGmzg", "updateCommitment": "EiBc3mku8EsI3ToZr_46Vj3JF2Gmpb4Mawz7YmSJtpIEPA" }