'use strict';
const {
MAGIC_BYTES,
VERSION_NUMBER,
HEADER_SIZE,
MAX_MESSAGE_SIZE,
OP_CYCLE,
GENERIC_MESSAGE_TYPE,
LOG_MESSAGE_TYPE,
GENERIC_LIST_TYPE,
P2P_GENERIC,
P2P_IDENT_REQUEST,
P2P_IDENT_RESPONSE,
P2P_ROOT,
P2P_PING,
P2P_PONG,
P2P_START_CHAIN,
P2P_INSTRUCTION,
P2P_BASE_MESSAGE,
P2P_CHAIN_SYNC_REQUEST,
P2P_STATE_ROOT,
P2P_STATE_COMMITTMENT,
P2P_STATE_CHANGE,
P2P_STATE_REQUEST,
P2P_TRANSACTION,
P2P_CALL,
CHAT_MESSAGE,
DOCUMENT_PUBLISH_TYPE,
DOCUMENT_REQUEST_TYPE,
BLOCK_CANDIDATE,
PEER_CANDIDATE,
SESSION_START
} = require('../constants');
const HEADER_SIG_SIZE = 64;
// Dependencies
// const crypto = require('crypto');
const struct = require('struct');
// Fabric Types
const Actor = require('./actor');
const Hash256 = require('./hash256');
// const Signer = require('./signer');
// Function Definitions
const padDigits = require('../functions/padDigits');
/**
* The {@link Message} type defines the Application Messaging Protocol, or AMP.
* Each {@link Actor} in the network receives and broadcasts messages,
* selectively disclosing new routes to peers which may have open circuits.
* @type {Object}
*/
class Message extends Actor {
/**
* The `Message` type is standardized in {@link Fabric} as a {@link Array}, which can be added to any other vector to compute a resulting state.
* @param {Object} message Message vector. Will be serialized by {@link Array#_serialize}.
* @return {Message} Instance of the message.
*/
constructor (input = {}) {
super(input);
this.raw = {
magic: Buffer.alloc(4),
version: Buffer.alloc(4),
parent: Buffer.alloc(32),
author: Buffer.alloc(32),
type: Buffer.alloc(4), // TODO: 8, 32
size: Buffer.alloc(4), // TODO: 8, 32
hash: Buffer.alloc(32),
signature: Buffer.alloc(64),
data: null
};
this.raw.magic.write(MAGIC_BYTES.toString(16), 'hex');
this.raw.version.write(padDigits(VERSION_NUMBER.toString(16), 8), 'hex');
// Use provided signer
if (input.signer) {
this.signer = input.signer;
} else {
this.signer = null;
// this.signer = new Signer();
}
if (input.data && input.type) {
this.type = input.type;
if (typeof input.data !== 'string') {
this.data = JSON.stringify(input.data);
} else {
this.data = input.data;
}
}
// Set various properties to be unenumerable
for (let name of [
'@input',
'@entity',
'_state',
'config',
'settings',
'signer',
'stack',
'observer'
]) Object.defineProperty(this, name, { enumerable: false });
return this;
}
get body () {
return this.raw.data.toString('utf8');
}
get byte () {
const input = 0 + '';
const num = Buffer.from(`0x${padDigits(input, 8)}`, 'hex');
return num;
}
get tu16 () {
return parseInt(0);
}
get tu32 () {
return parseInt(0);
}
get tu64 () {
return parseInt(0);
}
get Uint256 () {
// 256 bits
return Buffer.from((this.raw && this.raw.hash) ? `0x${padDigits(this.raw.hash, 8)}` : Actor.randomBytes(32));
}
set signature (value) {
if (value instanceof Buffer) value = value.toString('hex');
this.raw.signature.write(value, 'hex');
}
toBuffer () {
return this.asRaw();
}
/**
* Returns a {@link Buffer} of the complete message.
* @return {Buffer} Buffer of the encoded {@link Message}.
*/
asRaw () {
return Buffer.concat([this.header, this.raw.data]);
}
toRaw () {
return this.asRaw();
}
asTypedArray () {
return new Uint8Array(this.asRaw());
// TODO: Node 12
// return new TypedArray(this.asRaw());
}
asBlob () {
return this.asRaw().map(byte => parseInt(byte, 16));
}
toObject () {
return {
headers: {
magic: parseInt(`${this.raw.magic.toString('hex')}`, 16),
version: parseInt(`${this.raw.version.toString('hex')}`, 16),
parent: this.raw.parent.toString('hex'),
author: this.raw.author.toString('hex'),
type: parseInt(`${this.raw.type.toString('hex')}`, 16),
size: parseInt(`${this.raw.size.toString('hex')}`, 16),
hash: this.raw.hash.toString('hex'),
signature: this.raw.signature.toString('hex'),
},
type: this.type,
data: this.data
};
}
fromObject (input) {
return new Message(input);
}
/**
* Signs the message using the associated signer.
* @returns {Message} Signed message.
*/
sign () {
if (!this.header) throw new Error('No header property.');
if (!this.raw) throw new Error('No raw property.');
const hash = Hash256.digest(this.raw.data);
const signature = this.signer.sign(Buffer.from(hash, 'hex'));
this.raw.author.write(this.signer.pubkey.toString('hex'), 'hex');
this.raw.signature.write(signature.toString('hex'), 'hex');
Object.freeze(this);
return this;
}
/**
* Verify a message's signature.
* @returns {Boolean} `true` if the signature is valid, `false` if not.
*/
verify () {
if (!this.header) throw new Error('No header property.');
if (!this.raw) throw new Error('No raw property.');
// Compute sha256 hash of message body
const hash = Hash256.digest(this.raw.data);
// If the raw header doesn't match the computed values, reject
if (this.raw.hash.toString('hex') !== hash.toString('hex')) {
return false;
}
const signature = this.raw.signature;
const verified = this.signer.verify(this.raw.author, hash, signature);
if (!verified) {
throw new Error('Did not verify.');
}
return true;
}
/**
* Sets the signer for the message.
* @param {Signer} signer Signer instance.
* @returns {Message} Instance of the Message with associated signer.
*/
_setSigner (signer) {
// if (this.signer) throw new Error('Cannot override signer.');
this.signer = signer;
return this;
}
static parseBuffer (buffer) {
const message = struct()
.charsnt('magic', 4, 'hex')
.charsnt('version', 4, 'hex')
.charsnt('parent', 32, 'hex')
.charsnt('type', 4, 'hex')
.charsnt('size', 4, 'hex')
.charsnt('hash', 32, 'hex')
.charsnt('signature', 64, 'hex')
.charsnt('data', buffer.length - HEADER_SIZE);
message.allocate();
message._setBuff(buffer);
return message;
}
static parseRawMessage (buffer) {
const message = {
magic: buffer.slice(0, 4),
version: buffer.slice(4, 8),
parent: buffer.slice(8, 40),
author: buffer.slice(40, 72),
type: buffer.slice(72, 76),
size: buffer.slice(76, 80),
hash: buffer.slice(80, 112),
signature: buffer.slice(112, HEADER_SIZE)
};
if (buffer.length >= HEADER_SIZE) {
message.data = buffer.slice(HEADER_SIZE, buffer.length);
}
return message;
};
static fromBuffer (buffer) {
return Message.fromRaw(buffer);
}
static fromRaw (input) {
if (!input) return null;
if (!(input instanceof Buffer)) throw new Error('Input must be a buffer.');
// if (input.length < HEADER_SIZE) return null;
// if (input.length > MAX_MESSAGE_SIZE) return new Error('Input too large.');
const message = new Message();
message.raw = {
magic: input.slice(0, 4),
version: input.slice(4, 8),
parent: input.slice(8, 40),
author: input.slice(40, 72),
type: input.slice(72, 76),
size: input.slice(76, 80),
hash: input.slice(80, 112),
signature: input.slice(112, HEADER_SIZE)
};
message.data = input.slice(HEADER_SIZE);
return message;
}
static fromVector (vector = ['LogMessage', 'No vector provided.']) {
let message = null;
try {
message = new Message({
type: vector[0],
data: vector[1]
});
} catch (exception) {
console.error('[FABRIC:MESSAGE]', 'Could not construct Message:', exception);
}
return message;
}
/* get [Symbol.toStringTag] () {
return `<Message | ${JSON.stringify(this.raw)}>`;
} */
get id () {
return Hash256.digest(this.asRaw());
}
get types () {
// Message Types
return {
'GenericMessage': GENERIC_MESSAGE_TYPE,
'GenericLogMessage': LOG_MESSAGE_TYPE,
'GenericList': GENERIC_LIST_TYPE,
'GenericQueue': GENERIC_LIST_TYPE,
'FabricLogMessage': LOG_MESSAGE_TYPE,
'FabricServiceLogMessage': LOG_MESSAGE_TYPE,
'GenericTransferQueue': GENERIC_LIST_TYPE,
// TODO: document Generic type
// P2P Commands
'Generic': P2P_GENERIC,
'Cycle': OP_CYCLE,
'IdentityRequest': P2P_IDENT_REQUEST,
'IdentityResponse': P2P_IDENT_RESPONSE,
'ChainSyncRequest': P2P_CHAIN_SYNC_REQUEST,
// TODO: restore this type
// 'StateRoot': P2P_ROOT,
'Ping': P2P_PING,
'Pong': P2P_PONG,
'DocumentRequest': DOCUMENT_REQUEST_TYPE,
'DocumentPublish': DOCUMENT_PUBLISH_TYPE,
'BlockCandidate': BLOCK_CANDIDATE,
'PeerCandidate': PEER_CANDIDATE,
'PeerInstruction': P2P_INSTRUCTION,
'PeerMessage': P2P_BASE_MESSAGE,
'StartSession': SESSION_START,
'ChatMessage': CHAT_MESSAGE,
'StartChain': P2P_START_CHAIN,
// TODO: restore above StateRoot type
'StateRoot': P2P_STATE_ROOT,
'StateCommitment': P2P_STATE_COMMITTMENT,
'StateChange': P2P_STATE_CHANGE,
'StateRequest': P2P_STATE_REQUEST,
'Transaction': P2P_TRANSACTION,
'Call': P2P_CALL,
'LogMessage': LOG_MESSAGE_TYPE
};
}
get codes () {
return Object.entries(this.types).reduce((ret, entry) => {
const [ key, value ] = entry;
ret[ value ] = key;
return ret;
}, {});
}
get magic () {
return this.raw.magic;
}
get signature () {
return parseInt(Buffer.from(this.raw.signature, 'hex'));
}
get size () {
return parseInt(Buffer.from(this.raw.size, 'hex'));
}
get version () {
return parseInt(Buffer.from(this.raw.version));
}
get header () {
const parts = [
Buffer.from(this.raw.magic, 'hex'),
Buffer.from(this.raw.version, 'hex'),
Buffer.from(this.raw.parent, 'hex'),
Buffer.from(this.raw.author, 'hex'),
Buffer.from(this.raw.type, 'hex'),
Buffer.from(this.raw.size, 'hex'),
Buffer.from(this.raw.hash, 'hex'),
Buffer.from(this.raw.signature, 'hex')
];
return Buffer.concat(parts);
}
}
Object.defineProperty(Message.prototype, 'type', {
get () {
const code = parseInt(this.raw.type.toString('hex'), 16);
switch (code) {
case GENERIC_MESSAGE_TYPE:
return 'GenericMessage';
case LOG_MESSAGE_TYPE:
return 'GenericLogMessage';
case GENERIC_LIST_TYPE:
return 'GenericList';
case DOCUMENT_PUBLISH_TYPE:
return 'DocumentPublish';
case DOCUMENT_REQUEST_TYPE:
return 'DocumentRequest';
case BLOCK_CANDIDATE:
return 'BlockCandidate';
case OP_CYCLE:
return 'Cycle';
case P2P_PING:
return 'Ping';
case P2P_PONG:
return 'Pong';
case P2P_GENERIC:
return 'Generic';
case P2P_CHAIN_SYNC_REQUEST:
return 'ChainSyncRequest';
case P2P_IDENT_REQUEST:
return 'IdentityRequest';
case P2P_IDENT_RESPONSE:
return 'IdentityResponse';
case P2P_BASE_MESSAGE:
return 'PeerMessage';
case P2P_STATE_ROOT:
return 'StateRoot';
case P2P_STATE_CHANGE:
return 'StateChange';
case P2P_STATE_REQUEST:
return 'StateRequest';
case P2P_TRANSACTION:
return 'Transaction';
case P2P_CALL:
return 'Call';
case PEER_CANDIDATE:
return 'PeerCandidate';
case SESSION_START:
return 'StartSession';
case CHAT_MESSAGE:
return 'ChatMessage';
case P2P_START_CHAIN:
return 'StartChain';
default:
return 'GenericMessage';
}
},
set (value) {
let code = this.types[value];
// Default to GenericMessage;
if (!code) {
this.emit('warning', `Unknown message type: ${value}`);
code = this.types['GenericMessage'];
}
const padded = padDigits(code.toString(16), 8);
this['@type'] = value;
this.raw.type.write(padded, 'hex');
}
});
Object.defineProperty(Message.prototype, 'data', {
get () {
if (!this.raw.data) return '';
return this.raw.data.toString('utf8');
},
set (value) {
if (!value) value = '';
this.raw.hash = Hash256.digest(value.toString('utf8'));
this.raw.data = Buffer.from(value);
this.raw.size.write(padDigits(this.raw.data.byteLength.toString(16), 8), 'hex');
}
});
module.exports = Message;