Skip to content

Commit

Permalink
feat(ref-imp): #766 - SIP 1 - Moved provisionalProofFileUri into map …
Browse files Browse the repository at this point in the history
…file
  • Loading branch information
thehenrytsai authored Nov 13, 2020
1 parent e64bcd3 commit de15a6d
Show file tree
Hide file tree
Showing 15 changed files with 308 additions and 130 deletions.
35 changes: 10 additions & 25 deletions lib/core/versions/latest/AnchorFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import ArrayMethods from './util/ArrayMethods';
import Compressor from './util/Compressor';
import CreateOperation from './CreateOperation';
import DeactivateOperation from './DeactivateOperation';
import Encoder from './Encoder';
import ErrorCode from './ErrorCode';
import InputValidator from './InputValidator';
import JsonAsync from './util/JsonAsync';
import Multihash from './Multihash';
import ProtocolParameters from './ProtocolParameters';
import RecoverOperation from './RecoverOperation';
import SidetreeError from '../../../common/SidetreeError';
Expand Down Expand Up @@ -49,7 +48,7 @@ export default class AnchorFile {
throw SidetreeError.createFromError(ErrorCode.AnchorFileNotJson, e);
}

const allowedProperties = new Set(['mapFileUri', 'coreProofFileUri', 'provisionalProofFileUri', 'operations', 'writerLockId']);
const allowedProperties = new Set(['mapFileUri', 'coreProofFileUri', 'operations', 'writerLockId']);
for (const property in anchorFileModel) {
if (!allowedProperties.has(property)) {
throw new SidetreeError(ErrorCode.AnchorFileHasUnknownProperty);
Expand All @@ -75,7 +74,7 @@ export default class AnchorFile {

// Map file URI validations.
const mapFileUri = anchorFileModel.mapFileUri;
AnchorFile.validateCasFileUri(mapFileUri);
InputValidator.validateCasFileUri(mapFileUri, 'map file URI');

// `operations` validations.

Expand Down Expand Up @@ -141,7 +140,7 @@ export default class AnchorFile {

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

// Validate provisional proof file URI.
if (anchorFileModel.provisionalProofFileUri !== undefined) {
AnchorFile.validateCasFileUri(anchorFileModel.provisionalProofFileUri);
}

const anchorFile = new AnchorFile(anchorFileModel, didUniqueSuffixes, createOperations, recoverOperations, deactivateOperations);
return anchorFile;
}
Expand All @@ -167,7 +161,6 @@ export default class AnchorFile {
writerLockId: string | undefined,
mapFileHash: string,
coreProofFileHash: string | undefined,
provisionalProofFileHash: string | undefined,
createOperationArray: CreateOperation[],
recoverOperationArray: RecoverOperation[],
deactivateOperationArray: DeactivateOperation[]
Expand Down Expand Up @@ -205,14 +198,18 @@ export default class AnchorFile {
writerLockId,
mapFileUri: mapFileHash,
coreProofFileUri: coreProofFileHash,
provisionalProofFileUri: provisionalProofFileHash,
operations: {
create: createOperations,
recover: recoverOperations,
deactivate: deactivateOperations
}
};

// Only insert `coreProofFileUri` property if a value is given.
if (coreProofFileHash !== undefined) {
anchorFileModel.coreProofFileUri = coreProofFileHash;
}

return anchorFileModel;
}

Expand All @@ -223,31 +220,19 @@ export default class AnchorFile {
writerLockId: string | undefined,
mapFileHash: string,
coreProofFileHash: string | undefined,
provisionalProofFileHash: string | undefined,
createOperations: CreateOperation[],
recoverOperations: RecoverOperation[],
deactivateOperations: DeactivateOperation[]
): Promise<Buffer> {
const anchorFileModel = await AnchorFile.createModel(
writerLockId, mapFileHash, coreProofFileHash, provisionalProofFileHash, createOperations, recoverOperations, deactivateOperations
writerLockId, mapFileHash, coreProofFileHash, createOperations, recoverOperations, deactivateOperations
);
const anchorFileJson = JSON.stringify(anchorFileModel);
const anchorFileBuffer = Buffer.from(anchorFileJson);

return Compressor.compress(anchorFileBuffer);
}

private static validateCasFileUri (casFileUri: any) {
if (typeof casFileUri !== 'string') {
throw new SidetreeError(ErrorCode.AnchorFileCasFileUriNotString);
}

const casFileUriAsHashBuffer = Encoder.decodeAsBuffer(casFileUri);
if (!Multihash.isComputedUsingHashAlgorithm(casFileUriAsHashBuffer, ProtocolParameters.hashAlgorithmInMultihashCode)) {
throw new SidetreeError(ErrorCode.AnchorFileCasFileUriUnsupported, `CAS file URI '${casFileUri}' is computed using an unsupported hash algorithm.`);
}
}

private static validateWriterLockId (writerLockId: string) {
// Max size check.
const writerLockIdSizeInBytes = Buffer.from(writerLockId).length;
Expand Down
3 changes: 1 addition & 2 deletions lib/core/versions/latest/BatchWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export default class BatchWriter implements IBatchWriter {
console.info(LogColor.lightBlue(`Wrote chunk file ${LogColor.green(chunkFileHash)} to content addressable store.`));

// Write the map file to content addressable store.
const mapFileBuffer = await MapFile.createBuffer(chunkFileHash, updateOperations);
const mapFileBuffer = await MapFile.createBuffer(chunkFileHash, provisionalProofFileHash, updateOperations);
const mapFileHash = await this.cas.write(mapFileBuffer);
console.info(LogColor.lightBlue(`Wrote map file ${LogColor.green(mapFileHash)} to content addressable store.`));

Expand All @@ -86,7 +86,6 @@ export default class BatchWriter implements IBatchWriter {
writerLockId,
mapFileHash,
coreProofFileHash,
provisionalProofFileHash,
createOperations,
recoverOperations,
deactivateOperations
Expand Down
7 changes: 5 additions & 2 deletions lib/core/versions/latest/ErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
* Error codes used ONLY by this version of the protocol.
*/
export default {
AnchorFileCasFileUriNotString: 'anchor_file_cas_file_uri_not_string',
AnchorFileCasFileUriUnsupported: 'anchor_file_cas_file_uri_unsupported',
AnchorFileCoreProofFileUriNotAllowed: 'anchor_file_core_proof_file_uri_not_allowed',
AnchorFileCreatePropertyNotArray: 'anchor_file_create_property_not_array',
AnchorFileDeactivatePropertyNotArray: 'anchor_file_deactivate_property_not_array',
Expand Down Expand Up @@ -36,6 +34,7 @@ export default {
CoreProofFileHasNoProofs: 'core_proof_file_has_no_proofs',
CoreProofFileNotJson: 'core_proof_file_not_json',
CoreProofFileOperationsNotFound: 'core_proof_file_operations_not_found',
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',
Expand Down Expand Up @@ -100,6 +99,8 @@ export default {
DocumentNotValidOriginalDocument: 'document_not_valid_original_document',
EncoderValidateBase64UrlStringInputNotBase64UrlString: 'encoder_validate_base64_url_string_input_not_base64_url_string',
EncoderValidateBase64UrlStringInputNotString: 'encoder_validate_base64_url_string_input_not_string',
InputValidatorCasFileUriNotString: 'input_validator_cas_file_uri_not_string',
InputValidatorCasFileUriUnsupported: 'input_validator_cas_file_uri_unsupported',
InputValidatorInputCannotBeAnArray: 'input_validator_input_cannot_be_an_array',
InputValidatorInputContainsNowAllowedProperty: 'input_validator_input_contains_now_allowed_property',
InputValidatorInputIsNotAnObject: 'input_validator_input_is_not_an_object',
Expand All @@ -126,6 +127,7 @@ export default {
MapFileMultipleOperationsForTheSameDid: 'map_file_multiple_operations_for_the_same_did',
MapFileNotJson: 'map_file_not_json',
MapFileOperationsPropertyHasMissingOrUnknownProperty: 'map_file_operations_property_has_missing_or_unknown_property',
MapFileProvisionalProofFileUriNotAllowed: 'map_file_provisional_proof_file_uri_not_allowed',
MapFileUpdateOperationsNotArray: 'map_file_update_operations_not_array',
MultihashNotLatestSupportedHashAlgorithm: 'multihash_not_latest_supported_hash_algorithm',
MultihashUnsupportedHashAlgorithm: 'multihash_unsupported_hash_algorithm',
Expand All @@ -143,6 +145,7 @@ export default {
ProvisionalProofFileHasNoProofs: 'provisional_proof_file_has_no_proofs',
ProvisionalProofFileNotJson: 'provisional_proof_file_not_json',
ProvisionalProofFileOperationsNotFound: 'provisional_proof_file_operations_not_found',
ProvisionalProofFileProofCountNotTheSameAsOperationCountInMapFile: 'provisional_proof_file_proof_count_not_the_same_as_operation_count_in_map_file',
ProvisionalProofFileUpdatePropertyNotAnArray: 'provisional_proof_file_update_property_not_an_array',
QueueingMultipleOperationsPerDidNotAllowed: 'queueing_multiple_operations_per_did_not_allowed',
RecoverOperationMissingOrInvalidDidUniqueSuffix: 'recover_operation_missing_or_invalid_did_unique_suffix',
Expand Down
26 changes: 26 additions & 0 deletions lib/core/versions/latest/InputValidator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import Encoder from './Encoder';
import ErrorCode from './ErrorCode';
import Multihash from './Multihash';
import ProtocolParameters from './ProtocolParameters';
import SidetreeError from '../../../common/SidetreeError';

/**
Expand Down Expand Up @@ -34,4 +37,27 @@ export default class InputValidator {
}
}
}

/**
* Validates that the given input is a valid CAS File URI.
* @param inputContextForErrorLogging This string is used for error logging purposes only. e.g. 'document', or 'suffix data'.
*/
public static validateCasFileUri (casFileUri: any, inputContextForErrorLogging: string) {
const casFileUriType = typeof casFileUri;
if (casFileUriType !== 'string') {
throw new SidetreeError(
ErrorCode.InputValidatorCasFileUriNotString,
`Input ${inputContextForErrorLogging} CAS file URI '${casFileUri}' needs to be of string type, but is of ${casFileUriType} type instead.`
);
}

const casFileUriAsHashBuffer = Encoder.decodeAsBuffer(casFileUri);
const hashAlgorithmInMultihashCode = ProtocolParameters.hashAlgorithmInMultihashCode;
if (!Multihash.isComputedUsingHashAlgorithm(casFileUriAsHashBuffer, hashAlgorithmInMultihashCode)) {
throw new SidetreeError(
ErrorCode.InputValidatorCasFileUriUnsupported,
`Input ${inputContextForErrorLogging} CAS file URI '${casFileUri}' is not computed using hash algorithm of code ${hashAlgorithmInMultihashCode}.`
);
}
}
}
23 changes: 20 additions & 3 deletions lib/core/versions/latest/MapFile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ArrayMethods from './util/ArrayMethods';
import Compressor from './util/Compressor';
import ErrorCode from './ErrorCode';
import InputValidator from './InputValidator';
import JsonAsync from './util/JsonAsync';
import MapFileModel from './models/MapFileModel';
import Multihash from './Multihash';
Expand Down Expand Up @@ -43,7 +44,7 @@ export default class MapFile {
throw SidetreeError.createFromError(ErrorCode.MapFileNotJson, error);
}

const allowedProperties = new Set(['chunks', 'operations']);
const allowedProperties = new Set(['chunks', 'operations', 'provisionalProofFileUri']);
for (const property in mapFileModel) {
if (!allowedProperties.has(property)) {
throw new SidetreeError(ErrorCode.MapFileHasUnknownProperty);
Expand All @@ -55,6 +56,18 @@ export default class MapFile {
const updateOperations = await MapFile.parseOperationsProperty(mapFileModel.operations);
const didUniqueSuffixes = updateOperations.map(operation => operation.didUniqueSuffix);

// Validate provisional proof file URI.
if (updateOperations.length > 0) {
InputValidator.validateCasFileUri(mapFileModel.provisionalProofFileUri, 'provisional proof file URI');
} else {
if (mapFileModel.provisionalProofFileUri !== undefined) {
throw new SidetreeError(
ErrorCode.MapFileProvisionalProofFileUriNotAllowed,
`Provisional proof file '${mapFileModel.provisionalProofFileUri}' not allowed in a map file with no updates.`
);
}
}

const mapFile = new MapFile(mapFileModel, didUniqueSuffixes, updateOperations);
return mapFile;
}
Expand Down Expand Up @@ -117,7 +130,9 @@ export default class MapFile {
/**
* Creates the Map File buffer.
*/
public static async createBuffer (chunkFileHash: string, updateOperationArray: UpdateOperation[]): Promise<Buffer> {
public static async createBuffer (
chunkFileHash: string, provisionalProofFileHash: string | undefined, updateOperationArray: UpdateOperation[]
): Promise<Buffer> {
const updateOperations = updateOperationArray.map(operation => {
return {
didSuffix: operation.didUniqueSuffix,
Expand All @@ -129,11 +144,13 @@ export default class MapFile {
chunks: [{ chunkFileUri: chunkFileHash }]
};

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

mapFileModel.provisionalProofFileUri = provisionalProofFileHash;
}

const rawData = JSON.stringify(mapFileModel);
Expand Down
51 changes: 45 additions & 6 deletions lib/core/versions/latest/TransactionProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,16 @@ export default class TransactionProcessor implements ITransactionProcessor {
const mapFile = await this.downloadAndVerifyMapFile(anchorFile, anchoredData.numberOfOperations);

// Download and verify core proof file.
await this.downloadAndVerifyCoreProofFile(anchorFile);
const coreProofFile = await this.downloadAndVerifyCoreProofFile(anchorFile);

// Download and verify provisional proof file.
await this.downloadAndVerifyProvisionalProofFile(anchorFile);
const provisionalProofFile = await this.downloadAndVerifyProvisionalProofFile(mapFile);

// Download and verify chunk file.
const chunkFileModel = await this.downloadAndVerifyChunkFile(mapFile);

// Compose into operations from all the files downloaded.
const operations = await this.composeAnchoredOperationModels(transaction, anchorFile, mapFile, chunkFileModel);
const operations = await this.composeAnchoredOperationModels(transaction, anchorFile, mapFile, coreProofFile, provisionalProofFile, chunkFileModel);

// If the code reaches here, it means that the batch of operations is valid, store the operations.
await this.operationStore.put(operations);
Expand Down Expand Up @@ -132,20 +132,39 @@ export default class TransactionProcessor implements ITransactionProcessor {
const fileBuffer = await this.downloadFileFromCas(coreProofFileUri, ProtocolParameters.maxProofFileSizeInBytes);
const coreProofFile = await CoreProofFile.parse(fileBuffer, anchorFile.deactivateOperations.map(operation => operation.didUniqueSuffix));

const recoverAndDeactivateCount = anchorFile.deactivateOperations.length + anchorFile.recoverOperations.length;
const proofCountInCoreProofFile = coreProofFile.deactivateProofs.length + coreProofFile.recoverProofs.length;
if (recoverAndDeactivateCount !== proofCountInCoreProofFile) {
throw new SidetreeError(
ErrorCode.CoreProofFileProofCountNotTheSameAsOperationCountInAnchorFile,
`Proof count of ${proofCountInCoreProofFile} in core proof file different to recover + deactivate count of ${recoverAndDeactivateCount} in anchor file.`
);
}

return coreProofFile;
}

private async downloadAndVerifyProvisionalProofFile (anchorFile: AnchorFile): Promise<ProvisionalProofFile | undefined> {
const provisionalProofFileUri = anchorFile.model.provisionalProofFileUri;
if (provisionalProofFileUri === undefined) {
private async downloadAndVerifyProvisionalProofFile (mapFile: MapFile | undefined): Promise<ProvisionalProofFile | undefined> {
// If there is no provisional proof file to download, just return.
if (mapFile === undefined || mapFile.model.provisionalProofFileUri === undefined) {
return;
}

const provisionalProofFileUri = mapFile.model.provisionalProofFileUri;
console.info(`Downloading provisional proof file '${provisionalProofFileUri}', max file size limit ${ProtocolParameters.maxProofFileSizeInBytes}...`);

const fileBuffer = await this.downloadFileFromCas(provisionalProofFileUri, ProtocolParameters.maxProofFileSizeInBytes);
const provisionalProofFile = await ProvisionalProofFile.parse(fileBuffer);

const operationCountInMapFile = mapFile.didUniqueSuffixes.length;
const proofCountInProvisionalProofFile = provisionalProofFile.updateProofs.length;
if (operationCountInMapFile !== proofCountInProvisionalProofFile) {
throw new SidetreeError(
ErrorCode.ProvisionalProofFileProofCountNotTheSameAsOperationCountInMapFile,
`Proof count ${proofCountInProvisionalProofFile} in provisional proof file is different from operation count ${operationCountInMapFile} in map file.`
);
}

return provisionalProofFile;
}

Expand Down Expand Up @@ -244,6 +263,8 @@ export default class TransactionProcessor implements ITransactionProcessor {
transaction: TransactionModel,
anchorFile: AnchorFile,
mapFile: MapFile | undefined,
coreProofFile: CoreProofFile | undefined,
provisionalProofFile: ProvisionalProofFile | undefined,
chunkFile: ChunkFileModel | undefined
): Promise<AnchoredOperationModel[]> {

Expand All @@ -262,6 +283,19 @@ export default class TransactionProcessor implements ITransactionProcessor {
// TODO: Issue 442 - https://github.com/decentralized-identity/sidetree/issues/442
// Use actual operation request object instead of buffer.

// Prepare proofs to compose the original operation requests.
const proofs: (string | undefined)[] = createOperations.map(() => undefined); // Creates do not have proofs.
if (coreProofFile !== undefined) {
const recoverProofs = coreProofFile.recoverProofs.map((proof) => proof.signedDataJws.toCompactJws());
const deactivateProofs = coreProofFile.deactivateProofs.map((proof) => proof.signedDataJws.toCompactJws());
proofs.push(...recoverProofs);
proofs.push(...deactivateProofs);
}
if (provisionalProofFile !== undefined) {
const updateProofs = provisionalProofFile.updateProofs.map((proof) => proof.signedDataJws.toCompactJws());
proofs.push(...updateProofs);
}

// NOTE: The last set of `operations` are deactivates, they don't have `delta` property.
const anchoredOperationModels = [];
for (let i = 0; i < operations.length; i++) {
Expand All @@ -277,6 +311,11 @@ export default class TransactionProcessor implements ITransactionProcessor {
operationObject.delta = chunkFile.deltas[i];
}

// Add the `signedData` property unless it is a create operation.
if (operation.type !== OperationType.Create) {
operationObject.signedData = proofs[i];
}

const patchedOperationBuffer = Buffer.from(JSON.stringify(operationObject));
const anchoredOperationModel: AnchoredOperationModel = {
didUniqueSuffix: operation.didUniqueSuffix,
Expand Down
1 change: 0 additions & 1 deletion lib/core/versions/latest/models/AnchorFileModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export default interface AnchorFileModel {
writerLockId: string | undefined;
mapFileUri: string;
coreProofFileUri?: string;
provisionalProofFileUri?: string;
operations: {
create?: {
suffixData: {
Expand Down
8 changes: 4 additions & 4 deletions lib/core/versions/latest/models/MapFileModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
* Defines the external Map File structure.
*/
export default interface MapFileModel {
chunks: {
chunkFileUri: string
}[];

provisionalProofFileUri?: string;
operations?: {
update: {
didSuffix: string,
signedData: string
}[]
};
chunks: {
chunkFileUri: string
}[];
}
Loading

0 comments on commit de15a6d

Please sign in to comment.