From 56879acc007c7785542bd1902d7cca8f969d1cc6 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Fri, 20 Apr 2018 20:48:09 -0400 Subject: [PATCH] Reorganizing code --- app/gas-relayer/config/config.json | 22 +- app/gas-relayer/src/contract-settings.js | 111 ++++++++++ app/gas-relayer/src/message-processor.js | 182 +++++++++++++++ app/gas-relayer/src/service.js | 270 ++++------------------- app/gas-relayer/test/sampleContracts.sol | 2 - 5 files changed, 347 insertions(+), 240 deletions(-) create mode 100644 app/gas-relayer/src/contract-settings.js create mode 100644 app/gas-relayer/src/message-processor.js diff --git a/app/gas-relayer/config/config.json b/app/gas-relayer/config/config.json index 67fde4f..e6fc8bb 100644 --- a/app/gas-relayer/config/config.json +++ b/app/gas-relayer/config/config.json @@ -1,19 +1,17 @@ { - "blockchain": { - "account": "0x9e14016ba37b23498885864053fded5226161a3a", - "protocol": "ws", - "host": "localhost", - "port": 8545 - }, - - "whisper": { - "symKey": "0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b", + "node": { "protocol": "ws", "host": "localhost", "port": 8546, - "ttl": 20, - "minPow": 0.8, - "powTime": 1000 + "blockchain": { + "account": "0x9e14016ba37b23498885864053fded5226161a3a" + }, + "whisper": { + "symKey": "0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b", + "ttl": 20, + "minPow": 0.8, + "powTime": 1000 + } }, "tokens": { diff --git a/app/gas-relayer/src/contract-settings.js b/app/gas-relayer/src/contract-settings.js new file mode 100644 index 0000000..a8e49a2 --- /dev/null +++ b/app/gas-relayer/src/contract-settings.js @@ -0,0 +1,111 @@ +const md5 = require('md5'); + +class ContractSettings { + + constructor(config, web3, eventEmitter){ + this.tokens = config.tokens; + this.topics = []; + this.contracts = config.contracts; + + this.web3 = web3; + this.events = eventEmitter; + + this.pendingToLoad = 0; + + this.events.on('setup:bytecode-address', this._obtainContractBytecode.bind(this)) + } + + process(){ + this._setTokenPricePlugin(); + this._processContracts(); + } + + getToken(token){ + return this.tokens[token]; + } + + getContractByTopic(topicName){ + return this.contracts[topicName]; + } + + getTopicName(contractName){ + return this.web3.utils.toHex(contractName).slice(0, 10); + } + + _setTokenPricePlugin(){ + for(let token in this.tokens){ + if(this.tokens[token].pricePlugin !== undefined){ + let PricePlugin = require(this.tokens[token].pricePlugin); + this.tokens[token].pricePlugin = new PricePlugin(this.tokens[token]); + } + } + } + + _determineBytecodeAddress(topicName, i){ + let contractAddress = this.contracts[topicName].address; + if(this.contracts[topicName].isIdentity){ + this.pendingToLoad++; + const lastKernelSignature = "0x4ac99424"; // REFACTOR + this.web3.eth.call({to: this.contracts[topicName].factoryAddress, data: lastKernelSignature}) + .then(kernel => { + contractAddress = '0x' + kernel.slice(26); + this.events.emit('setup:bytecode-address', topicName, contractAddress); + }) + } + } + + _obtainContractBytecode(topicName, contractAddress){ + this.web3.eth.getCode(contractAddress) + .then(code => { + this.contracts[topicName].code = md5(code); + this.pendingToLoad--; + if(this.pendingToLoad == 0) this.events.emit("setup:complete", this); + }) + .catch(function(err){ + console.error("Invalid contract for " + contractName); + console.error(err); + process.exit(); + }); + } + + _extractFunctions(topicName){ + const contract = this.getContractByTopic(topicName); + + for(let i = 0; i < contract.allowedFunctions.length; i++){ + contract.allowedFunctions[i].functionName = contract.allowedFunctions[i].function.slice(0, contract.allowedFunctions[i].function.indexOf('(')); + + // Extracting input + contract.allowedFunctions[i].inputs = contract.abi.filter(x => x.name == contract.allowedFunctions[i].functionName && x.type == "function")[0].inputs; + + // Obtaining function signatures + let functionSignature = this.web3.utils.sha3(contract.allowedFunctions[i].function).slice(0, 10); + contract.allowedFunctions[functionSignature] = contract.allowedFunctions[i]; + delete this.contracts[topicName].allowedFunctions[i]; + } + + contract.functionSignatures = Object.keys(contract.allowedFunctions); + this.contracts[topicName] = contract; + } + + _processContracts(){ + for(let contractName in this.contracts){ + // Obtaining the abis + this.contracts[contractName].abi = require(this.contracts[contractName].abiFile); + + const topicName = this.getTopicName(contractName); + + // Extracting topic + this.topics.push(topicName); + this.contracts[topicName] = this.contracts[contractName]; + this.contracts[topicName].name = contractName; + delete this.contracts[contractName]; + + this._determineBytecodeAddress(topicName); + + this._extractFunctions(topicName); + } + } +} + + +module.exports = ContractSettings; \ No newline at end of file diff --git a/app/gas-relayer/src/message-processor.js b/app/gas-relayer/src/message-processor.js new file mode 100644 index 0000000..d210259 --- /dev/null +++ b/app/gas-relayer/src/message-processor.js @@ -0,0 +1,182 @@ +const md5 = require('md5'); +const erc20ABI = require('../abi/ERC20.json'); +const ganache = require("ganache-cli"); + +class MessageProcessor { + + constructor(config, settings, web3, kId){ + this.config = config; + this.settings = settings; + this.web3 = web3; + this.kId = kId; + } + + + _reply(text, message){ + if(message.sig !== undefined){ + this.web3.shh.post({ + pubKey: message.sig, + sig: this.kId, + ttl: this.config.node.whisper.ttl, + powTarget:this.config.node.whisper.minPow, + powTime: this.config.node.whisper.powTime, + topic: message.topic, + payload: this.web3.utils.fromAscii(text) + }).catch(console.error); + } + } + + + async _validateInput(message, input){ + const contract = this.settings.getContractByTopic(message.topic); + + if(!/^0x[0-9a-f]{40}$/i.test(input.address)){ + this._reply('Invalid address', message); + return false; + } + + if(contract == undefined){ + this._reply('Invalid topic', message); + return false; + } + + if(!contract.functionSignatures.includes(input.functionName)){ + this._reply('Function not allowed', message); + return false; + } + + // Get code from address and compare it against the contract code + const code = md5(await this.web3.eth.getCode(input.address)); + if(code != contract.code){ + this._reply('Invalid contract code', message); + return false; + } + + return true; + } + + + _extractInput(message){ + return { + address: message.payload.slice(0, 42), + functionName: '0x' + message.payload.slice(42, 50), + functionParameters: '0x' + message.payload.slice(50), + payload: '0x' + message.payload.slice(42) + } + } + + + _obtainParametersFunc(contract, input){ + const parameterList = this.web3.eth.abi.decodeParameters(contract.allowedFunctions[input.functionName].inputs, input.functionParameters); + return function(parameterName){ + return parameterList[contract.allowedFunctions[input.functionName][parameterName]]; + } + } + + _getFactor(input, contract, gasToken){ + if(contract.allowedFunctions[input.functionName].isToken){ + return this.web3.utils.toBN(this.settings.getToken(gasToken).pricePlugin.getFactor()); + } else { + return this.web3.utils.toBN(1); + } + } + + + async getBalance(token, input){ + // Determining balances of token used + if(token.symbol == "ETH") + return new this.web3.utils.BN(await this.web3.eth.getBalance(input.address)); + else { + const Token = new this.web3.eth.Contract(erc20ABI); + Token.options.address = params('gasToken'); + return new this.web3.utils.BN(await Token.methods.balanceOf(input.address).call()); + } + } + + + async process(error, message){ + + if(error){ + console.error(error); + } else { + + let input = this._extractInput(message); + + const contract = this.settings.getContractByTopic(message.topic); + + console.info("Processing request to: %s, %s", input.address, input.functionName); + + if(!this._validateInput(message, input)) return; // TODO Log + + const params = this._obtainParametersFunc(contract, input); + + const token = this.settings.getToken(params('gasToken')); + if(token == undefined) + return reply("Token not allowed", message); + + const gasPrice = this.web3.utils.toBN(params('gasPrice')); + const gasLimit = this.web3.utils.toBN(params('gasLimit')); + + + // Determine if enough balance for baseToken + if(contract.allowedFunctions[input.functionName].isToken){ + const Token = new this.web3.eth.Contract(erc20ABI); + Token.options.address = params('token'); + const baseToken = new this.web3.utils.BN(await Token.methods.balanceOf(input.address).call()); + if(balance.lt(this.web3.utils.BN(params('value')))){ + this._reply("Not enough balance", message); + return; + } + } + + const balance = await this.getBalance(token, input); + const gasToken = params('gasToken'); + const factor = this._getFactor(input, contract, gasToken); + + + const balanceInETH = balance.div(factor); + const gasLimitInETH = gasLimit.div(factor); + + if(balanceInETH.lt(this.web3.utils.toBN(gasPrice.mul(gasLimit)))) { + this._reply("Not enough balance", message); + return; + } + + + + // Estimate costs + const web3Sim = new Web3(ganache.provider({fork: `${config.node.protocol}://${config.node.host}:${config.node.port}`})); + const simAccounts = await web3Sim.eth.getAccounts(); + let simulatedReceipt = await web3Sim.eth.sendTransaction({ + from: simAccounts[0], + to: input.address, + value: 0, + data: input.payload + }); + + const estimatedGas = web3.utils.toBN(simulatedReceipt.gasUsed); + if(gasLimit.lt(estimatedGas)) { + return this._reply("Gas limit below estimated gas", message); + } + + this.web3.eth.sendTransaction({ + from: config.node.blockchain.account, + to: address, + value: 0, + data: input.payload, + gasLimit: gasLimitInETH + }) + .then(function(receipt){ + return this._reply("Transaction mined;" + receipt.transactionHash, message); + }).catch(function(err){ + this._reply("Couldn't mine transaction", message); + // TODO log this? + console.error(err); + }); + + + } + } +} + +module.exports = MessageProcessor; \ No newline at end of file diff --git a/app/gas-relayer/src/service.js b/app/gas-relayer/src/service.js index 77deba1..aee111d 100644 --- a/app/gas-relayer/src/service.js +++ b/app/gas-relayer/src/service.js @@ -1,245 +1,71 @@ -const md5 = require('md5'); +const EventEmitter = require('events'); const Web3 = require('web3'); const config = require('../config/config.json'); -const web3 = new Web3(`${config.whisper.protocol}://${config.whisper.host}:${config.whisper.port}`); -var ganache = require("ganache-cli"); -const erc20ABI = require('../abi/ERC20.json'); +const ContractSettings = require('./contract-settings'); +const MessageProcessor = require('./message-processor'); -console.info("Starting...") // TODO A node should call an API (probably from a status node) to register itself as a // token gas relayer. -async function start(){ -for(token in config.tokens){ - if(config.tokens[token].pricePlugin !== undefined){ - let PricePlugin = require(config.tokens[token].pricePlugin); - config.tokens[token].pricePlugin = new PricePlugin(config.tokens) - } -} +console.info("Starting..."); +const events = new EventEmitter(); -config.topics = []; -for(let contractName in config.contracts){ - - // Obtaining the abis - config.contracts[contractName].abi = require(config.contracts[contractName].abiFile); +// Web3 Connection +let connectionURL = `${config.node.protocol}://${config.node.host}:${config.node.port}`; +const web3 = new Web3(connectionURL); - const lngt = config.contracts[contractName].allowedFunctions.length; - for(i = 0; i < lngt; i++){ - config.contracts[contractName].allowedFunctions[i].functionName = config.contracts[contractName].allowedFunctions[i].function.slice(0, config.contracts[contractName].allowedFunctions[i].function.indexOf('(')); - - // Extracting input - config.contracts[contractName].allowedFunctions[i].inputs = config.contracts[contractName].abi.filter(x => x.name == config.contracts[contractName].allowedFunctions[i].functionName && x.type == "function")[0].inputs; - - // Obtaining function signatures - let functionSignature = web3.utils.sha3(config.contracts[contractName].allowedFunctions[i].function).slice(0, 10); - config.contracts[contractName].allowedFunctions[functionSignature] = config.contracts[contractName].allowedFunctions[i]; - delete config.contracts[contractName].allowedFunctions[i]; - } - - config.contracts[contractName].functionSignatures = Object.keys(config.contracts[contractName].allowedFunctions); - - // Extracting topics and available functions - let topicName = web3.utils.toHex(contractName).slice(0, 10); - config.topics.push(topicName); - config.contracts[topicName] = config.contracts[contractName]; - config.contracts[topicName].name = contractName; - delete config.contracts[contractName]; - - // Get Contract Bytecode - let contractAddress = config.contracts[topicName].address; - if(config.contracts[topicName].isIdentity){ - const lastKernelSignature = "0x4ac99424"; - let kernel = await web3.eth.call({to: config.contracts[topicName].factoryAddress, data: lastKernelSignature}); - contractAddress = '0x' + kernel.slice(26); - } +web3.eth.net.isListening() +.then(listening => events.emit('web3:connected', connectionURL)) +.catch(error => { + console.error(error); + process.exit(); +}); - try { - config.contracts[topicName].code = md5(await web3.eth.getCode(contractAddress)); - } catch(err){ - console.error("Invalid contract for " + contractName); - console.error(err); - process.exit(); - } -} +events.on('web3:connected', connURL => { + console.info("Connected to '%s'", connURL); + let settings = new ContractSettings(config, web3, events); + settings.process(); +}); -// Setting up Whisper options -const shhOptions = { - ttl: config.whisper.ttl, - minPow: config.whisper.minPow, -}; -let kId; -let symKId; -// Listening to whisper +events.on('setup:complete', (settings) => { + // Setting up Whisper options + const shhOptions = { + ttl: config.node.whisper.ttl, + minPow: config.node.whisper.minPow, + }; -web3.shh.addSymKey(config.whisper.symKey) + let kId; + let symKId; + + // Listening to whisper + web3.shh.addSymKey(config.node.whisper.symKey) .then(symKeyId => { - symKId = symKeyId; - return web3.shh.newKeyPair(); + symKId = symKeyId; + return web3.shh.newKeyPair(); }) .then(keyId => { shhOptions.symKeyId = symKId; - - kId = keyId; + shhOptions.kId = keyId; - console.info(`Sym Key: ${config.whisper.symKey}`); + console.info(`Sym Key: ${config.node.whisper.symKey}`); console.info("Topics Available:"); - - config.topics = []; - for(let contractName in config.contracts) { - console.info("- %s: %s [%s]", config.contracts[contractName].name, contractName, Object.keys(config.contracts[contractName].allowedFunctions).join(', ')); - shhOptions.topics = [contractName]; - web3.shh.subscribe('messages', shhOptions, processMessages); + for(let contract in settings.contracts) { + console.info("- %s: %s [%s]", settings.getContractByTopic(contract).name, contract, Object.keys(settings.getContractByTopic(contract).allowedFunctions).join(', ')); + shhOptions.topics = [contract]; + events.emit('server:listen', shhOptions, settings); } - - console.info("Started."); - console.info("Listening for messages...") }); - - - -const reply = async function(text, message){ - try { - if(message.sig !== undefined){ - let shhOptions = { - pubKey: message.sig, - sig: kId, - ttl: config.whisper.ttl, - powTarget:config.whisper.minPow, - powTime: config.whisper.powTime, - topic: message.topic, - payload: web3.utils.fromAscii(text) - }; - await web3.shh.post(shhOptions); - } - } catch(Err){ - // TODO - console.error(Err); - } -} - - -// Process individual whisper message -const processMessages = async function(error, message, subscription){ - if(error){ - // TODO log - console.error(error); - } else { - const address = message.payload.slice(0, 42); - const functionName = '0x' + message.payload.slice(42, 50); - const functionParameters = '0x' + message.payload.slice(50); - const payload = '0x' + message.payload.slice(42); - - console.info("Processing request to: %s, %s", address, functionName); - - if(!/^0x[0-9a-f]{40}$/i.test(address)) - return reply('Invalid address', message); - - if(config.contracts[message.topic] == undefined) - return reply('Invalid topic', message); - - const contract = config.contracts[message.topic]; - if(!contract.functionSignatures.includes(functionName)) - return reply('Function not allowed', message) // TODO Log this - - // Get code from address and compare it against the contract code - const code = md5(await web3.eth.getCode(address)); - if(code != contract.code){ - return reply('Invalid contract code', message); // TODO Log this - } - - const params = web3.eth.abi.decodeParameters(contract.allowedFunctions[functionName].inputs, functionParameters); - const token = config.tokens[params[contract.allowedFunctions[functionName].gasToken]]; - if(token == undefined){ - return reply("Token not allowed", message); - } - - const gasPrice = web3.utils.toBN(params[contract.allowedFunctions[functionName].gasPrice]); - const gasLimit = web3.utils.toBN(params[contract.allowedFunctions[functionName].gasLimit]); - - // Determining balances of token used - let balance; - if(token.symbol == "ETH") - balance = new web3.utils.BN(await web3.eth.getBalance(address)); - else { - const Token = new web3.eth.Contract(erc20ABI); - Token.options.address = params[contract.allowedFunctions[functionName].gasToken]; - balance = new web3.utils.BN(await Token.methods.balanceOf(address).call()); - } - - // Determine if enough balance for baseToken - if(contract.allowedFunctions[functionName].isToken){ - const Token = new web3.eth.Contract(erc20ABI); - Token.options.address = params[contract.allowedFunctions[functionName].token]; - balance = new web3.utils.BN(await Token.methods.balanceOf(address).call()); - if(balance.lt(web3.utils.BN(params[contract.allowedFunctions[functionName].value]))){ - return reply("Not enough balance", message); - } - } - - // Obtain factor - let factor; - if(contract.allowedFunctions[functionName].isToken){ - factor =web3.utils.toBN(config.tokens[tokenAddress].pricePlugin.getFactor()); - } else { - factor = web3.utils.toBN(1); - } - - const balanceInETH = balance.div(factor); - const gasLimitInETH = gasLimit.div(factor); - - if(balanceInETH.lt(web3.utils.toBN(gasPrice.mul(gasLimit)))) { - return reply("Not enough balance", message); - } - - // Estimate costs - const web3Sim = new Web3(ganache.provider({fork: `http://localhost:8545`})); - const simAccounts = await web3Sim.eth.getAccounts(); - let simulatedReceipt = await web3Sim.eth.sendTransaction({ - from: simAccounts[0], - to: address, - value: 0, - data: payload - }); - - const estimatedGas = web3.utils.toBN(simulatedReceipt.gasUsed); - console.log(simulatedReceipt); - if(gasLimit.lt(estimatedGas)) { - return reply("Gas limit below estimated gas", message); - } - - - web3.eth.sendTransaction({ - from: config.blockchain.account, - to: address, - value: 0, - data: payload, - gasLimit: gasLimitInETH - }) - .then(function(receipt){ - return reply("Transaction mined;" + receipt.transactionHash, message); - }).catch(function(err){ - reply("Couldn't mine transaction", message); - // TODO log this? - //console.error(err); - }); - } -} - -} - - - - - -start(); - - +}); +events.on('server:listen', (shhOptions, settings) => { + let processor = new MessageProcessor(config, settings, web3, shhOptions.kId); + web3.shh.subscribe('messages', shhOptions, (error, message, subscription) => processor.process(error, message)); +}); // Daemon helper functions @@ -248,14 +74,6 @@ process.on("uncaughtException", function(err) { }); -process.on("SIGUSR1", function() { - log("Reloading..."); - - - log("Reloaded."); -}); - process.once("SIGTERM", function() { log("Stopping..."); -}); - +}); \ No newline at end of file diff --git a/app/gas-relayer/test/sampleContracts.sol b/app/gas-relayer/test/sampleContracts.sol index eeca5dd..673be11 100644 --- a/app/gas-relayer/test/sampleContracts.sol +++ b/app/gas-relayer/test/sampleContracts.sol @@ -40,8 +40,6 @@ contract TestIdentityFactory { function TestIdentityFactory(){ latestKernel = address(new TestIdentityGasRelay()); } - - } contract TestSNTController {