diff --git a/.gitignore b/.gitignore index e5d4fe4..30bd7e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ -node_modules -npm-debug.log -.idea -src/*.js -src/*.map +Dewblock-cloud-server/node_modules +Dewblock-cloud-server/npm-debug.log +Dewblock-cloud-server/src/*.js +Dewblock-cloud-server/src/*.map +Dewblock-dew-server/node_modules +Dewblock-dew-server/npm-debug.log +Dewblock-dew-server/src/*.js +Dewblock-dew-server/src/*.map + + diff --git a/License.txt b/Dewblock-cloud-server/License.txt similarity index 100% rename from License.txt rename to Dewblock-cloud-server/License.txt diff --git a/Dewblock-cloud-server/README.md b/Dewblock-cloud-server/README.md new file mode 100644 index 0000000..9052e1f --- /dev/null +++ b/Dewblock-cloud-server/README.md @@ -0,0 +1,71 @@ +# Dewblock-cloud-server Package + +This package has no modes. Dewblock-cloud-server behaves like Naivecoin. Especially, it provides the whole blockchain to other nodes to fulfil the responsibility a full blockchain node. It also uses the complete blochchain to garantee the accuracy of all the transactions. + + +#### Installation and System Start + +``` +npm install +npm start +``` + +##### Set Dew Server Address +``` +curl -H "Content-type:application/json" --data '{"address" : "192.168.1.186"}' http://localhost:3001/setdewaddress +``` +For Windows: +``` +curl -H "Content-type:application/json" --data "{\"address\" : \"192.168.1.186\"}" http://localhost:3001/setdewaddress +``` + +##### Query the whole blockchain +``` +curl http://localhost:3001/blocks +``` + +##### Query all accounts + +``` +curl http://localhost:3001/accounts +``` + +##### Query the transaction pool +``` +curl http://localhost:3001/transactionPool +``` + + +#### Query all connected peers +``` +curl http://localhost:3001/peers +``` + + +##### Query a block with block hash + +``` +curl http://localhost:3001/block/:hash +``` +In dew mode, this is useless. + + + +##### Query a transaction with transaction id + +``` +curl http://localhost:3001/transaction/:id +``` +In dew mode, this is useless. + + +##### Query an account with account address + +``` +curl http://localhost:3001/account/:address +``` + +#### Stop the server +``` +curl -X POST http://localhost:3001/stop +``` diff --git a/node/wallet/.gitignore b/Dewblock-cloud-server/node/wallet/.gitignore similarity index 100% rename from node/wallet/.gitignore rename to Dewblock-cloud-server/node/wallet/.gitignore diff --git a/package-lock.json b/Dewblock-cloud-server/package-lock.json similarity index 100% rename from package-lock.json rename to Dewblock-cloud-server/package-lock.json diff --git a/package.json b/Dewblock-cloud-server/package.json similarity index 100% rename from package.json rename to Dewblock-cloud-server/package.json diff --git a/src/blockchain.ts b/Dewblock-cloud-server/src/blockchain.ts similarity index 64% rename from src/blockchain.ts rename to Dewblock-cloud-server/src/blockchain.ts index 85aedc3..ea8d71f 100644 --- a/src/blockchain.ts +++ b/Dewblock-cloud-server/src/blockchain.ts @@ -1,12 +1,20 @@ import * as CryptoJS from 'crypto-js'; import * as _ from 'lodash'; import {broadcastLatest, broadCastTransactionPool} from './p2p'; + +/* import { getCoinbaseTransaction, isValidAddress, processTransactions, Transaction, UnspentTxOut } from './transaction'; -import {addToTransactionPool, getTransactionPool, updateTransactionPool} from './transactionPool'; +*/ +import { + getCoinbaseTransaction, isValidAddress, processTransactions, Transaction, Account, findAccount, getTransactionId +} from './transaction'; + +import {addToTransactionPool, getTransactionPool, updateTransactionPool, updateTransactionPoolReplace} from './transactionPool'; import {hexToBinary} from './util'; -import {createTransaction, findUnspentTxOuts, getBalance, getPrivateFromWallet, getPublicFromWallet} from './wallet'; +//import {createTransaction, findUnspentTxOuts, getBalance, getPrivateFromWallet, getPublicFromWallet} from './wallet'; +import {createTransaction, getBalance, getPrivateFromWallet, getPublicFromWallet} from './wallet'; class Block { @@ -30,6 +38,9 @@ class Block { } } +const getCurrentTimestamp = (): number => Math.round(new Date().getTime() / 1000); + +/* const genesisTransaction = { 'txIns': [{'signature': '', 'txOutId': '', 'txOutIndex': 0}], 'txOuts': [{ @@ -38,25 +49,47 @@ const genesisTransaction = { }], 'id': 'e655f6a5f26dc9b4cac6e46f52336428287759cf81ef5ff10854f69d68f43fa3' }; +*/ +const genesisTransaction = new Transaction('coinbase', + '04c0a0903203a721fa43581cc53c9361862e2e5522310909fbfbf14203371c464f5bdbba42e16349e237e1adf65952d8301e071a40a76b1709954c8f0e3f253b2c', 50); +genesisTransaction.timestamp = 1533641911; +genesisTransaction.id = getTransactionId(genesisTransaction); +genesisTransaction.signature = ''; +/* const genesisBlock: Block = new Block( 0, '91a73664bc84c0baa1fc75ea6e4aa6d1d20c5df664c724e3159aefc2e1186627', '', 1465154705, [genesisTransaction], 0, 0 ); +*/ +const genesisBlock: Block = new Block( + 0, '91a73664bc84c0baa1fc75ea6e4aa6d1d20c5df664c724e3159aefc2e1186627', '', 1533641911, [genesisTransaction], 0, 0 +); let blockchain: Block[] = [genesisBlock]; // the unspent txOut of genesis block is set to unspentTxOuts on startup -let unspentTxOuts: UnspentTxOut[] = processTransactions(blockchain[0].data, [], 0); +//let unspentTxOuts: UnspentTxOut[] = processTransactions(blockchain[0].data, []); +let accounts: Account[] = processTransactions(blockchain[0].data, []); const getBlockchain = (): Block[] => blockchain; -const getUnspentTxOuts = (): UnspentTxOut[] => _.cloneDeep(unspentTxOuts); +//const getUnspentTxOuts = (): UnspentTxOut[] => _.cloneDeep(unspentTxOuts); +//const getAccounts = (): Account[] => _.cloneDeep(accounts); +const getAccounts = (): Account[] => accounts; + + // and txPool should be only updated at the same time +/* const setUnspentTxOuts = (newUnspentTxOut: UnspentTxOut[]) => { console.log('replacing unspentTxouts with: %s', newUnspentTxOut); unspentTxOuts = newUnspentTxOut; }; +*/ +const setAccounts = (newAccount: Account[]) => { + //console.log('replacing accounts with: %s', newAccount); + accounts = newAccount; +}; const getLatestBlock = (): Block => blockchain[blockchain.length - 1]; @@ -88,8 +121,7 @@ const getAdjustedDifficulty = (latestBlock: Block, aBlockchain: Block[]) => { } }; -const getCurrentTimestamp = (): number => Math.round(new Date().getTime() / 1000); - +//// const generateRawNextBlock = (blockData: Transaction[]) => { const previousBlock: Block = getLatestBlock(); const difficulty: number = getDifficulty(getBlockchain()); @@ -106,16 +138,29 @@ const generateRawNextBlock = (blockData: Transaction[]) => { }; // gets the unspent transaction outputs owned by the wallet +/* const getMyUnspentTransactionOutputs = () => { return findUnspentTxOuts(getPublicFromWallet(), getUnspentTxOuts()); }; +*/ +const getMyAccount = () => { + return findAccount(getPublicFromWallet(), getAccounts()); +}; +/* const generateNextBlock = () => { const coinbaseTx: Transaction = getCoinbaseTransaction(getPublicFromWallet(), getLatestBlock().index + 1); const blockData: Transaction[] = [coinbaseTx].concat(getTransactionPool()); return generateRawNextBlock(blockData); }; +*/ +const generateNextBlock = () => { + const coinbaseTx: Transaction = getCoinbaseTransaction(getPublicFromWallet()); + const blockData: Transaction[] = [coinbaseTx].concat(getTransactionPool()); + return generateRawNextBlock(blockData); +}; +/* const generatenextBlockWithTransaction = (receiverAddress: string, amount: number) => { if (!isValidAddress(receiverAddress)) { throw Error('invalid address'); @@ -128,6 +173,26 @@ const generatenextBlockWithTransaction = (receiverAddress: string, amount: numbe const blockData: Transaction[] = [coinbaseTx, tx]; return generateRawNextBlock(blockData); }; +*/ +const generatenextBlockWithTransaction = (receiverAddress: string, amount: number) => { + let blockData: Transaction[]; + + if (!isValidAddress(receiverAddress)) { + throw Error('invalid address'); + } + if (typeof amount !== 'number') { + throw Error('invalid amount'); + } + const coinbaseTx: Transaction = getCoinbaseTransaction(getPublicFromWallet()); + const tx: Transaction = createTransaction(receiverAddress, amount, getPrivateFromWallet(), getAccounts(), getTransactionPool()); + if(tx == undefined){ + blockData = [coinbaseTx]; + console.log('The transaction is invalid. The block was mined.'); + }else{ + blockData = [coinbaseTx, tx]; + } + return generateRawNextBlock(blockData); +}; const findBlock = (index: number, previousHash: string, timestamp: number, data: Transaction[], difficulty: number): Block => { let nonce = 0; @@ -140,16 +205,34 @@ const findBlock = (index: number, previousHash: string, timestamp: number, data: } }; +/* const getAccountBalance = (): number => { return getBalance(getPublicFromWallet(), getUnspentTxOuts()); }; +*/ +const getAccountBalance = (): number => { + return getBalance(getPublicFromWallet(), getAccounts()); +}; +/* const sendTransaction = (address: string, amount: number): Transaction => { const tx: Transaction = createTransaction(address, amount, getPrivateFromWallet(), getUnspentTxOuts(), getTransactionPool()); addToTransactionPool(tx, getUnspentTxOuts()); broadCastTransactionPool(); return tx; }; +*/ +const sendTransaction = (address: string, amount: number): Transaction => { + const tx: Transaction = createTransaction(address, amount, getPrivateFromWallet(), getAccounts(), getTransactionPool()); + if(tx == undefined){ + console.log('This transaction cannot be created.'); + return undefined; + }else{ + addToTransactionPool(tx, getAccounts()); + broadCastTransactionPool(); + return tx; + } +}; const calculateHashForBlock = (block: Block): string => calculateHash(block.index, block.previousHash, block.timestamp, block.data, block.difficulty, block.nonce); @@ -225,6 +308,7 @@ const hashMatchesDifficulty = (hash: string, difficulty: number): boolean => { /* Checks if the given blockchain is valid. Return the unspent txOuts if the chain is valid */ +/* const isValidChain = (blockchainToValidate: Block[]): UnspentTxOut[] => { console.log('isValidChain:'); console.log(JSON.stringify(blockchainToValidate)); @@ -235,10 +319,10 @@ const isValidChain = (blockchainToValidate: Block[]): UnspentTxOut[] => { if (!isValidGenesis(blockchainToValidate[0])) { return null; } - /* - Validate each block in the chain. The block is valid if the block structure is valid - and the transaction are valid - */ + + //Validate each block in the chain. The block is valid if the block structure is valid + // and the transaction are valid + let aUnspentTxOuts: UnspentTxOut[] = []; for (let i = 0; i < blockchainToValidate.length; i++) { @@ -255,7 +339,38 @@ const isValidChain = (blockchainToValidate: Block[]): UnspentTxOut[] => { } return aUnspentTxOuts; }; +*/ +const isValidChain = (blockchainToValidate: Block[]): Account[] => { + //console.log(JSON.stringify(blockchainToValidate)); + const isValidGenesis = (block: Block): boolean => { + return JSON.stringify(block) === JSON.stringify(genesisBlock); + }; + + if (!isValidGenesis(blockchainToValidate[0])) { + return null; + } + /* + Validate each block in the chain. The block is valid if the block structure is valid + and the transaction are valid + */ + let accts: Account[] = []; + + for (let i = 0; i < blockchainToValidate.length; i++) { + const currentBlock: Block = blockchainToValidate[i]; + if (i !== 0 && !isValidNewBlock(blockchainToValidate[i], blockchainToValidate[i - 1])) { + return null; + } + + accts = processTransactions(currentBlock.data, accts); + if (accts === null) { + console.log('invalid transactions in blockchain'); + return null; + } + } + return accts; +}; +/* const addBlockToChain = (newBlock: Block): boolean => { if (isValidNewBlock(newBlock, getLatestBlock())) { const retVal: UnspentTxOut[] = processTransactions(newBlock.data, getUnspentTxOuts(), newBlock.index); @@ -271,7 +386,25 @@ const addBlockToChain = (newBlock: Block): boolean => { } return false; }; +*/ +const addBlockToChain = (newBlock: Block): boolean => { + if (isValidNewBlock(newBlock, getLatestBlock())) { + const retVal: Account[] = processTransactions(newBlock.data, getAccounts()); + if (retVal === null) { + console.log('block is not valid in terms of transactions'); + return false; + } else { + blockchain.push(newBlock); + setAccounts(retVal); + //updateTransactionPool(accounts); + updateTransactionPool(newBlock.data); + return true; + } + } + return false; +}; +/* const replaceChain = (newBlocks: Block[]) => { const aUnspentTxOuts = isValidChain(newBlocks); const validChain: boolean = aUnspentTxOuts !== null; @@ -286,14 +419,57 @@ const replaceChain = (newBlocks: Block[]) => { console.log('Received blockchain invalid'); } }; +*/ +const replaceChain = (newBlocks: Block[]) => { + const accts = isValidChain(newBlocks); + const validChain: boolean = accts !== null; + if (validChain && + getAccumulatedDifficulty(newBlocks) > getAccumulatedDifficulty(getBlockchain())) { + console.log('Received blockchain is valid. Replacing current blockchain with received blockchain'); + blockchain = newBlocks; + setAccounts(accts); + updateTransactionPoolReplace(accounts); + broadcastLatest(); + } else { + console.log('Received blockchain invalid'); + } +}; +/* const handleReceivedTransaction = (transaction: Transaction) => { addToTransactionPool(transaction, getUnspentTxOuts()); }; +*/ +const handleReceivedTransaction = (transaction: Transaction) => { + addToTransactionPool(transaction, getAccounts()); +}; +const validateAccount = (account: Account) => { +/* + if (!validateTransaction(tx, accounts)) { + throw Error('Trying to add invalid tx to pool. Tx: ' + tx.id); + } + + if (!isValidTxForPool(tx, transactionPool, accounts)) { + throw Error('Tx is not valid for the pool, Tx: ' + tx.id); + } + //console.log('adding to txPool: %s', JSON.stringify(tx)); + transactionPool.push(tx); +*/ +}; + + +/* export { Block, getBlockchain, getUnspentTxOuts, getLatestBlock, sendTransaction, generateRawNextBlock, generateNextBlock, generatenextBlockWithTransaction, handleReceivedTransaction, getMyUnspentTransactionOutputs, getAccountBalance, isValidBlockStructure, replaceChain, addBlockToChain }; +*/ +export { + Block, getBlockchain, getAccounts, setAccounts, getLatestBlock, sendTransaction, + generateRawNextBlock, generateNextBlock, generatenextBlockWithTransaction, + handleReceivedTransaction, validateAccount, getMyAccount, + getAccountBalance, isValidBlockStructure, replaceChain, addBlockToChain, getCurrentTimestamp, findBlock +}; diff --git a/Dewblock-cloud-server/src/config.ts b/Dewblock-cloud-server/src/config.ts new file mode 100644 index 0000000..4fa71da --- /dev/null +++ b/Dewblock-cloud-server/src/config.ts @@ -0,0 +1,12 @@ +let dewAddress = '127.0.0.1:6001'; +//let dewAddress = '192.168.1.186:6001'; +//let dewAddress = '192.168.1.114:6001'; +//If dew address is not correct, it can also be set using HTTP command. See Readme. +//Please notice: if only one machine is involved in testing, default config files are OK. +//If more than one machine is involved, config files should not use localhost or 127.0.0.1 at all. + + + +const getDew = (): string => {return dewAddress}; +const setDew = (address: string) => {dewAddress = address}; +export {getDew, setDew}; diff --git a/src/main.ts b/Dewblock-cloud-server/src/main.ts similarity index 67% rename from src/main.ts rename to Dewblock-cloud-server/src/main.ts index a272ffe..b496033 100644 --- a/src/main.ts +++ b/Dewblock-cloud-server/src/main.ts @@ -1,17 +1,31 @@ import * as bodyParser from 'body-parser'; import * as express from 'express'; import * as _ from 'lodash'; + +/* import { Block, generateNextBlock, generatenextBlockWithTransaction, generateRawNextBlock, getAccountBalance, getBlockchain, getMyUnspentTransactionOutputs, getUnspentTxOuts, sendTransaction } from './blockchain'; +*/ +import { + Block, generateNextBlock, generatenextBlockWithTransaction, generateRawNextBlock, getAccountBalance, + getBlockchain, getMyAccount, getAccounts, sendTransaction +} from './blockchain'; + import {connectToPeers, getSockets, initP2PServer} from './p2p'; -import {UnspentTxOut} from './transaction'; + +//import {UnspentTxOut} from './transaction'; +import {Account, findAccount} from './transaction'; + import {getTransactionPool} from './transactionPool'; import {getPublicFromWallet, initWallet} from './wallet'; -const httpPort: number = parseInt(process.env.HTTP_PORT) || 3001; -const p2pPort: number = parseInt(process.env.P2P_PORT) || 6001; +//dewcoin +import {getDew, setDew} from './config'; + +const httpPort: number = parseInt(process.env.HTTP_PORT) || 4001; +const p2pPort: number = parseInt(process.env.P2P_PORT) || 7001; const initHttpServer = (myHttpPort: number) => { const app = express(); @@ -23,6 +37,12 @@ const initHttpServer = (myHttpPort: number) => { } }); + app.post('/SetDewAddress', (req, res) => { + setDew(req.body.address); + console.log('Dew address: ' + getDew()); + res.send(); + }); + app.get('/blocks', (req, res) => { res.send(getBlockchain()); }); @@ -40,20 +60,48 @@ const initHttpServer = (myHttpPort: number) => { res.send(tx); }); + /* app.get('/address/:address', (req, res) => { const unspentTxOuts: UnspentTxOut[] = _.filter(getUnspentTxOuts(), (uTxO) => uTxO.address === req.params.address); res.send({'unspentTxOuts': unspentTxOuts}); }); + */ + app.get('/account/:address', (req, res) => { + let acc: Account = findAccount(req.params.address, getAccounts()); + if(acc == undefined){ + res.send({'Error:': "address is wrong"}); + } + res.send({'Account': acc}); + }); + /* app.get('/unspentTransactionOutputs', (req, res) => { res.send(getUnspentTxOuts()); }); + */ + //display all accounts + app.get('/accounts', (req, res) => { + res.send(getAccounts()); + }); + /* app.get('/myUnspentTransactionOutputs', (req, res) => { res.send(getMyUnspentTransactionOutputs()); }); + */ + /* + app.get('/myaccount', (req, res) => { + let acc: Account = findAccount(getPublicFromWallet(), getAccounts()); + if(acc == undefined){ + res.send({'Error': 'No account was found.'}) + }else{ + res.send({'My Account': acc}); + } + }); + */ + /* app.post('/mineRawBlock', (req, res) => { if (req.body.data == null) { res.send('data parameter is missing'); @@ -66,7 +114,8 @@ const initHttpServer = (myHttpPort: number) => { res.send(newBlock); } }); - + */ + /* app.post('/mineBlock', (req, res) => { const newBlock: Block = generateNextBlock(); if (newBlock === null) { @@ -75,17 +124,27 @@ const initHttpServer = (myHttpPort: number) => { res.send(newBlock); } }); - - app.get('/balance', (req, res) => { - const balance: number = getAccountBalance(); - res.send({'balance': balance}); + */ + + /* + app.get('/mybalance', (req, res) => { + let acc: Account = findAccount(getPublicFromWallet(), getAccounts()); + if(acc == undefined){ + res.send({'Error:': "No such account."}); + }else{ + res.send({'Balance': acc.balance}); + } }); + */ - app.get('/address', (req, res) => { + /* + app.get('/myaddress', (req, res) => { const address: string = getPublicFromWallet(); res.send({'address': address}); }); + */ + /* app.post('/mineTransaction', (req, res) => { const address = req.body.address; const amount = req.body.amount; @@ -97,7 +156,9 @@ const initHttpServer = (myHttpPort: number) => { res.status(400).send(e.message); } }); + */ + /* app.post('/sendTransaction', (req, res) => { try { const address = req.body.address; @@ -113,24 +174,29 @@ const initHttpServer = (myHttpPort: number) => { res.status(400).send(e.message); } }); + */ - app.get('/transactionPool', (req, res) => { + app.get('/transactionpool', (req, res) => { res.send(getTransactionPool()); }); app.get('/peers', (req, res) => { res.send(getSockets().map((s: any) => s._socket.remoteAddress + ':' + s._socket.remotePort)); }); - app.post('/addPeer', (req, res) => { + + /* + app.post('/addpeer', (req, res) => { connectToPeers(req.body.peer); res.send(); }); - + */ + app.post('/stop', (req, res) => { res.send({'msg' : 'stopping server'}); process.exit(); }); + app.listen(myHttpPort, () => { console.log('Listening http on port: ' + myHttpPort); }); @@ -138,4 +204,4 @@ const initHttpServer = (myHttpPort: number) => { initHttpServer(httpPort); initP2PServer(p2pPort); -initWallet(); +//initWallet(); diff --git a/Dewblock-cloud-server/src/p2p.ts b/Dewblock-cloud-server/src/p2p.ts new file mode 100644 index 0000000..d842328 --- /dev/null +++ b/Dewblock-cloud-server/src/p2p.ts @@ -0,0 +1,430 @@ +import * as WebSocket from 'ws'; +import {Server} from 'ws'; +/* +import { + addBlockToChain, Block, getBlockchain, getLatestBlock, handleReceivedTransaction, validateAccount, isValidBlockStructure, + replaceChain +} from './blockchain'; +*/ +import { + addBlockToChain, Block, getBlockchain, getLatestBlock, handleReceivedTransaction, validateAccount, isValidBlockStructure, + replaceChain, setAccounts, getAccounts, findBlock +} from './blockchain'; +//import {Transaction} from './transaction'; +import {Transaction, Account, verifyAccounts} from './transaction'; +import {getTransactionPool} from './transactionPool'; + +//dewcoin +import {getDew} from './config'; + +const sockets: WebSocket[] = []; +const getSockets = () => sockets; + +//dewcoin +let wsCloud: WebSocket; + + +enum MessageType { + QUERY_LATEST = 0, + QUERY_ALL = 1, + RESPONSE_BLOCKCHAIN = 2, + QUERY_TRANSACTION_POOL = 3, + RESPONSE_TRANSACTION_POOL = 4, + QUERY_ACCOUNTS = 5, + RESPONSE_ACCOUNTS = 6, + ALTERNATE_ADDRESS = 7, + MINING_REQUEST = 8 +} + +class Message { + public type: MessageType; + public data: any; +} + + +/* +const initP2PServer = (p2pPort: number) => { + const server: Server = new WebSocket.Server({port: p2pPort}); + server.on('connection', (ws: WebSocket) => { + initConnection(ws); + }); + console.log('listening websocket p2p port on: ' + p2pPort); +}; +*/ +const initP2PServer = (p2pPort: number) => { + const server: Server = new WebSocket.Server({port: p2pPort}); + server.on('connection', (ws: WebSocket, req) => { + console.log('New websocket connection from %s:%d', req.connection.remoteAddress.substring(7), req.connection.remotePort); + const dew: string = getDew(); + const address: string = dew.substring(0, dew.search(':')); + //console.log('Address: %s', address); + if(req.connection.remoteAddress.substring(7) == address){ + initCloudConnection(ws); + }else{ + initConnection(ws); + } + }); + console.log('listening websocket p2p port on: ' + p2pPort); +}; + + +/* +const initConnection = (ws: WebSocket) => { + sockets.push(ws); + initMessageHandler(ws); + initErrorHandler(ws); + write(ws, queryChainLengthMsg()); + + // query transactions pool only some time after chain query + setTimeout(() => { + broadcast(queryTransactionPoolMsg()); + }, 500); +}; +*/ +const initConnection = (ws: WebSocket) => { + console.log('A socket is created: ' + ws.url); + sockets.push(ws); + initMessageHandler(ws); + initErrorHandler(ws); + write(ws, queryChainLengthMsg()); + + // query transactions pool only some time after chain query + setTimeout(() => { + broadcast(queryTransactionPoolMsg()); + }, 500); +}; + + +const initCloudConnection = (ws: WebSocket) => { + console.log('Dew sent in connection.'); + wsCloud = ws; + initCloudMessageHandler(wsCloud); + wsCloud.on('error', () => { + console.log('connection with the dew failed'); + }); + wsCloud.on('close', () => { + console.log('connection with the dew was closed'); + }); + + write(wsCloud, queryChainLengthMsg()); + // query transactions pool only some time after chain query + setTimeout(() => { + write(wsCloud, queryTransactionPoolMsg()); + }, 500); +}; + + + +const JSONToObject = (data: string): T => { + try { + return JSON.parse(data); + } catch (e) { + console.log(e); + return null; + } +}; + +/* +const initMessageHandler = (ws: WebSocket) => { + ws.on('message', (data: string) => { + try { + const message: Message = JSONToObject(data); + if (message === null) { + console.log('could not parse received JSON message: ' + data); + return; + } + console.log('Received message: %s', JSON.stringify(message)); + switch (message.type) { + case MessageType.QUERY_LATEST: + write(ws, responseLatestMsg()); + break; + case MessageType.QUERY_ALL: + write(ws, responseChainMsg()); + break; + case MessageType.RESPONSE_BLOCKCHAIN: + const receivedBlocks: Block[] = JSONToObject(message.data); + if (receivedBlocks === null) { + console.log('invalid blocks received: %s', JSON.stringify(message.data)); + break; + } + handleBlockchainResponse(receivedBlocks); + break; + case MessageType.QUERY_TRANSACTION_POOL: + write(ws, responseTransactionPoolMsg()); + break; + case MessageType.RESPONSE_TRANSACTION_POOL: + const receivedTransactions: Transaction[] = JSONToObject(message.data); + if (receivedTransactions === null) { + console.log('invalid transaction received: %s', JSON.stringify(message.data)); + break; + } + receivedTransactions.forEach((transaction: Transaction) => { + try { + handleReceivedTransaction(transaction); + // if no error is thrown, transaction was indeed added to the pool + // let's broadcast transaction pool + broadCastTransactionPool(); + } catch (e) { + console.log(e.message); + } + }); + break; + } + } catch (e) { + console.log(e); + } + }); +}; +*/ +const initMessageHandler = (ws: WebSocket) => { + ws.on('message', (data: string) => { + + try { + const message: Message = JSONToObject(data); + if (message === null) { + console.log('could not parse received JSON message: ' + data); + return; + } + console.log('[Received message: %s', data); + switch (message.type) { + case MessageType.QUERY_LATEST: + write(ws, responseLatestMsg()); + break; + case MessageType.QUERY_ALL: + write(ws, responseChainMsg()); + break; + case MessageType.RESPONSE_BLOCKCHAIN: + const receivedBlocks: Block[] = JSONToObject(message.data); + if (receivedBlocks === null) { + console.log('invalid blocks received: %s', JSON.stringify(message.data)); + break; + } + handleBlockchainResponse(receivedBlocks); + break; + case MessageType.QUERY_TRANSACTION_POOL: + write(ws, responseTransactionPoolMsg()); + break; + case MessageType.RESPONSE_TRANSACTION_POOL: + const receivedTransactions: Transaction[] = JSONToObject(message.data); + if (receivedTransactions === null) { + console.log('invalid transaction received: %s', JSON.stringify(message.data)); + break; + } + receivedTransactions.forEach((transaction: Transaction) => { + try { + handleReceivedTransaction(transaction); + // if no error is thrown, transaction was indeed added to the pool + // let's broadcast transaction pool + broadCastTransactionPool(); + } catch (e) { + console.log(e.message); + } + }); + break; + case MessageType.ALTERNATE_ADDRESS: + //console.log('Alternate address received: %s', message.data); + //connectToPeers(message.data); + break; + } + } catch (e) { + console.log(e); + } + console.log('message processing]'); + }); +}; + + +const initCloudMessageHandler = (ws: WebSocket) => { + ws.on('message', (data: string) => { + //console.log('New message from %s:%d', req.connection.remoteAddress.substring(7), req.connection.remotePort); + + try { + const message: Message = JSONToObject(data); + if (message === null) { + console.log('could not parse received JSON message: ' + data); + return; + } + console.log('[Received cloud message: %s', data); + switch (message.type) { + case MessageType.QUERY_LATEST: + write(ws, responseLatestMsg()); + break; + case MessageType.QUERY_ALL: + write(ws, responseChainMsg()); + break; + case MessageType.RESPONSE_BLOCKCHAIN: + const receivedBlocks: Block[] = JSONToObject(message.data); + if (receivedBlocks === null) { + console.log('invalid blocks received: %s', JSON.stringify(message.data)); + break; + } + handleBlockchainResponse(receivedBlocks); + break; + case MessageType.QUERY_TRANSACTION_POOL: + write(ws, responseTransactionPoolMsg()); + break; + case MessageType.RESPONSE_TRANSACTION_POOL: + const receivedTransactions: Transaction[] = JSONToObject(message.data); + if (receivedTransactions === null) { + console.log('invalid transaction received: %s', JSON.stringify(message.data)); + break; + } + receivedTransactions.forEach((transaction: Transaction) => { + try { + handleReceivedTransaction(transaction); + // if no error is thrown, transaction was indeed added to the pool + // let's broadcast transaction pool + broadCastTransactionPool(); + } catch (e) { + console.log(e.message); + } + }); + break; + case MessageType.QUERY_ACCOUNTS: + const dewAccounts: Account[] = JSONToObject(message.data); + if (dewAccounts === null) { + console.log('invalid dew accounts received: %s', JSON.stringify(message.data)); + break; + } + const consistent: boolean = verifyAccounts(dewAccounts, getAccounts()); + if (!consistent){ + write(ws, responseAccountsMsg()); + } + break; + case MessageType.RESPONSE_ACCOUNTS: + const receivedAccounts: Account[] = JSONToObject(message.data); + if (receivedAccounts === null) { + console.log('invalid account received: %s', JSON.stringify(message.data)); + break; + } + receivedAccounts.forEach((account: Account) => { + try { + validateAccount(account); + // if no error is thrown, account was indeed added to the account pool + // let's broadcast account pool + // broadCastTransactionPool(); + } catch (e) { + console.log(e.message); + } + }); + setAccounts(receivedAccounts); + break; + case MessageType.MINING_REQUEST: + const aBlock: Block = JSONToObject(message.data); + if (aBlock === null) { + console.log('invalid block received: %s', JSON.stringify(message.data)); + break; + } + const newBlock: Block = findBlock(aBlock.index, aBlock.previousHash, aBlock.timestamp, aBlock.data, aBlock.difficulty); + if (addBlockToChain(newBlock)) { + broadcastLatest(); + } + break; + case MessageType.ALTERNATE_ADDRESS: + console.log('Alternate address received: %s', message.data); + connectToPeers('ws://' + message.data); + break; + } + } catch (e) { + console.log(e); + } + console.log('Cloud message processing]'); + }); +}; + + +const write = (ws: WebSocket, message: Message): void => ws.send(JSON.stringify(message)); +//const broadcast = (message: Message): void => sockets.forEach((socket) => write(socket, message)); +const broadcast = (message: Message): void => { + sockets.forEach((socket) => write(socket, message)); + if(wsCloud.readyState == 1){ + write(wsCloud, message); + } +} + +const queryChainLengthMsg = (): Message => ({'type': MessageType.QUERY_LATEST, 'data': null}); + +const queryAllMsg = (): Message => ({'type': MessageType.QUERY_ALL, 'data': null}); + +const responseChainMsg = (): Message => ({ + 'type': MessageType.RESPONSE_BLOCKCHAIN, 'data': JSON.stringify(getBlockchain()) +}); + +const responseLatestMsg = (): Message => ({ + 'type': MessageType.RESPONSE_BLOCKCHAIN, + 'data': JSON.stringify([getLatestBlock()]) +}); + +const queryTransactionPoolMsg = (): Message => ({ + 'type': MessageType.QUERY_TRANSACTION_POOL, + 'data': null +}); + +const responseTransactionPoolMsg = (): Message => ({ + 'type': MessageType.RESPONSE_TRANSACTION_POOL, + 'data': JSON.stringify(getTransactionPool()) +}); + +const responseAccountsMsg = (): Message => ({ + 'type': MessageType.RESPONSE_ACCOUNTS, + 'data': JSON.stringify(getAccounts()) +}); + +const initErrorHandler = (ws: WebSocket) => { + const closeConnection = (myWs: WebSocket) => { + console.log('connection failed to peer: ' + myWs.url); + sockets.splice(sockets.indexOf(myWs), 1); + }; + ws.on('close', () => closeConnection(ws)); + ws.on('error', () => closeConnection(ws)); +}; + +const handleBlockchainResponse = (receivedBlocks: Block[]) => { + if (receivedBlocks.length === 0) { + console.log('received block chain size of 0'); + return; + } + const latestBlockReceived: Block = receivedBlocks[receivedBlocks.length - 1]; + if (!isValidBlockStructure(latestBlockReceived)) { + console.log('block structuture not valid'); + return; + } + const latestBlockHeld: Block = getLatestBlock(); + if (latestBlockReceived.index > latestBlockHeld.index) { + console.log('blockchain possibly behind. We got: ' + + latestBlockHeld.index + ' Peer got: ' + latestBlockReceived.index); + if (latestBlockHeld.hash === latestBlockReceived.previousHash) { + if (addBlockToChain(latestBlockReceived)) { + broadcast(responseLatestMsg()); + } + } else if (receivedBlocks.length === 1) { + console.log('We have to query the chain from our peer'); + broadcast(queryAllMsg()); + } else { + console.log('Received blockchain is longer than current blockchain'); + replaceChain(receivedBlocks); + } + } else { + console.log('received blockchain is not longer than received blockchain. Do nothing'); + } +}; + +const broadcastLatest = (): void => { + broadcast(responseLatestMsg()); +}; + +const connectToPeers = (newPeer: string): void => { + const ws: WebSocket = new WebSocket(newPeer); + ws.on('open', () => { + initConnection(ws); + }); + ws.on('error', () => { + console.log('connection failed'); + }); +}; + +const broadCastTransactionPool = () => { + broadcast(responseTransactionPoolMsg()); +}; + +export {connectToPeers, broadcastLatest, broadCastTransactionPool, initP2PServer, getSockets}; diff --git a/Dewblock-cloud-server/src/transaction.ts b/Dewblock-cloud-server/src/transaction.ts new file mode 100644 index 0000000..a050427 --- /dev/null +++ b/Dewblock-cloud-server/src/transaction.ts @@ -0,0 +1,695 @@ +import * as CryptoJS from 'crypto-js'; +import * as ecdsa from 'elliptic'; +import * as _ from 'lodash'; + +//dewcoin +import {getCurrentTimestamp} from './blockchain'; +import {getTransactionPool} from './transactionPool';//debug + +const ec = new ecdsa.ec('secp256k1'); +const COINBASE_AMOUNT: number = 50; + +//dewcoin +const ACCOUNT_ACTIVE_PERIOD: number = 60*60*24*31; //31 days. +const ACCOUNT_PURGE_PERIOD: number = 60*60*24*32; //32 days. + +/* +class UnspentTxOut { + public readonly txOutId: string; + public readonly txOutIndex: number; + public readonly address: string; + public readonly amount: number; + + constructor(txOutId: string, txOutIndex: number, address: string, amount: number) { + this.txOutId = txOutId; + this.txOutIndex = txOutIndex; + this.address = address; + this.amount = amount; + } +} +*/ +class Account { + public readonly address: string; + public balance: number; + public available: number; + public txHistory: TxHistory[]; + + constructor(address: string) { + this.address = address; + this.balance = 0; + this.available = 0; + this.txHistory = []; + } +} + + + +/* +class TxIn { + public txOutId: string; + public txOutIndex: number; + public signature: string; +} + +class TxOut { + public address: string; + public amount: number; + + constructor(address: string, amount: number) { + this.address = address; + this.amount = amount; + } +} +class Transaction { + + public id: string; + + public txIns: TxIn[]; + public txOuts: TxOut[]; +} +*/ +class Transaction { + public id: string; + public sender: string; + public receiver: string; + public amount: number; + public timestamp: number; + public signature: string; + + constructor(sender: string, receiver: string, amount: number) { + this.sender = sender; + this.receiver = receiver; + this.amount = amount; + } +} +class TxHistory { + public id: string; + public txAccount: string; //the account that this transaction involvs. + public prevBalance: number; + public amount: number; // plus or minus. reflect the change of the balance. + public afterBalance: number; + public timestamp: number; + + constructor(txAccount: string, amount: number) { + this.txAccount = txAccount; + this.amount = amount; + } +} + + +const verifyAccounts = (accounts1: Account[], accounts2: Account[]): boolean => { + //console.log('accounts1: ' + JSON.stringify(accounts1)); + //console.log('accounts2: ' + JSON.stringify(accounts2)); + const a1: Account[] = _.sortBy(accounts1, 'id'); + const a2: Account[] = _.sortBy(accounts2, 'id'); + const s1: string = JSON.stringify(a1); + const s2: string = JSON.stringify(a2); + //console.log('s1:' + s1); + //console.log('s2:' + s2); + return s1 === s2; +} + + + +/* +const getTransactionId = (transaction: Transaction): string => { + const txInContent: string = transaction.txIns + .map((txIn: TxIn) => txIn.txOutId + txIn.txOutIndex) + .reduce((a, b) => a + b, ''); + + const txOutContent: string = transaction.txOuts + .map((txOut: TxOut) => txOut.address + txOut.amount) + .reduce((a, b) => a + b, ''); + + return CryptoJS.SHA256(txInContent + txOutContent).toString(); +}; +*/ +const getTransactionId = (transaction: Transaction): string => { + return CryptoJS.SHA256(transaction.sender + transaction.receiver + transaction.amount + transaction.timestamp).toString(); +}; + + + +/* +const validateTransaction = (transaction: Transaction, aUnspentTxOuts: UnspentTxOut[]): boolean => { + + if (!isValidTransactionStructure(transaction)) { + return false; + } + + if (getTransactionId(transaction) !== transaction.id) { + console.log('invalid tx id: ' + transaction.id); + return false; + } + const hasValidTxIns: boolean = transaction.txIns + .map((txIn) => validateTxIn(txIn, transaction, aUnspentTxOuts)) + .reduce((a, b) => a && b, true); + + if (!hasValidTxIns) { + console.log('some of the txIns are invalid in tx: ' + transaction.id); + return false; + } + + const totalTxInValues: number = transaction.txIns + .map((txIn) => getTxInAmount(txIn, aUnspentTxOuts)) + .reduce((a, b) => (a + b), 0); + + const totalTxOutValues: number = transaction.txOuts + .map((txOut) => txOut.amount) + .reduce((a, b) => (a + b), 0); + + if (totalTxOutValues !== totalTxInValues) { + console.log('totalTxOutValues !== totalTxInValues in tx: ' + transaction.id); + return false; + } + + return true; +}; +*/ +const existAccount = (address: string, accounts: Account[]): boolean => { + return _(accounts).map((acc: Account) => acc.address).includes(address); +} +const createAccount = (address: string, accounts: Account[]): boolean => { + let acc: Account = new Account(address); + accounts.push(acc); + return true; +} +const findAccount = (address: string, accounts: Account[]): Account => { + let account: Account = _.find(accounts, (acc: Account) => {return acc.address == address}); + return account; +} +const validateTransaction = (transaction: Transaction, accounts: Account[]): boolean => { + + if (!isValidTransactionStructure(transaction)) { + return false; + } + + if (getTransactionId(transaction) !== transaction.id) { + console.log('validateTransaction: invalid tx id: ' + transaction.id); + console.log(getTransactionId(transaction)); + return false; + } + + const hasValidSignature: boolean = validateSignature(transaction); + if (!hasValidSignature) { + console.log('The signature is invalid in tx: ' + transaction.id); + return false; + } + if (transaction.timestamp < (getCurrentTimestamp() - ACCOUNT_ACTIVE_PERIOD)){ + console.log('The transaction is too old. tx: ' + transaction.id); + return false; + } + let accSender: Account = findAccount(transaction.sender, accounts); + if(accSender == undefined){ + console.log('validateTransaction: no account found.'); + return false; + } + if (_(accSender.txHistory).map((th: TxHistory) => {return th.id}).includes(transaction.id)){ + console.log(JSON.stringify(getTransactionPool())); + console.log('validateTransaction: The transaction is duplicated with the chain. tx: ' + transaction.id); + console.log(JSON.stringify(accSender.txHistory)); + return false; + } + + return true; +}; + + + +/* +const validateBlockTransactions = (aTransactions: Transaction[], aUnspentTxOuts: UnspentTxOut[], blockIndex: number): boolean => { + const coinbaseTx = aTransactions[0]; + if (!validateCoinbaseTx(coinbaseTx, blockIndex)) { + console.log('invalid coinbase transaction: ' + JSON.stringify(coinbaseTx)); + return false; + } + + const txIns: TxIn[] = _(aTransactions) + .map((tx) => tx.txIns) + .flatten() + .value(); + + if (hasDuplicates(txIns)) { + return false; + } + + // all but coinbase transactions + const normalTransactions: Transaction[] = aTransactions.slice(1); + return normalTransactions.map((tx) => validateTransaction(tx, aUnspentTxOuts)) + .reduce((a, b) => (a && b), true); + +}; +*/ +const validateBlockTransactions = (aTransactions: Transaction[], accounts: Account[]): boolean => { + let accSender: Account; + + const coinbaseTx = aTransactions[0]; + if (!validateCoinbaseTx(coinbaseTx)) { + console.log('invalid coinbase transaction: ' + JSON.stringify(coinbaseTx)); + return false; + } + const txIds: string[] = _(aTransactions) + .map((tx) => tx.id) + .value(); + + if (hasDuplicates(txIds)) { + console.log('Transactions in block are duplicated.'); + return false; + } + + for(let i=0; i < accounts.length; i++){ + accounts[i].available = accounts[i].balance; + } + for(let j=1; j < aTransactions.length; j++){ + accSender = findAccount(aTransactions[j].sender, accounts); + if(accSender == undefined){ + console.log('validateBlockTransactions: no account found.'); + return false; + } + if (aTransactions[j].amount > accSender.available) { + console.log('The sender does not have enough coins. tx: ' + aTransactions[j].id); + return false; + } + accSender.available -=aTransactions[j].amount; + } + // all but coinbase transactions + const normalTransactions: Transaction[] = aTransactions.slice(1); + return normalTransactions.map((tx) => validateTransaction(tx, accounts)) + .reduce((a, b) => (a && b), true); +}; + + +/* +const hasDuplicates = (txIns: TxIn[]): boolean => { + const groups = _.countBy(txIns, (txIn: TxIn) => txIn.txOutId + txIn.txOutIndex); + return _(groups) + .map((value, key) => { + if (value > 1) { + console.log('duplicate txIn: ' + key); + return true; + } else { + return false; + } + }) + .includes(true); +}; +*/ +const hasDuplicates = (names: string[]): boolean => { + const groups = _.countBy(names); + return _(groups) + .map((value, key) => { + if (value > 1) { + console.log('duplicate string: ' + key); + return true; + } else { + return false; + } + }) + .includes(true); +}; + + +/* +const validateCoinbaseTx = (transaction: Transaction, blockIndex: number): boolean => { + if (transaction == null) { + console.log('the first transaction in the block must be coinbase transaction'); + return false; + } + if (getTransactionId(transaction) !== transaction.id) { + console.log('invalid coinbase tx id: ' + transaction.id); + return false; + } + if (transaction.txIns.length !== 1) { + console.log('one txIn must be specified in the coinbase transaction'); + return; + } + if (transaction.txIns[0].txOutIndex !== blockIndex) { + console.log('the txIn signature in coinbase tx must be the block height'); + return false; + } + if (transaction.txOuts.length !== 1) { + console.log('invalid number of txOuts in coinbase transaction'); + return false; + } + if (transaction.txOuts[0].amount !== COINBASE_AMOUNT) { + console.log('invalid coinbase amount in coinbase transaction'); + return false; + } + return true; +}; +*/ +const validateCoinbaseTx = (transaction: Transaction): boolean => { + if (transaction == null) { + console.log('the first transaction in the block must be coinbase transaction'); + return false; + } + if (getTransactionId(transaction) !== transaction.id) { + console.log('invalid coinbase tx id: ' + transaction.id); + return false; + } + if (transaction.sender !== "coinbase") { + console.log('It is not a coinbase transaction'); + return; + } + if (transaction.amount !== COINBASE_AMOUNT) { + console.log('invalid coinbase amount in coinbase transaction'); + return false; + } + return true; +}; + + + +/* +const validateTxIn = (txIn: TxIn, transaction: Transaction, aUnspentTxOuts: UnspentTxOut[]): boolean => { + const referencedUTxOut: UnspentTxOut = + aUnspentTxOuts.find((uTxO) => uTxO.txOutId === txIn.txOutId && uTxO.txOutIndex === txIn.txOutIndex); + if (referencedUTxOut == null) { + console.log('referenced txOut not found: ' + JSON.stringify(txIn)); + return false; + } + const address = referencedUTxOut.address; + + const key = ec.keyFromPublic(address, 'hex'); + const validSignature: boolean = key.verify(transaction.id, txIn.signature); + if (!validSignature) { + console.log('invalid txIn signature: %s txId: %s address: %s', txIn.signature, transaction.id, referencedUTxOut.address); + return false; + } + return true; +}; +*/ +const validateSignature = (transaction: Transaction): boolean => { + const key = ec.keyFromPublic(transaction.sender, 'hex'); + const validSignature: boolean = key.verify(transaction.id, transaction.signature); + if (!validSignature) { + console.log('invalid tx signature: %s txId: %s address: %s', transaction.signature, transaction.id, transaction.sender); + return false; + } + return true; +}; + + + +/* +const getTxInAmount = (txIn: TxIn, aUnspentTxOuts: UnspentTxOut[]): number => { + return findUnspentTxOut(txIn.txOutId, txIn.txOutIndex, aUnspentTxOuts).amount; +}; +const findUnspentTxOut = (transactionId: string, index: number, aUnspentTxOuts: UnspentTxOut[]): UnspentTxOut => { + return aUnspentTxOuts.find((uTxO) => uTxO.txOutId === transactionId && uTxO.txOutIndex === index); +}; +*/ + + +/* +const getCoinbaseTransaction = (address: string, blockIndex: number): Transaction => { + const t = new Transaction(); + const txIn: TxIn = new TxIn(); + txIn.signature = ''; + txIn.txOutId = ''; + txIn.txOutIndex = blockIndex; + + t.txIns = [txIn]; + t.txOuts = [new TxOut(address, COINBASE_AMOUNT)]; + t.id = getTransactionId(t); + return t; +}; +*/ +const getCoinbaseTransaction = (miner: string): Transaction => { + const t = new Transaction("coinbase", miner, COINBASE_AMOUNT); + t.timestamp = getCurrentTimestamp(); + t.id = getTransactionId(t); + t.signature = ''; + return t; +}; + + +/* +const signTxIn = (transaction: Transaction, txInIndex: number, + privateKey: string, aUnspentTxOuts: UnspentTxOut[]): string => { + const txIn: TxIn = transaction.txIns[txInIndex]; + + const dataToSign = transaction.id; + const referencedUnspentTxOut: UnspentTxOut = findUnspentTxOut(txIn.txOutId, txIn.txOutIndex, aUnspentTxOuts); + if (referencedUnspentTxOut == null) { + console.log('could not find referenced txOut'); + throw Error(); + } + const referencedAddress = referencedUnspentTxOut.address; + + if (getPublicKey(privateKey) !== referencedAddress) { + console.log('trying to sign an input with private' + + ' key that does not match the address that is referenced in txIn'); + throw Error(); + } + const key = ec.keyFromPrivate(privateKey, 'hex'); + const signature: string = toHexString(key.sign(dataToSign).toDER()); + + return signature; +}; +*/ +const signTransaction = (transaction: Transaction, privateKey: string): string => { + const dataToSign = transaction.id; + if (getPublicKey(privateKey) !== transaction.sender) { + console.log('trying to sign an input with private' + + ' key that does not match the address'); + throw Error(); + } + const key = ec.keyFromPrivate(privateKey, 'hex'); + const signature: string = toHexString(key.sign(dataToSign).toDER()); + + return signature; +}; + + +/* +const updateUnspentTxOuts = (aTransactions: Transaction[], aUnspentTxOuts: UnspentTxOut[]): UnspentTxOut[] => { + const newUnspentTxOuts: UnspentTxOut[] = aTransactions + .map((t) => { + return t.txOuts.map((txOut, index) => new UnspentTxOut(t.id, index, txOut.address, txOut.amount)); + }) + .reduce((a, b) => a.concat(b), []); + + const consumedTxOuts: UnspentTxOut[] = aTransactions + .map((t) => t.txIns) + .reduce((a, b) => a.concat(b), []) + .map((txIn) => new UnspentTxOut(txIn.txOutId, txIn.txOutIndex, '', 0)); + + const resultingUnspentTxOuts = aUnspentTxOuts + .filter(((uTxO) => !findUnspentTxOut(uTxO.txOutId, uTxO.txOutIndex, consumedTxOuts))) + .concat(newUnspentTxOuts); + + return resultingUnspentTxOuts; +}; +*/ +const updateAccounts = (aTransactions: Transaction[], accounts: Account[]): Account[] => { + let amt: number; + let thSender: TxHistory; + let thReceiver: TxHistory; + let accSender: Account; + let accReceiver: Account; + let now: number; + + for(let i=0; i {return (th.timestamp < (now + ACCOUNT_PURGE_PERIOD))}).push(thSender).value(); + //clean txHistory at the same time. + } + if (!existAccount(aTransactions[i].receiver, accounts)) { + createAccount(aTransactions[i].receiver, accounts); + } + accReceiver = findAccount(aTransactions[i].receiver, accounts); + if(accReceiver == undefined){ + console.log('validateTransaction: no account found.'); + return undefined; + } + thReceiver = new TxHistory(aTransactions[i].sender, amt); + thReceiver.id = aTransactions[i].id; + thReceiver.timestamp = aTransactions[i].timestamp; + thReceiver.prevBalance = accReceiver.balance; + accReceiver.balance += amt; + accReceiver.available = accReceiver.balance; + thReceiver.afterBalance = accReceiver.balance; + accReceiver.txHistory = _(accReceiver.txHistory).filter((th: TxHistory) => {return (th.timestamp < (now + ACCOUNT_PURGE_PERIOD))}).push(thReceiver).value(); + //clean txHistory at the same time. + } + return accounts;//check again +}; + + +/* +const processTransactions = (aTransactions: Transaction[], aUnspentTxOuts: UnspentTxOut[], blockIndex: number) => { + + if (!validateBlockTransactions(aTransactions, aUnspentTxOuts, blockIndex)) { + console.log('invalid block transactions'); + return null; + } + return updateUnspentTxOuts(aTransactions, aUnspentTxOuts); +}; +*/ +const processTransactions = (aTransactions: Transaction[], accounts: Account[]) => { + + if (!validateBlockTransactions(aTransactions, accounts)) { + console.log('invalid block transactions'); + return null; + } + return updateAccounts(aTransactions, accounts); +}; + + + + +const toHexString = (byteArray): string => { + return Array.from(byteArray, (byte: any) => { + return ('0' + (byte & 0xFF).toString(16)).slice(-2); + }).join(''); +}; + +const getPublicKey = (aPrivateKey: string): string => { + return ec.keyFromPrivate(aPrivateKey, 'hex').getPublic().encode('hex'); +}; + + +/* +const isValidTxInStructure = (txIn: TxIn): boolean => { + if (txIn == null) { + console.log('txIn is null'); + return false; + } else if (typeof txIn.signature !== 'string') { + console.log('invalid signature type in txIn'); + return false; + } else if (typeof txIn.txOutId !== 'string') { + console.log('invalid txOutId type in txIn'); + return false; + } else if (typeof txIn.txOutIndex !== 'number') { + console.log('invalid txOutIndex type in txIn'); + return false; + } else { + return true; + } +}; +const isValidTxOutStructure = (txOut: TxOut): boolean => { + if (txOut == null) { + console.log('txOut is null'); + return false; + } else if (typeof txOut.address !== 'string') { + console.log('invalid address type in txOut'); + return false; + } else if (!isValidAddress(txOut.address)) { + console.log('invalid TxOut address'); + return false; + } else if (typeof txOut.amount !== 'number') { + console.log('invalid amount type in txOut'); + return false; + } else { + return true; + } +}; +*/ + + + +/* +const isValidTransactionStructure = (transaction: Transaction) => { + if (typeof transaction.id !== 'string') { + console.log('transactionId missing'); + return false; + } + if (!(transaction.txIns instanceof Array)) { + console.log('invalid txIns type in transaction'); + return false; + } + if (!transaction.txIns + .map(isValidTxInStructure) + .reduce((a, b) => (a && b), true)) { + return false; + } + + if (!(transaction.txOuts instanceof Array)) { + console.log('invalid txIns type in transaction'); + return false; + } + + if (!transaction.txOuts + .map(isValidTxOutStructure) + .reduce((a, b) => (a && b), true)) { + return false; + } + return true; +}; +*/ +const isValidTransactionStructure = (transaction: Transaction) => { + if (typeof transaction.id !== 'string') { + console.log('transactionId missing'); + return false; + } + if (typeof transaction.sender !== 'string') { + console.log('transaction sender missing'); + return false; + } + if (typeof transaction.receiver !== 'string') { + console.log('transaction receiver missing'); + return false; + } + if (typeof transaction.amount !== 'number') { + console.log('transaction amount missing'); + return false; + } + if (typeof transaction.timestamp !== 'number') { + console.log('transaction timestamp missing'); + return false; + } + if (typeof transaction.signature !== 'string') { + console.log('transaction signature missing'); + return false; + } + return true; +}; + + +// valid address is a valid ecdsa public key in the 04 + X-coordinate + Y-coordinate format +const isValidAddress = (address: string): boolean => { + if (address.length !== 130) { + console.log(address); + console.log('invalid public key length'); + return false; + } else if (address.match('^[a-fA-F0-9]+$') === null) { + console.log('public key must contain only hex characters'); + return false; + } else if (!address.startsWith('04')) { + console.log('public key must start with 04'); + return false; + } + return true; +}; + + +/* +export { + processTransactions, signTxIn, getTransactionId, isValidAddress, validateTransaction, + UnspentTxOut, TxIn, TxOut, getCoinbaseTransaction, getPublicKey, hasDuplicates, + Transaction +}; +*/ +export { + processTransactions, signTransaction, getTransactionId, isValidAddress, validateTransaction, + Account, findAccount, existAccount, createAccount, verifyAccounts, getCoinbaseTransaction, getPublicKey, hasDuplicates, + Transaction +}; diff --git a/Dewblock-cloud-server/src/transactionPool.ts b/Dewblock-cloud-server/src/transactionPool.ts new file mode 100644 index 0000000..4d20902 --- /dev/null +++ b/Dewblock-cloud-server/src/transactionPool.ts @@ -0,0 +1,166 @@ +import * as _ from 'lodash'; + +//import {Transaction, TxIn, UnspentTxOut, validateTransaction} from './transaction'; +import {Transaction, Account, findAccount, validateTransaction} from './transaction'; + +let transactionPool: Transaction[] = []; + +const getTransactionPool = () => { + return _.cloneDeep(transactionPool); +}; + +/* +const addToTransactionPool = (tx: Transaction, unspentTxOuts: UnspentTxOut[]) => { + + if (!validateTransaction(tx, unspentTxOuts)) { + throw Error('Trying to add invalid tx to pool'); + } + + if (!isValidTxForPool(tx, transactionPool)) { + throw Error('Trying to add invalid tx to pool'); + } + console.log('adding to txPool: %s', JSON.stringify(tx)); + transactionPool.push(tx); +}; +*/ +const addToTransactionPool = (tx: Transaction, accounts: Account[]) => { + + if (!validateTransaction(tx, accounts)) { + throw Error('Trying to add invalid tx to pool. Tx: ' + tx.id); + } + + if (!isValidTxForPool(tx, transactionPool, accounts)) { + throw Error('Tx is not valid for the pool, Tx: ' + tx.id); + } + //console.log('adding to txPool: %s', JSON.stringify(tx)); + transactionPool.push(tx); +}; + +/* +const hasTxIn = (txIn: TxIn, unspentTxOuts: UnspentTxOut[]): boolean => { + const foundTxIn = unspentTxOuts.find((uTxO: UnspentTxOut) => { + return uTxO.txOutId === txIn.txOutId && uTxO.txOutIndex === txIn.txOutIndex; + }); + return foundTxIn !== undefined; +}; +*/ + +/* +const updateTransactionPool = (unspentTxOuts: UnspentTxOut[]) => { + const invalidTxs = []; + for (const tx of transactionPool) { + for (const txIn of tx.txIns) { + if (!hasTxIn(txIn, unspentTxOuts)) { + invalidTxs.push(tx); + break; + } + } + } + if (invalidTxs.length > 0) { + console.log('removing the following transactions from txPool: %s', JSON.stringify(invalidTxs)); + transactionPool = _.without(transactionPool, ...invalidTxs); + } +}; +*/ +const updateTransactionPoolReplace = (accounts: Account[]) => { + let accSender; + + transactionPool = _(transactionPool).uniqBy('id').filter(tx => validateTransaction(tx, accounts)).value(); + + for(let i=0; i < accounts.length; i++){ + accounts[i].available = accounts[i].balance; + } + const invalidTxs = []; + for (const tx of transactionPool) { + accSender = findAccount(tx.sender, accounts); + if(accSender == undefined){ + console.log('updateTransactionPool: no account found.'); + return false; + } + if (tx.amount > accSender.available) { + console.log('The sender does not have enough coins. Transaction be removed from pool. tx: ' + tx.id); + invalidTxs.push(tx); + }else{ + accSender.available -=tx.amount; + } + } + if (invalidTxs.length > 0) { + console.log('removing the following transactions from txPool: %s', JSON.stringify(invalidTxs)); + transactionPool = _.without(transactionPool, ...invalidTxs); + } +}; +const updateTransactionPool = (transactions: Transaction[]) => { + //console.log('tansactionPool: '+JSON.stringify(transactionPool)); + if (transactions.length > 0) { + //console.log('removing the following transactions from txPool: %s', JSON.stringify(transactions)); + let txIDs: string[] = _.map(transactions, (tx: Transaction) => tx.id); + transactionPool = _.filter(transactionPool, (tx: Transaction) => !_.includes(txIDs, tx.id)); + } + //console.log('tansactionPool: '+JSON.stringify(transactionPool)); +}; + +/* +const getTxPoolIns = (aTransactionPool: Transaction[]): TxIn[] => { + return _(aTransactionPool) + .map((tx) => tx.txIns) + .flatten() + .value(); +}; +*/ + +/* +const isValidTxForPool = (tx: Transaction, aTtransactionPool: Transaction[]): boolean => { + const txPoolIns: TxIn[] = getTxPoolIns(aTtransactionPool); + + const containsTxIn = (txIns: TxIn[], txIn: TxIn) => { + return _.find(txPoolIns, ((txPoolIn) => { + return txIn.txOutIndex === txPoolIn.txOutIndex && txIn.txOutId === txPoolIn.txOutId; + })); + }; + + for (const txIn of tx.txIns) { + if (containsTxIn(txPoolIns, txIn)) { + console.log('txIn already found in the txPool'); + return false; + } + } + return true; +}; +*/ +// This transaction tx has been validated in another function. id duplicate was checked in validation. +const isValidTxForPool = (tx: Transaction, aTransactionPool: Transaction[], accounts: Account[]): boolean => { + let accSender: Account; + + for(let i=0; i < accounts.length; i++){ + accounts[i].available = accounts[i].balance; + } + for (const trx of aTransactionPool) { + if(tx.id == trx.id){ + console.log('The transaction is duplicated with the existing transaction pool. trx: ' + trx.id); + return false; + } + accSender = findAccount(trx.sender, accounts); + if(accSender == undefined){ + console.log('isValideTxFor Pool: no account found.'); + return false; + } + if (trx.amount > accSender.available) { + console.log('The sender does not have enough coins. The existing transaction pool has problems. trx: ' + trx.id); + return false; + }else{ + accSender.available -= trx.amount; + } + } + accSender = findAccount(tx.sender, accounts); + if(accSender == undefined){ + console.log('isValidTxForPool: no account found.'); + return false; + } + if (tx.amount > accSender.available) { + console.log('The transaction is not valid for the pool. tx: ' + tx.id); + return false; + } + return true; +}; + +export {addToTransactionPool, getTransactionPool, updateTransactionPool, updateTransactionPoolReplace}; diff --git a/src/util.ts b/Dewblock-cloud-server/src/util.ts similarity index 100% rename from src/util.ts rename to Dewblock-cloud-server/src/util.ts diff --git a/src/wallet.ts b/Dewblock-cloud-server/src/wallet.ts similarity index 71% rename from src/wallet.ts rename to Dewblock-cloud-server/src/wallet.ts index 315a939..4760054 100644 --- a/src/wallet.ts +++ b/Dewblock-cloud-server/src/wallet.ts @@ -1,7 +1,11 @@ import {ec} from 'elliptic'; import {existsSync, readFileSync, unlinkSync, writeFileSync} from 'fs'; import * as _ from 'lodash'; -import {getPublicKey, getTransactionId, signTxIn, Transaction, TxIn, TxOut, UnspentTxOut} from './transaction'; +//import {getPublicKey, getTransactionId, signTxIn, Transaction, TxIn, TxOut, UnspentTxOut} from './transaction'; +import {getPublicKey, getTransactionId, signTransaction, Transaction, Account, findAccount, existAccount, createAccount} from './transaction'; + +//dewcoin +import {getCurrentTimestamp, getAccounts} from './blockchain'; const EC = new ec('secp256k1'); const privateKeyLocation = process.env.PRIVATE_KEY || 'node/wallet/private_key'; @@ -26,12 +30,20 @@ const generatePrivateKey = (): string => { const initWallet = () => { // let's not override existing private keys if (existsSync(privateKeyLocation)) { + if (!existAccount(getPublicFromWallet(), getAccounts())) { + createAccount(getPublicFromWallet(), getAccounts()); + console.log('an account was created.'); + } return; } const newPrivateKey = generatePrivateKey(); writeFileSync(privateKeyLocation, newPrivateKey); console.log('new wallet with private key created to : %s', privateKeyLocation); + if (!existAccount(getPublicFromWallet(), getAccounts())) { + createAccount(getPublicFromWallet(), getAccounts()); + console.log('an account was created.'); + } }; const deleteWallet = () => { @@ -40,16 +52,29 @@ const deleteWallet = () => { } }; +/* const getBalance = (address: string, unspentTxOuts: UnspentTxOut[]): number => { return _(findUnspentTxOuts(address, unspentTxOuts)) .map((uTxO: UnspentTxOut) => uTxO.amount) .sum(); }; +*/ +const getBalance = (address: string, accounts: Account[]): number => { + let acct: Account = findAccount(address, accounts); + if(acct == undefined){ + console.log('getBalance: no account found.'); + return 0; + } + return acct.balance; +}; +/* const findUnspentTxOuts = (ownerAddress: string, unspentTxOuts: UnspentTxOut[]) => { return _.filter(unspentTxOuts, (uTxO: UnspentTxOut) => uTxO.address === ownerAddress); }; +*/ +/* const findTxOutsForAmount = (amount: number, myUnspentTxOuts: UnspentTxOut[]) => { let currentAmount = 0; const includedUnspentTxOuts = []; @@ -66,7 +91,9 @@ const findTxOutsForAmount = (amount: number, myUnspentTxOuts: UnspentTxOut[]) => ' Required amount:' + amount + '. Available unspentTxOuts:' + JSON.stringify(myUnspentTxOuts); throw Error(eMsg); }; +*/ +/* const createTxOuts = (receiverAddress: string, myAddress: string, amount, leftOverAmount: number) => { const txOut1: TxOut = new TxOut(receiverAddress, amount); if (leftOverAmount === 0) { @@ -76,7 +103,9 @@ const createTxOuts = (receiverAddress: string, myAddress: string, amount, leftOv return [txOut1, leftOverTx]; } }; +*/ +/* const filterTxPoolTxs = (unspentTxOuts: UnspentTxOut[], transactionPool: Transaction[]): UnspentTxOut[] => { const txIns: TxIn[] = _(transactionPool) .map((tx: Transaction) => tx.txIns) @@ -97,7 +126,9 @@ const filterTxPoolTxs = (unspentTxOuts: UnspentTxOut[], transactionPool: Transac return _.without(unspentTxOuts, ...removable); }; +*/ +/* const createTransaction = (receiverAddress: string, amount: number, privateKey: string, unspentTxOuts: UnspentTxOut[], txPool: Transaction[]): Transaction => { @@ -131,6 +162,32 @@ const createTransaction = (receiverAddress: string, amount: number, privateKey: return tx; }; +*/ +const createTransaction = (receiverAddress: string, amount: number, privateKey: string, + accounts: Account[], txPool: Transaction[]): Transaction => { + + //console.log('txPool: %s', JSON.stringify(txPool)); + const myAddress: string = getPublicKey(privateKey); + + const myAccount: Account = findAccount(myAddress, accounts); + if(amount > myAccount.balance){ + console.log('No enough coins.'); + return undefined; + } + + const tx: Transaction = new Transaction(myAddress, receiverAddress, amount); + tx.timestamp = getCurrentTimestamp(); + tx.id = getTransactionId(tx); + tx.signature = signTransaction(tx, privateKey); + //what is the role of transaction pool? + return tx; +}; + +/* export {createTransaction, getPublicFromWallet, getPrivateFromWallet, getBalance, generatePrivateKey, initWallet, deleteWallet, findUnspentTxOuts}; + +*/ +export {createTransaction, getPublicFromWallet, + getPrivateFromWallet, getBalance, generatePrivateKey, initWallet, deleteWallet}; diff --git a/tsconfig.json b/Dewblock-cloud-server/tsconfig.json similarity index 100% rename from tsconfig.json rename to Dewblock-cloud-server/tsconfig.json diff --git a/tslint.json b/Dewblock-cloud-server/tslint.json similarity index 100% rename from tslint.json rename to Dewblock-cloud-server/tslint.json diff --git a/Dewblock-dew-server/License.txt b/Dewblock-dew-server/License.txt new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/Dewblock-dew-server/License.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Dewblock-dew-server/README.md b/Dewblock-dew-server/README.md new file mode 100644 index 0000000..4d50331 --- /dev/null +++ b/Dewblock-dew-server/README.md @@ -0,0 +1,147 @@ +# Dewblock-dew-server Package + +This package has two modes: local mode and cloud-dew mode (for short from now on we refer it as dew mode). + +In local mode, Dewblock behaves like Naivecoin. + +In dew mode, Dewblock behaves like Naivecoin without the blockchain. Thus, Dewblock-dew-server does not need huge memory like a normal blockchain system. + +#### Installation and System Start + +``` +npm install +npm start +``` + +##### Set Mode Local +``` +curl http://localhost:3001/setmodelocal +``` + + +##### Set Mode Dew +``` +curl http://localhost:3001/setmodedew +``` + + +##### Fetch Accounts +Send dew server accounts to the cloud server for verification. If consistent, the cloud server will do nothing; if not consistent, the cloud server accounts will be fetched to the dew server and replace the dew server accounts. +``` +curl http://localhost:3001/fetchaccounts +``` + + +##### Query the whole blockchain +``` +curl http://localhost:3001/blocks +``` +In dew mode, the blockchain has only the genesis block. + + +##### Query all accounts + +``` +curl http://localhost:3001/accounts +``` + +##### Query the transaction pool +``` +curl http://localhost:3001/transactionPool +``` + + +#### Query all connected peers +``` +curl http://localhost:3001/peers +``` + + +##### Query my account + +``` +curl http://localhost:3001/myaccount +``` + +##### Query my account balance +``` +curl http://localhost:3001/mybalance +``` + + +##### Query my account address + +``` +curl http://localhost:3001/myaddress +``` + + + +##### Query a block with block hash + +``` +curl http://localhost:3001/block/:hash +``` +In dew mode, this is useless. + + + +##### Query a transaction with transaction id + +``` +curl http://localhost:3001/transaction/:id +``` +In dew mode, this is useless. + + +##### Query an account with account address + +``` +curl http://localhost:3001/account/:address +``` + +##### Mine a block +The block will contain the transactions in the pool. +``` +curl -X POST http://localhost:3001/mineBlock +``` + +##### Mine a block in the cloud +The block will contain the transactions in the pool. +``` +curl -X POST http://localhost:3001/mineInCloud +``` + +##### Send a transaction to the pool +``` +curl -H "Content-type: application/json" --data '{"address": "04bfcab8722991ae774db48f934ca79cfb7dd991229153b9f732ba5334aafcd8e7266e47076996b55a14bf9913ee3145ce0cfc1372ada8ada74bd287450313534b", "amount" : 35}' http://localhost:3001/sendTransaction +``` +For Windows: +``` +curl -H "Content-type: application/json" --data "{\"address\": \"04b3e56e277a9a7cf8216982cf85f1b8edd51de012f1222bd2b37bb1217a42d31f8feda18be34aa09a759d2a70c5d6d0cc6cdd67e4e8c1761beb27e680bddd89b6\", \"amount\" : 25}" http://localhost:3001/sendTransaction +``` + +##### Mine a block with a transaction +If the transaction is invalid, the block will still be mined. +``` +curl -H "Content-type: application/json" --data '{"address": "04bfcab8722991ae774db48f934ca79cfb7dd991229153b9f732ba5334aafcd8e7266e47076996b55a14bf9913ee3145ce0cfc1372ada8ada74bd287450313534b", "amount" : 35}' http://localhost:3001/mineTransaction +``` +For Windows: +``` +curl -H "Content-type: application/json" --data "{\"address\": \"04b3e56e277a9a7cf8216982cf85f1b8edd51de012f1222bd2b37bb1217a42d31f8feda18be34aa09a759d2a70c5d6d0cc6cdd67e4e8c1761beb27e680bddd89b6\", \"amount\" : 20}" http://localhost:3001/mineTransaction +``` + +##### Add peer +``` +curl -H "Content-type:application/json" --data '{"peer" : "ws://localhost:6001"}' http://localhost:3001/addpeer +``` +For Windows: +``` +curl -H "Content-type:application/json" --data "{\"peer\" : \"ws://localhost:6001\"}" http://localhost:3001/addpeer +curl -H "Content-type:application/json" --data "{\"peer\" : \"ws://192.168.1.114:6001\"}" http://localhost:3001/addpeer +``` + +#### Stop the server +``` +curl -X POST http://localhost:3001/stop +``` diff --git a/Dewblock-dew-server/node/wallet/.gitignore b/Dewblock-dew-server/node/wallet/.gitignore new file mode 100644 index 0000000..aa8f673 --- /dev/null +++ b/Dewblock-dew-server/node/wallet/.gitignore @@ -0,0 +1,2 @@ +!.gitignore +private_key diff --git a/Dewblock-dew-server/package-lock.json b/Dewblock-dew-server/package-lock.json new file mode 100644 index 0000000..985c1ef --- /dev/null +++ b/Dewblock-dew-server/package-lock.json @@ -0,0 +1,1077 @@ +{ + "name": "naivechain", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/body-parser": { + "version": "1.16.4", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.16.4.tgz", + "integrity": "sha512-y8GxleWZ4ep0GG9IFMg+HpZWqLPjAjqc65cAopXPAWONWGCWGT0FCPVlXbUEBOPWpYtFrvlp2D7EJJnrqLUnEQ==", + "dev": true, + "requires": { + "@types/express": "4.0.36", + "@types/node": "8.0.9" + } + }, + "@types/crypto-js": { + "version": "3.1.33", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-3.1.33.tgz", + "integrity": "sha1-fmyRYDUz4M2fJ5qEBam543mIDF0=", + "dev": true + }, + "@types/express": { + "version": "4.0.36", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.0.36.tgz", + "integrity": "sha512-bT9q2eqH/E72AGBQKT50dh6AXzheTqigGZ1GwDiwmx7vfHff0bZOrvUWjvGpNWPNkRmX1vDF6wonG6rlpBHb1A==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "4.0.49", + "@types/serve-static": "1.7.31" + } + }, + "@types/express-serve-static-core": { + "version": "4.0.49", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.0.49.tgz", + "integrity": "sha512-b7mVHoURu1xaP/V6xw1sYwyv9V0EZ7euyi+sdnbnTZxEkAh4/hzPsI6Eflq+ZzHQ/Tgl7l16Jz+0oz8F46MLnA==", + "dev": true, + "requires": { + "@types/node": "8.0.9" + } + }, + "@types/lodash": { + "version": "4.14.85", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.85.tgz", + "integrity": "sha512-HrZiwDl62if0z31+rB99CLlg7WzS7b+KmyW75XAHEl/ZG0De2ACo6skZ89Zh3jOWkjKObN0Apq3MUezg7u9NKQ==", + "dev": true + }, + "@types/mime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.1.tgz", + "integrity": "sha512-rek8twk9C58gHYqIrUlJsx8NQMhlxqHzln9Z9ODqiNgv3/s+ZwIrfr+djqzsnVM12xe9hL98iJ20lj2RvCBv6A==", + "dev": true + }, + "@types/node": { + "version": "8.0.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.9.tgz", + "integrity": "sha512-UkiiJp6Iz2h4xzapN8BPKjhq+/BlyXcISwPVk2Kd7VJ/I1TREFokjBtvM6hftANXdsfo1IoWMXhmg8G8X+SS8Q==", + "dev": true + }, + "@types/serve-static": { + "version": "1.7.31", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.7.31.tgz", + "integrity": "sha1-FUVt6NmNa0z/Mb5savdJKuY/Uho=", + "dev": true, + "requires": { + "@types/express-serve-static-core": "4.0.49", + "@types/mime": "1.3.1" + } + }, + "@types/ws": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-3.0.1.tgz", + "integrity": "sha512-cfBmtvCOil/OIeNU+qB0cfKaVcVqnnJQDSy1jwBltV76mWIxtieITsvKEyIC1bYn4rx/M8q0Pn9qa52sMbtRGg==", + "dev": true, + "requires": { + "@types/node": "8.0.9" + } + }, + "accepts": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz", + "integrity": "sha1-5fHzkoxtlf2WVYw27D2dDeSm7Oo=", + "requires": { + "mime-types": "2.1.15", + "negotiator": "0.5.3" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.1.0.tgz", + "integrity": "sha1-CcIC1ckX7CMYjKpcnLkXnNlUd1A=", + "requires": { + "color-convert": "1.9.0" + } + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + }, + "axios": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz", + "integrity": "sha1-uk+S8XFn37q0CYN4VFS5rBScPG0=", + "requires": { + "follow-redirects": "1.2.4", + "is-buffer": "1.1.5" + } + }, + "babel-code-frame": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", + "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + } + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bn.js": { + "version": "4.11.7", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.7.tgz", + "integrity": "sha512-LxFiV5mefv0ley0SzqkOPR1bC4EbpPx8LkOz5vMe/Yi15t5hzwgO/G+tc7wOtL4PZTYjwHu8JnEiSLumuSjSfA==" + }, + "body-parser": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.17.2.tgz", + "integrity": "sha1-+IkqvI+eYn1Crtr7yma/WrmRBO4=", + "requires": { + "bytes": "2.4.0", + "content-type": "1.0.2", + "debug": "2.6.7", + "depd": "1.1.0", + "http-errors": "1.6.1", + "iconv-lite": "0.4.15", + "on-finished": "2.3.0", + "qs": "6.4.0", + "raw-body": "2.2.0", + "type-is": "1.6.15" + } + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "bytes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", + "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=" + }, + "chalk": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.0.1.tgz", + "integrity": "sha512-Mp+FXEI+FrwY/XYV45b2YD3E8i3HwnEAoFcM0qlZzq/RZ9RwWitt2Y/c7cqRAz70U7hfekqx6qNYthuKFO6K0g==", + "requires": { + "ansi-styles": "3.1.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.2.0" + } + }, + "color-convert": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", + "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", + "requires": { + "color-name": "1.1.2" + } + }, + "color-name": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.2.tgz", + "integrity": "sha1-XIq3K2S9IhXWF66VWeuxSEdc+Y0=" + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "content-disposition": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz", + "integrity": "sha1-QoT+auBjCHRjnkToCkGMKTQTXp4=" + }, + "content-type": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz", + "integrity": "sha1-t9ETrueo3Se9IRM8TcJSnfFyHu0=" + }, + "cookie": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.2.tgz", + "integrity": "sha1-cv7D0k5Io0Mgc9kMEmQgBQYQBLE=" + }, + "cookie-signature": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.5.tgz", + "integrity": "sha1-oSLj8VA+yg9TVXlbBxG7I2jUUPk=" + }, + "crc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.2.1.tgz", + "integrity": "sha1-XZyPt3okXNXsopHl0tAFM0urAII=" + }, + "cross-env": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.0.1.tgz", + "integrity": "sha1-/05y6kO0faJIa0On8gQ7JgnkSRM=", + "requires": { + "cross-spawn": "5.1.0", + "is-windows": "1.0.1" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "lru-cache": "4.1.1", + "shebang-command": "1.2.0", + "which": "1.2.14" + } + }, + "crypto-js": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.8.tgz", + "integrity": "sha1-cV8HC/YBTyrpkqmLOSkli3E/CNU=" + }, + "debug": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", + "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", + "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" + }, + "destroy": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz", + "integrity": "sha1-tDO0ck5x/YVR2YhRdIUcX8N34sk=" + }, + "diff": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.0.tgz", + "integrity": "sha512-w0XZubFWn0Adlsapj9EAWX0FqWdO4tz8kc3RiYdWLh4k/V8PTb6i0SMgXt0vRM3zyKnT8tKO7mUlieRQHIjMNg==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "requires": { + "bn.js": "4.11.7", + "brorand": "1.1.0", + "hash.js": "1.1.3", + "hmac-drbg": "1.0.1", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "escape-html": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz", + "integrity": "sha1-GBoobq05ejmpKFfPsdQwUuNWv/A=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.5.1.tgz", + "integrity": "sha1-VMUN4E7kJpVWKSWsVmWIKRvn6eo=", + "requires": { + "crc": "3.2.1" + } + }, + "express": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.11.2.tgz", + "integrity": "sha1-jfPVqayEhYXwCgd3YBgj+uzTsUg=", + "requires": { + "accepts": "1.2.13", + "content-disposition": "0.5.0", + "cookie": "0.1.2", + "cookie-signature": "1.0.5", + "debug": "2.1.3", + "depd": "1.0.1", + "escape-html": "1.0.1", + "etag": "1.5.1", + "finalhandler": "0.3.3", + "fresh": "0.2.4", + "media-typer": "0.3.0", + "merge-descriptors": "0.0.2", + "methods": "1.1.2", + "on-finished": "2.2.1", + "parseurl": "1.3.1", + "path-to-regexp": "0.1.3", + "proxy-addr": "1.0.10", + "qs": "2.3.3", + "range-parser": "1.0.3", + "send": "0.11.1", + "serve-static": "1.8.1", + "type-is": "1.5.7", + "utils-merge": "1.0.0", + "vary": "1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", + "integrity": "sha1-zoqxte6PvuK/o7Yzyrk9NmtjQY4=", + "requires": { + "ms": "0.7.0" + } + }, + "depd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz", + "integrity": "sha1-gK7GTJ1tl+ZcwqnKqTwKpqv3Oqo=" + }, + "ee-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz", + "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q=" + }, + "mime-db": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz", + "integrity": "sha1-PQxjGA9FjrENMlqqN9fFiuMS6dc=" + }, + "mime-types": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz", + "integrity": "sha1-MQ4VnbI+B3+Lsit0jav6SVcUCqY=", + "requires": { + "mime-db": "1.12.0" + } + }, + "ms": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz", + "integrity": "sha1-hlvpTC5zl62KV9pqYzpuLzB5i4M=" + }, + "on-finished": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.2.1.tgz", + "integrity": "sha1-XIXBzDYpn3gCllP2Z/J7a5nrwCk=", + "requires": { + "ee-first": "1.1.0" + } + }, + "qs": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz", + "integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ=" + }, + "type-is": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.5.7.tgz", + "integrity": "sha1-uTaKWTzG730GReeLL0xky+zQXpA=", + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.0.14" + } + } + } + }, + "finalhandler": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.3.3.tgz", + "integrity": "sha1-saCaoeamB7NUFmmwm8tyf0YM1CY=", + "requires": { + "debug": "2.1.3", + "escape-html": "1.0.1", + "on-finished": "2.2.1" + }, + "dependencies": { + "debug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", + "integrity": "sha1-zoqxte6PvuK/o7Yzyrk9NmtjQY4=", + "requires": { + "ms": "0.7.0" + } + }, + "ee-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz", + "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q=" + }, + "ms": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz", + "integrity": "sha1-hlvpTC5zl62KV9pqYzpuLzB5i4M=" + }, + "on-finished": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.2.1.tgz", + "integrity": "sha1-XIXBzDYpn3gCllP2Z/J7a5nrwCk=", + "requires": { + "ee-first": "1.1.0" + } + } + } + }, + "follow-redirects": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.2.4.tgz", + "integrity": "sha512-Suw6KewLV2hReSyEOeql+UUkBVyiBm3ok1VPrVFRZnQInWpdoZbbiG5i8aJVSjTr0yQ4Ava0Sh6/joCg1Brdqw==", + "requires": { + "debug": "2.6.7" + } + }, + "forwarded": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz", + "integrity": "sha1-Ge+YdMSuHCl7zweP3mOgm2aoQ2M=" + }, + "fresh": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.2.4.tgz", + "integrity": "sha1-NYJJkgbJcjcUGQ7ddLRgT+tKYUw=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + }, + "hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.0" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "1.1.3", + "minimalistic-assert": "1.0.0", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "http-errors": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.1.tgz", + "integrity": "sha1-X4uO2YrKVFZWv1cplzh/kEpyIlc=", + "requires": { + "depd": "1.1.0", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.3.1" + } + }, + "iconv-lite": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz", + "integrity": "sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz", + "integrity": "sha1-X6eM8wG4JceKvDBC2BJyMEnqI8c=" + }, + "is-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" + }, + "is-windows": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.1.tgz", + "integrity": "sha1-MQ23D3QtJZoWo2kgK1GvhCMzENk=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "make-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.0.tgz", + "integrity": "sha1-Uq06M5zPEM5itAQLcI/nByRLi5Y=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-0.0.2.tgz", + "integrity": "sha1-w2pSp4FDdRPFcnXzndnTF1FKyMc=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=" + }, + "mime-db": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" + }, + "mime-types": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", + "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", + "requires": { + "mime-db": "1.27.0" + } + }, + "minimalistic-assert": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", + "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz", + "integrity": "sha1-Jp1cR2gQ7JLtvntsLygxY4T5p+g=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "parseurl": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", + "integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.3.tgz", + "integrity": "sha1-IbmrgidCed4lsVbqCP0SylG4rss=" + }, + "proxy-addr": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz", + "integrity": "sha1-DUCoL4Afw1VWfS7LZe/j8HfxIcU=", + "requires": { + "forwarded": "0.1.0", + "ipaddr.js": "1.0.5" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + }, + "range-parser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz", + "integrity": "sha1-aHKCNTXGkuLCoBA4Jq/YLC4P8XU=" + }, + "raw-body": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.2.0.tgz", + "integrity": "sha1-mUl2z2pQlqQRYoQEkvC9xdbn+5Y=", + "requires": { + "bytes": "2.4.0", + "iconv-lite": "0.4.15", + "unpipe": "1.0.0" + } + }, + "resolve": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz", + "integrity": "sha1-ZVkHw0aahoDcLeOidaj91paR8OU=", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "safe-buffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", + "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=" + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + }, + "send": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.11.1.tgz", + "integrity": "sha1-G+q/1C+eJwn5kCivMHisErRwktU=", + "requires": { + "debug": "2.1.3", + "depd": "1.0.1", + "destroy": "1.0.3", + "escape-html": "1.0.1", + "etag": "1.5.1", + "fresh": "0.2.4", + "mime": "1.2.11", + "ms": "0.7.0", + "on-finished": "2.2.1", + "range-parser": "1.0.3" + }, + "dependencies": { + "debug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", + "integrity": "sha1-zoqxte6PvuK/o7Yzyrk9NmtjQY4=", + "requires": { + "ms": "0.7.0" + } + }, + "depd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz", + "integrity": "sha1-gK7GTJ1tl+ZcwqnKqTwKpqv3Oqo=" + }, + "ee-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz", + "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q=" + }, + "ms": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz", + "integrity": "sha1-hlvpTC5zl62KV9pqYzpuLzB5i4M=" + }, + "on-finished": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.2.1.tgz", + "integrity": "sha1-XIXBzDYpn3gCllP2Z/J7a5nrwCk=", + "requires": { + "ee-first": "1.1.0" + } + } + } + }, + "serve-static": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.8.1.tgz", + "integrity": "sha1-CPq9OZmfBQ/DEUQ/RtWIinfs/Hw=", + "requires": { + "escape-html": "1.0.1", + "parseurl": "1.3.1", + "send": "0.11.1", + "utils-merge": "1.0.0" + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=" + }, + "source-map-support": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz", + "integrity": "sha1-AyAt9lwG0r2MfsI2KhkwVv7407E=", + "requires": { + "source-map": "0.5.6" + } + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "supports-color": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.0.tgz", + "integrity": "sha512-Ts0Mu/A1S1aZxEJNG88I4Oc9rcZSBFNac5e27yh4j2mqbhZSSzR1Ah79EYwSn9Zuh7lrlGD2cVGzw1RKGzyLSg==", + "requires": { + "has-flag": "2.0.0" + } + }, + "ts-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.2.0.tgz", + "integrity": "sha1-mBTwwBQXhJAM8S/vEZetS39NI9E=", + "requires": { + "arrify": "1.0.1", + "chalk": "2.0.1", + "diff": "3.3.0", + "make-error": "1.3.0", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "source-map-support": "0.4.15", + "tsconfig": "6.0.0", + "v8flags": "2.1.1", + "yn": "2.0.0" + } + }, + "tsconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-6.0.0.tgz", + "integrity": "sha1-aw6DdgA9evGGT434+J3QBZ/80DI=", + "requires": { + "strip-bom": "3.0.0", + "strip-json-comments": "2.0.1" + } + }, + "tslib": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.7.1.tgz", + "integrity": "sha1-vIAEFkaRkjp5/oN4u+s9ogF1OOw=", + "dev": true + }, + "tslint": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.5.0.tgz", + "integrity": "sha1-EOjas+MGH6YelELozuOYKs8gpqo=", + "dev": true, + "requires": { + "babel-code-frame": "6.22.0", + "colors": "1.1.2", + "commander": "2.11.0", + "diff": "3.3.0", + "glob": "7.1.2", + "minimatch": "3.0.4", + "resolve": "1.3.3", + "semver": "5.3.0", + "tslib": "1.7.1", + "tsutils": "2.6.0" + } + }, + "tsutils": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.6.0.tgz", + "integrity": "sha1-5emceaiszTl3zhjYP98dI1psLrs=", + "dev": true, + "requires": { + "tslib": "1.7.1" + } + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.15" + } + }, + "typescript": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.4.1.tgz", + "integrity": "sha1-w8yxbdqgsjFN4DHn5v7onlujRrw=" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "user-home": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", + "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=" + }, + "utils-merge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", + "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" + }, + "v8flags": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", + "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", + "requires": { + "user-home": "1.1.1" + } + }, + "vary": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz", + "integrity": "sha1-meSYFWaihhGN+yuBc1ffeZM3bRA=" + }, + "which": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", + "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", + "requires": { + "isexe": "2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.0.0.tgz", + "integrity": "sha1-mN2wAFbIOQy3Ued4h4hJf5kQO2w=", + "requires": { + "safe-buffer": "5.0.1", + "ultron": "1.1.0" + }, + "dependencies": { + "ultron": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz", + "integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ=" + } + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=" + } + } +} diff --git a/Dewblock-dew-server/package.json b/Dewblock-dew-server/package.json new file mode 100644 index 0000000..8e10554 --- /dev/null +++ b/Dewblock-dew-server/package.json @@ -0,0 +1,34 @@ +{ + "name": "naivechain", + "version": "1.0.0", + "description": "", + "scripts": { + "prestart": "npm run compile", + "start": "node src/main.js", + "compile": "tsc" + }, + "dependencies": { + "axios": "^0.16.2", + "body-parser": "^1.15.2", + "cross-env": "^5.0.1", + "crypto-js": "^3.1.6", + "elliptic": "^6.4.0", + "express": "~4.11.1", + "lodash": "^4.17.4", + "ts-node": "^3.2.0", + "typescript": "^2.4.1", + "ws": "^3.0.0" + }, + "engines": { + "node": ">=8.5.0" + }, + "devDependencies": { + "@types/body-parser": "^1.16.4", + "@types/crypto-js": "^3.1.33", + "@types/express": "^4.0.36", + "@types/lodash": "^4.14.85", + "@types/node": "^8.0.9", + "@types/ws": "^3.0.1", + "tslint": "^5.5.0" + } +} diff --git a/Dewblock-dew-server/src/blockchain.ts b/Dewblock-dew-server/src/blockchain.ts new file mode 100644 index 0000000..4637b1c --- /dev/null +++ b/Dewblock-dew-server/src/blockchain.ts @@ -0,0 +1,589 @@ +import * as CryptoJS from 'crypto-js'; +import * as _ from 'lodash'; +//import {broadcastLatest, broadCastTransactionPool} from './p2p'; +import {broadcastLatest, broadCastTransactionPool, sendMiningRequest} from './p2p'; + +/* +import { + getCoinbaseTransaction, isValidAddress, processTransactions, Transaction, UnspentTxOut +} from './transaction'; +*/ +import { + getCoinbaseTransaction, isValidAddress, processTransactions, Transaction, Account, findAccount, getTransactionId +} from './transaction'; + +import {addToTransactionPool, getTransactionPool, updateTransactionPool, updateTransactionPoolReplace} from './transactionPool'; +import {hexToBinary} from './util'; +//import {createTransaction, findUnspentTxOuts, getBalance, getPrivateFromWallet, getPublicFromWallet} from './wallet'; +import {createTransaction, getBalance, getPrivateFromWallet, getPublicFromWallet} from './wallet'; + +//dewcoin +import {getMode, setMode} from './config'; +let mode:string; + + +class Block { + + public index: number; + public hash: string; + public previousHash: string; + public timestamp: number; + public data: Transaction[]; + public difficulty: number; + public nonce: number; + + constructor(index: number, hash: string, previousHash: string, + timestamp: number, data: Transaction[], difficulty: number, nonce: number) { + this.index = index; + this.previousHash = previousHash; + this.timestamp = timestamp; + this.data = data; + this.hash = hash; + this.difficulty = difficulty; + this.nonce = nonce; + } +} + +const getCurrentTimestamp = (): number => Math.round(new Date().getTime() / 1000); + +/* +const genesisTransaction = { + 'txIns': [{'signature': '', 'txOutId': '', 'txOutIndex': 0}], + 'txOuts': [{ + 'address': '04bfcab8722991ae774db48f934ca79cfb7dd991229153b9f732ba5334aafcd8e7266e47076996b55a14bf9913ee3145ce0cfc1372ada8ada74bd287450313534a', + 'amount': 50 + }], + 'id': 'e655f6a5f26dc9b4cac6e46f52336428287759cf81ef5ff10854f69d68f43fa3' +}; +*/ +const genesisTransaction = new Transaction('coinbase', + '04c0a0903203a721fa43581cc53c9361862e2e5522310909fbfbf14203371c464f5bdbba42e16349e237e1adf65952d8301e071a40a76b1709954c8f0e3f253b2c', 50); +genesisTransaction.timestamp = 1533641911; +genesisTransaction.id = getTransactionId(genesisTransaction); +genesisTransaction.signature = ''; + +/* +const genesisBlock: Block = new Block( + 0, '91a73664bc84c0baa1fc75ea6e4aa6d1d20c5df664c724e3159aefc2e1186627', '', 1465154705, [genesisTransaction], 0, 0 +); +*/ +const genesisBlock: Block = new Block( + 0, '91a73664bc84c0baa1fc75ea6e4aa6d1d20c5df664c724e3159aefc2e1186627', '', 1533641911, [genesisTransaction], 0, 0 +); +let blockchain: Block[] = [genesisBlock]; +const getBlockchain = (): Block[] => blockchain; +//dewcoin +const resetBlockchain = () => {blockchain = [genesisBlock];}; + +// the unspent txOut of genesis block is set to unspentTxOuts on startup +//let unspentTxOuts: UnspentTxOut[] = processTransactions(blockchain[0].data, []); +let accounts: Account[] = processTransactions(blockchain[0].data, []); +//const getUnspentTxOuts = (): UnspentTxOut[] => _.cloneDeep(unspentTxOuts); +//const getAccounts = (): Account[] => _.cloneDeep(accounts); +const getAccounts = (): Account[] => accounts; +// and txPool should be only updated at the same time +/* +const setUnspentTxOuts = (newUnspentTxOut: UnspentTxOut[]) => { + console.log('replacing unspentTxouts with: %s', newUnspentTxOut); + unspentTxOuts = newUnspentTxOut; +}; +*/ +const setAccounts = (newAccounts: Account[]) => { + //console.log('replacing accounts with: %s', newAccounts); + accounts = newAccounts; +}; + +//dewcoin +let theLatestBlock: Block = genesisBlock; +let previousAdjustBlock: Block = genesisBlock; +let accuDiff = 1; +//const getLatestBlock = (): Block => blockchain[blockchain.length - 1]; +const getLatestBlock = (): Block => { + mode = getMode(); + if(mode == 'local'){ + return blockchain[blockchain.length - 1]; + }else if(mode == 'dew'){ + return theLatestBlock; + } +} + +// in seconds +const BLOCK_GENERATION_INTERVAL: number = 10; + +// in blocks +const DIFFICULTY_ADJUSTMENT_INTERVAL: number = 10; + + + +const getDifficulty = (aBlockchain: Block[]): number => { + const latestBlock: Block = aBlockchain[blockchain.length - 1]; + if (latestBlock.index % DIFFICULTY_ADJUSTMENT_INTERVAL === 0 && latestBlock.index !== 0) { + return getAdjustedDifficulty(latestBlock, aBlockchain); + } else { + return latestBlock.difficulty; + } +}; + +const getDifficultyChain1 = (): number => { + if (theLatestBlock.index % DIFFICULTY_ADJUSTMENT_INTERVAL === 0 && theLatestBlock.index !== 0) { + return getAdjustedDifficultyChain1(theLatestBlock); + } else { + return theLatestBlock.difficulty; + } +}; + +const getAdjustedDifficulty = (latestBlock: Block, aBlockchain: Block[]) => { + const prevAdjustmentBlock: Block = aBlockchain[blockchain.length - DIFFICULTY_ADJUSTMENT_INTERVAL]; + const timeExpected: number = BLOCK_GENERATION_INTERVAL * DIFFICULTY_ADJUSTMENT_INTERVAL; + const timeTaken: number = latestBlock.timestamp - prevAdjustmentBlock.timestamp; + if (timeTaken < timeExpected / 2) { + return prevAdjustmentBlock.difficulty + 1; + } else if (timeTaken > timeExpected * 2) { + return prevAdjustmentBlock.difficulty - 1; + } else { + return prevAdjustmentBlock.difficulty; + } +}; +const getAdjustedDifficultyChain1 = (latestBlock: Block) => { + const prevAdjustmentBlock: Block = previousAdjustBlock; + const timeExpected: number = BLOCK_GENERATION_INTERVAL * DIFFICULTY_ADJUSTMENT_INTERVAL; + const timeTaken: number = latestBlock.timestamp - prevAdjustmentBlock.timestamp; + if (timeTaken < timeExpected / 2) { + return prevAdjustmentBlock.difficulty + 1; + } else if (timeTaken > timeExpected * 2) {//&& (prevAdjustmentBlock.difficulty > 0) + return prevAdjustmentBlock.difficulty - 1; + } else { + return prevAdjustmentBlock.difficulty; + } +}; + + + +/* +const generateRawNextBlock = (blockData: Transaction[]) => { + const previousBlock: Block = getLatestBlock(); + const difficulty: number = getDifficulty(getBlockchain()); + const nextIndex: number = previousBlock.index + 1; + const nextTimestamp: number = getCurrentTimestamp(); + const newBlock: Block = findBlock(nextIndex, previousBlock.hash, nextTimestamp, blockData, difficulty); + if (addBlockToChain(newBlock)) { + broadcastLatest(); + return newBlock; + } else { + return null; + } + +}; +*/ +const generateRawNextBlock = (blockData: Transaction[]) => { + const previousBlock: Block = getLatestBlock(); + let difficulty: number; + mode = getMode(); + if(mode == 'local'){ + difficulty = getDifficulty(getBlockchain()); + }else if(mode == 'dew'){ + difficulty = getDifficultyChain1(); + } + const nextIndex: number = previousBlock.index + 1; + const nextTimestamp: number = getCurrentTimestamp(); + const newBlock: Block = findBlock(nextIndex, previousBlock.hash, nextTimestamp, blockData, difficulty); + if (addBlockToChain(newBlock)) { + broadcastLatest(); + return newBlock; + } else { + return null; + } +}; + +const generateRawNextBlockInCloud = (blockData: Transaction[]) => { + const previousBlock: Block = getLatestBlock(); + let difficulty: number; + mode = getMode(); + if(mode == 'local'){ + difficulty = getDifficulty(getBlockchain()); + }else if(mode == 'dew'){ + difficulty = getDifficultyChain1(); + } + const nextIndex: number = previousBlock.index + 1; + const nextTimestamp: number = getCurrentTimestamp(); + const newBlock: Block = new Block(nextIndex, '', previousBlock.hash, nextTimestamp, blockData, difficulty, 0); + sendMiningRequest(newBlock); +}; + + +// gets the unspent transaction outputs owned by the wallet +/* +const getMyUnspentTransactionOutputs = () => { + return findUnspentTxOuts(getPublicFromWallet(), getUnspentTxOuts()); +}; +*/ +const getMyAccount = () => { + return findAccount(getPublicFromWallet(), getAccounts()); +}; + +/* +const generateNextBlock = () => { + const coinbaseTx: Transaction = getCoinbaseTransaction(getPublicFromWallet(), getLatestBlock().index + 1); + const blockData: Transaction[] = [coinbaseTx].concat(getTransactionPool()); + return generateRawNextBlock(blockData); +}; +*/ +const generateNextBlock = () => { + const coinbaseTx: Transaction = getCoinbaseTransaction(getPublicFromWallet()); + const blockData: Transaction[] = [coinbaseTx].concat(getTransactionPool()); + return generateRawNextBlock(blockData); +}; + + +const mineInCloud = () => { + const coinbaseTx: Transaction = getCoinbaseTransaction(getPublicFromWallet()); + const blockData: Transaction[] = [coinbaseTx].concat(getTransactionPool()); + generateRawNextBlockInCloud(blockData); +}; + + +/* +const generatenextBlockWithTransaction = (receiverAddress: string, amount: number) => { + if (!isValidAddress(receiverAddress)) { + throw Error('invalid address'); + } + if (typeof amount !== 'number') { + throw Error('invalid amount'); + } + const coinbaseTx: Transaction = getCoinbaseTransaction(getPublicFromWallet(), getLatestBlock().index + 1); + const tx: Transaction = createTransaction(receiverAddress, amount, getPrivateFromWallet(), getUnspentTxOuts(), getTransactionPool()); + const blockData: Transaction[] = [coinbaseTx, tx]; + return generateRawNextBlock(blockData); +}; +*/ +const generatenextBlockWithTransaction = (receiverAddress: string, amount: number) => { + let blockData: Transaction[]; + + if (!isValidAddress(receiverAddress)) { + throw Error('invalid address'); + } + if (typeof amount !== 'number') { + throw Error('invalid amount'); + } + const coinbaseTx: Transaction = getCoinbaseTransaction(getPublicFromWallet()); + const tx: Transaction = createTransaction(receiverAddress, amount, getPrivateFromWallet(), getAccounts(), getTransactionPool()); + if(tx == undefined){ + blockData = [coinbaseTx]; + console.log('The transaction is invalid. The block was mined.'); + }else{ + blockData = [coinbaseTx, tx]; + } + return generateRawNextBlock(blockData); +}; + +const findBlock = (index: number, previousHash: string, timestamp: number, data: Transaction[], difficulty: number): Block => { + let nonce = 0; + while (true) { + const hash: string = calculateHash(index, previousHash, timestamp, data, difficulty, nonce); + if (hashMatchesDifficulty(hash, difficulty)) { + return new Block(index, hash, previousHash, timestamp, data, difficulty, nonce); + } + nonce++; + } +}; + +/* +const getAccountBalance = (): number => { + return getBalance(getPublicFromWallet(), getUnspentTxOuts()); +}; +*/ +const getAccountBalance = (): number => { + return getBalance(getPublicFromWallet(), getAccounts()); +}; + +/* +const sendTransaction = (address: string, amount: number): Transaction => { + const tx: Transaction = createTransaction(address, amount, getPrivateFromWallet(), getUnspentTxOuts(), getTransactionPool()); + addToTransactionPool(tx, getUnspentTxOuts()); + broadCastTransactionPool(); + return tx; +}; +*/ +const sendTransaction = (address: string, amount: number): Transaction => { + const tx: Transaction = createTransaction(address, amount, getPrivateFromWallet(), getAccounts(), getTransactionPool()); + if(tx == undefined){ + console.log('This transaction cannot be created.'); + return undefined; + }else{ + addToTransactionPool(tx, getAccounts()); + broadCastTransactionPool(); + return tx; + } +}; + +const calculateHashForBlock = (block: Block): string => + calculateHash(block.index, block.previousHash, block.timestamp, block.data, block.difficulty, block.nonce); + +const calculateHash = (index: number, previousHash: string, timestamp: number, data: Transaction[], + difficulty: number, nonce: number): string => + CryptoJS.SHA256(index + previousHash + timestamp + data + difficulty + nonce).toString(); + +const isValidBlockStructure = (block: Block): boolean => { + return typeof block.index === 'number' + && typeof block.hash === 'string' + && typeof block.previousHash === 'string' + && typeof block.timestamp === 'number' + && typeof block.data === 'object'; +}; + +const isValidNewBlock = (newBlock: Block, previousBlock: Block): boolean => { + if (!isValidBlockStructure(newBlock)) { + console.log('invalid block structure: %s', JSON.stringify(newBlock)); + return false; + } + if (previousBlock.index + 1 !== newBlock.index) { + console.log('invalid index'); + return false; + } else if (previousBlock.hash !== newBlock.previousHash) { + console.log('invalid previoushash'); + return false; + } else if (!isValidTimestamp(newBlock, previousBlock)) { + console.log('invalid timestamp'); + return false; + } else if (!hasValidHash(newBlock)) { + return false; + } + return true; +}; + + +const getAccumulatedDifficulty = (aBlockchain: Block[]): number => { + return aBlockchain + .map((block) => block.difficulty) + .map((difficulty) => Math.pow(2, difficulty)) + .reduce((a, b) => a + b); +}; + +const getAccumulatedDifficultyChain1 = (): number => { + return accuDiff; +}; + +const isValidTimestamp = (newBlock: Block, previousBlock: Block): boolean => { + return ( previousBlock.timestamp - 60 < newBlock.timestamp ) + && newBlock.timestamp - 60 < getCurrentTimestamp(); +}; + +const hasValidHash = (block: Block): boolean => { + + if (!hashMatchesBlockContent(block)) { + console.log('invalid hash, got:' + block.hash); + return false; + } + + if (!hashMatchesDifficulty(block.hash, block.difficulty)) { + console.log('block difficulty not satisfied. Expected: ' + block.difficulty + 'got: ' + block.hash); + } + return true; +}; + +const hashMatchesBlockContent = (block: Block): boolean => { + const blockHash: string = calculateHashForBlock(block); + return blockHash === block.hash; +}; + +const hashMatchesDifficulty = (hash: string, difficulty: number): boolean => { + const hashInBinary: string = hexToBinary(hash); + const requiredPrefix: string = '0'.repeat(difficulty); + return hashInBinary.startsWith(requiredPrefix); +}; + +/* + Checks if the given blockchain is valid. Return the unspent txOuts if the chain is valid + */ +/* +const isValidChain = (blockchainToValidate: Block[]): UnspentTxOut[] => { + console.log('isValidChain:'); + console.log(JSON.stringify(blockchainToValidate)); + const isValidGenesis = (block: Block): boolean => { + return JSON.stringify(block) === JSON.stringify(genesisBlock); + }; + + if (!isValidGenesis(blockchainToValidate[0])) { + return null; + } + + //Validate each block in the chain. The block is valid if the block structure is valid + // and the transaction are valid + + let aUnspentTxOuts: UnspentTxOut[] = []; + + for (let i = 0; i < blockchainToValidate.length; i++) { + const currentBlock: Block = blockchainToValidate[i]; + if (i !== 0 && !isValidNewBlock(blockchainToValidate[i], blockchainToValidate[i - 1])) { + return null; + } + + aUnspentTxOuts = processTransactions(currentBlock.data, aUnspentTxOuts, currentBlock.index); + if (aUnspentTxOuts === null) { + console.log('invalid transactions in blockchain'); + return null; + } + } + return aUnspentTxOuts; +}; +*/ +//only used for chain2 +const isValidChain = (blockchainToValidate: Block[]): Account[] => { + console.log('isValidChain:'); + console.log(JSON.stringify(blockchainToValidate)); + const isValidGenesis = (block: Block): boolean => { + return JSON.stringify(block) === JSON.stringify(genesisBlock); + }; + + if (!isValidGenesis(blockchainToValidate[0])) { + return null; + } + /* + Validate each block in the chain. The block is valid if the block structure is valid + and the transaction are valid + */ + let accountsTemp: Account[] = []; + + for (let i = 0; i < blockchainToValidate.length; i++) { + const currentBlock: Block = blockchainToValidate[i]; + if (i !== 0 && !isValidNewBlock(blockchainToValidate[i], blockchainToValidate[i - 1])) { + return null; + } + + accountsTemp = processTransactions(currentBlock.data, accountsTemp); + if (accountsTemp === null) { + console.log('invalid transactions in blockchain'); + return null; + } + } + return accountsTemp; +}; + + +/* +const addBlockToChain = (newBlock: Block): boolean => { + if (isValidNewBlock(newBlock, getLatestBlock())) { + const retVal: UnspentTxOut[] = processTransactions(newBlock.data, getUnspentTxOuts(), newBlock.index); + if (retVal === null) { + console.log('block is not valid in terms of transactions'); + return false; + } else { + blockchain.push(newBlock); + setUnspentTxOuts(retVal); + updateTransactionPool(unspentTxOuts); + return true; + } + } + return false; +}; +*/ +const addBlockToChain = (newBlock: Block): boolean => { + mode = getMode(); + if (isValidNewBlock(newBlock, getLatestBlock())) { + const retVal: Account[] = processTransactions(newBlock.data, getAccounts()); + if (retVal === null) { + console.log('block is not valid in terms of transactions'); + return false; + } else { + if(mode == 'local'){ + blockchain.push(newBlock); + } + setAccounts(retVal); + updateTransactionPool(newBlock.data); + //The following is done even it is in local mode, preparing for mode change. + theLatestBlock = newBlock; + if (newBlock.index % DIFFICULTY_ADJUSTMENT_INTERVAL === 0) { + previousAdjustBlock = newBlock; + } + accuDiff += Math.pow(2, newBlock.difficulty); + return true; + } + } + return false; +}; + + +/* +const replaceChain = (newBlocks: Block[]) => { + const aUnspentTxOuts = isValidChain(newBlocks); + const validChain: boolean = aUnspentTxOuts !== null; + if (validChain && + getAccumulatedDifficulty(newBlocks) > getAccumulatedDifficulty(getBlockchain())) { + console.log('Received blockchain is valid. Replacing current blockchain with received blockchain'); + blockchain = newBlocks; + setUnspentTxOuts(aUnspentTxOuts); + updateTransactionPool(unspentTxOuts); + broadcastLatest(); + } else { + console.log('Received blockchain invalid'); + } +}; +*/ +const replaceChain = (newBlocks: Block[]) => { + const accountsTemp = isValidChain(newBlocks); + const validChain: boolean = accountsTemp !== null; + mode = getMode(); + let currentDifficulty: number; + if(mode == 'local'){ + currentDifficulty = getAccumulatedDifficulty(getBlockchain()); + } else if(mode == 'dew'){ + currentDifficulty = getAccumulatedDifficultyChain1(); + } + if (validChain && + getAccumulatedDifficulty(newBlocks) > currentDifficulty) { + console.log('Received blockchain is valid. Replacing current blockchain with received blockchain'); + if(mode == 'local'){ + blockchain = newBlocks; + } + setAccounts(accountsTemp); + updateTransactionPoolReplace(accounts); + theLatestBlock = newBlocks[newBlocks.length - 1]; + previousAdjustBlock = newBlocks[theLatestBlock.index - theLatestBlock.index % DIFFICULTY_ADJUSTMENT_INTERVAL]; + accuDiff = getAccumulatedDifficulty(newBlocks); + broadcastLatest(); + } else { + console.log('Received blockchain invalid'); + } +}; + + +/* +const handleReceivedTransaction = (transaction: Transaction) => { + addToTransactionPool(transaction, getUnspentTxOuts()); +}; +*/ +const handleReceivedTransaction = (transaction: Transaction) => { +//console.log('***handleReceivedTransacton:***'); + addToTransactionPool(transaction, getAccounts()); +}; + + +const validateAccount = (account: Account) => { +/* + if (!validateTransaction(tx, accounts)) { + throw Error('Trying to add invalid tx to pool. Tx: ' + tx.id); + } + + if (!isValidTxForPool(tx, transactionPool, accounts)) { + throw Error('Tx is not valid for the pool, Tx: ' + tx.id); + } + //console.log('adding to txPool: %s', JSON.stringify(tx)); + transactionPool.push(tx); +*/ +}; + + +/* +export { + Block, getBlockchain, getUnspentTxOuts, getLatestBlock, sendTransaction, + generateRawNextBlock, generateNextBlock, generatenextBlockWithTransaction, + handleReceivedTransaction, getMyUnspentTransactionOutputs, + getAccountBalance, isValidBlockStructure, replaceChain, addBlockToChain +}; +*/ +export { + Block, getBlockchain, resetBlockchain, getAccounts, setAccounts, getLatestBlock, sendTransaction, + generateRawNextBlock, generateNextBlock, mineInCloud, generatenextBlockWithTransaction, + handleReceivedTransaction, validateAccount, getMyAccount, + getAccountBalance, isValidBlockStructure, replaceChain, addBlockToChain, getCurrentTimestamp +}; diff --git a/Dewblock-dew-server/src/config.ts b/Dewblock-dew-server/src/config.ts new file mode 100644 index 0000000..2d204f5 --- /dev/null +++ b/Dewblock-dew-server/src/config.ts @@ -0,0 +1,20 @@ +let mode: string = 'dew'; +//If mode is dew and configured properly, dew can cloud can be automatically lined together through sockets. +//If mode is local, it has no connection with the cloud; it will have the complete blockchain. +//modes can be switched through HTTP commands. + +let cloudAddress = '127.0.0.1:7001'; +//let cloudAddress = '192.168.1.186:7001'; +//let cloudAddress = '192.168.1.114:7001'; +//If dew address is not correct, it can also be set using HTTP command. See Readme. +//Please notice: if only one machine is involved in testing, default config files are OK. +//If more than one machine is involved, config files should not use localhost or 127.0.0.1 at all. + + + +const getMode = ():string => {return mode;}; +const setMode = (s: string) => {mode = s}; +const getCloud = (): string => {return cloudAddress}; +const setCloud = (address: string) => {cloudAddress = address}; + +export {setMode, getMode, getCloud, setCloud}; diff --git a/Dewblock-dew-server/src/main.ts b/Dewblock-dew-server/src/main.ts new file mode 100644 index 0000000..0d91729 --- /dev/null +++ b/Dewblock-dew-server/src/main.ts @@ -0,0 +1,215 @@ +import * as bodyParser from 'body-parser'; +import * as express from 'express'; +import * as _ from 'lodash'; + +/* +import { + Block, generateNextBlock, generatenextBlockWithTransaction, generateRawNextBlock, getAccountBalance, + getBlockchain, getMyUnspentTransactionOutputs, getUnspentTxOuts, sendTransaction +} from './blockchain'; +*/ +import { + Block, generateNextBlock, mineInCloud, generatenextBlockWithTransaction, generateRawNextBlock, getAccountBalance, + getBlockchain, getMyAccount, getAccounts, sendTransaction +} from './blockchain'; + +//import {connectToPeers, getSockets, initP2PServer} from './p2p'; +import {connectToPeers, getSockets, initP2PServer, setModeLocal, setModeDew, fetchAccounts} from './p2p'; + +//import {UnspentTxOut} from './transaction'; +import {Account, findAccount} from './transaction'; + +//dewcoin +import {getMode} from './config'; + +import {getTransactionPool} from './transactionPool'; +import {getPublicFromWallet, initWallet} from './wallet'; + +let mode:string = 'dew'; + +const httpPort: number = parseInt(process.env.HTTP_PORT) || 3001; +const p2pPort: number = parseInt(process.env.P2P_PORT) || 6001; + +const initHttpServer = (myHttpPort: number) => { + const app = express(); + app.use(bodyParser.json()); + + app.use((err, req, res, next) => { + if (err) { + res.status(400).send(err.message); + } + }); + + app.get('/setmodelocal', (req, res) => { + setModeLocal(); + mode = getMode(); + res.send('The system is in ' + mode + ' mode.'); + }); + + app.get('/setmodedew', (req, res) => { + setModeDew(); + mode = getMode(); + res.send('The system is in ' + mode + ' mode.'); + }); + + app.get('/fetchAccounts', (req, res) => { + //This one should not be available in local mode. + fetchAccounts() + res.send('Accounts fetch requested.'); + }); + + app.get('/blocks', (req, res) => { + mode = getMode(); + //if(mode == 'local'){ + res.send(getBlockchain()); + //}else if(mode == 'dew'){ + // res.send('This request is not available in dew mode.'); + //} + }); + + app.get('/block/:hash', (req, res) => { + const block = _.find(getBlockchain(), {'hash' : req.params.hash}); + res.send(block); + }); + + app.get('/transaction/:id', (req, res) => { + const tx = _(getBlockchain()) + .map((blocks) => blocks.data) + .flatten() + .find({'id': req.params.id}); + res.send(tx); + }); + + /* + app.get('/address/:address', (req, res) => { + const unspentTxOuts: UnspentTxOut[] = + _.filter(getUnspentTxOuts(), (uTxO) => uTxO.address === req.params.address); + res.send({'unspentTxOuts': unspentTxOuts}); + }); + */ + app.get('/account/:address', (req, res) => { + let acc: Account = findAccount(req.params.address, getAccounts()); + if(acc == undefined){ + res.send({'Error:': "address is wrong"}); + } + res.send({'Account': acc}); + }); + + /* + app.get('/unspentTransactionOutputs', (req, res) => { + res.send(getUnspentTxOuts()); + }); + */ + //display all accounts + app.get('/accounts', (req, res) => { + res.send(getAccounts()); + }); + + /* + app.get('/myUnspentTransactionOutputs', (req, res) => { + res.send(getMyUnspentTransactionOutputs()); + }); + */ + app.get('/myaccount', (req, res) => { + let acc: Account = findAccount(getPublicFromWallet(), getAccounts()); + if(acc == undefined){ + res.send({'Error': 'No account was found.'}) + }else{ + res.send({'My Account': acc}); + } + }); + + app.post('/mineRawBlock', (req, res) => { + if (req.body.data == null) { + res.send('data parameter is missing'); + return; + } + const newBlock: Block = generateRawNextBlock(req.body.data); + if (newBlock === null) { + res.status(400).send('could not generate block'); + } else { + res.send(newBlock); + } + }); + + app.post('/mineBlock', (req, res) => { + const newBlock: Block = generateNextBlock(); + if (newBlock === null) { + res.status(400).send('could not generate block'); + } else { + res.send(newBlock); + } + }); + + app.post('/mineInCloud', (req, res) => { + mineInCloud(); + res.send(); + }); + + app.get('/mybalance', (req, res) => { + let acc: Account = findAccount(getPublicFromWallet(), getAccounts()); + if(acc == undefined){ + res.send({'Error:': "No such account."}); + }else{ + res.send({'Balance': acc.balance}); + } + }); + + app.get('/myaddress', (req, res) => { + const address: string = getPublicFromWallet(); + res.send({'address': address}); + }); + + app.post('/mineTransaction', (req, res) => { + const address = req.body.address; + const amount = req.body.amount; + try { + const resp = generatenextBlockWithTransaction(address, amount); + res.send(resp); + } catch (e) { + console.log(e.message); + res.status(400).send(e.message); + } + }); + + app.post('/sendTransaction', (req, res) => { + try { + const address = req.body.address; + const amount = req.body.amount; + + if (address === undefined || amount === undefined) { + throw Error('invalid address or amount'); + } + const resp = sendTransaction(address, amount); + res.send(resp); + } catch (e) { + console.log(e.message); + res.status(400).send(e.message); + } + }); + + app.get('/transactionpool', (req, res) => { + res.send(getTransactionPool()); + }); + + app.get('/peers', (req, res) => { + res.send(getSockets().map((s: any) => s._socket.remoteAddress + ':' + s._socket.remotePort)); + }); + app.post('/addpeer', (req, res) => { + connectToPeers(req.body.peer); + res.send(); + }); + + app.post('/stop', (req, res) => { + res.send({'msg' : 'stopping server'}); + process.exit(); + }); + + app.listen(myHttpPort, () => { + console.log('Listening http on port: ' + myHttpPort); + }); +}; + +initHttpServer(httpPort); +initP2PServer(p2pPort); +initWallet(); diff --git a/Dewblock-dew-server/src/p2p.ts b/Dewblock-dew-server/src/p2p.ts new file mode 100644 index 0000000..95a6e8e --- /dev/null +++ b/Dewblock-dew-server/src/p2p.ts @@ -0,0 +1,465 @@ +import * as WebSocket from 'ws'; +import {Server} from 'ws'; +/* +import { + addBlockToChain, Block, getBlockchain, getLatestBlock, handleReceivedTransaction, isValidBlockStructure, + replaceChain +} from './blockchain'; +*/ +import { + addBlockToChain, Block, getBlockchain, resetBlockchain, getLatestBlock, handleReceivedTransaction, validateAccount, isValidBlockStructure, + replaceChain, setAccounts, getAccounts +} from './blockchain'; +//import {Transaction} from './transaction'; +import {Transaction, Account} from './transaction'; +import {getTransactionPool} from './transactionPool'; + +//dewcoin +import {getMode, setMode, getCloud} from './config'; + +let mode: string; +let wsCloud: WebSocket; +const getWsCloud = (): WebSocket => wsCloud; + +const sockets: WebSocket[] = []; + +enum MessageType { + QUERY_LATEST = 0, + QUERY_ALL = 1, + RESPONSE_BLOCKCHAIN = 2, + QUERY_TRANSACTION_POOL = 3, + RESPONSE_TRANSACTION_POOL = 4, + QUERY_ACCOUNTS = 5, + RESPONSE_ACCOUNTS = 6, + ALTERNATE_ADDRESS = 7, + MINING_REQUEST = 8 +} + +class Message { + public type: MessageType; + public data: any; +} + +/* +const initP2PServer = (p2pPort: number) => { + const server: Server = new WebSocket.Server({port: p2pPort}); + server.on('connection', (ws: WebSocket) => { + initConnection(ws); + }); + console.log('listening websocket p2p port on: ' + p2pPort); +}; +*/ +const initP2PServer = (p2pPort: number) => { + const server: Server = new WebSocket.Server({port: p2pPort}); + server.on('connection', (ws: WebSocket) => { + //incoming connections. + initConnection(ws); + }); + console.log('listening websocket p2p port on: ' + p2pPort); + mode = getMode(); + if(mode == 'dew'){ + setModeDew(); + }else if(mode == 'local'){ + setModeLocal() + }; + console.log('running on ' + mode + ' mode.'); +}; + +const getSockets = () => sockets; + + +const initConnection = (ws: WebSocket) => { + console.log('A socket is added: ' + ws.url); + sockets.push(ws); + initMessageHandler(ws); + initErrorHandler(ws); + write(ws, queryChainLengthMsg()); + + // query transactions pool only some time after chain query + setTimeout(() => { + broadcast(queryTransactionPoolMsg()); + }, 500); +}; + +const JSONToObject = (data: string): T => { + try { + return JSON.parse(data); + } catch (e) { + console.log(e); + return null; + } +}; + + +/* +const initMessageHandler = (ws: WebSocket) => { + ws.on('message', (data: string) => { + + try { + const message: Message = JSONToObject(data); + if (message === null) { + console.log('could not parse received JSON message: ' + data); + return; + } + console.log('Received message: %s', JSON.stringify(message)); + switch (message.type) { + case MessageType.QUERY_LATEST: + write(ws, responseLatestMsg()); + break; + case MessageType.QUERY_ALL: + write(ws, responseChainMsg()); + break; + case MessageType.RESPONSE_BLOCKCHAIN: + const receivedBlocks: Block[] = JSONToObject(message.data); + if (receivedBlocks === null) { + console.log('invalid blocks received: %s', JSON.stringify(message.data)); + break; + } + handleBlockchainResponse(receivedBlocks); + break; + case MessageType.QUERY_TRANSACTION_POOL: + write(ws, responseTransactionPoolMsg()); + break; + case MessageType.RESPONSE_TRANSACTION_POOL: + const receivedTransactions: Transaction[] = JSONToObject(message.data); + if (receivedTransactions === null) { + console.log('invalid transaction received: %s', JSON.stringify(message.data)); + break; + } + receivedTransactions.forEach((transaction: Transaction) => { + try { + handleReceivedTransaction(transaction); + // if no error is thrown, transaction was indeed added to the pool + // let's broadcast transaction pool + broadCastTransactionPool(); + } catch (e) { + console.log(e.message); + } + }); + break; + } + } catch (e) { + console.log(e); + } + }); +}; +*/ +const initMessageHandler = (ws: WebSocket) => { + ws.on('message', (data: string) => { + mode = getMode(); + try { + const message: Message = JSONToObject(data); + if (message === null) { + console.log('could not parse received JSON message: ' + data); + return; + } + console.log('[Received message: %s', data); + switch (message.type) { + case MessageType.QUERY_LATEST: + write(ws, responseLatestMsg()); + break; + case MessageType.QUERY_ALL: + if(mode == 'local'){ + write(ws, responseChainMsg()); + } + break; + case MessageType.RESPONSE_BLOCKCHAIN: + const receivedBlocks: Block[] = JSONToObject(message.data); + if (receivedBlocks === null) { + console.log('invalid blocks received: %s', JSON.stringify(message.data)); + break; + } + handleBlockchainResponse(receivedBlocks); + break; + case MessageType.QUERY_TRANSACTION_POOL: + write(ws, responseTransactionPoolMsg()); + break; + case MessageType.RESPONSE_TRANSACTION_POOL: + const receivedTransactions: Transaction[] = JSONToObject(message.data); + if (receivedTransactions === null) { + console.log('invalid transaction received: %s', JSON.stringify(message.data)); + break; + } + receivedTransactions.forEach((transaction: Transaction) => { + try { + handleReceivedTransaction(transaction); + // if no error is thrown, transaction was indeed added to the pool + // let's broadcast transaction pool + broadCastTransactionPool(); + } catch (e) { + console.log(e.message); + } + }); + break; + case MessageType.ALTERNATE_ADDRESS: + console.log('Alternate address received: %s', message.data); + if(getMode() == 'dew'){ + write(wsCloud, message); + } else { + connectToPeers('ws://' + message.data); + } + break; + } + } catch (e) { + console.log(e); + } + console.log('message processing]'); + }); +}; + + +//for cloud-dew channel +const initCloudMessageHandler = (ws: WebSocket) => { + ws.on('message', (data: string) => { + + try { + const message: Message = JSONToObject(data); + if (message === null) { + console.log('could not parse received JSON message: ' + data); + return; + } + console.log('[Received cloud message: %s', data); + switch (message.type) { + case MessageType.QUERY_LATEST: + write(ws, responseLatestMsg()); + break; + case MessageType.QUERY_ALL: + write(ws, responseChainMsg()); + break; + case MessageType.RESPONSE_BLOCKCHAIN: + const receivedBlocks: Block[] = JSONToObject(message.data); + if (receivedBlocks === null) { + console.log('invalid blocks received: %s', JSON.stringify(message.data)); + break; + } + handleBlockchainResponse(receivedBlocks); + break; + case MessageType.QUERY_TRANSACTION_POOL: + write(ws, responseTransactionPoolMsg()); + break; + case MessageType.RESPONSE_TRANSACTION_POOL: + const receivedTransactions: Transaction[] = JSONToObject(message.data); + if (receivedTransactions === null) { + console.log('invalid transaction received: %s', JSON.stringify(message.data)); + break; + } + receivedTransactions.forEach((transaction: Transaction) => { + try { + handleReceivedTransaction(transaction); + // if no error is thrown, transaction was indeed added to the pool + // let's broadcast transaction pool + broadCastTransactionPool(); + } catch (e) { + console.log(e.message); + } + }); + break; + case MessageType.QUERY_ACCOUNTS: + write(ws, responseAccountsMsg()); + break; + case MessageType.RESPONSE_ACCOUNTS: + const receivedAccounts: Account[] = JSONToObject(message.data); + if (receivedAccounts === null) { + console.log('invalid accounts received: %s', JSON.stringify(message.data)); + break; + } + receivedAccounts.forEach((account: Account) => { + try { + validateAccount(account); + // if no error is thrown, accounts is valid + } catch (e) { + console.log(e.message); + } + }); + setAccounts(receivedAccounts); + break; + } + } catch (e) { + console.log(e); + } + console.log('Cloud message processing]'); + }); +}; + + +const write = (ws: WebSocket, message: Message): void => ws.send(JSON.stringify(message)); +//const broadcast = (message: Message): void => sockets.forEach((socket) => write(socket, message)); +const broadcast = (message: Message): void => { + sockets.forEach((socket) => write(socket, message)); + mode = getMode(); + if(mode == 'dew'){ + write(wsCloud, message); + } +} + +const queryChainLengthMsg = (): Message => ({'type': MessageType.QUERY_LATEST, 'data': null}); + +const queryAllMsg = (): Message => ({'type': MessageType.QUERY_ALL, 'data': null}); + +const responseChainMsg = (): Message => ({ + 'type': MessageType.RESPONSE_BLOCKCHAIN, 'data': JSON.stringify(getBlockchain()) +}); + +const responseLatestMsg = (): Message => ({ + 'type': MessageType.RESPONSE_BLOCKCHAIN, + 'data': JSON.stringify([getLatestBlock()]) +}); + +const queryTransactionPoolMsg = (): Message => ({ + 'type': MessageType.QUERY_TRANSACTION_POOL, + 'data': null +}); + +const responseTransactionPoolMsg = (): Message => ({ + 'type': MessageType.RESPONSE_TRANSACTION_POOL, + 'data': JSON.stringify(getTransactionPool()) +}); + +const queryAccountsMsg = (): Message => ({ + 'type': MessageType.QUERY_ACCOUNTS, + 'data': JSON.stringify(getAccounts()) +}); + +const responseAccountsMsg = (): Message => ({ + 'type': MessageType.RESPONSE_ACCOUNTS, + 'data': JSON.stringify(getAccounts()) +}); + +const alternateAddressMsg = (address: string): Message => ({ + 'type': MessageType.ALTERNATE_ADDRESS, + 'data': address +}); + +const sendMiningRequest = (newBlock: Block) => ( + write(wsCloud, {'type': MessageType.MINING_REQUEST, 'data': JSON.stringify(newBlock)}) +); + + +const initErrorHandler = (ws: WebSocket) => { + const closeConnection = (myWs: WebSocket) => { + console.log('connection failed to peer: ' + myWs.url); + sockets.splice(sockets.indexOf(myWs), 1); + }; + ws.on('close', () => closeConnection(ws)); + ws.on('error', () => closeConnection(ws)); +}; + +const fetchAccounts = (): void => { + if((typeof wsCloud !== undefined) && (wsCloud.readyState == 1)){ + write(wsCloud, queryAccountsMsg()); + console.log('Accounts fetched.'); + } else { + console.log('Accounts fetch failed.'); + } +}; + + +const handleBlockchainResponse = (receivedBlocks: Block[]) => { + if (receivedBlocks.length === 0) { + console.log('received block chain size of 0'); + return; + } + const latestBlockReceived: Block = receivedBlocks[receivedBlocks.length - 1]; + if (!isValidBlockStructure(latestBlockReceived)) { + console.log('block structuture not valid'); + return; + } + const latestBlockHeld: Block = getLatestBlock(); + if (latestBlockReceived.index > latestBlockHeld.index) { + console.log('blockchain possibly behind. We got: ' + + latestBlockHeld.index + ' Peer got: ' + latestBlockReceived.index); + if (latestBlockHeld.hash === latestBlockReceived.previousHash) { + if (addBlockToChain(latestBlockReceived)) { + broadcast(responseLatestMsg()); + } + } else if (receivedBlocks.length === 1) { + console.log('We have to query the chain from our peer'); + broadcast(queryAllMsg()); + } else { + console.log('Received blockchain is longer than current blockchain'); + replaceChain(receivedBlocks); + } + } else { + console.log('received blockchain is not longer than received blockchain. Do nothing'); + } +}; + +const broadcastLatest = (): void => { + broadcast(responseLatestMsg()); +}; + +/* +const connectToPeers = (newPeer: string): void => { + const ws: WebSocket = new WebSocket(newPeer); + ws.on('open', () => { + initConnection(ws); + }); + ws.on('error', () => { + console.log('connection failed'); + }); +}; +*/ +const connectToPeers = (newPeer: string): void => { + const ws: WebSocket = new WebSocket(newPeer); + ws.on('open', () => { + if(getMode() == 'dew'){ + write(ws, alternateAddressMsg(getCloud())); + } + initConnection(ws); + }); + ws.on('error', () => { + console.log('connection failed'); + }); +}; + +const broadCastTransactionPool = () => { + broadcast(responseTransactionPoolMsg()); +}; + + +const setModeLocal = () => { + setMode('local'); + mode = getMode(); + console.log('running on ' + mode + ' mode.'); + connectToPeers('ws://' + getCloud()); + if (typeof wsCloud !== "undefined") { + wsCloud.close(); + }; + broadcast(queryChainLengthMsg()); + //query transactions pool only some time after chain query + setTimeout(() => { + broadcast(queryTransactionPoolMsg()); + }, 500); +}; + +const setModeDew = () => { + setMode('dew'); + mode = getMode(); + console.log('running on ' + mode + ' mode.'); + + wsCloud = new WebSocket('ws://' + getCloud()); + wsCloud.on('open', () => { + console.log('connection with the cloud established'); + initCloudMessageHandler(wsCloud); + resetBlockchain(); + + write(wsCloud, queryChainLengthMsg()); + //query transactions pool only some time after chain query + setTimeout(() => { + write(wsCloud, queryTransactionPoolMsg()); + //broadcast(queryTransactionPoolMsg()); + }, 500); + }); + wsCloud.on('error', () => { + console.log('connection with the cloud failed'); + }); + wsCloud.on('close', () => { + console.log('connection with the cloud was closed'); + }); + +}; + + +//export {connectToPeers, broadcastLatest, broadCastTransactionPool, initP2PServer, getSockets}; +export {connectToPeers, broadcastLatest, broadCastTransactionPool, initP2PServer, getSockets, +setModeLocal, setModeDew, fetchAccounts, sendMiningRequest}; diff --git a/src/transaction.ts b/Dewblock-dew-server/src/transaction.ts similarity index 51% rename from src/transaction.ts rename to Dewblock-dew-server/src/transaction.ts index 277c287..d286311 100644 --- a/src/transaction.ts +++ b/Dewblock-dew-server/src/transaction.ts @@ -2,10 +2,18 @@ import * as CryptoJS from 'crypto-js'; import * as ecdsa from 'elliptic'; import * as _ from 'lodash'; -const ec = new ecdsa.ec('secp256k1'); +//dewcoin +import {getCurrentTimestamp} from './blockchain'; +import {getTransactionPool} from './transactionPool';//debug +const ec = new ecdsa.ec('secp256k1'); const COINBASE_AMOUNT: number = 50; +//dewcoin +const ACCOUNT_ACTIVE_PERIOD: number = 60*60*24*31; //31 days. +const ACCOUNT_PURGE_PERIOD: number = 60*60*24*32; //32 days. + +/* class UnspentTxOut { public readonly txOutId: string; public readonly txOutIndex: number; @@ -19,7 +27,24 @@ class UnspentTxOut { this.amount = amount; } } +*/ +class Account { + public readonly address: string; + public balance: number; + public available: number; + public txHistory: TxHistory[]; + + constructor(address: string) { + this.address = address; + this.balance = 0; + this.available = 0; + this.txHistory = []; + } +} + + +/* class TxIn { public txOutId: string; public txOutIndex: number; @@ -35,7 +60,6 @@ class TxOut { this.amount = amount; } } - class Transaction { public id: string; @@ -43,7 +67,53 @@ class Transaction { public txIns: TxIn[]; public txOuts: TxOut[]; } +*/ +class Transaction { + public id: string; + public sender: string; + public receiver: string; + public amount: number; + public timestamp: number; + public signature: string; + + constructor(sender: string, receiver: string, amount: number) { + this.sender = sender; + this.receiver = receiver; + this.amount = amount; + } +} +class TxHistory { + public id: string; + public txAccount: string; //the account that this transaction involvs. + public prevBalance: number; + public amount: number; // plus or minus. reflect the change of the balance. + public afterBalance: number; + public timestamp: number; + + constructor(txAccount: string, amount: number) { + this.txAccount = txAccount; + this.amount = amount; + } +} + +const existAccount = (address: string, accounts: Account[]): boolean => { + //console.log('address: ' + address); + //console.log('accounts: '+ JSON.stringify(accounts)); + return _(accounts).map((acc: Account) => acc.address).includes(address); +} +const createAccount = (address: string, accounts: Account[]): boolean => { + let acc: Account = new Account(address); + accounts.push(acc); + return true; +} +const findAccount = (address: string, accounts: Account[]): Account => { + let account: Account = _.find(accounts, (acc: Account) => {return acc.address == address}); + return account; +} + + +/* const getTransactionId = (transaction: Transaction): string => { const txInContent: string = transaction.txIns .map((txIn: TxIn) => txIn.txOutId + txIn.txOutIndex) @@ -55,7 +125,14 @@ const getTransactionId = (transaction: Transaction): string => { return CryptoJS.SHA256(txInContent + txOutContent).toString(); }; +*/ +const getTransactionId = (transaction: Transaction): string => { + return CryptoJS.SHA256(transaction.sender + transaction.receiver + transaction.amount + transaction.timestamp).toString(); +}; + + +/* const validateTransaction = (transaction: Transaction, aUnspentTxOuts: UnspentTxOut[]): boolean => { if (!isValidTransactionStructure(transaction)) { @@ -90,7 +167,46 @@ const validateTransaction = (transaction: Transaction, aUnspentTxOuts: UnspentTx return true; }; +*/ +const validateTransaction = (transaction: Transaction, accounts: Account[]): boolean => { + + if (!isValidTransactionStructure(transaction)) { + return false; + } + + if (getTransactionId(transaction) !== transaction.id) { + console.log('validateTransaction: invalid tx id: ' + transaction.id); + console.log(getTransactionId(transaction)); + return false; + } + + const hasValidSignature: boolean = validateSignature(transaction); + if (!hasValidSignature) { + console.log('The signature is invalid in tx: ' + transaction.id); + return false; + } + if (transaction.timestamp < (getCurrentTimestamp() - ACCOUNT_ACTIVE_PERIOD)){ + console.log('The transaction is too old. tx: ' + transaction.id); + return false; + } + let accSender: Account = findAccount(transaction.sender, accounts); + if(accSender == undefined){ + console.log('validateTransaction: no account found.'); + return false; + } + if (_(accSender.txHistory).map((th: TxHistory) => {return th.id}).includes(transaction.id)){ + console.log(JSON.stringify(getTransactionPool())); + console.log('validateTransaction: The transaction is duplicated with the chain. tx: ' + transaction.id); + console.log(JSON.stringify(accSender.txHistory)); + return false; + } + + return true; +}; + + +/* const validateBlockTransactions = (aTransactions: Transaction[], aUnspentTxOuts: UnspentTxOut[], blockIndex: number): boolean => { const coinbaseTx = aTransactions[0]; if (!validateCoinbaseTx(coinbaseTx, blockIndex)) { @@ -98,7 +214,6 @@ const validateBlockTransactions = (aTransactions: Transaction[], aUnspentTxOuts: return false; } - // check for duplicate txIns. Each txIn can be included only once const txIns: TxIn[] = _(aTransactions) .map((tx) => tx.txIns) .flatten() @@ -114,7 +229,47 @@ const validateBlockTransactions = (aTransactions: Transaction[], aUnspentTxOuts: .reduce((a, b) => (a && b), true); }; +*/ +const validateBlockTransactions = (aTransactions: Transaction[], accounts: Account[]): boolean => { + let accSender: Account; + + const coinbaseTx = aTransactions[0]; + if (!validateCoinbaseTx(coinbaseTx)) { + console.log('invalid coinbase transaction: ' + JSON.stringify(coinbaseTx)); + return false; + } + const txIds: string[] = _(aTransactions) + .map((tx) => tx.id) + .value(); + + if (hasDuplicates(txIds)) { + console.log('Transactions in block are duplicated.'); + return false; + } + + for(let i=0; i < accounts.length; i++){ + accounts[i].available = accounts[i].balance; + } + for(let j=1; j < aTransactions.length; j++){ + accSender = findAccount(aTransactions[j].sender, accounts); + if(accSender == undefined){ + console.log('validateBlockTransactions: no account found.'); + return false; + } + if (aTransactions[j].amount > accSender.available) { + console.log('The sender does not have enough coins. tx: ' + aTransactions[j].id); + return false; + } + accSender.available -=aTransactions[j].amount; + } + // all but coinbase transactions + const normalTransactions: Transaction[] = aTransactions.slice(1); + return normalTransactions.map((tx) => validateTransaction(tx, accounts)) + .reduce((a, b) => (a && b), true); +}; + +/* const hasDuplicates = (txIns: TxIn[]): boolean => { const groups = _.countBy(txIns, (txIn: TxIn) => txIn.txOutId + txIn.txOutIndex); return _(groups) @@ -128,7 +283,23 @@ const hasDuplicates = (txIns: TxIn[]): boolean => { }) .includes(true); }; +*/ +const hasDuplicates = (names: string[]): boolean => { + const groups = _.countBy(names); + return _(groups) + .map((value, key) => { + if (value > 1) { + console.log('duplicate string: ' + key); + return true; + } else { + return false; + } + }) + .includes(true); +}; + +/* const validateCoinbaseTx = (transaction: Transaction, blockIndex: number): boolean => { if (transaction == null) { console.log('the first transaction in the block must be coinbase transaction'); @@ -156,7 +327,30 @@ const validateCoinbaseTx = (transaction: Transaction, blockIndex: number): boole } return true; }; +*/ +const validateCoinbaseTx = (transaction: Transaction): boolean => { + if (transaction == null) { + console.log('the first transaction in the block must be coinbase transaction'); + return false; + } + if (getTransactionId(transaction) !== transaction.id) { + console.log('invalid coinbase tx id: ' + transaction.id); + return false; + } + if (transaction.sender !== "coinbase") { + console.log('It is not a coinbase transaction'); + return; + } + if (transaction.amount !== COINBASE_AMOUNT) { + console.log('invalid coinbase amount in coinbase transaction'); + return false; + } + return true; +}; + + +/* const validateTxIn = (txIn: TxIn, transaction: Transaction, aUnspentTxOuts: UnspentTxOut[]): boolean => { const referencedUTxOut: UnspentTxOut = aUnspentTxOuts.find((uTxO) => uTxO.txOutId === txIn.txOutId && uTxO.txOutIndex === txIn.txOutIndex); @@ -174,15 +368,30 @@ const validateTxIn = (txIn: TxIn, transaction: Transaction, aUnspentTxOuts: Unsp } return true; }; +*/ +const validateSignature = (transaction: Transaction): boolean => { + const key = ec.keyFromPublic(transaction.sender, 'hex'); + const validSignature: boolean = key.verify(transaction.id, transaction.signature); + if (!validSignature) { + console.log('invalid tx signature: %s txId: %s address: %s', transaction.signature, transaction.id, transaction.sender); + return false; + } + return true; +}; + + +/* const getTxInAmount = (txIn: TxIn, aUnspentTxOuts: UnspentTxOut[]): number => { return findUnspentTxOut(txIn.txOutId, txIn.txOutIndex, aUnspentTxOuts).amount; }; - const findUnspentTxOut = (transactionId: string, index: number, aUnspentTxOuts: UnspentTxOut[]): UnspentTxOut => { return aUnspentTxOuts.find((uTxO) => uTxO.txOutId === transactionId && uTxO.txOutIndex === index); }; +*/ + +/* const getCoinbaseTransaction = (address: string, blockIndex: number): Transaction => { const t = new Transaction(); const txIn: TxIn = new TxIn(); @@ -195,7 +404,17 @@ const getCoinbaseTransaction = (address: string, blockIndex: number): Transactio t.id = getTransactionId(t); return t; }; +*/ +const getCoinbaseTransaction = (miner: string): Transaction => { + const t = new Transaction("coinbase", miner, COINBASE_AMOUNT); + t.timestamp = getCurrentTimestamp(); + t.id = getTransactionId(t); + t.signature = ''; + return t; +}; + +/* const signTxIn = (transaction: Transaction, txInIndex: number, privateKey: string, aUnspentTxOuts: UnspentTxOut[]): string => { const txIn: TxIn = transaction.txIns[txInIndex]; @@ -218,7 +437,22 @@ const signTxIn = (transaction: Transaction, txInIndex: number, return signature; }; +*/ +const signTransaction = (transaction: Transaction, privateKey: string): string => { + const dataToSign = transaction.id; + if (getPublicKey(privateKey) !== transaction.sender) { + console.log('trying to sign an input with private' + + ' key that does not match the address'); + throw Error(); + } + const key = ec.keyFromPrivate(privateKey, 'hex'); + const signature: string = toHexString(key.sign(dataToSign).toDER()); + return signature; +}; + + +/* const updateUnspentTxOuts = (aTransactions: Transaction[], aUnspentTxOuts: UnspentTxOut[]): UnspentTxOut[] => { const newUnspentTxOuts: UnspentTxOut[] = aTransactions .map((t) => { @@ -237,7 +471,60 @@ const updateUnspentTxOuts = (aTransactions: Transaction[], aUnspentTxOuts: Unspe return resultingUnspentTxOuts; }; +*/ +const updateAccounts = (aTransactions: Transaction[], accounts: Account[]): Account[] => { + let amt: number; + let thSender: TxHistory; + let thReceiver: TxHistory; + let accSender: Account; + let accReceiver: Account; + let now: number; + + for(let i=0; i {return (th.timestamp < (now + ACCOUNT_PURGE_PERIOD))}).push(thSender).value(); + //clean txHistory at the same time. + } + if (!existAccount(aTransactions[i].receiver, accounts)) { + createAccount(aTransactions[i].receiver, accounts); + } + accReceiver = findAccount(aTransactions[i].receiver, accounts); + if(accReceiver == undefined){ + console.log('validateTransaction: no account found.'); + return undefined; + } + thReceiver = new TxHistory(aTransactions[i].sender, amt); + thReceiver.id = aTransactions[i].id; + thReceiver.timestamp = aTransactions[i].timestamp; + thReceiver.prevBalance = accReceiver.balance; + accReceiver.balance += amt; + accReceiver.available = accReceiver.balance; + thReceiver.afterBalance = accReceiver.balance; + accReceiver.txHistory = _(accReceiver.txHistory).filter((th: TxHistory) => {return (th.timestamp < (now + ACCOUNT_PURGE_PERIOD))}).push(thReceiver).value(); + //clean txHistory at the same time. + } + return accounts;//check again +}; + +/* const processTransactions = (aTransactions: Transaction[], aUnspentTxOuts: UnspentTxOut[], blockIndex: number) => { if (!validateBlockTransactions(aTransactions, aUnspentTxOuts, blockIndex)) { @@ -246,6 +533,18 @@ const processTransactions = (aTransactions: Transaction[], aUnspentTxOuts: Unspe } return updateUnspentTxOuts(aTransactions, aUnspentTxOuts); }; +*/ +const processTransactions = (aTransactions: Transaction[], accounts: Account[]) => { + + if (!validateBlockTransactions(aTransactions, accounts)) { + console.log('invalid block transactions'); + return null; + } + return updateAccounts(aTransactions, accounts); +}; + + + const toHexString = (byteArray): string => { return Array.from(byteArray, (byte: any) => { @@ -257,6 +556,8 @@ const getPublicKey = (aPrivateKey: string): string => { return ec.keyFromPrivate(aPrivateKey, 'hex').getPublic().encode('hex'); }; + +/* const isValidTxInStructure = (txIn: TxIn): boolean => { if (txIn == null) { console.log('txIn is null'); @@ -274,7 +575,6 @@ const isValidTxInStructure = (txIn: TxIn): boolean => { return true; } }; - const isValidTxOutStructure = (txOut: TxOut): boolean => { if (txOut == null) { console.log('txOut is null'); @@ -292,7 +592,11 @@ const isValidTxOutStructure = (txOut: TxOut): boolean => { return true; } }; +*/ + + +/* const isValidTransactionStructure = (transaction: Transaction) => { if (typeof transaction.id !== 'string') { console.log('transactionId missing'); @@ -320,6 +624,35 @@ const isValidTransactionStructure = (transaction: Transaction) => { } return true; }; +*/ +const isValidTransactionStructure = (transaction: Transaction) => { + if (typeof transaction.id !== 'string') { + console.log('transactionId missing'); + return false; + } + if (typeof transaction.sender !== 'string') { + console.log('transaction sender missing'); + return false; + } + if (typeof transaction.receiver !== 'string') { + console.log('transaction receiver missing'); + return false; + } + if (typeof transaction.amount !== 'number') { + console.log('transaction amount missing'); + return false; + } + if (typeof transaction.timestamp !== 'number') { + console.log('transaction timestamp missing'); + return false; + } + if (typeof transaction.signature !== 'string') { + console.log('transaction signature missing'); + return false; + } + return true; +}; + // valid address is a valid ecdsa public key in the 04 + X-coordinate + Y-coordinate format const isValidAddress = (address: string): boolean => { @@ -337,8 +670,16 @@ const isValidAddress = (address: string): boolean => { return true; }; + +/* export { processTransactions, signTxIn, getTransactionId, isValidAddress, validateTransaction, UnspentTxOut, TxIn, TxOut, getCoinbaseTransaction, getPublicKey, hasDuplicates, Transaction }; +*/ +export { + processTransactions, signTransaction, getTransactionId, isValidAddress, validateTransaction, + Account, findAccount, existAccount, createAccount, getCoinbaseTransaction, + getPublicKey, hasDuplicates, Transaction +}; diff --git a/Dewblock-dew-server/src/transactionPool.ts b/Dewblock-dew-server/src/transactionPool.ts new file mode 100644 index 0000000..4d20902 --- /dev/null +++ b/Dewblock-dew-server/src/transactionPool.ts @@ -0,0 +1,166 @@ +import * as _ from 'lodash'; + +//import {Transaction, TxIn, UnspentTxOut, validateTransaction} from './transaction'; +import {Transaction, Account, findAccount, validateTransaction} from './transaction'; + +let transactionPool: Transaction[] = []; + +const getTransactionPool = () => { + return _.cloneDeep(transactionPool); +}; + +/* +const addToTransactionPool = (tx: Transaction, unspentTxOuts: UnspentTxOut[]) => { + + if (!validateTransaction(tx, unspentTxOuts)) { + throw Error('Trying to add invalid tx to pool'); + } + + if (!isValidTxForPool(tx, transactionPool)) { + throw Error('Trying to add invalid tx to pool'); + } + console.log('adding to txPool: %s', JSON.stringify(tx)); + transactionPool.push(tx); +}; +*/ +const addToTransactionPool = (tx: Transaction, accounts: Account[]) => { + + if (!validateTransaction(tx, accounts)) { + throw Error('Trying to add invalid tx to pool. Tx: ' + tx.id); + } + + if (!isValidTxForPool(tx, transactionPool, accounts)) { + throw Error('Tx is not valid for the pool, Tx: ' + tx.id); + } + //console.log('adding to txPool: %s', JSON.stringify(tx)); + transactionPool.push(tx); +}; + +/* +const hasTxIn = (txIn: TxIn, unspentTxOuts: UnspentTxOut[]): boolean => { + const foundTxIn = unspentTxOuts.find((uTxO: UnspentTxOut) => { + return uTxO.txOutId === txIn.txOutId && uTxO.txOutIndex === txIn.txOutIndex; + }); + return foundTxIn !== undefined; +}; +*/ + +/* +const updateTransactionPool = (unspentTxOuts: UnspentTxOut[]) => { + const invalidTxs = []; + for (const tx of transactionPool) { + for (const txIn of tx.txIns) { + if (!hasTxIn(txIn, unspentTxOuts)) { + invalidTxs.push(tx); + break; + } + } + } + if (invalidTxs.length > 0) { + console.log('removing the following transactions from txPool: %s', JSON.stringify(invalidTxs)); + transactionPool = _.without(transactionPool, ...invalidTxs); + } +}; +*/ +const updateTransactionPoolReplace = (accounts: Account[]) => { + let accSender; + + transactionPool = _(transactionPool).uniqBy('id').filter(tx => validateTransaction(tx, accounts)).value(); + + for(let i=0; i < accounts.length; i++){ + accounts[i].available = accounts[i].balance; + } + const invalidTxs = []; + for (const tx of transactionPool) { + accSender = findAccount(tx.sender, accounts); + if(accSender == undefined){ + console.log('updateTransactionPool: no account found.'); + return false; + } + if (tx.amount > accSender.available) { + console.log('The sender does not have enough coins. Transaction be removed from pool. tx: ' + tx.id); + invalidTxs.push(tx); + }else{ + accSender.available -=tx.amount; + } + } + if (invalidTxs.length > 0) { + console.log('removing the following transactions from txPool: %s', JSON.stringify(invalidTxs)); + transactionPool = _.without(transactionPool, ...invalidTxs); + } +}; +const updateTransactionPool = (transactions: Transaction[]) => { + //console.log('tansactionPool: '+JSON.stringify(transactionPool)); + if (transactions.length > 0) { + //console.log('removing the following transactions from txPool: %s', JSON.stringify(transactions)); + let txIDs: string[] = _.map(transactions, (tx: Transaction) => tx.id); + transactionPool = _.filter(transactionPool, (tx: Transaction) => !_.includes(txIDs, tx.id)); + } + //console.log('tansactionPool: '+JSON.stringify(transactionPool)); +}; + +/* +const getTxPoolIns = (aTransactionPool: Transaction[]): TxIn[] => { + return _(aTransactionPool) + .map((tx) => tx.txIns) + .flatten() + .value(); +}; +*/ + +/* +const isValidTxForPool = (tx: Transaction, aTtransactionPool: Transaction[]): boolean => { + const txPoolIns: TxIn[] = getTxPoolIns(aTtransactionPool); + + const containsTxIn = (txIns: TxIn[], txIn: TxIn) => { + return _.find(txPoolIns, ((txPoolIn) => { + return txIn.txOutIndex === txPoolIn.txOutIndex && txIn.txOutId === txPoolIn.txOutId; + })); + }; + + for (const txIn of tx.txIns) { + if (containsTxIn(txPoolIns, txIn)) { + console.log('txIn already found in the txPool'); + return false; + } + } + return true; +}; +*/ +// This transaction tx has been validated in another function. id duplicate was checked in validation. +const isValidTxForPool = (tx: Transaction, aTransactionPool: Transaction[], accounts: Account[]): boolean => { + let accSender: Account; + + for(let i=0; i < accounts.length; i++){ + accounts[i].available = accounts[i].balance; + } + for (const trx of aTransactionPool) { + if(tx.id == trx.id){ + console.log('The transaction is duplicated with the existing transaction pool. trx: ' + trx.id); + return false; + } + accSender = findAccount(trx.sender, accounts); + if(accSender == undefined){ + console.log('isValideTxFor Pool: no account found.'); + return false; + } + if (trx.amount > accSender.available) { + console.log('The sender does not have enough coins. The existing transaction pool has problems. trx: ' + trx.id); + return false; + }else{ + accSender.available -= trx.amount; + } + } + accSender = findAccount(tx.sender, accounts); + if(accSender == undefined){ + console.log('isValidTxForPool: no account found.'); + return false; + } + if (tx.amount > accSender.available) { + console.log('The transaction is not valid for the pool. tx: ' + tx.id); + return false; + } + return true; +}; + +export {addToTransactionPool, getTransactionPool, updateTransactionPool, updateTransactionPoolReplace}; diff --git a/Dewblock-dew-server/src/util.ts b/Dewblock-dew-server/src/util.ts new file mode 100644 index 0000000..32d8f47 --- /dev/null +++ b/Dewblock-dew-server/src/util.ts @@ -0,0 +1,19 @@ +const hexToBinary = (s: string): string => { + let ret: string = ''; + const lookupTable = { + '0': '0000', '1': '0001', '2': '0010', '3': '0011', '4': '0100', + '5': '0101', '6': '0110', '7': '0111', '8': '1000', '9': '1001', + 'a': '1010', 'b': '1011', 'c': '1100', 'd': '1101', + 'e': '1110', 'f': '1111' + }; + for (let i: number = 0; i < s.length; i = i + 1) { + if (lookupTable[s[i]]) { + ret += lookupTable[s[i]]; + } else { + return null; + } + } + return ret; +}; + +export {hexToBinary}; diff --git a/Dewblock-dew-server/src/wallet.ts b/Dewblock-dew-server/src/wallet.ts new file mode 100644 index 0000000..4760054 --- /dev/null +++ b/Dewblock-dew-server/src/wallet.ts @@ -0,0 +1,193 @@ +import {ec} from 'elliptic'; +import {existsSync, readFileSync, unlinkSync, writeFileSync} from 'fs'; +import * as _ from 'lodash'; +//import {getPublicKey, getTransactionId, signTxIn, Transaction, TxIn, TxOut, UnspentTxOut} from './transaction'; +import {getPublicKey, getTransactionId, signTransaction, Transaction, Account, findAccount, existAccount, createAccount} from './transaction'; + +//dewcoin +import {getCurrentTimestamp, getAccounts} from './blockchain'; + +const EC = new ec('secp256k1'); +const privateKeyLocation = process.env.PRIVATE_KEY || 'node/wallet/private_key'; + +const getPrivateFromWallet = (): string => { + const buffer = readFileSync(privateKeyLocation, 'utf8'); + return buffer.toString(); +}; + +const getPublicFromWallet = (): string => { + const privateKey = getPrivateFromWallet(); + const key = EC.keyFromPrivate(privateKey, 'hex'); + return key.getPublic().encode('hex'); +}; + +const generatePrivateKey = (): string => { + const keyPair = EC.genKeyPair(); + const privateKey = keyPair.getPrivate(); + return privateKey.toString(16); +}; + +const initWallet = () => { + // let's not override existing private keys + if (existsSync(privateKeyLocation)) { + if (!existAccount(getPublicFromWallet(), getAccounts())) { + createAccount(getPublicFromWallet(), getAccounts()); + console.log('an account was created.'); + } + return; + } + const newPrivateKey = generatePrivateKey(); + + writeFileSync(privateKeyLocation, newPrivateKey); + console.log('new wallet with private key created to : %s', privateKeyLocation); + if (!existAccount(getPublicFromWallet(), getAccounts())) { + createAccount(getPublicFromWallet(), getAccounts()); + console.log('an account was created.'); + } +}; + +const deleteWallet = () => { + if (existsSync(privateKeyLocation)) { + unlinkSync(privateKeyLocation); + } +}; + +/* +const getBalance = (address: string, unspentTxOuts: UnspentTxOut[]): number => { + return _(findUnspentTxOuts(address, unspentTxOuts)) + .map((uTxO: UnspentTxOut) => uTxO.amount) + .sum(); +}; +*/ +const getBalance = (address: string, accounts: Account[]): number => { + let acct: Account = findAccount(address, accounts); + if(acct == undefined){ + console.log('getBalance: no account found.'); + return 0; + } + return acct.balance; +}; + +/* +const findUnspentTxOuts = (ownerAddress: string, unspentTxOuts: UnspentTxOut[]) => { + return _.filter(unspentTxOuts, (uTxO: UnspentTxOut) => uTxO.address === ownerAddress); +}; +*/ + +/* +const findTxOutsForAmount = (amount: number, myUnspentTxOuts: UnspentTxOut[]) => { + let currentAmount = 0; + const includedUnspentTxOuts = []; + for (const myUnspentTxOut of myUnspentTxOuts) { + includedUnspentTxOuts.push(myUnspentTxOut); + currentAmount = currentAmount + myUnspentTxOut.amount; + if (currentAmount >= amount) { + const leftOverAmount = currentAmount - amount; + return {includedUnspentTxOuts, leftOverAmount}; + } + } + + const eMsg = 'Cannot create transaction from the available unspent transaction outputs.' + + ' Required amount:' + amount + '. Available unspentTxOuts:' + JSON.stringify(myUnspentTxOuts); + throw Error(eMsg); +}; +*/ + +/* +const createTxOuts = (receiverAddress: string, myAddress: string, amount, leftOverAmount: number) => { + const txOut1: TxOut = new TxOut(receiverAddress, amount); + if (leftOverAmount === 0) { + return [txOut1]; + } else { + const leftOverTx = new TxOut(myAddress, leftOverAmount); + return [txOut1, leftOverTx]; + } +}; +*/ + +/* +const filterTxPoolTxs = (unspentTxOuts: UnspentTxOut[], transactionPool: Transaction[]): UnspentTxOut[] => { + const txIns: TxIn[] = _(transactionPool) + .map((tx: Transaction) => tx.txIns) + .flatten() + .value(); + const removable: UnspentTxOut[] = []; + for (const unspentTxOut of unspentTxOuts) { + const txIn = _.find(txIns, (aTxIn: TxIn) => { + return aTxIn.txOutIndex === unspentTxOut.txOutIndex && aTxIn.txOutId === unspentTxOut.txOutId; + }); + + if (txIn === undefined) { + + } else { + removable.push(unspentTxOut); + } + } + + return _.without(unspentTxOuts, ...removable); +}; +*/ + +/* +const createTransaction = (receiverAddress: string, amount: number, privateKey: string, + unspentTxOuts: UnspentTxOut[], txPool: Transaction[]): Transaction => { + + console.log('txPool: %s', JSON.stringify(txPool)); + const myAddress: string = getPublicKey(privateKey); + const myUnspentTxOutsA = unspentTxOuts.filter((uTxO: UnspentTxOut) => uTxO.address === myAddress); + + const myUnspentTxOuts = filterTxPoolTxs(myUnspentTxOutsA, txPool); + + // filter from unspentOutputs such inputs that are referenced in pool + const {includedUnspentTxOuts, leftOverAmount} = findTxOutsForAmount(amount, myUnspentTxOuts); + + const toUnsignedTxIn = (unspentTxOut: UnspentTxOut) => { + const txIn: TxIn = new TxIn(); + txIn.txOutId = unspentTxOut.txOutId; + txIn.txOutIndex = unspentTxOut.txOutIndex; + return txIn; + }; + + const unsignedTxIns: TxIn[] = includedUnspentTxOuts.map(toUnsignedTxIn); + + const tx: Transaction = new Transaction(); + tx.txIns = unsignedTxIns; + tx.txOuts = createTxOuts(receiverAddress, myAddress, amount, leftOverAmount); + tx.id = getTransactionId(tx); + + tx.txIns = tx.txIns.map((txIn: TxIn, index: number) => { + txIn.signature = signTxIn(tx, index, privateKey, unspentTxOuts); + return txIn; + }); + + return tx; +}; +*/ +const createTransaction = (receiverAddress: string, amount: number, privateKey: string, + accounts: Account[], txPool: Transaction[]): Transaction => { + + //console.log('txPool: %s', JSON.stringify(txPool)); + const myAddress: string = getPublicKey(privateKey); + + const myAccount: Account = findAccount(myAddress, accounts); + + if(amount > myAccount.balance){ + console.log('No enough coins.'); + return undefined; + } + + const tx: Transaction = new Transaction(myAddress, receiverAddress, amount); + tx.timestamp = getCurrentTimestamp(); + tx.id = getTransactionId(tx); + tx.signature = signTransaction(tx, privateKey); + //what is the role of transaction pool? + return tx; +}; + +/* +export {createTransaction, getPublicFromWallet, + getPrivateFromWallet, getBalance, generatePrivateKey, initWallet, deleteWallet, findUnspentTxOuts}; + +*/ +export {createTransaction, getPublicFromWallet, + getPrivateFromWallet, getBalance, generatePrivateKey, initWallet, deleteWallet}; diff --git a/Dewblock-dew-server/tsconfig.json b/Dewblock-dew-server/tsconfig.json new file mode 100644 index 0000000..73e4689 --- /dev/null +++ b/Dewblock-dew-server/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2015", + "sourceMap": true + }, + "include": [ + "src/main.ts", + "src/block.ts", + "src/p2p.ts", + "src/util.ts", + "src/transaction.ts", + "src/wallet.ts", + "src/transactionPool.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/Dewblock-dew-server/tslint.json b/Dewblock-dew-server/tslint.json new file mode 100644 index 0000000..3434404 --- /dev/null +++ b/Dewblock-dew-server/tslint.json @@ -0,0 +1,26 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "no-bitwise": false, + "eofline": false, + "no-empty": false, + "max-line-length": [140], + "max-classes-per-file": [4], + "prefer-for-of": false, + "radix": false, + "trailing-comma": false, + "object-literal-sort-keys": false, + "object-literal-key-quotes": false, + "no-console": false, + "quotemark": [ + true, + "single", + "avoid-escape" + ] + }, + "rulesDirectory": [] +} diff --git a/README.md b/README.md index b1664b6..f28ba73 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,39 @@ -# Naivecoin +# Dewblock -The repository for the naivecoin tutorial: https://lhartikk.github.io/ +Dewblock is a blockchain cryptocurrency system that is based on dew computing principles. The code is pretty detailed but still not detailed enough for immediate production operation. It is basically a Proof of Concept system. The mechanism of dewblock will be introduced separately. Some information about dew computing can be found in: http://www.dewcomputing.org/ -``` -npm install -npm start -``` +Dewblock is based on Naivecoin. The introduction of Naivecoin can be found in: A tutorial for building a cryptocurrency https://lhartikk.github.io/ -##### Get blockchain -``` -curl http://localhost:3001/blocks -``` -##### Mine a block -``` -curl -X POST http://localhost:3001/mineBlock -``` +## Package Placement -##### Send transaction -``` -curl -H "Content-type: application/json" --data '{"address": "04bfcab8722991ae774db48f934ca79cfb7dd991229153b9f732ba5334aafcd8e7266e47076996b55a14bf9913ee3145ce0cfc1372ada8ada74bd287450313534b", "amount" : 35}' http://localhost:3001/sendTransaction -``` +Dewblock has two components: Dewblock-cloud-server package and Dewblock-dew-server package. Ideally, Dewblock-cloud-server package should be deployed in a cloud service or a computer that is running all the time and has a fixed IP address so that it can be accessed easily; Dewblock-dew-server package can be deployed in a local computer, such as a desktop/laptop computer or a mobile device. Dewblock-cloud-server package and Dewblock-dew-server package could be stay in the same computer for testing. Different nodes cannot be deployed into the same computer because each node needs to have its unique IP address. -##### Query transaction pool -``` -curl http://localhost:3001/transactionPool -``` +## Package Configuraton -##### Mine transaction -``` -curl -H "Content-type: application/json" --data '{"address": "04bfcab8722991ae774db48f934ca79cfb7dd991229153b9f732ba5334aafcd8e7266e47076996b55a14bf9913ee3145ce0cfc1372ada8ada74bd287450313534b", "amount" : 35}' http://localhost:3001/mineTransaction -``` +Configuraton files: +Dewblock-dew-server/src/config.ts +Dewblock-cloud-server/src/config.ts -##### Get balance -``` -curl http://localhost:3001/balance -``` +Detailed configuration guidelines can be found in these files. + +Please notice: If more than one machine is involved, config files should use actual IP addresses, not use localhost or 127.0.0.1 at all. -#### Query information about a specific address -``` -curl http://localhost:3001/address/04f72a4541275aeb4344a8b049bfe2734b49fe25c08d56918f033507b96a61f9e3c330c4fcd46d0854a712dc878b9c280abe90c788c47497e06df78b25bf60ae64 -``` -##### Add peer +## Package Installation + +Both packages should be installed in Node.js environment. + +Installation command: ``` -curl -H "Content-type:application/json" --data '{"peer" : "ws://localhost:6001"}' http://localhost:3001/addPeer +npm install ``` -#### Query connected peers +Running command: ``` -curl http://localhost:3001/peers +npm start ``` +## Dewblock Operation + +Dewblock system can be operated through an API composed of a group of HTTP commands. These commands can be issued through browsers, designed web forms, or HTTP clients such as curl. + +We use curl to describe the API, but it does not mean curl is the only way to operate Dewblock. diff --git a/src/p2p.ts b/src/p2p.ts deleted file mode 100644 index d15c8de..0000000 --- a/src/p2p.ts +++ /dev/null @@ -1,191 +0,0 @@ -import * as WebSocket from 'ws'; -import {Server} from 'ws'; -import { - addBlockToChain, Block, getBlockchain, getLatestBlock, handleReceivedTransaction, isValidBlockStructure, - replaceChain -} from './blockchain'; -import {Transaction} from './transaction'; -import {getTransactionPool} from './transactionPool'; - -const sockets: WebSocket[] = []; - -enum MessageType { - QUERY_LATEST = 0, - QUERY_ALL = 1, - RESPONSE_BLOCKCHAIN = 2, - QUERY_TRANSACTION_POOL = 3, - RESPONSE_TRANSACTION_POOL = 4 -} - -class Message { - public type: MessageType; - public data: any; -} - -const initP2PServer = (p2pPort: number) => { - const server: Server = new WebSocket.Server({port: p2pPort}); - server.on('connection', (ws: WebSocket) => { - initConnection(ws); - }); - console.log('listening websocket p2p port on: ' + p2pPort); -}; - -const getSockets = () => sockets; - -const initConnection = (ws: WebSocket) => { - sockets.push(ws); - initMessageHandler(ws); - initErrorHandler(ws); - write(ws, queryChainLengthMsg()); - - // query transactions pool only some time after chain query - setTimeout(() => { - broadcast(queryTransactionPoolMsg()); - }, 500); -}; - -const JSONToObject = (data: string): T => { - try { - return JSON.parse(data); - } catch (e) { - console.log(e); - return null; - } -}; - -const initMessageHandler = (ws: WebSocket) => { - ws.on('message', (data: string) => { - - try { - const message: Message = JSONToObject(data); - if (message === null) { - console.log('could not parse received JSON message: ' + data); - return; - } - console.log('Received message: %s', JSON.stringify(message)); - switch (message.type) { - case MessageType.QUERY_LATEST: - write(ws, responseLatestMsg()); - break; - case MessageType.QUERY_ALL: - write(ws, responseChainMsg()); - break; - case MessageType.RESPONSE_BLOCKCHAIN: - const receivedBlocks: Block[] = JSONToObject(message.data); - if (receivedBlocks === null) { - console.log('invalid blocks received: %s', JSON.stringify(message.data)); - break; - } - handleBlockchainResponse(receivedBlocks); - break; - case MessageType.QUERY_TRANSACTION_POOL: - write(ws, responseTransactionPoolMsg()); - break; - case MessageType.RESPONSE_TRANSACTION_POOL: - const receivedTransactions: Transaction[] = JSONToObject(message.data); - if (receivedTransactions === null) { - console.log('invalid transaction received: %s', JSON.stringify(message.data)); - break; - } - receivedTransactions.forEach((transaction: Transaction) => { - try { - handleReceivedTransaction(transaction); - // if no error is thrown, transaction was indeed added to the pool - // let's broadcast transaction pool - broadCastTransactionPool(); - } catch (e) { - console.log(e.message); - } - }); - break; - } - } catch (e) { - console.log(e); - } - }); -}; - -const write = (ws: WebSocket, message: Message): void => ws.send(JSON.stringify(message)); -const broadcast = (message: Message): void => sockets.forEach((socket) => write(socket, message)); - -const queryChainLengthMsg = (): Message => ({'type': MessageType.QUERY_LATEST, 'data': null}); - -const queryAllMsg = (): Message => ({'type': MessageType.QUERY_ALL, 'data': null}); - -const responseChainMsg = (): Message => ({ - 'type': MessageType.RESPONSE_BLOCKCHAIN, 'data': JSON.stringify(getBlockchain()) -}); - -const responseLatestMsg = (): Message => ({ - 'type': MessageType.RESPONSE_BLOCKCHAIN, - 'data': JSON.stringify([getLatestBlock()]) -}); - -const queryTransactionPoolMsg = (): Message => ({ - 'type': MessageType.QUERY_TRANSACTION_POOL, - 'data': null -}); - -const responseTransactionPoolMsg = (): Message => ({ - 'type': MessageType.RESPONSE_TRANSACTION_POOL, - 'data': JSON.stringify(getTransactionPool()) -}); - -const initErrorHandler = (ws: WebSocket) => { - const closeConnection = (myWs: WebSocket) => { - console.log('connection failed to peer: ' + myWs.url); - sockets.splice(sockets.indexOf(myWs), 1); - }; - ws.on('close', () => closeConnection(ws)); - ws.on('error', () => closeConnection(ws)); -}; - -const handleBlockchainResponse = (receivedBlocks: Block[]) => { - if (receivedBlocks.length === 0) { - console.log('received block chain size of 0'); - return; - } - const latestBlockReceived: Block = receivedBlocks[receivedBlocks.length - 1]; - if (!isValidBlockStructure(latestBlockReceived)) { - console.log('block structuture not valid'); - return; - } - const latestBlockHeld: Block = getLatestBlock(); - if (latestBlockReceived.index > latestBlockHeld.index) { - console.log('blockchain possibly behind. We got: ' - + latestBlockHeld.index + ' Peer got: ' + latestBlockReceived.index); - if (latestBlockHeld.hash === latestBlockReceived.previousHash) { - if (addBlockToChain(latestBlockReceived)) { - broadcast(responseLatestMsg()); - } - } else if (receivedBlocks.length === 1) { - console.log('We have to query the chain from our peer'); - broadcast(queryAllMsg()); - } else { - console.log('Received blockchain is longer than current blockchain'); - replaceChain(receivedBlocks); - } - } else { - console.log('received blockchain is not longer than received blockchain. Do nothing'); - } -}; - -const broadcastLatest = (): void => { - broadcast(responseLatestMsg()); -}; - -const connectToPeers = (newPeer: string): void => { - const ws: WebSocket = new WebSocket(newPeer); - ws.on('open', () => { - initConnection(ws); - }); - ws.on('error', () => { - console.log('connection failed'); - }); -}; - -const broadCastTransactionPool = () => { - broadcast(responseTransactionPoolMsg()); -}; - -export {connectToPeers, broadcastLatest, broadCastTransactionPool, initP2PServer, getSockets}; diff --git a/src/transactionPool.ts b/src/transactionPool.ts deleted file mode 100644 index 5296b72..0000000 --- a/src/transactionPool.ts +++ /dev/null @@ -1,71 +0,0 @@ -import * as _ from 'lodash'; -import {Transaction, TxIn, UnspentTxOut, validateTransaction} from './transaction'; - -let transactionPool: Transaction[] = []; - -const getTransactionPool = () => { - return _.cloneDeep(transactionPool); -}; - -const addToTransactionPool = (tx: Transaction, unspentTxOuts: UnspentTxOut[]) => { - - if (!validateTransaction(tx, unspentTxOuts)) { - throw Error('Trying to add invalid tx to pool'); - } - - if (!isValidTxForPool(tx, transactionPool)) { - throw Error('Trying to add invalid tx to pool'); - } - console.log('adding to txPool: %s', JSON.stringify(tx)); - transactionPool.push(tx); -}; - -const hasTxIn = (txIn: TxIn, unspentTxOuts: UnspentTxOut[]): boolean => { - const foundTxIn = unspentTxOuts.find((uTxO: UnspentTxOut) => { - return uTxO.txOutId === txIn.txOutId && uTxO.txOutIndex === txIn.txOutIndex; - }); - return foundTxIn !== undefined; -}; - -const updateTransactionPool = (unspentTxOuts: UnspentTxOut[]) => { - const invalidTxs = []; - for (const tx of transactionPool) { - for (const txIn of tx.txIns) { - if (!hasTxIn(txIn, unspentTxOuts)) { - invalidTxs.push(tx); - break; - } - } - } - if (invalidTxs.length > 0) { - console.log('removing the following transactions from txPool: %s', JSON.stringify(invalidTxs)); - transactionPool = _.without(transactionPool, ...invalidTxs); - } -}; - -const getTxPoolIns = (aTransactionPool: Transaction[]): TxIn[] => { - return _(aTransactionPool) - .map((tx) => tx.txIns) - .flatten() - .value(); -}; - -const isValidTxForPool = (tx: Transaction, aTtransactionPool: Transaction[]): boolean => { - const txPoolIns: TxIn[] = getTxPoolIns(aTtransactionPool); - - const containsTxIn = (txIns: TxIn[], txIn: TxIn) => { - return _.find(txPoolIns, ((txPoolIn) => { - return txIn.txOutIndex === txPoolIn.txOutIndex && txIn.txOutId === txPoolIn.txOutId; - })); - }; - - for (const txIn of tx.txIns) { - if (containsTxIn(txPoolIns, txIn)) { - console.log('txIn already found in the txPool'); - return false; - } - } - return true; -}; - -export {addToTransactionPool, getTransactionPool, updateTransactionPool};