Source: protocol/references.js

'use strict';
/**
 * @module lib/protocol/references
 * @summary Whiteflag message reference validation module
 * @description Module for reference verification functions
 * @tutorial modules
 * @tutorial protocol
 */
module.exports = {
    // Message validation functions
    verify: verifyReference
};

// Node.js core and external modules //
const fs = require('fs');

// Whiteflag common functions and classes //
const array = require('../common/arrays');
const { ProtocolError } = require('../common/errors');

// Whiteflag modules //
const wfRetrieve = require('./retrieve');

// Module constants //
const TESTMESSAGECODE = 'T';
const wfMessageSchema = JSON.parse(fs.readFileSync('./static/protocol/message.schema.json'));

// MAIN MODULE FUNCTIONS //
/**
 * Checks if message references antother message correclty and updates metaheader accordingly
 * @function verifyReference
 * @alias module:lib/protocol/validation.verifyReference
 * @param {wfMessage} wfMessage a Whiteflag message
 * @param {function(Error, wfMessage)} callback function to be called upon completion
 */
function verifyReference(wfMessage, callback) {
    // Check message
    if (!wfMessage.MetaHeader) {
        return callback(new ProtocolError('Missing metaheader', null, 'WF_METAHEADER_ERROR'), wfMessage);
    }
    if (!wfMessage.MessageHeader) {
        wfMessage.MetaHeader.formatValid = false;
        return callback(new ProtocolError('Missing message header', null, 'WF_FORMAT_ERROR'), wfMessage);
    }
    // If rerference indcator is 0, then referenced transcation hash may be anything
    if (wfMessage.MessageHeader.ReferenceIndicator === '0') {
        wfMessage.MetaHeader.referenceValid = true;
        return callback(null, wfMessage);
    }
    // Message referenced; but no transaction hash
    if (/^0{64}$/.test(wfMessage.MessageHeader.ReferencedMessage)) {
        wfMessage.MetaHeader.referenceValid = false;
        return callback(new ProtocolError('Illegal reference', null, 'WF_REFERENCE_ERROR'), wfMessage);
    }
    // Retrieve referenced message and validate reference
    wfRetrieve.getMessage(wfMessage.MessageHeader.ReferencedMessage, wfMessage.MetaHeader.blockchain,
        function verifyReferenceRetrieveCb(err, wfMessages) {
            // Return if referenced message cannot be retrieved
            if (err) return callback(err, wfMessage);
            if (wfMessages.length === 0) return callback(new Error('Could not retrieve referenced message'), wfMessage);

            // Call evaluation function and return result
            evaluateReference(wfMessage, wfMessages[0], function verifyReferenceEvalCb(err) {
                if (err) {
                    if (err instanceof ProtocolError) wfMessage.MetaHeader.referenceValid = false;
                    return callback(err, wfMessage);
                }
                wfMessage.MetaHeader.referenceValid = true;
                return callback(null, wfMessage);
            }
        );
    });
}

// PRIVATE MODULE FUNCTIONS //
/**
 * Evaluates reference by checking referenced message
 * @private
 * @param {wfMessage} wfMessage a Whiteflag message
 * @param {Object} wfRefMessage the refereced Whiteflag message
 * @param {function(Error)} callback function to be called upon completion
 */
function evaluateReference(wfMessage, wfRefMessage, callback) {
    // Get reference metadata and specification
    const header = wfMessage.MessageHeader;
    if (header.MessageCode === TESTMESSAGECODE) {
        // Test message may reference any message
        return callback(null);
    }
    const refMessageHeader = wfRefMessage.MessageHeader;
    const wfSpec = wfMessageSchema.specifications;

    // Array for errors
    let referenceErrors = [];

    // Execute reference checks
    try {
        // CHECK 1: may the message type reference the referenced message type?
        const mesageTypeIndex = wfSpec.MessageCode.findIndex(
            mesageCode => mesageCode.const === header.MessageCode
        );
        if (mesageTypeIndex < 0) {
            return callback(new Error(`Cannot evaluate reference for non-existing message type: ${header.MessageCode}`));
        }
        const wfSpecMessageType = wfSpec.MessageCode[mesageTypeIndex];
        const allowedReferenceIndex = wfSpecMessageType.allowedToReference.findIndex(
            allowed => allowed.referencedMessageCode === refMessageHeader.MessageCode
        );
        if (allowedReferenceIndex < 0) {
            referenceErrors.push(`Message type ${header.MessageCode} may not reference message type ${refMessageHeader.MessageCode}`);
        } else {
            // CHECK 2: is the reference code allowed?
            const wfSpecAllowedReferences = wfSpecMessageType.allowedToReference[allowedReferenceIndex].allowedReferenceIndicator;
            if (!wfSpecAllowedReferences.includes(header.ReferenceIndicator)) {
                referenceErrors.push(`Message type ${header.MessageCode} may not use reference code ${header.ReferenceIndicator} to reference message type ${refMessageHeader.MessageCode}`);
            }
            // CHECK 3: are the reference codes of both messages compatible (i.e. meaningful)?
            const refIndicatorIndex = wfSpec.ReferenceIndicator.findIndex(
                referenceIndicator => referenceIndicator.const === header.ReferenceIndicator
            );
            if (refIndicatorIndex < 0) {
                referenceErrors.push(`Reference Indicator ${header.ReferenceIndicator} is not allowed`);
            } else {
                // Check if allowed from same originator
                if (
                    !wfSpec.ReferenceIndicator[refIndicatorIndex].allowedToReferenceSameOriginator
                    && wfMessage.MetaHeader.originatorAddress.toLowerCase() === wfRefMessage.MetaHeader.originatorAddress.toLowerCase()
                ) {
                    referenceErrors.push(`Reference code ${header.ReferenceIndicator} may not be used by the same originator`);
                }
                // Check if allowed from different originator
                if (
                    !wfSpec.ReferenceIndicator[refIndicatorIndex].allowedToReferenceDifferentOriginator
                    && wfMessage.MetaHeader.originatorAddress.toLowerCase() !== wfRefMessage.MetaHeader.originatorAddress.toLowerCase()
                ) {
                    referenceErrors.push(`Reference code ${header.ReferenceIndicator} may not be used by a different originator`);
                }
                // Check if reference code may reference reference code
                const wfSpecAllowedRefIndicators = wfSpec.ReferenceIndicator[refIndicatorIndex].allowedToReference;
                if (!wfSpecAllowedRefIndicators.includes(refMessageHeader.ReferenceIndicator)) {
                    referenceErrors.push(`Reference code ${header.ReferenceIndicator} cannot meaningfully reference reference code ${refMessageHeader.ReferenceIndicator}`);
                }
            }
        }
    } catch(err) {
        return callback(err);
    }
    // Evaluate and return result
    if (referenceErrors.length > 0) {
        wfMessage.MetaHeader.validationErrors = array.addArray(wfMessage.MetaHeader.validationErrors, referenceErrors);
        return callback(new ProtocolError('Invalid message contents', referenceErrors, 'WF_FORMAT_ERROR'));
    }
    return callback(null);
}