/*
* @Author: amitshah
* @Date: 2018-04-17 03:38:26
* @Last Modified by: amitshah
* @Last Modified time: 2018-04-28 19:38:44
*/
const util = require('ethereumjs-util')
const sjcl = require('sjcl');
const rlp = require('rlp');
const abi = require("ethereumjs-abi");
/**
* @namespace message
*/
/**
* @const {Buffer} EMPTY_32BYTE_BUFFER
* @memberof message
*/
EMPTY_32BYTE_BUFFER= Buffer.alloc(32);
/**
* @const {Buffer} EMPTY_20BYTE_BUFFER
* @memberof message
*/
EMPTY_20BYTE_BUFFER = Buffer.alloc(20);
/** @class A hashable interface class
* @memberof message
*/
class Hashable{
/** getMessageHash - must implement */
getMessageHash(){
throw new Error("unimplemented getMessageHash");
}
}
/** Convert a base 16 int to a BN
* @param {int} value - convert base 16 value to bn
* @returns {BN}
* @memberof message
*/
function TO_BN(value){
if(util.BN.isBN(value)){
return value;
}else{
return new util.BN(value,16);
}
}
/** A reviver function to be sent to JSON.parse to handle buffer serialization and deserialization
* @param {} k
* @param {} v
* @returns {} - deserialized value
* @memberof message
*/
function JSON_REVIVER_FUNC(k,v) {
if (
v !== null &&
typeof v === 'object' &&
'type' in v &&
v.type === 'Buffer' &&
'data' in v &&
Array.isArray(v.data)) {
return new util.toBuffer(v.data);
}
return v;
}
/** Serialize message object
* @param {SignedMessage} msg - message.SignedMessage base class type
* @returns {string} - serialized value
* @memberof message
*/
function SERIALIZE(msg){
return JSON.stringify(msg);
}
/** Deserialize message object
* @param {string} data - serialized value
* @return{SignedMessage} - message type
* @memberof message
*/
function DESERIALIZE(data){
return JSON.parse(data, JSON_REVIVER_FUNC);
}
/** Deserialize a received message and create the appropriate object type based on classType property
* @param {string} data - serialized value
* @returns {SignedMessage} - message type
* @memberof message
*/
function DESERIALIZE_AND_DECODE_MESSAGE(data){
var jsonObj = DESERIALIZE(data);
if(jsonObj.hasOwnProperty("classType")){
switch(jsonObj.classType){
case "SignedMessage":
return new SignedMessage(jsonObj);
case "Proof":
return new Proof(jsonObj);
case "ProofMessage":
return new ProofMessage(jsonObj);
case "Lock":
return new Lock(jsonObj);
case "OpenLock":
return new OpenLock(jsonObj);
case "DirectTransfer":
return new DirectTransfer(jsonObj);
case "LockedTransfer":
return new LockedTransfer(jsonObj);
case "MediatedTransfer":
return new MediatedTransfer(jsonObj);
case "RequestSecret":
return new RequestSecret(jsonObj);
case "RevealSecret":
return new RevealSecret(jsonObj);
case "SecretToProof":
return new SecretToProof(jsonObj);
case "Ack":
return new Ack(jsonObj);
default:
throw new Error("Invalid Message: unknown classType")
}
}
throw new Error("Invalid Message: not a recoginized GOT message type");
}
/**
* Signature Type defintion from ethereumjs
* @typedef {Object} Signature
* @property {Buffer} r
* @property {Buffer} s
* @property {int} v
*/
/** @class Signed message base class that generates a keccak256 hash and signs using ECDSA
* @property {string} classType - base class type used for reflection
* @property {Signature} signature - the signature for this message
* @memberof message
*/
class SignedMessage{
/** @constructor
* @param {object} options
* @param {Signature} [options.signature] - sets the signature of the message, useful during deserilaization of SignedMessage
*/
constructor(options){
this.classType = this.constructor.name;
this.signature = options.signature || null;
}
/** getHash - child classes must override implementation
*/
getHash(){
throw Error("unimplemented getHash()");
}
/** sign - signs the message with the private key and sets the signature property
* @param {Buffer} privateKey
*/
sign(privateKey){
//Geth and thus web3 prepends the string \x19Ethereum Signed Message:\n<length of message>
//to all data before signing it (https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign).
//If you want to verify such a signature from Solidity from web3/geth, you'll have to prepend
//the same string in solidity before doing the ecrecovery.
var buffer = this.getHash();
console.log("SIGNING buffer:"+ buffer.toString('hex'));
this.signature = util.ecsign(buffer,privateKey);
}
/** _recoverAddress - recovers the ethereum address form the signature and message hash
* @returns {Buffer} - 20 byte Buffer representing the ethereum address
*/
_recoverAddress(){
var buffer = this.getHash();
var pk = util.ecrecover(buffer,this.signature.v,util.toBuffer(this.signature.r),util.toBuffer(this.signature.s));
var address = util.pubToAddress(pk);
return address;
}
/** @property {Buffer} from - the calculate from based on the message hash and signature
* @throws "no signature to recover address from"
*/
get from() {
if(!this.signature){
throw new Error("no signature to recover address from");
}
return this._recoverAddress();
}
/** isSigned
* @returns {bool}
*/
isSigned(){
return !(this.signature === null);
}
}
/** @class Encapsulates a snapshot instance of a message and represents a proof that can be submitted to the blockchain during settlement
* @extends SignedMessage
* @property {BN} nonce
* @property {BN} transferredAmount
* @property {Buffer} locksRoot
* @property {Buffer} channelAddress
* @property {Buffer} messageHash
* @property {Signature} signature
* @memberof message
*/
class Proof extends SignedMessage{
constructor(options){
super(options);
this.nonce = TO_BN(options.nonce) || new util.BN(0);
this.transferredAmount = TO_BN(options.transferredAmount) || new util.BN(0);
this.locksRoot = options.locksRoot || EMPTY_32BYTE_BUFFER;
this.channelAddress = options.channelAddress || EMPTY_20BYTE_BUFFER;
this.messageHash = options.messageHash || EMPTY_32BYTE_BUFFER;
this.signature = options.signature || null;
}
getHash(){
var solidityHash = abi.soliditySHA3(
[ "uint256", "uint256", "address","bytes32","bytes32" ],
[this.nonce,
this.transferredAmount,
this.channelAddress,
this.locksRoot,
this.messageHash]);
return solidityHash;
}
}
/** @class
* @extends SignedMessage
* @property {BN} nonce
* @property {BN} transferredAmount
* @property {Buffer} locksRoot
* @property {Buffer} channelAddress
* @property {Buffer} messageHash
* @property {Signature} signature
* @memberof message
*/
class ProofMessage extends SignedMessage{
constructor(options){
super(options);
this.nonce = TO_BN(options.nonce) || new util.BN(0);
this.transferredAmount = TO_BN(options.transferredAmount) || new util.BN(0);
this.locksRoot = options.locksRoot || EMPTY_32BYTE_BUFFER;
this.channelAddress = options.channelAddress || EMPTY_20BYTE_BUFFER;
this.messageHash = options.messageHash || EMPTY_32BYTE_BUFFER;
this.signature = options.signature || null;
}
getHash(){
var solidityHash = abi.soliditySHA3(
[ "uint256", "uint256", "address","bytes32","bytes32" ],
[this.nonce,
this.transferredAmount,
this.channelAddress,
this.locksRoot,
this.getMessageHash()]);
return solidityHash;
}
getMessageHash(){
throw new Error("unimplemented getMessageHash");
}
toProof(){
return new Proof({
nonce:this.nonce,
transferredAmount:this.transferredAmount,
locksRoot:this.locksRoot,
channelAddress:this.channelAddress,
messageHash:this.getMessageHash(),
signature:this.signature
});
}
}
/** @class A hashed lock that prevents transfers from being completed until secret is provided
* @extends Hashable
* @property {BN} amount - the amount of money that will be transferred if the secret is revealed
* @property {BN} expiration - the absolute blockNumber where this lock is no longer valid and cannot be redeemed
* @property {Buffer} hashLock - the keccak256 32 byte hash of the secret
* @memberof message
*/
class Lock extends Hashable{
/** @constructor
* @param {object} options
* @param {(int|BN)} options.amount=0
* @param {(int|BN)} options.expiration=0
* @param {Buffer} options.hashLock=EMPTY_32BYTE_BUFFER
*/
constructor(options){
super(options);
this.amount = TO_BN(options.amount) || new util.BN(0);
this.expiration= TO_BN(options.expiration) || new util.BN(0);
this.hashLock = options.hashLock || EMPTY_32BYTE_BUFFER;
}
getMessageHash(){
var hash = abi.soliditySHA3(['uint256','uint256','bytes32'],[
this.amount, this.expiration, this.hashLock]);
return hash;
}
/** encode - solidity pack the properties into a serilazed lock object that can be unpacked or hashed by EVM
* @returns {Buffer} - 96 Byte Buffer encoding amount,expiration,hashLock
*/
encode(){
var value = abi.solidityPack(['uint256','uint256','bytes32'],[
this.amount, this.expiration, this.hashLock]);
return value;
}
}
/** @class A hashed lock that prevents transfers from being completed until secret is provided
* @extends Lock
* @property {Buffer} secret - the 32 byte secret
* @memberof message
*/
class OpenLock extends Lock{
constructor(lock,secret){
super(lock);
this.secret = secret;
}
encode(){
var value = abi.solidityPack(['uint256','uint256','bytes32','bytes32'],[
this.amount, this.expiration, this.hashLock,this.secret]);
return value;
}
}
/** @class A direct transfer that can be sent to an engine instance to immediately complete a transfer of funds.
* Once a direct transfer is sent, the actor sending the message can consider the funds transferred (Given a reliable transport)
* @extends ProofMessage
* @property {BN} msgID - incrementing msgID for transport management
* @property {Buffer} to - Ethereum Address of intended recipient
* @memberof message
*/
class DirectTransfer extends ProofMessage{
constructor(options){
super(options);
this.msgID = TO_BN(options.msgID) || new util.BN(0);
this.to = options.to || EMPTY_20BYTE_BUFFER;
}
getMessageHash(){
var solidityHash = abi.soliditySHA3(
["uint256", "uint256", "uint256", "address","bytes32","address"],
[this.msgID,
this.nonce,
this.transferredAmount,
this.channelAddress,
this.locksRoot,
this.to]);
return solidityHash;
}
}
/** @class A locked transfer that can be sent to an engine instance to begin lock process transfer.
* Locked transfers complete asynchronously, as such, there maybe many in-flight and outstanding lock messages.
* @extends DirectTransfer
* @property {Lock} lock
* @memberof message
*/
class LockedTransfer extends DirectTransfer{
constructor(options){
super(options);
if(!options.lock){
options.lock = new Lock({});
}else if(options.lock instanceof Lock){
this.lock = options.lock;
}else if( options.lock instanceof Object){
this.lock = new Lock(options.lock);
}
}
getMessageHash(){
var solidityHash = abi.soliditySHA3(
["uint256", "uint256", "uint256", "address","bytes32","address","bytes32" ],
[this.msgID,
this.nonce,
this.transferredAmount,
this.channelAddress,
this.locksRoot,
this.to,
this.lock.getMessageHash()]);
return solidityHash;
}
}
/** @class similar to a locked transfer however, this message has a target and to field.
* This message type is the foundation for mediated transfers.
* @extends LockedTransfer
* @property {Buffer} target - Ethereum address of mediating target
* @memberof message
*/
class MediatedTransfer extends LockedTransfer{
constructor(options){
super(options);
this.target = options.target || EMPTY_20BYTE_BUFFER; //EthAddress
this.initiator = options.initiator || EMPTY_20BYTE_BUFFER;//EthAddress
}
getMessageHash(){
var solidityHash = abi.soliditySHA3(
["uint256", "uint256", "uint256", "address","bytes32","address","address","address","bytes32" ],
[this.msgID,
this.nonce,
this.transferredAmount,
this.channelAddress,
this.locksRoot,
this.to,
this.target,
this.initiator,
this.lock.getMessageHash()]);
return solidityHash;
}
}
/** @class used during the lifecyle of unlocking a locked message
* @extends SignedMessage
* @property {BN} msgID
* @property {Buffer} to - Ethereum Address
* @property {Buffer} hashLock - the hash to which you are requesting the secret
* @property {BN} amount - the amount the secret unlocks
* @memberof message
*/
class RequestSecret extends SignedMessage{
constructor(options){
super(options);
this.msgID = TO_BN(options.msgID) || new util.BN(0);
this.to = options.to || EMPTY_20BYTE_BUFFER;
this.hashLock = options.hashLock || EMPTY_32BYTE_BUFFER; //Serializable Lock Object
this.amount = TO_BN(options.amount) || util.BN(0);
}
getHash(){
//we cannot include the expiration as this value is modified by hops at times
return abi.soliditySHA3(
[ "uint256", "address", "bytes32","uint256"],
[this.msgID,this.to, this.hashLock, this.amount]
);
}
}
/** @class RevealSecret - in response to a RequestSecret
* @extends SignedMessage
* @property {Buffer} to - Ethereum Address
* @property {Buffer} secret - the hash secret
* @memberof message
*/
class RevealSecret extends SignedMessage{
constructor(options){
super(options);
this.secret = options.secret || EMPTY_32BYTE_BUFFER;
this.to = options.to || EMPTY_20BYTE_BUFFER;
}
getHash(){
var solidityHash = abi.soliditySHA3(
[ "bytes32", "address"],
[this.secret,
this.to]);
return solidityHash;
}
get hashLock(){
return util.sha3(this.secret);
}
}
/** @class Once a secret is known, if we want to keep the payment channel alive longer
* convert any openLocks into transferredAmounts. This message facilitates that and allows state channels to
* have indefinite lifetime. Without this message type, channels would require on-chain withdrawal at the min(openLock.expiration) time.
* This message effectively sets proof.transferredAmount += lock.amount and removes the lock from the merkle tree so it cannot be double spent
* @extends ProofMessage
* @property {BN} msgID
* @property {Buffer} to - Ethereum Address
* @property {Buffer} secret - the lock secret whos amount will be added to the transferredAmount of this messages proof
* @memberof message
*/
class SecretToProof extends ProofMessage{
constructor(options){
super(options);
this.msgID = TO_BN(options.msgID) || new util.BN(0);
this.to = options.to || EMPTY_20BYTE_BUFFER;
this.secret = options.secret || EMPTY_32BYTE_BUFFER;
}
getMessageHash(){
var solidityHash = abi.soliditySHA3(
[ "uint256", "uint256", "uint256", "address","bytes32","address","bytes32" ],
[this.msgID,
this.nonce,
this.transferredAmount,
this.channelAddress,
this.locksRoot, // locksRoot - sha3(secret)
this.to,
this.secret]);
return solidityHash;
}
get hashLock(){
return util.sha3(this.secret);
}
}
/** @class An Ack message that identifies a particular msgID has been delivered.
* @property {BN} msgID
* @property {Buffer} to - Ethereum Address
* @property {Buffer} messageHash - the messageHash of the acked message
* @memberof message
*/
class Ack{
constructor(options){
this.to = options.to || EMPTY_20BYTE_BUFFER;
this.messageHash = options.messageHash || EMPTY_32BYTE_BUFFER;
this.msgID = options.msgID || new util.BN(0);
}
}
/** Entropy collector for SJCL when generating random secrets. Currently, this is broken on mobile platforms and seeding should be done manually.
* Refer to: https://github.com/bitwiseshiftleft/sjcl/wiki/Symmetric-Crypto#seeding-the-generator
* @memberof message
*/
function StartEntropyCollector(){
sjcl.random.startCollectors();
}
/**
* Secret Hash Pair
* @typedef {Object} SecretHashPair
* @property {Buffer} secret - a cryptographically secure 32 Byte hash if the entropy of sjcl.random is completed appropriately
* @property {Buffer} hash - keccak256 hash of secret
*/
/** Generate random secret and corresponding keccak256 hash
@returns {SecretHashPair}
* @memberof message
*/
function GenerateRandomSecretHashPair(){
var randomBuffer = sjcl.random.randomWords(256/(4*8));
var secret = util.addHexPrefix(sjcl.codec.hex.fromBits(randomBuffer));
var hash= util.sha3(secret);
return {'secret': secret, 'hash':hash};
}
module.exports= {
SignedMessage,ProofMessage,DirectTransfer,LockedTransfer,MediatedTransfer,
RequestSecret,RevealSecret,SecretToProof,Ack,Lock, JSON_REVIVER_FUNC,
GenerateRandomSecretHashPair,StartEntropyCollector,TO_BN,OpenLock,SERIALIZE,DESERIALIZE,DESERIALIZE_AND_DECODE_MESSAGE,
EMPTY_20BYTE_BUFFER,EMPTY_32BYTE_BUFFER
}