Skip to content

Commit

Permalink
feat(ref-imp): #766 - introduced create references to align with rest…
Browse files Browse the repository at this point in the history
… of operation references
  • Loading branch information
thehenrytsai committed Nov 25, 2020
1 parent a84366a commit b733694
Show file tree
Hide file tree
Showing 12 changed files with 216 additions and 234 deletions.
36 changes: 27 additions & 9 deletions lib/core/versions/latest/AnchorFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ArrayMethods from './util/ArrayMethods';
import Compressor from './util/Compressor';
import CreateOperation from './CreateOperation';
import DeactivateOperation from './DeactivateOperation';
import Did from './Did';
import ErrorCode from './ErrorCode';
import InputValidator from './InputValidator';
import JsonAsync from './util/JsonAsync';
Expand All @@ -11,6 +12,14 @@ import OperationReferenceModel from './models/OperationReferenceModel';
import ProtocolParameters from './ProtocolParameters';
import RecoverOperation from './RecoverOperation';
import SidetreeError from '../../../common/SidetreeError';
import SuffixDataModel from './models/SuffixDataModel';

/**
* Create reference model internally used in a core index file.
*/
interface CreateReferenceModel {
suffixData: SuffixDataModel
}

/**
* Class containing Anchor File related operations.
Expand All @@ -25,7 +34,7 @@ export default class AnchorFile {
private constructor (
public readonly model: AnchorFileModel,
public readonly didUniqueSuffixes: string[],
public readonly createOperations: CreateOperation[],
public readonly createDidSuffixes: string[],
public readonly recoverDidSuffixes: string[],
public readonly deactivateDidSuffixes: string[]) { }

Expand Down Expand Up @@ -95,18 +104,16 @@ export default class AnchorFile {
const didUniqueSuffixes: string[] = [];

// Validate `create` if exists.
const createOperations: CreateOperation[] = [];
let createDidSuffixes: string[] = [];
if (operations.create !== undefined) {
if (!Array.isArray(operations.create)) {
throw new SidetreeError(ErrorCode.AnchorFileCreatePropertyNotArray);
}

// Validate every create operation.
for (const operation of operations.create) {
const createOperation = await CreateOperation.parseOperationFromAnchorFile(operation);
createOperations.push(createOperation);
didUniqueSuffixes.push(createOperation.didUniqueSuffix);
}
// Validate every create reference.
AnchorFile.validateCreateReferences(operations.create);
createDidSuffixes = (operations.create as CreateReferenceModel[]).map(operation => Did.computeUniqueSuffix(operation.suffixData));
didUniqueSuffixes.push(...createDidSuffixes);
}

// Validate `recover` if exists.
Expand Down Expand Up @@ -151,7 +158,7 @@ export default class AnchorFile {
}
}

const anchorFile = new AnchorFile(anchorFileModel, didUniqueSuffixes, createOperations, recoverDidSuffixes, deactivateDidSuffixes);
const anchorFile = new AnchorFile(anchorFileModel, didUniqueSuffixes, createDidSuffixes, recoverDidSuffixes, deactivateDidSuffixes);
return anchorFile;
}

Expand Down Expand Up @@ -258,4 +265,15 @@ export default class AnchorFile {
);
}
}

/**
* Validates the given create operation references.
*/
private static validateCreateReferences (operationReferences: any[]) {
for (const operationReference of operationReferences) {
// Only `suffixData` is allowed.
InputValidator.validateObjectContainsOnlyAllowedProperties(operationReference, ['suffixData'], `create operation reference`);
InputValidator.validateSuffixData(operationReference.suffixData);
}
}
}
113 changes: 22 additions & 91 deletions lib/core/versions/latest/CreateOperation.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import DeltaModel from './models/DeltaModel';
import Did from './Did';
import Encoder from './Encoder';
import ErrorCode from './ErrorCode';
import InputValidator from './InputValidator';
import JsonAsync from './util/JsonAsync';
import JsonCanonicalizer from './util/JsonCanonicalizer';
import Multihash from './Multihash';
import Operation from './Operation';
import OperationModel from './models/OperationModel';
import OperationType from '../../enums/OperationType';
import SidetreeError from '../../../common/SidetreeError';

interface SuffixDataModel {
deltaHash: string;
recoveryCommitment: string;
type?: string;
}
import SuffixDataModel from './models/SuffixDataModel';

/**
* A class that represents a create operation.
Expand Down Expand Up @@ -70,27 +67,6 @@ export default class CreateOperation implements OperationModel {
return encodedMultihash;
}

/**
* Computes the DID unique suffix given the encoded suffix data object.
* @param suffixData the suffix data object to calculate unique suffix from
*/
private static computeJcsDidUniqueSuffix (suffixData: object): string {
const suffixDataBuffer = JsonCanonicalizer.canonicalizeAsBuffer(suffixData);
const multihash = Multihash.hash(suffixDataBuffer);
const encodedMultihash = Encoder.encode(multihash);
return encodedMultihash;
}

/**
* Parses the given input as a create operation entry in the anchor file.
*/
public static async parseOperationFromAnchorFile (input: any): Promise<CreateOperation> {
// Issue #442 - Replace `operationBuffer` in `OperationModel` and `AnchoredOperationModel` with actual operation request
const operationBuffer = Buffer.from(JSON.stringify(input));
const operation = await CreateOperation.parseJcsObject(input, operationBuffer, true);
return operation;
}

/**
* Parses the given buffer as a `CreateOperation`.
*/
Expand All @@ -102,7 +78,7 @@ export default class CreateOperation implements OperationModel {
// TODO: SIP 2 #781 deprecates this. Should be deleted when fully switched over
createOperation = await CreateOperation.parseObject(operationObject, operationBuffer, false);
} else {
createOperation = CreateOperation.parseJcsObject(operationObject, operationBuffer, false);
createOperation = CreateOperation.parseJcsObject(operationObject, operationBuffer);
}
return createOperation;
}
Expand All @@ -114,50 +90,38 @@ export default class CreateOperation implements OperationModel {
* JSON parsing is not required to be performed more than once when an operation buffer of an unknown operation type is given.
* @param operationObject The operationObject is a json object with no encoding
* @param operationBuffer The buffer format of the operationObject
* @param anchorFileMode If set to true, then `delta` and `type` properties are expected to be absent.
*/
public static parseJcsObject (operationObject: any, operationBuffer: Buffer, anchorFileMode: boolean): CreateOperation {
let expectedPropertyCount = 3;
if (anchorFileMode) {
expectedPropertyCount = 1;
}
public static parseJcsObject (operationObject: any, operationBuffer: Buffer): CreateOperation {
const expectedPropertyCount = 3;

const properties = Object.keys(operationObject);
if (properties.length !== expectedPropertyCount) {
throw new SidetreeError(ErrorCode.CreateOperationMissingOrUnknownProperty);
}

CreateOperation.validateSuffixData(operationObject.suffixData);
const suffixData: SuffixDataModel = operationObject.suffixData;

if (operationObject.suffixData.type !== undefined) {
suffixData.type = operationObject.suffixData.type;
if (operationObject.type !== OperationType.Create) {
throw new SidetreeError(ErrorCode.CreateOperationTypeIncorrect);
}

// For compatibility with data pruning, we have to assume that `delta` may be unavailable,
// thus an operation with invalid `delta` needs to be processed as an operation with unavailable `delta`,
// so here we let `delta` be `undefined`.
const suffixData = operationObject.suffixData;
InputValidator.validateSuffixData(suffixData);

let delta;
let encodedDelta;
if (!anchorFileMode) {
if (operationObject.type !== OperationType.Create) {
throw new SidetreeError(ErrorCode.CreateOperationTypeIncorrect);
}

try {
Operation.validateDelta(operationObject.delta);
delta = operationObject.delta;
} catch {
// For compatibility with data pruning, we have to assume that `delta` may be unavailable,
// thus an operation with invalid `delta` needs to be processed as an operation with unavailable `delta`,
// so here we let `delta` be `undefined`.
}
try {
Operation.validateDelta(operationObject.delta);
delta = operationObject.delta;
// TODO: SIP 2 #781 remove encoded delta and encoded suffix data when old long form is fully deprecated.
encodedDelta = Encoder.encode(JsonCanonicalizer.canonicalizeAsBuffer(operationObject.delta));
} catch {
// For compatibility with data pruning, we have to assume that `delta` may be unavailable,
// thus an operation with invalid `delta` needs to be processed as an operation with unavailable `delta`,
// so here we let `delta` be `undefined`.
}

const didUniqueSuffix = CreateOperation.computeJcsDidUniqueSuffix(operationObject.suffixData);
const didUniqueSuffix = Did.computeUniqueSuffix(suffixData);

const encodedSuffixData = Encoder.encode(JsonCanonicalizer.canonicalizeAsBuffer(operationObject.suffixData));
const encodedSuffixData = Encoder.encode(JsonCanonicalizer.canonicalizeAsBuffer(suffixData));
return new CreateOperation(operationBuffer, didUniqueSuffix, encodedSuffixData, suffixData, encodedDelta, delta);
}

Expand Down Expand Up @@ -205,47 +169,14 @@ export default class CreateOperation implements OperationModel {
return new CreateOperation(operationBuffer, didUniqueSuffix, encodedSuffixData, suffixData, encodedDelta, delta);
}

private static validateSuffixData (suffixData: any): void {
if (typeof suffixData !== 'object') {
throw new SidetreeError(ErrorCode.CreateOperationSuffixDataIsNotObject);
}

const properties = Object.keys(suffixData);
// will have 3 if has type
if (properties.length !== 3 && properties.length !== 2) {
throw new SidetreeError(ErrorCode.CreateOperationSuffixDataMissingOrUnknownProperty);
}

const deltaHash = Encoder.decodeAsBuffer(suffixData.deltaHash);
const nextRecoveryCommitment = Encoder.decodeAsBuffer(suffixData.recoveryCommitment);

Multihash.verifyHashComputedUsingLatestSupportedAlgorithm(deltaHash);
Multihash.verifyHashComputedUsingLatestSupportedAlgorithm(nextRecoveryCommitment);

// type has to be max 4 character long string with only base64url character set
if (properties.length === 3) {
if (typeof suffixData.type !== 'string') {
throw new SidetreeError(ErrorCode.CreateOperationSuffixDataTypeIsNotString);
}

if (suffixData.type.length > 4) {
throw new SidetreeError(ErrorCode.CreateOperationSuffixDataTypeLengthGreaterThanFour);
}

if (!Encoder.isBase64UrlString(suffixData.type)) {
throw new SidetreeError(ErrorCode.CreateOperationSuffixDataTypeInvalidCharacter);
}
}
}

private static async parseSuffixData (suffixDataEncodedString: any): Promise<SuffixDataModel> {
if (typeof suffixDataEncodedString !== 'string') {
throw new SidetreeError(ErrorCode.CreateOperationSuffixDataMissingOrNotString);
}

const suffixDataJsonString = Encoder.decodeAsString(suffixDataEncodedString);
const suffixData = await JsonAsync.parse(suffixDataJsonString);
CreateOperation.validateSuffixData(suffixData);
InputValidator.validateSuffixData(suffixData);

return {
deltaHash: suffixData.deltaHash,
Expand Down
12 changes: 11 additions & 1 deletion lib/core/versions/latest/Did.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ export default class Did {
return did;
}

/**
* Computes the DID unique suffix given the suffix data object.
*/
public static computeUniqueSuffix (suffixData: object): string {
const suffixDataBuffer = JsonCanonicalizer.canonicalizeAsBuffer(suffixData);
const multihash = Multihash.hash(suffixDataBuffer);
const encodedMultihash = Encoder.encode(multihash);
return encodedMultihash;
}

private static getInitialStateFromDidStringWithQueryParameter (didString: string, methodNameWithNetworkId: string): string {
let didStringUrl;
try {
Expand Down Expand Up @@ -186,7 +196,7 @@ export default class Did {
delta: initialStateObject.delta
};
const createOperationBuffer = Buffer.from(JSON.stringify(createOperationRequest));
const createOperation = CreateOperation.parseJcsObject(createOperationRequest, createOperationBuffer, false);
const createOperation = CreateOperation.parseJcsObject(createOperationRequest, createOperationBuffer);
return createOperation;
}

Expand Down
8 changes: 3 additions & 5 deletions lib/core/versions/latest/ErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,7 @@ export default {
CoreProofFileProofCountNotTheSameAsOperationCountInAnchorFile: 'core_proof_file_proof_count_not_the_same_as_operation_count_in_anchor_file',
CoreProofFileRecoverPropertyNotAnArray: 'core_proof_file_recover_property_not_an_array',
CreateOperationMissingOrUnknownProperty: 'create_operation_missing_or_unknown_property',
CreateOperationSuffixDataIsNotObject: 'create_operation_suffix_data_is_not_object',
CreateOperationSuffixDataMissingOrNotString: 'create_operation_suffix_data_missing_or_not_string',
CreateOperationSuffixDataMissingOrUnknownProperty: 'create_operation_suffix_data_missing_or_unknown_property',
CreateOperationSuffixDataTypeInvalidCharacter: 'create_operation_suffix_data_type_invalid_character',
CreateOperationSuffixDataTypeIsNotString: 'create_operation_suffix_data_type_is_not_string',
CreateOperationSuffixDataTypeLengthGreaterThanFour: 'create_operation_suffix_data_type_length_greater_than_four',
CreateOperationTypeIncorrect: 'create_operation_type_incorrect',
DeactivateOperationMissingOrInvalidDidUniqueSuffix: 'deactivate_operation_missing_or_invalid_did_unique_suffix',
DeactivateOperationMissingOrUnknownProperty: 'deactivate_operation_missing_or_unknown_property',
Expand Down Expand Up @@ -154,6 +149,9 @@ export default {
RecoverOperationSignedDataMissingOrUnknownProperty: 'recover_operation_signed_data_missing_or_unknown_property',
RecoverOperationTypeIncorrect: 'recover_operation_type_incorrect',
RequestHandlerUnknownOperationType: 'request_handler_unknown_operation_type',
SuffixDataTypeInvalidCharacter: 'suffix_data_type_invalid_character',
SuffixDataTypeIsNotString: 'suffix_data_type_is_not_string',
SuffixDataTypeLengthGreaterThanFour: 'suffix_data_type_length_greater_than_four',
TransactionFeePaidInvalid: 'transaction_fee_paid_invalid',
TransactionFeePaidLessThanNormalizedFee: 'transaction_fee_paid_less_than_normalized_fee',
TransactionProcessorPaidOperationCountExceedsLimit: 'transaction_processor_paid_operation_count_exceeds_limit',
Expand Down
41 changes: 39 additions & 2 deletions lib/core/versions/latest/InputValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ export default class InputValidator {
}

/**
* Validates the given recover/deactivate/update operation reference.
* Validates the given recover/deactivate/update operation references.
*/
public static validateOperationReferences (operationReferences: any, inputContextForErrorLogging: string) {
public static validateOperationReferences (operationReferences: any[], inputContextForErrorLogging: string) {
for (const operationReference of operationReferences) {
InputValidator.validateObjectContainsOnlyAllowedProperties(operationReference, ['didSuffix', 'revealValue'], `${inputContextForErrorLogging} operation reference`);

Expand All @@ -85,4 +85,41 @@ export default class InputValidator {
}
}
}

/**
* Validates the given suffix data.
*/
public static validateSuffixData (suffixData: any) {
InputValidator.validateNonArrayObject(suffixData, 'suffix data');
InputValidator.validateObjectContainsOnlyAllowedProperties(suffixData, ['deltaHash', 'recoveryCommitment', 'type'], `suffix data`);

const deltaHash = Encoder.decodeAsBuffer(suffixData.deltaHash);
const nextRecoveryCommitment = Encoder.decodeAsBuffer(suffixData.recoveryCommitment);
Multihash.verifyHashComputedUsingLatestSupportedAlgorithm(deltaHash);
Multihash.verifyHashComputedUsingLatestSupportedAlgorithm(nextRecoveryCommitment);

InputValidator.validateDidType(suffixData.type);
}

private static validateDidType (type: string | undefined): void {
// Nothing to verify if type is undefined, since it is an optional property.
if (type === undefined) {
return;
}

// `type` has to be max 4 character long string with only Base64URL character set.

const typeOfType = typeof type;
if (typeOfType !== 'string') {
throw new SidetreeError(ErrorCode.SuffixDataTypeIsNotString, `DID type must be a string, but is of type ${typeOfType}.`);
}

if (type.length > 4) {
throw new SidetreeError(ErrorCode.SuffixDataTypeLengthGreaterThanFour, `DID type string '${type}' cannot be longer than 4 characters.`);
}

if (!Encoder.isBase64UrlString(type)) {
throw new SidetreeError(ErrorCode.SuffixDataTypeInvalidCharacter, `DID type string '${type}' contains a non-Base64URL character.`);
}
}
}
3 changes: 1 addition & 2 deletions lib/core/versions/latest/Operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@ export default class Operation {
const operationJsonString = operationBuffer.toString();
const operationObject = JSON.parse(operationJsonString);
const operationType = operationObject.type;
const isAnchorFileMode = false;

if (operationType === OperationType.Create) {
return CreateOperation.parseJcsObject(operationObject, operationBuffer, isAnchorFileMode);
return CreateOperation.parseJcsObject(operationObject, operationBuffer);
} else if (operationType === OperationType.Update) {
return UpdateOperation.parseObject(operationObject, operationBuffer);
} else if (operationType === OperationType.Recover) {
Expand Down
Loading

0 comments on commit b733694

Please sign in to comment.