From 2b49ecb6a70c1d25976461064b42df20265e04a7 Mon Sep 17 00:00:00 2001 From: "Phu Minh (Catto)" <53084840+nguyenphuminh@users.noreply.github.com> Date: Mon, 15 Jul 2024 21:27:26 +0700 Subject: [PATCH] Big fix 6 - Removed unnecessary imports. - Removed unnecessary code that immensely slowed things down. - Removed most .keys().all() that immensely slowed things down. - Fixed an error where contract's state and storage are not up to date when executing transactions sequentially. - Fixed a typo in RPC code. --- package.json | 2 +- src/consensus/consensus.js | 3 - src/core/block.js | 106 ++++++++++++++++---------------- src/core/runtime.js | 63 ++++++++++--------- src/core/state.js | 3 +- src/core/transaction.js | 14 +++-- src/core/txPool.js | 6 -- src/node/server.js | 122 +++++++++++++++++++------------------ src/rpc/rpc.js | 4 +- 9 files changed, 159 insertions(+), 164 deletions(-) diff --git a/package.json b/package.json index b478fb3..04ad7d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jechain", - "version": "0.30.3", + "version": "0.30.4", "description": "Node for JeChain - an experimental smart contract blockchain network", "main": "./index.js", "scripts": { diff --git a/src/consensus/consensus.js b/src/consensus/consensus.js index b6b5337..acd70c9 100644 --- a/src/consensus/consensus.js +++ b/src/consensus/consensus.js @@ -46,9 +46,6 @@ async function verifyBlock(newBlock, chainInfo, stateDB, codeDB, enableLogging = // Check block number newBlock.blockNumber - 1 === chainInfo.latestBlock.blockNumber && - // Check gas limit - Block.hasValidGasLimit(newBlock) && - // Check transactions and transit state right after await Block.verifyTxAndTransit(newBlock, stateDB, codeDB, enableLogging) ) diff --git a/src/core/block.js b/src/core/block.js index 8daae21..4a594e9 100644 --- a/src/core/block.js +++ b/src/core/block.js @@ -2,7 +2,6 @@ const { Level } = require('level'); const crypto = require("crypto"), SHA256 = message => crypto.createHash("sha256").update(message).digest("hex"); -const EC = require("elliptic").ec, ec = new EC("secp256k1"); const Transaction = require("./transaction"); const Merkle = require("./merkle"); const { BLOCK_REWARD, BLOCK_GAS_LIMIT, EMPTY_HASH } = require("../config.json"); @@ -130,39 +129,52 @@ class Block { for (const tx of block.transactions) { if (!(await Transaction.isValid(tx, stateDB))) return false; } - - // Get all existing addresses - const addressesInBlock = block.transactions.map(tx => SHA256(Transaction.getPubKey(tx))); - const existedAddresses = await stateDB.keys().all(); - - // If senders' address doesn't exist, return false - if (!addressesInBlock.every(address => existedAddresses.includes(address))) return false; // Start state replay to check if transactions are legit - let states = {}, code = {}, storage = {}; + const states = {}, code = {}, storage = {}; + + let totalTxGas = 0n; + // Execute transactions and add them to the block sequentially for (const tx of block.transactions) { + // If packed transactions exceed block gas limit, stop + if (totalTxGas + BigInt(tx.additionalData.contractGas || 0) >= BigInt(BLOCK_GAS_LIMIT)) return false; + const txSenderPubkey = Transaction.getPubKey(tx); const txSenderAddress = SHA256(txSenderPubkey); const totalAmountToPay = BigInt(tx.amount) + BigInt(tx.gas) + BigInt(tx.additionalData.contractGas || 0); - + + // Cache the state of sender if (!states[txSenderAddress]) { const senderState = deserializeState(await stateDB.get(txSenderAddress)); states[txSenderAddress] = senderState; - code[senderState.codeHash] = await codeDB.get(senderState.codeHash); + } + + // If sender does not have enough money or is now a contract, skip + if (states[txSenderAddress].codeHash !== EMPTY_HASH || BigInt(states[txSenderAddress].balance) < totalAmountToPay) return false; - if (senderState.codeHash !== EMPTY_HASH || BigInt(senderState.balance) < totalAmountToPay) return false; - - states[txSenderAddress].balance = (BigInt(senderState.balance) - totalAmountToPay).toString(); - } else { - if (states[txSenderAddress].codeHash !== EMPTY_HASH || BigInt(states[txSenderAddress].balance) < totalAmountToPay) return false; + // Update balance of sender + states[txSenderAddress].balance = (BigInt(states[txSenderAddress].balance) - BigInt(tx.amount) - BigInt(tx.gas) - BigInt(tx.additionalData.contractGas || 0)).toString(); - states[txSenderAddress].balance = (BigInt(states[txSenderAddress].balance) - totalAmountToPay).toString(); + // Cache the state of recipient + if (!states[tx.recipient]) { + try { // If account exists but is not cached + states[tx.recipient] = deserializeState(await stateDB.get(tx.recipient)); + code[states[tx.recipient].codeHash] = await codeDB.get(states[tx.recipient].codeHash); + } catch (e) { // If account does not exist and is not cached + states[tx.recipient] = { balance: "0", codeHash: EMPTY_HASH, nonce: 0, storageRoot: EMPTY_HASH } + code[EMPTY_HASH] = ""; + } } + // Update balance of recipient + states[tx.recipient].balance = (BigInt(states[tx.recipient].balance) + BigInt(tx.amount)).toString(); + + // console.log(tx.recipient, states[tx.recipient].balance, block); + // Contract deployment if ( states[txSenderAddress].codeHash === EMPTY_HASH && @@ -175,44 +187,41 @@ class Block { // Update nonce states[txSenderAddress].nonce += 1; - if (BigInt(states[txSenderAddress].balance) < 0n) return false; - - if (!existedAddresses.includes(tx.recipient) && !states[tx.recipient]) { - states[tx.recipient] = { balance: "0", codeHash: EMPTY_HASH, nonce: 0, storageRoot: EMPTY_HASH } - code[EMPTY_HASH] = ""; - } - - if (existedAddresses.includes(tx.recipient) && !states[tx.recipient]) { - states[tx.recipient] = deserializeState(await stateDB.get(tx.recipient)); - code[states[tx.recipient].codeHash] = await codeDB.get(states[tx.recipient].codeHash); - } - - states[tx.recipient].balance = (BigInt(states[tx.recipient].balance) + BigInt(tx.amount)).toString(); - // Contract execution if (states[tx.recipient].codeHash !== EMPTY_HASH) { const contractInfo = { address: tx.recipient }; - const [ newState, newStorage ] = await jelscript(code[states[tx.recipient].codeHash], states, BigInt(tx.additionalData.contractGas || 0), stateDB, block, tx, contractInfo, enableLogging); - + const [ newState, newStorage ] = await jelscript( + code[states[tx.recipient].codeHash], + states, + storage[tx.recipient] || {}, + BigInt(tx.additionalData.contractGas || 0), + stateDB, + block, + tx, + contractInfo, + enableLogging + ); + for (const account of Object.keys(newState)) { states[account] = newState[account]; } storage[tx.recipient] = newStorage; } - } - // Reward - - if (!existedAddresses.includes(block.coinbase) && !states[block.coinbase]) { - states[block.coinbase] = { balance: "0", codeHash: EMPTY_HASH, nonce: 0, storageRoot: EMPTY_HASH } - code[EMPTY_HASH] = ""; + // console.log(tx.recipient, states[tx.recipient].balance, block); } - - if (existedAddresses.includes(block.coinbase) && !states[block.coinbase]) { - states[block.coinbase] = deserializeState(await stateDB.get(block.coinbase)); - code[states[block.coinbase].codeHash] = await codeDB.get(states[block.coinbase].codeHash); + + // Send reward to coinbase's address + if (!states[block.coinbase]) { + try { // If account exists but is not cached + states[block.coinbase] = deserializeState(await stateDB.get(block.coinbase)); + code[states[block.coinbase].codeHash] = await codeDB.get(states[block.coinbase].codeHash); + } catch (e) { // If account does not exist and is not cached + states[block.coinbase] = { balance: "0", codeHash: EMPTY_HASH, nonce: 0, storageRoot: EMPTY_HASH } + code[EMPTY_HASH] = ""; + } } let gas = 0n; @@ -222,7 +231,6 @@ class Block { states[block.coinbase].balance = (BigInt(states[block.coinbase].balance) + BigInt(BLOCK_REWARD) + gas).toString(); // Finalize state and contract storage into DB - for (const address in storage) { const storageDB = new Level("./log/accountStore/" + address); const keys = Object.keys(storage[address]); @@ -275,16 +283,6 @@ class Block { return true; } - - static hasValidGasLimit(block) { - let totalGas = 0n; - - for (const tx of block.transactions) { - totalGas += BigInt(tx.additionalData.contractGas || 0); - } - - return totalGas <= BigInt(BLOCK_GAS_LIMIT); - } } module.exports = Block; diff --git a/src/core/runtime.js b/src/core/runtime.js index 2ac7b5e..c1a5788 100644 --- a/src/core/runtime.js +++ b/src/core/runtime.js @@ -9,7 +9,7 @@ const { EMPTY_HASH } = require("../config.json"); const crypto = require("crypto"), SHA256 = message => crypto.createHash("sha256").update(message).digest("hex"); -async function jelscript(input, originalState = {}, gas, stateDB, block, txInfo, contractInfo, enableLogging = false) { +async function jelscript(input, originalState = {}, originalStorage = {}, gas, stateDB, block, txInfo, contractInfo, enableLogging = false) { // Prepare code, memory, state, storage placeholder const instructions = input.trim().replace(/\t/g, "").split("\n").map(ins => ins.trim()).filter(ins => ins !== ""); @@ -20,15 +20,22 @@ async function jelscript(input, originalState = {}, gas, stateDB, block, txInfo, let ptr = 0; - // Get contract state and storage - const storageDB = new Level("./log/accountStore/" + contractInfo.address); + // Get contract storage + if (Object.keys(originalStorage).length === 0) { + const storageDB = new Level("./log/accountStore/" + contractInfo.address); - for (const key of (await storageDB.keys().all())) { - storage[key] = await storageDB.get(key); + for (const key of (await storageDB.keys().all())) { + storage[key] = await storageDB.get(key); + } + + await storageDB.close(); } - const contractState = deserializeState(await stateDB.get(contractInfo.address)); - state[contractInfo.address] = contractState; + // Get contract state + if (!state[contractInfo.address]) { + const contractState = deserializeState(await stateDB.get(contractInfo.address)); + state[contractInfo.address] = contractState; + } while ( @@ -241,18 +248,14 @@ async function jelscript(input, originalState = {}, gas, stateDB, block, txInfo, case "balance": // Get balance from address const address = getValue(args[1]).slice(2); // Get address - const existedAddresses = await stateDB.keys().all(); - - if (!existedAddresses.includes(address) && !state[address]) { - setMem(c, "0x0"); - } - - if (existedAddresses.includes(address) && !state[address]) { - setMem(c, "0x" + BigInt(deserializeState((await stateDB.get(address))).balance).toString(16)); - } - - if (!existedAddresses.includes(address) && state[address]) { + if (state[address]) { setMem(c, "0x" + BigInt(state[address].balance).toString(16)); + } else { + try { + setMem(c, "0x" + BigInt(deserializeState((await stateDB.get(address))).balance).toString(16)); + } catch (e) { + setMem(c, "0x0"); + } } break; @@ -263,18 +266,20 @@ async function jelscript(input, originalState = {}, gas, stateDB, block, txInfo, const balance = state[contractInfo.address].balance; if (BigInt(balance) >= amount) { - if (!(await stateDB.keys().all()).includes(target) && !state[target]) { - state[target] = { - balance: amount.toString(), - codeHash: EMPTY_HASH, - nonce: 0, - storageRoot: EMPTY_HASH - } - } else { - if (!state[target]) { + if (!state[target]) { + try { state[target] = deserializeState(await stateDB.get(target)); - } + state[target].balance = BigInt(state[target].balance) + amount; + } catch (e) { + state[target] = { + balance: amount.toString(), + codeHash: EMPTY_HASH, + nonce: 0, + storageRoot: EMPTY_HASH + } + } + } else { state[target].balance = BigInt(state[target].balance) + amount; } @@ -349,8 +354,6 @@ async function jelscript(input, originalState = {}, gas, stateDB, block, txInfo, return storage[key] ? storage[key] : "0x0"; } - await storageDB.close(); - return [state, storage]; } diff --git a/src/core/state.js b/src/core/state.js index fce1815..6c7129f 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -64,7 +64,7 @@ async function changeState(newBlock, stateDB, codeDB, enableLogging = false) { / if (dataFromRecipient.codeHash !== EMPTY_HASH) { const contractInfo = { address: tx.recipient }; - const [ newState, newStorage ] = await jelscript(await codeDB.get(dataFromRecipient.codeHash), {}, BigInt(tx.additionalData.contractGas || 0), stateDB, newBlock, tx, contractInfo, enableLogging); + const [ newState, newStorage ] = await jelscript(await codeDB.get(dataFromRecipient.codeHash), {}, {}, BigInt(tx.additionalData.contractGas || 0), stateDB, newBlock, tx, contractInfo, enableLogging); const storageDB = new Level("./log/accountStore/" + tx.recipient); const keys = Object.keys(newStorage); @@ -87,7 +87,6 @@ async function changeState(newBlock, stateDB, codeDB, enableLogging = false) { / } // Reward - let gas = 0n; for (const tx of newBlock.transactions) { gas += BigInt(tx.gas) + BigInt(tx.additionalData.contractGas || 0) } diff --git a/src/core/transaction.js b/src/core/transaction.js index 23a1d35..62af85a 100644 --- a/src/core/transaction.js +++ b/src/core/transaction.js @@ -182,13 +182,15 @@ class Transaction { const txSenderAddress = SHA256(txSenderPubkey); // If sender does not exist return false - if (!(await stateDB.keys().all()).includes(txSenderAddress)) return false; - - // Fetch sender's state object - const dataFromSender = deserializeState(await stateDB.get(txSenderAddress)); + try { + // Fetch sender's state object + const dataFromSender = deserializeState(await stateDB.get(txSenderAddress)); - // If sender is a contract address, then it's not supposed to be used to send money, so return false if it is. - if (dataFromSender.codeHash !== EMPTY_HASH) return false; + // If sender is a contract address, then it's not supposed to be used to send money, so return false if it is. + if (dataFromSender.codeHash !== EMPTY_HASH) return false; + } catch (e) { + return false; + } return true; diff --git a/src/core/txPool.js b/src/core/txPool.js index 89840ec..462fbbf 100644 --- a/src/core/txPool.js +++ b/src/core/txPool.js @@ -32,12 +32,6 @@ async function addTransaction(transaction, chainInfo, stateDB) { const txSenderPubkey = Transaction.getPubKey(transaction); const txSenderAddress = SHA256(txSenderPubkey); - if (!(await stateDB.keys().all()).includes(txSenderAddress)) { - console.log(`\x1b[31mERROR\x1b[0m [${(new Date()).toISOString()}] Failed to add one transaction to pool: Sender does not exist.`); - - return; - } - // Check nonce let maxNonce = deserializeState(await stateDB.get(txSenderAddress)).nonce; diff --git a/src/node/server.js b/src/node/server.js index 522b11c..2b152b7 100644 --- a/src/node/server.js +++ b/src/node/server.js @@ -70,7 +70,7 @@ async function startServer(options) { console.log(`\x1b[32mLOG\x1b[0m [${(new Date()).toISOString()}] P2P server listening on PORT`, PORT.toString()); - server.on("connection", async (socket, req) => { + server.on("connection", async (socket) => { // Message handler socket.on("message", async message => { const _message = parseJSON(message); // Parse binary message to JSON @@ -155,7 +155,6 @@ async function startServer(options) { // Its message body must contain a transaction. // Weakly verify the transation, full verification is achieved in block production. - let transaction; try { @@ -170,12 +169,6 @@ async function startServer(options) { // Get public key and address from sender const txSenderPubkey = Transaction.getPubKey(transaction); const txSenderAddress = SHA256(txSenderPubkey); - - if (!(await stateDB.keys().all()).includes(txSenderAddress)) break; - - // After transaction is added, the transaction must be broadcasted to others since the sender might only send it to a few nodes. - - // This is pretty much the same as addTransaction, but we will send the transaction to other connected nodes if it's valid. // Check nonce let maxNonce = deserializeState(await stateDB.get(txSenderAddress)).nonce; @@ -193,6 +186,9 @@ async function startServer(options) { console.log(`\x1b[32mLOG\x1b[0m [${(new Date()).toISOString()}] New transaction received, broadcasted and added to pool.`); + // After transaction is added, the transaction must be broadcasted to others since the sender might only send it to a few nodes. + // This is pretty much the same as addTransaction, but we will send the transaction to other connected nodes if it's valid. + chainInfo.transactionPool.push(transaction); // Broadcast the transaction @@ -414,9 +410,9 @@ async function sendTransaction(transaction) { await addTransaction(transaction, chainInfo, stateDB); } -async function mine(publicKey, ENABLE_LOGGING) { +async function mine(publicKey) { function mine(block, difficulty) { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { worker.addListener("message", message => resolve(message.result)); worker.send({ type: "MINE", data: [block, difficulty] }); // Send a message to the worker thread, asking it to mine. @@ -435,12 +431,12 @@ async function mine(publicKey, ENABLE_LOGGING) { // Collect a list of transactions to mine const transactionsToMine = [], states = {}, code = {}, storage = {}, skipped = {}; - let totalContractGas = 0n, totalTxGas = 0n; - - const existedAddresses = await stateDB.keys().all(); + let totalTxGas = 0n; + // Execute transactions and add them to the block sequentially for (const tx of chainInfo.transactionPool) { - if (totalContractGas + BigInt(tx.additionalData.contractGas || 0) >= BigInt(BLOCK_GAS_LIMIT)) break; + // If packed transactions exceed block gas limit, stop + if (totalTxGas + BigInt(tx.additionalData.contractGas || 0) >= BigInt(BLOCK_GAS_LIMIT)) break; const txSenderPubkey = Transaction.getPubKey(tx); const txSenderAddress = SHA256(txSenderPubkey); @@ -449,38 +445,35 @@ async function mine(publicKey, ENABLE_LOGGING) { const totalAmountToPay = BigInt(tx.amount) + BigInt(tx.gas) + BigInt(tx.additionalData.contractGas || 0); - // Normal coin transfers + // Cache the state of sender if (!states[txSenderAddress]) { const senderState = deserializeState(await stateDB.get(txSenderAddress)); states[txSenderAddress] = senderState; code[senderState.codeHash] = await codeDB.get(senderState.codeHash); - - if (senderState.codeHash !== EMPTY_HASH || BigInt(senderState.balance) < totalAmountToPay) { - skipped[txSenderAddress] = true; - continue; - } - - states[txSenderAddress].balance = (BigInt(senderState.balance) - BigInt(tx.amount) - BigInt(tx.gas) - BigInt(tx.additionalData.contractGas || 0)).toString(); - } else { - if (states[txSenderAddress].codeHash !== EMPTY_HASH || BigInt(states[txSenderAddress].balance) < totalAmountToPay) { - skipped[txSenderAddress] = true; - continue; - } - - states[txSenderAddress].balance = (BigInt(states[txSenderAddress].balance) - BigInt(tx.amount) - BigInt(tx.gas) - BigInt(tx.additionalData.contractGas || 0)).toString(); } - if (!existedAddresses.includes(tx.recipient) && !states[tx.recipient]) { - states[tx.recipient] = { balance: "0", codeHash: EMPTY_HASH, nonce: 0, storageRoot: EMPTY_HASH } - code[EMPTY_HASH] = ""; + // If sender does not have enough money or is now a contract, skip + if (states[txSenderAddress].codeHash !== EMPTY_HASH || BigInt(states[txSenderAddress].balance) < totalAmountToPay) { + skipped[txSenderAddress] = true; + continue; } - - if (existedAddresses.includes(tx.recipient) && !states[tx.recipient]) { - states[tx.recipient] = deserializeState(await stateDB.get(tx.recipient)); - code[states[tx.recipient].codeHash] = await codeDB.get(states[tx.recipient].codeHash); + + // Update balance of sender + states[txSenderAddress].balance = (BigInt(states[txSenderAddress].balance) - BigInt(tx.amount) - BigInt(tx.gas) - BigInt(tx.additionalData.contractGas || 0)).toString(); + + // Cache the state of recipient + if (!states[tx.recipient]) { + try { // If account exists but is not cached + states[tx.recipient] = deserializeState(await stateDB.get(tx.recipient)); + code[states[tx.recipient].codeHash] = await codeDB.get(states[tx.recipient].codeHash); + } catch (e) { // If account does not exist and is not cached + states[tx.recipient] = { balance: "0", codeHash: EMPTY_HASH, nonce: 0, storageRoot: EMPTY_HASH } + code[EMPTY_HASH] = ""; + } } - + + // Update balance of recipient states[tx.recipient].balance = (BigInt(states[tx.recipient].balance) + BigInt(tx.amount)).toString(); // Contract deployment @@ -495,6 +488,29 @@ async function mine(publicKey, ENABLE_LOGGING) { // Update nonce states[txSenderAddress].nonce += 1; + // Contract execution + if (states[tx.recipient].codeHash !== EMPTY_HASH) { + const contractInfo = { address: tx.recipient }; + + const [ newState, newStorage ] = await jelscript( + code[states[tx.recipient].codeHash], + states, + storage[tx.recipient] || {}, + BigInt(tx.additionalData.contractGas || 0), + stateDB, + block, + tx, + contractInfo, + false + ); + + for (const account of Object.keys(newState)) { + states[account] = newState[account]; + } + + storage[tx.recipient] = newStorage; + } + // Decide to drop or add transaction to block if (BigInt(states[txSenderAddress].balance) < 0n) { skipped[txSenderAddress] = true; @@ -502,22 +518,8 @@ async function mine(publicKey, ENABLE_LOGGING) { } else { transactionsToMine.push(tx); - totalContractGas += BigInt(tx.additionalData.contractGas || 0); totalTxGas += BigInt(tx.gas) + BigInt(tx.additionalData.contractGas || 0); } - - // Contract execution - if (states[tx.recipient].codeHash !== EMPTY_HASH) { - const contractInfo = { address: tx.recipient }; - - const [ newState, newStorage ] = await jelscript(code[states[tx.recipient].codeHash], states, BigInt(tx.additionalData.contractGas || 0), stateDB, block, tx, contractInfo, false); - - for (const account of Object.keys(newState)) { - states[account] = newState[account]; - - storage[tx.recipient] = newStorage; - } - } } const transactionsAsObj = [...transactionsToMine]; @@ -547,15 +549,15 @@ async function mine(publicKey, ENABLE_LOGGING) { chainInfo.latestBlock = result; // Update latest block cache // Reward - - if (!existedAddresses.includes(result.coinbase) && !states[result.coinbase]) { - states[result.coinbase] = { balance: "0", codeHash: EMPTY_HASH, nonce: 0, storageRoot: EMPTY_HASH } - code[EMPTY_HASH] = ""; - } - - if (existedAddresses.includes(result.coinbase) && !states[result.coinbase]) { - states[result.coinbase] = deserializeState(await stateDB.get(result.coinbase)); - code[states[result.coinbase].codeHash] = await codeDB.get(states[result.coinbase].codeHash); + // Send reward to coinbase's address + if (!states[result.coinbase]) { + try { // If account exists but is not cached + states[result.coinbase] = deserializeState(await stateDB.get(result.coinbase)); + code[states[result.coinbase].codeHash] = await codeDB.get(states[result.coinbase].codeHash); + } catch (e) { // If account does not exist and is not cached + states[result.coinbase] = { balance: "0", codeHash: EMPTY_HASH, nonce: 0, storageRoot: EMPTY_HASH } + code[EMPTY_HASH] = ""; + } } let gas = 0n; diff --git a/src/rpc/rpc.js b/src/rpc/rpc.js index 26f3602..3e70326 100644 --- a/src/rpc/rpc.js +++ b/src/rpc/rpc.js @@ -72,7 +72,7 @@ function rpc(PORT, client, transactionHandler, keyPair, stateDB, blockDB, bhashD reply.send({ success: false, - payload: null, + payload, error: { message } }); } @@ -399,7 +399,7 @@ function rpc(PORT, client, transactionHandler, keyPair, stateDB, blockDB, bhashD } }); - fastify.listen(PORT, (err, address) => { + fastify.listen(PORT, (err) => { if (err) { console.log(`\x1b[31mERROR\x1b[0m [${(new Date()).toISOString()}] Error at RPC server: Fastify: `, err); process.exit(1);