|
'use strict'; |
|
var crypto = require("crypto-js"); |
|
var express = require("express"); |
|
var bodyParser = require("body-parser"); |
|
var WebSocket = require("ws"); |
|
|
|
// HTTP port |
|
var httpPort = process.env.HTTP_PORT || 3000; |
|
var p2pPort = process.env.P2P_PORT || 6000; |
|
var peers = process.env.PEERS ? process.env.PEERS.split(',') : []; |
|
|
|
// Blockchain (in memory) - array of blocks |
|
var blockchain = []; |
|
var sockets = []; |
|
var MessageType = { |
|
QUERY_LATEST: 0, |
|
QUERY_ALL: 1, |
|
RESPONSE_BLOCKCHAIN: 2 |
|
}; |
|
|
|
// Block |
|
class Block { |
|
constructor(index, previousHash, timestamp, data, hash) { |
|
this.index = index; |
|
this.previousHash = previousHash.toString(); |
|
this.timestamp = timestamp; |
|
this.data = data; |
|
this.hash = hash.toString(); |
|
} |
|
|
|
calculateHash() { |
|
return chain.calculateHash(this.index, this.previousHash, this.timestamp, this.data); |
|
} |
|
} |
|
|
|
// chain object |
|
var chain = { |
|
calculateHash: (index, previousHash, timestamp, data) => { |
|
return crypto.SHA256(index + previousHash + timestamp + data).toString(); |
|
}, |
|
|
|
calculateHashBlock: (block) => { |
|
return crypto.SHA256(block.index + block.previousHash + block.timestamp + block.data).toString(); |
|
}, |
|
|
|
// Block number |
|
blockNumber: () => { |
|
return blockchain.length - 1; |
|
}, |
|
|
|
// Latest block |
|
latestBlock: () => { |
|
return blockchain[blockchain.length - 1]; |
|
}, |
|
|
|
// Generate genesis block |
|
generateGenesisBlock: () => { |
|
return new Block(0, "0", 1465154705, "First test blockchain", "3f52d6da5790b8bb7fb000cfa395cee4a087b3a855987b53edf47d3f1b2b2200"); |
|
}, |
|
|
|
// Generate block |
|
generateBlock: (blockData) => { |
|
var previousBlock = chain.latestBlock(); |
|
var index = previousBlock.index + 1; |
|
var timestamp = new Date().getTime() / 1000; |
|
var hash = chain.calculateHash(index, previousBlock.hash, timestamp, blockData); |
|
return new Block(index, previousBlock.hash, timestamp, blockData, hash); |
|
}, |
|
|
|
addBlock: (newBlock) => { |
|
if (chain.isNewBlockValid(newBlock, chain.latestBlock())) { |
|
blockchain.push(newBlock); |
|
} |
|
}, |
|
|
|
isNewBlockValid: (newBlock, previousBlock) => { |
|
if (previousBlock.index + 1 !== newBlock.index) { |
|
console.log('invalid index'); |
|
return false; |
|
} |
|
|
|
if (previousBlock.hash !== newBlock.previousHash) { |
|
console.log('invalid previoushash'); |
|
return false; |
|
} |
|
|
|
var newBlockHash = chain.calculateHashBlock(newBlock); |
|
if (newBlockHash !== newBlock.hash) { |
|
console.log(typeof (newBlock.hash) + ' ' + typeof newBlockHash); |
|
console.log('invalid hash: ' + newBlockHash + ' ' + newBlock.hash); |
|
return false; |
|
} |
|
|
|
return true; |
|
}, |
|
|
|
isValidChain: (blockchainToValidate) => { |
|
if (JSON.stringify(blockchainToValidate[0]) !== JSON.stringify(chain.generateGenesisBlock())) { |
|
return false; |
|
} |
|
|
|
var tempBlocks = [blockchainToValidate[0]]; |
|
for (var i = 1; i < blockchainToValidate.length; i++) { |
|
if (chain.isNewBlockValid(blockchainToValidate[i], tempBlocks[i - 1])) { |
|
tempBlocks.push(blockchainToValidate[i]); |
|
} else { |
|
return false; |
|
} |
|
} |
|
return true; |
|
}, |
|
|
|
replaceChain: (newBlocks) => { |
|
if (chain.isValidChain(newBlocks) && newBlocks.length > blockchain.length) { |
|
console.log('Received blockchain is valid. Replacing current blockchain with received blockchain'); |
|
blockchain = newBlocks; |
|
wobj.broadcast(wobj.responseLatestMsg()); |
|
} else { |
|
console.log('Received blockchain invalid'); |
|
} |
|
} |
|
}; |
|
|
|
// WS Object |
|
var wobj = { |
|
p2pServer: () => { |
|
var server = new WebSocket.Server({port: p2pPort}); |
|
server.on('connection', ws => wobj.initConnection(ws)); |
|
console.log('Listening websocket p2p port on: ' + p2pPort); |
|
}, |
|
|
|
initConnection: (ws) => { |
|
sockets.push(ws); |
|
wobj.messageHandler(ws); |
|
wobj.errorHandler(ws); |
|
wobj.write(ws, wobj.queryChainLengthMsg()); |
|
}, |
|
|
|
queryChainLengthMsg: () => { |
|
return { |
|
'type': MessageType.QUERY_LATEST |
|
} |
|
}, |
|
|
|
queryAllMsg: () => { |
|
return { |
|
'type': MessageType.QUERY_ALL |
|
} |
|
}, |
|
|
|
responseChainMsg: () => { |
|
return { |
|
'type': MessageType.RESPONSE_BLOCKCHAIN, 'data': JSON.stringify(blockchain) |
|
} |
|
}, |
|
|
|
responseLatestMsg: () => { |
|
return { |
|
'type': MessageType.RESPONSE_BLOCKCHAIN, |
|
'data': JSON.stringify([chain.latestBlock()]) |
|
} |
|
}, |
|
|
|
write: (ws, message) => ws.send(JSON.stringify(message)), |
|
broadcast: (message) => sockets.forEach(socket => wobj.write(socket, message)), |
|
|
|
messageHandler: (ws) => { |
|
ws.on('message', (data) => { |
|
var message = JSON.parse(data); |
|
console.log('Received message' + JSON.stringify(message)); |
|
switch (message.type) { |
|
case MessageType.QUERY_LATEST: |
|
wobj.write(ws, wobj.responseLatestMsg()); |
|
break; |
|
case MessageType.QUERY_ALL: |
|
wobj.write(ws, wobj.responseChainMsg()); |
|
break; |
|
case MessageType.RESPONSE_BLOCKCHAIN: |
|
wobj.handleBlockchainResponse(message); |
|
break; |
|
} |
|
}); |
|
}, |
|
|
|
errorHandler: (ws) => { |
|
var closeConnection = (ws) => { |
|
console.log('connection failed to peer: ' + ws.url); |
|
sockets.splice(sockets.indexOf(ws), 1); |
|
}; |
|
ws.on('close', () => closeConnection(ws)); |
|
ws.on('error', () => closeConnection(ws)); |
|
}, |
|
|
|
handleBlockchainResponse: (message) => { |
|
var receivedBlocks = JSON.parse(message.data).sort((b1, b2) => (b1.index - b2.index)); |
|
var latestBlockReceived = receivedBlocks[receivedBlocks.length - 1]; |
|
var latestBlockHeld = chain.latestBlock(); |
|
if (latestBlockReceived.index > latestBlockHeld.index) { |
|
console.log('blockchain possibly behind. We got: ' + latestBlockHeld.index + ' Peer got: ' + latestBlockReceived.index); |
|
if (latestBlockHeld.hash === latestBlockReceived.previousHash) { |
|
console.log("We can append the received block to our chain"); |
|
blockchain.push(latestBlockReceived); |
|
wobj.broadcast(wobj.responseLatestMsg()); |
|
} else if (receivedBlocks.length === 1) { |
|
console.log("We have to query the chain from our peer"); |
|
wobj.broadcast(wobj.queryAllMsg()); |
|
} else { |
|
console.log("Received blockchain is longer than current blockchain"); |
|
chain.replaceChain(receivedBlocks); |
|
} |
|
} else { |
|
console.log('received blockchain is not longer than received blockchain. Do nothing'); |
|
} |
|
}, |
|
|
|
connectToPeers: (newPeers) => { |
|
newPeers.forEach((peer) => { |
|
var ws = new WebSocket(peer); |
|
ws.on('open', () => wobj.initConnection(ws)); |
|
ws.on('error', () => { |
|
console.log('connection failed') |
|
}); |
|
}); |
|
} |
|
}; |
|
|
|
|
|
// HTTP server |
|
var httpServer = () => { |
|
var app = express(); |
|
app.use(bodyParser.json()); |
|
|
|
// Get all blocks |
|
app.get('/blocks', (req, res) => { |
|
res.send(JSON.stringify(blockchain)) |
|
}); |
|
|
|
app.post('/mineBlock', (req, res) => { |
|
var newBlock = chain.generateBlock(req.body.data); |
|
chain.addBlock(newBlock); |
|
wobj.broadcast(wobj.responseLatestMsg()); |
|
console.log('block added: ' + JSON.stringify(newBlock)); |
|
res.send(); |
|
}); |
|
|
|
app.get('/peers', (req, res) => { |
|
res.send(sockets.map(s => s._socket.remoteAddress + ':' + s._socket.remotePort)); |
|
}); |
|
|
|
app.post('/addPeer', (req, res) => { |
|
wobj.connectToPeers([req.body.peer]); |
|
res.send(); |
|
}); |
|
|
|
app.listen(httpPort, () => console.log('Listening http on port: ' + httpPort)); |
|
}; |
|
|
|
|
|
// add genesis block |
|
blockchain.push(chain.generateGenesisBlock()); |
|
|
|
// start HTTP server |
|
httpServer(); |
|
|
|
// connect to peers |
|
wobj.connectToPeers(peers); |
|
|
|
// Init p2p server |
|
wobj.p2pServer(); |