Skip to content

Commit

Permalink
feat(ref-imp): #766 - Updated anchor (core index) file schema
Browse files Browse the repository at this point in the history
  • Loading branch information
thehenrytsai committed Nov 24, 2020
1 parent e5bbfdd commit 8a62900
Show file tree
Hide file tree
Showing 16 changed files with 362 additions and 223 deletions.
99 changes: 57 additions & 42 deletions lib/core/versions/latest/AnchorFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import DeactivateOperation from './DeactivateOperation';
import ErrorCode from './ErrorCode';
import InputValidator from './InputValidator';
import JsonAsync from './util/JsonAsync';
import Multihash from './Multihash';
import OperationReferenceModel from './models/OperationReferenceModel';
import ProtocolParameters from './ProtocolParameters';
import RecoverOperation from './RecoverOperation';
import SidetreeError from '../../../common/SidetreeError';
Expand All @@ -24,8 +26,8 @@ export default class AnchorFile {
public readonly model: AnchorFileModel,
public readonly didUniqueSuffixes: string[],
public readonly createOperations: CreateOperation[],
public readonly recoverOperations: RecoverOperation[],
public readonly deactivateOperations: DeactivateOperation[]) { }
public readonly recoverDidSuffixes: string[],
public readonly deactivateDidSuffixes: string[]) { }

/**
* Parses and validates the given anchor file buffer.
Expand Down Expand Up @@ -55,6 +57,9 @@ export default class AnchorFile {
}
}

// TODO: #631 - If `operations` does not exist, then `mapFileUri` MUST exist. ie. There must be at least one operation in a batch.
// TODO: #631 - If `mapFileUri` does not exist, then `operations` MUST have just deactivates. ie. non-deactivates have delta in chunk file.

if (!('mapFileUri' in anchorFileModel)) {
throw new SidetreeError(ErrorCode.AnchorFileMapFileUriMissing);
}
Expand Down Expand Up @@ -105,41 +110,37 @@ export default class AnchorFile {
}

// Validate `recover` if exists.
const recoverOperations: RecoverOperation[] = [];
let recoverDidSuffixes: string[] = [];
if (operations.recover !== undefined) {
if (!Array.isArray(operations.recover)) {
throw new SidetreeError(ErrorCode.AnchorFileRecoverPropertyNotArray);
}

// Validate every recover operation.
for (const operation of operations.recover) {
const recoverOperation = await RecoverOperation.parseOperationFromAnchorFile(operation);
recoverOperations.push(recoverOperation);
didUniqueSuffixes.push(recoverOperation.didUniqueSuffix);
}
// Validate every recover reference.
InputValidator.validateOperationReferences(operations.recover, 'recover');
recoverDidSuffixes = (operations.recover as OperationReferenceModel[]).map(operation => operation.didSuffix);
didUniqueSuffixes.push(...recoverDidSuffixes);
}

// Validate `deactivate` if exists.
const deactivateOperations: DeactivateOperation[] = [];
let deactivateDidSuffixes: string[] = [];
if (operations.deactivate !== undefined) {
if (!Array.isArray(operations.deactivate)) {
throw new SidetreeError(ErrorCode.AnchorFileDeactivatePropertyNotArray);
}

// Validate every operation.
for (const operation of operations.deactivate) {
const deactivateOperation = await DeactivateOperation.parseOperationFromAnchorFile(operation);
deactivateOperations.push(deactivateOperation);
didUniqueSuffixes.push(deactivateOperation.didUniqueSuffix);
}
// Validate every deactivate reference.
InputValidator.validateOperationReferences(operations.deactivate, 'deactivate');
deactivateDidSuffixes = (operations.deactivate as OperationReferenceModel[]).map(operation => operation.didSuffix);
didUniqueSuffixes.push(...deactivateDidSuffixes);
}

if (ArrayMethods.hasDuplicates(didUniqueSuffixes)) {
throw new SidetreeError(ErrorCode.AnchorFileMultipleOperationsForTheSameDid);
}

// Validate core proof file URI.
if (recoverOperations.length > 0 || deactivateOperations.length > 0) {
if (recoverDidSuffixes.length > 0 || deactivateDidSuffixes.length > 0) {
InputValidator.validateCasFileUri(anchorFileModel.coreProofFileUri, 'core proof file URI');
} else {
if (anchorFileModel.coreProofFileUri !== undefined) {
Expand All @@ -150,7 +151,7 @@ export default class AnchorFile {
}
}

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

Expand All @@ -159,7 +160,7 @@ export default class AnchorFile {
*/
public static async createModel (
writerLockId: string | undefined,
mapFileHash: string,
mapFileUri: string | undefined,
coreProofFileHash: string | undefined,
createOperationArray: CreateOperation[],
recoverOperationArray: RecoverOperation[],
Expand All @@ -170,7 +171,19 @@ export default class AnchorFile {
AnchorFile.validateWriterLockId(writerLockId);
}

const createOperations = createOperationArray.map(operation => {
const anchorFileModel: AnchorFileModel = {
writerLockId,
mapFileUri
};

// Only insert `operations` property if there is at least one operation reference.
if (createOperationArray.length > 0 ||
recoverOperationArray.length > 0 ||
deactivateOperationArray.length > 0) {
anchorFileModel.operations = { };
}

const createReferences = createOperationArray.map(operation => {
return {
suffixData: {
deltaHash: operation.suffixData.deltaHash,
Expand All @@ -180,30 +193,32 @@ export default class AnchorFile {
};
});

const recoverOperations = recoverOperationArray.map(operation => {
return {
didSuffix: operation.didUniqueSuffix,
signedData: operation.signedDataJws.toCompactJws()
};
// Only insert `create` property if there are create operation references.
if (createReferences.length > 0) {
anchorFileModel.operations!.create = createReferences;
}

const recoverReferences = recoverOperationArray.map(operation => {
const revealValue = Multihash.canonicalizeThenHashThenEncode(operation.signedData.recoveryKey);

return { didSuffix: operation.didUniqueSuffix, revealValue };
});

const deactivateOperations = deactivateOperationArray.map(operation => {
return {
didSuffix: operation.didUniqueSuffix,
signedData: operation.signedDataJws.toCompactJws()
};
// Only insert `recover` property if there are recover operation references.
if (recoverReferences.length > 0) {
anchorFileModel.operations!.recover = recoverReferences;
}

const deactivateReferences = deactivateOperationArray.map(operation => {
const revealValue = Multihash.canonicalizeThenHashThenEncode(operation.signedData.recoveryKey);

return { didSuffix: operation.didUniqueSuffix, revealValue };
});

const anchorFileModel = {
writerLockId,
mapFileUri: mapFileHash,
coreProofFileUri: coreProofFileHash,
operations: {
create: createOperations,
recover: recoverOperations,
deactivate: deactivateOperations
}
};
// Only insert `deactivate` property if there are deactivate operation references.
if (deactivateReferences.length > 0) {
anchorFileModel.operations!.deactivate = deactivateReferences;
}

// Only insert `coreProofFileUri` property if a value is given.
if (coreProofFileHash !== undefined) {
Expand All @@ -218,14 +233,14 @@ export default class AnchorFile {
*/
public static async createBuffer (
writerLockId: string | undefined,
mapFileHash: string,
mapFileUri: string | undefined,
coreProofFileHash: string | undefined,
createOperations: CreateOperation[],
recoverOperations: RecoverOperation[],
deactivateOperations: DeactivateOperation[]
): Promise<Buffer> {
const anchorFileModel = await AnchorFile.createModel(
writerLockId, mapFileHash, coreProofFileHash, createOperations, recoverOperations, deactivateOperations
writerLockId, mapFileUri, coreProofFileHash, createOperations, recoverOperations, deactivateOperations
);
const anchorFileJson = JSON.stringify(anchorFileModel);
const anchorFileBuffer = Buffer.from(anchorFileJson);
Expand Down
26 changes: 5 additions & 21 deletions lib/core/versions/latest/DeactivateOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,13 @@ export default class DeactivateOperation implements OperationModel {
this.signedData = signedData;
}

/**
* Parses the given input as a deactivate operation entry in the anchor file.
*/
public static async parseOperationFromAnchorFile (input: any): Promise<DeactivateOperation> {
const operationBuffer = Buffer.from(JSON.stringify(input));
const operation = await DeactivateOperation.parseObject(input, operationBuffer, true);
return operation;
}

/**
* Parses the given buffer as a `UpdateOperation`.
*/
public static async parse (operationBuffer: Buffer): Promise<DeactivateOperation> {
const operationJsonString = operationBuffer.toString();
const operationObject = await JsonAsync.parse(operationJsonString);
const deactivateOperation = await DeactivateOperation.parseObject(operationObject, operationBuffer, false);
const deactivateOperation = await DeactivateOperation.parseObject(operationObject, operationBuffer);
return deactivateOperation;
}

Expand All @@ -68,13 +59,9 @@ export default class DeactivateOperation implements OperationModel {
* The `operationBuffer` given is assumed to be valid and is assigned to the `operationBuffer` directly.
* NOTE: This method is purely intended to be used as an optimization method over the `parse` method in that
* JSON parsing is not required to be performed more than once when an operation buffer of an unknown operation type is given.
* @param anchorFileMode If set to true, then `type` is expected to be absent.
*/
public static async parseObject (operationObject: any, operationBuffer: Buffer, anchorFileMode: boolean): Promise<DeactivateOperation> {
let expectedPropertyCount = 3;
if (anchorFileMode) {
expectedPropertyCount = 2;
}
public static async parseObject (operationObject: any, operationBuffer: Buffer): Promise<DeactivateOperation> {
const expectedPropertyCount = 3;

const properties = Object.keys(operationObject);
if (properties.length !== expectedPropertyCount) {
Expand All @@ -89,11 +76,8 @@ export default class DeactivateOperation implements OperationModel {
const signedData = await DeactivateOperation.parseSignedDataPayload(
signedDataJws.payload, operationObject.didSuffix);

// If not in anchor file mode, we need to validate `type` property.
if (!anchorFileMode) {
if (operationObject.type !== OperationType.Deactivate) {
throw new SidetreeError(ErrorCode.DeactivateOperationTypeIncorrect);
}
if (operationObject.type !== OperationType.Deactivate) {
throw new SidetreeError(ErrorCode.DeactivateOperationTypeIncorrect);
}

return new DeactivateOperation(
Expand Down
4 changes: 2 additions & 2 deletions lib/core/versions/latest/ErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ export default {
OperationPayloadMissingOrIncorrectType: 'operation_payload_missing_or_incorrect_type',
OperationProcessorCreateOperationDoesNotHaveRevealValue: 'operation_processor_create_operation_does_not_have_reveal_value',
OperationProcessorUnknownOperationType: 'operation_processor_unknown_operation_type',
OperationReferenceDidSuffixIsNotAString: 'operation_reference_did_suffix_is_not_a_string',
OperationReferenceRevealValueIsNotAString: 'operation_reference_reveal_value_is_not_a_string',
OperationTypeUnknownOrMissing: 'operation_type_unknown_or_missing',
ProvisionalProofFileDecompressionFailure: 'provisional_proof_file_decompression_failure',
ProvisionalProofFileHasNoProofs: 'provisional_proof_file_has_no_proofs',
Expand All @@ -160,8 +162,6 @@ export default {
UpdateOperationMissingOrUnknownProperty: 'update_operation_missing_or_unknown_property',
UpdateOperationSignedDataHasMissingOrUnknownProperty: 'update_operation_signed_data_has_missing_or_unknown_property',
UpdateOperationTypeIncorrect: 'update_operation_type_incorrect',
UpdateReferenceDidSuffixIsNotAString: 'update_reference_did_suffix_is_not_a_string',
UpdateReferenceRevealValueIsNotAString: 'update_reference_reveal_value_is_not_a_string',
ValueTimeLockVerifierInvalidNumberOfOperations: 'value_time_lock_verifier_invalid_number_of_operations',
ValueTimeLockVerifierTransactionTimeOutsideLockRange: 'value_time_lock_verifier_transaction_time_outside_lock_range',
ValueTimeLockVerifierTransactionWriterLockOwnerMismatch: 'value_time_lock_verifier_transaction_writer_lock_owner_mismatch'
Expand Down
25 changes: 25 additions & 0 deletions lib/core/versions/latest/InputValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,29 @@ export default class InputValidator {
);
}
}

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

const didSuffixType = typeof operationReference.didSuffix;
if (didSuffixType !== 'string') {
throw new SidetreeError(
ErrorCode.OperationReferenceDidSuffixIsNotAString,
`Property 'didSuffix' in ${inputContextForErrorLogging} operation reference is of type ${didSuffixType}, but needs to be a string.`
);
}

const revealValueType = typeof operationReference.revealValue;
if (revealValueType !== 'string') {
throw new SidetreeError(
ErrorCode.OperationReferenceRevealValueIsNotAString,
`Property 'revealValue' in ${inputContextForErrorLogging} operation reference is of type ${revealValueType}, but needs to be a string.`
);
}
}
}
}
35 changes: 5 additions & 30 deletions lib/core/versions/latest/MapFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export default class MapFile {
}

// Validate all update operation references.
MapFile.validateUpdateOperationReferences(operations.update);
InputValidator.validateOperationReferences(operations.update, 'update');

// Make sure no operation with same DID.
const didSuffixes = (operations.update as OperationReferenceModel[]).map(operation => operation.didSuffix);
Expand All @@ -108,28 +108,6 @@ export default class MapFile {
return didSuffixes;
}

private static validateUpdateOperationReferences (updateReferences: any) {
for (const updateReference of updateReferences) {
InputValidator.validateObjectContainsOnlyAllowedProperties(updateReference, ['didSuffix', 'revealValue'], 'update operation reference');

const didSuffixType = typeof updateReference.didSuffix;
if (didSuffixType !== 'string') {
throw new SidetreeError(
ErrorCode.UpdateReferenceDidSuffixIsNotAString,
`Update reference property 'didSuffix' is of type ${didSuffixType}, but needs to be a string.`
);
}

const revealValueType = typeof updateReference.revealValue;
if (revealValueType !== 'string') {
throw new SidetreeError(
ErrorCode.UpdateReferenceRevealValueIsNotAString,
`Update reference property 'revealValue' is of type ${revealValueType}, but needs to be a string.`
);
}
}
}

/**
* Validates the given `chunks` property, throws error if the property fails validation.
*/
Expand Down Expand Up @@ -158,23 +136,20 @@ export default class MapFile {
public static async createBuffer (
chunkFileHash: string, provisionalProofFileHash: string | undefined, updateOperationArray: UpdateOperation[]
): Promise<Buffer> {
const updateOperations = updateOperationArray.map(operation => {
const updateReferences = updateOperationArray.map(operation => {
const revealValue = Multihash.canonicalizeThenHashThenEncode(operation.signedData.updateKey);

return {
didSuffix: operation.didUniqueSuffix,
revealValue
};
return { didSuffix: operation.didUniqueSuffix, revealValue };
});

const mapFileModel: MapFileModel = {
chunks: [{ chunkFileUri: chunkFileHash }]
};

// Only insert `operations` and `provisionalProofFileHash` properties if there are update operations.
if (updateOperations.length > 0) {
if (updateReferences.length > 0) {
mapFileModel.operations = {
update: updateOperations
update: updateReferences
};

mapFileModel.provisionalProofFileUri = provisionalProofFileHash;
Expand Down
4 changes: 2 additions & 2 deletions lib/core/versions/latest/Operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ export default class Operation {
} else if (operationType === OperationType.Update) {
return UpdateOperation.parseObject(operationObject, operationBuffer);
} else if (operationType === OperationType.Recover) {
return RecoverOperation.parseObject(operationObject, operationBuffer, isAnchorFileMode);
return RecoverOperation.parseObject(operationObject, operationBuffer);
} else if (operationType === OperationType.Deactivate) {
return DeactivateOperation.parseObject(operationObject, operationBuffer, isAnchorFileMode);
return DeactivateOperation.parseObject(operationObject, operationBuffer);
} else {
throw new SidetreeError(ErrorCode.OperationTypeUnknownOrMissing);
}
Expand Down
Loading

0 comments on commit 8a62900

Please sign in to comment.