From 409a827409e902bb60d86e4afffb01912797e462 Mon Sep 17 00:00:00 2001 From: Phu Minh <53084840+nguyenphuminh@users.noreply.github.com> Date: Thu, 27 Oct 2022 01:05:22 +0700 Subject: [PATCH 1/2] Big fix - Fixed Jelscript bugs, changed data format to hex, added `revert`, it is still weak but now works a little bit better. - Did state caching so probably a little bit more efficient. - Added proper transaction verification with proper state transition. - Fixed some other old bugs and vulnerabilities. - Lowered block gas limit. --- package.json | 2 +- src/config.json | 2 +- src/consensus/consensus.js | 10 +- src/core/block.js | 116 +++++++++++++------- src/core/runtime.js | 210 ++++++++++++++++--------------------- src/core/state.js | 37 +++---- src/core/txPool.js | 194 +++++++++++++++++++++++++++------- src/node/server.js | 116 ++++++++++++++------ src/utils/utils.js | 12 ++- 9 files changed, 436 insertions(+), 263 deletions(-) diff --git a/package.json b/package.json index 9dff8ad..9632e26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jechain", - "version": "0.19.0", + "version": "0.20.0", "description": "Node for JeChain - an experimental smart contract blockchain network", "main": "./index.js", "scripts": { diff --git a/src/config.json b/src/config.json index 53ed394..57e6bf1 100644 --- a/src/config.json +++ b/src/config.json @@ -1,6 +1,6 @@ { "BLOCK_REWARD": "202977000000000000", "BLOCK_TIME": 30000, - "BLOCK_GAS_LIMIT": "500000000000000", + "BLOCK_GAS_LIMIT": "50000000000000", "INITIAL_SUPPLY": "100000000000000000000000000" } diff --git a/src/consensus/consensus.js b/src/consensus/consensus.js index 9ba22be..8e9db0a 100644 --- a/src/consensus/consensus.js +++ b/src/consensus/consensus.js @@ -4,7 +4,7 @@ const { log16 } = require("../utils/utils"); const generateMerkleRoot = require("../core/merkle"); const { BLOCK_REWARD, BLOCK_TIME } = require("../config.json"); -async function verifyBlock(newBlock, chainInfo, stateDB) { +async function verifyBlock(newBlock, chainInfo, stateDB, enableLogging = false) { // Check if the block is valid or not, if yes, we will push it to the chain, update the difficulty, chain state and the transaction pool. // A block is valid under these factors: @@ -33,9 +33,6 @@ async function verifyBlock(newBlock, chainInfo, stateDB) { newBlock.hash.startsWith("00000" + Array(Math.floor(log16(chainInfo.difficulty)) + 1).join("0")) && newBlock.difficulty === chainInfo.difficulty && - // Check transactions - await Block.hasValidTransactions(newBlock, stateDB) && - // Check transactions ordering await Block.hasValidTxOrder(newBlock, stateDB) && @@ -50,7 +47,10 @@ async function verifyBlock(newBlock, chainInfo, stateDB) { generateMerkleRoot(newBlock.transactions) === newBlock.txRoot && // Check gas limit - Block.hasValidGasLimit(newBlock) + Block.hasValidGasLimit(newBlock) && + + // Check transactions and transit state rih + await Block.verifyTxAndTransit(newBlock, stateDB, enableLogging) ) } diff --git a/src/core/block.js b/src/core/block.js index 541837c..6fd1635 100644 --- a/src/core/block.js +++ b/src/core/block.js @@ -5,6 +5,7 @@ const EC = require("elliptic").ec, ec = new EC("secp256k1"); const Transaction = require("./transaction"); const generateMerkleRoot = require("./merkle"); const { BLOCK_REWARD, BLOCK_GAS_LIMIT } = require("../config.json"); +const jelscript = require("./runtime"); const MINT_PRIVATE_ADDRESS = "0700a1ad28a20e5b2a517c00242d3e25a88d84bf54dce9e1733e6096e6d6495e"; const MINT_KEY_PAIR = ec.keyFromPrivate(MINT_PRIVATE_ADDRESS, "hex"); @@ -49,59 +50,98 @@ class Block { ) } - static async hasValidTransactions(block, stateDB) { - // The transactions are valid under these criterias: - // - The subtraction of "reward" and "gas" should be the fixed reward, so that they can't get lower/higher reward. - // - Every transactions are valid on their own (checked by Transaction.isValid). - // - There is only one mint transaction. - // - Senders' balance after sending should be greater than 1, which means they have enough money to create their transactions. - - for (const transaction of block.transactions) { - if (!(await Transaction.isValid(transaction, stateDB))) { - return false; - } + static async verifyTxAndTransit(block, stateDB, enableLogging = false) { + for (const tx of block.transactions) { + if (!(await Transaction.isValid(tx, stateDB))) return false; } - // We will loop over the "data" prop, which holds all the transactions. - // If the sender's address is the mint address, we will store the amount into "reward". - // Gases are stored into "gas". - - // Senders' balance are stored into "balance" with the key being their address, the value being their balance. - // Their balance are changed based on "amount" and "gas" props in each transactions. - // Get all existing addresses - const addressesInBlock = block.transactions.map(transaction => SHA256(Transaction.getPubKey(transaction))); + 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; - let gas = BigInt(0), reward, balances = {}; + // Start state replay to check if transactions are legit + let gas = 0n, reward, states = {}; - for (const transaction of block.transactions) { - const txSenderPubkey = Transaction.getPubKey(transaction); + for (const tx of block.transactions) { + const txSenderPubkey = Transaction.getPubKey(tx); const txSenderAddress = SHA256(txSenderPubkey); - if (txSenderPubkey !== MINT_PUBLIC_ADDRESS) { - if (!balances[txSenderAddress]) { - const dataFromSender = await stateDB.get(txSenderAddress); - const senderBalance = dataFromSender.balance; - - balances[txSenderAddress] = BigInt(senderBalance) - BigInt(transaction.amount) - BigInt(transaction.gas) - BigInt(transaction.additionalData.contractGas || 0); - } else { - balances[txSenderAddress] -= BigInt(transaction.amount) + BigInt(transaction.gas) + BigInt(transaction.additionalData.contractGas || 0); - } - gas += BigInt(transaction.gas) + BigInt(transaction.additionalData.contractGas || 0); + if (!states[txSenderAddress]) { + const senderState = await stateDB.get(txSenderAddress); + + states[txSenderAddress] = senderState; + + if (senderState.body !== "") return false; + + states[txSenderAddress].balance = (BigInt(senderState.balance) - BigInt(tx.amount) - BigInt(tx.gas) - BigInt(tx.additionalData.contractGas || 0)).toString(); } else { - reward = BigInt(transaction.amount); + if (states[txSenderAddress].body !== "") return false; + + states[txSenderAddress].balance = (BigInt(states[txSenderAddress].balance) - BigInt(tx.amount) - BigInt(tx.gas) - BigInt(tx.additionalData.contractGas || 0)).toString(); + } + + // Contract deployment + if ( + states[txSenderAddress].body === "" && + typeof tx.additionalData.scBody === "string" && + txSenderPubkey !== MINT_PUBLIC_ADDRESS + ) { + states[txSenderAddress].body = tx.additionalData.scBody; + } + + // Update nonce + states[txSenderAddress].nonce += 1; + + if (states[txSenderAddress].balance < 0 && txSenderPubkey !== MINT_PUBLIC_ADDRESS) return false; + + if (!existedAddresses.includes(tx.recipient) && !states[tx.recipient]) { + states[tx.recipient] = { balance: "0", body: "", nonce: 0, storage: {} } + } + + if (existedAddresses.includes(tx.recipient) && !states[tx.recipient]) { + states[tx.recipient] = await stateDB.get(tx.recipient); + } + + states[tx.recipient].balance = (BigInt(states[tx.recipient].balance) + BigInt(tx.amount)).toString(); + + // Contract execution + if ( + txSenderPubkey !== MINT_PUBLIC_ADDRESS && + typeof states[tx.recipient].body === "string" && + states[tx.recipient].body !== "" + ) { + const contractInfo = { address: tx.recipient }; + + const newState = await jelscript(states[tx.recipient].body, states, BigInt(tx.additionalData.contractGas || 0), stateDB, block, tx, contractInfo, enableLogging); + + for (const account of Object.keys(newState)) { + states[account] = newState[account]; + } + } + + if (txSenderPubkey === MINT_PUBLIC_ADDRESS) { // Get mining reward + reward = BigInt(tx.amount); + } else { // Count gas used + gas += BigInt(tx.gas) + BigInt(tx.additionalData.contractGas || 0); } } - return ( - reward - gas === BigInt(BLOCK_REWARD) && - block.transactions.filter(transaction => Transaction.getPubKey(transaction) === MINT_PUBLIC_ADDRESS).length === 1 && - Object.values(balances).every(balance => balance >= 0) - ); + if ( + reward - gas === BigInt(BLOCK_REWARD) && + block.transactions.filter(tx => Transaction.getPubKey(tx) === MINT_PUBLIC_ADDRESS).length === 1 && + Transaction.getPubKey(block.transactions[0]) === MINT_PUBLIC_ADDRESS + ) { + for (const account of Object.keys(states)) { + await stateDB.put(account, states[account]); + } + + return true; + } + + return false; } static async hasValidTxOrder(block, stateDB) { diff --git a/src/core/runtime.js b/src/core/runtime.js index a25e3c5..59b809a 100644 --- a/src/core/runtime.js +++ b/src/core/runtime.js @@ -1,20 +1,22 @@ +const { bigIntable } = require("../utils/utils"); const Transaction = require("./transaction"); const crypto = require("crypto"), SHA256 = message => crypto.createHash("sha256").update(message).digest("hex"); -async function jelscript(input, gas, stateDB, block, txInfo, contractInfo, enableLogging) { +async function jelscript(input, originalState = {}, gas, stateDB, block, txInfo, contractInfo, enableLogging = false) { const instructions = input.trim().replace(/\t/g, "").split("\n").map(ins => ins.trim()).filter(ins => ins !== ""); - const memory = {}; + const memory = {}, state = originalState; - const userArgs = typeof txInfo.additionalData.txCallArgs !== "undefined" ? txInfo.additionalData.txCallArgs.map(arg => arg.toString()) : []; + const userArgs = typeof txInfo.additionalData.txCallArgs !== "undefined" ? txInfo.additionalData.txCallArgs.map(arg => "0x" + arg.toString(16)) : []; let ptr = 0; while ( ptr < instructions.length && gas >= BigInt("10000000") && - instructions[ptr].trim() !== "stop" + instructions[ptr].trim() !== "stop" && + instructions[ptr].trim() !== "revert" ) { const line = instructions[ptr].trim(); const command = line.split(" ").filter(tok => tok !== "")[0]; @@ -30,138 +32,87 @@ async function jelscript(input, gas, stateDB, block, txInfo, contractInfo, enabl break; case "add": // Add value to variable - setMem( - args[0], - ( BigInt(getValue("$" + args[0])) + BigInt(getValue(args[1])) ).toString() - ); + setMem(args[0], "0x" + ( BigInt(getValue("$" + args[0])) + BigInt(getValue(args[1])) ).toString(16)); break; case "sub": // Subtract value from variable - setMem( - args[0], - ( BigInt(getValue("$" + args[0])) - BigInt(getValue(args[1])) ).toString() - ); + setMem(args[0], "0x" + ( BigInt(getValue("$" + args[0])) - BigInt(getValue(args[1])) ).toString(16)); break; case "mul": // Multiply variable by value - setMem( - args[0], - ( BigInt(getValue("$" + args[0])) * BigInt(getValue(args[1])) ).toString() - ); + setMem(args[0], "0x" + ( BigInt(getValue("$" + args[0])) * BigInt(getValue(args[1])) ).toString(16)); break; case "div": // Divide variable by value - setMem( - args[0], - ( BigInt(getValue("$" + args[0])) / BigInt(getValue(args[1])) ).toString() - ); + setMem(args[0], "0x" + ( BigInt(getValue("$" + args[0])) / BigInt(getValue(args[1])) ).toString(16)); break; case "mod": // Modulo - setMem( - args[0], - ( BigInt(getValue("$" + args[0])) % BigInt(getValue(args[1])) ).toString() - ); + setMem(args[0], "0x" + ( BigInt(getValue("$" + args[0])) % BigInt(getValue(args[1])) ).toString(16)); break; case "and": - setMem( - args[0], - ( BigInt(getValue("$" + args[0])) & BigInt(getValue(args[1])) ).toString() - ); + setMem(args[0], "0x" + ( BigInt(getValue("$" + args[0])) & BigInt(getValue(args[1])) ).toString(16)); break; case "or": - setMem( - args[0], - ( BigInt(getValue("$" + args[0])) | BigInt(getValue(args[1])) ).toString() - ); + setMem(args[0], "0x" + ( BigInt(getValue("$" + args[0])) | BigInt(getValue(args[1])) ).toString(16)); break; case "xor": - setMem( - args[0], - ( BigInt(getValue("$" + args[0])) ^ BigInt(getValue(args[1])) ).toString() - ); + setMem(args[0], "0x" + ( BigInt(getValue("$" + args[0])) ^ BigInt(getValue(args[1])) ).toString(16)); break; case "ls": // Left shift - setMem( - args[0], - ( BigInt(getValue("$" + args[0])) << BigInt(getValue(args[1])) ).toString() - ); + setMem(args[0], "0x" + ( BigInt(getValue("$" + args[0])) << BigInt(getValue(args[1])) ).toString(16)); break; case "rs": // Right shift - setMem( - args[0], - ( BigInt(getValue("$" + args[0])) >> BigInt(getValue(args[1])) ).toString() - ); + setMem(args[0], "0x" + ( BigInt(getValue("$" + args[0])) >> BigInt(getValue(args[1])) ).toString(16)); break; case "not": - setMem( - args[0], - ( ~BigInt(getValue("$" + args[0])) ).toString() - ); + setMem(args[0], "0x" + ( ~BigInt(getValue("$" + args[0])) ).toString(16)); break; case "gtr": // Greater than - setMem( - args[0], - BigInt(getValue("$" + args[0])) > BigInt(getValue(args[1])) ? "1" : "0" - ); + setMem(args[0], "0x" + BigInt(getValue("$" + args[0])) > BigInt(getValue(args[1])) ? "0x1" : "0x0"); break; case "lss": // Less than - setMem( - args[0], - BigInt(getValue("$" + args[0])) < BigInt(getValue(args[1])) ? "1" : "0" - ); + setMem(args[0], "0x" + BigInt(getValue("$" + args[0])) < BigInt(getValue(args[1])) ? "0x1" : "0x0"); break; case "geq": // Greater or equal to - setMem( - args[0], - BigInt(getValue("$" + args[0])) >= BigInt(getValue(args[1])) ? "1" : "0" - ); + setMem(args[0], "0x" + BigInt(getValue("$" + args[0])) >= BigInt(getValue(args[1])) ? "0x1" : "0x0"); break; case "leq": // Less or equal to - setMem( - args[0], - BigInt(getValue("$" + args[0])) <= BigInt(getValue(args[1])) ? "1" : "0" - ); + setMem(args[0], "0x" + BigInt(getValue("$" + args[0])) <= BigInt(getValue(args[1])) ? "0x1" : "0x0"); break; case "equ": // Equal to - setMem( - args[0], - BigInt(getValue("$" + args[0])) === BigInt(getValue(args[1])) ? "1" : "0" - ); + setMem(args[0], "0x" + BigInt(getValue("$" + args[0])) === BigInt(getValue(args[1])) ? "0x1" : "0x0"); break; case "neq": // Not equal to - setMem( - args[0], - BigInt(getValue("$" + args[0])) !== BigInt(getValue(args[1])) ? "1" : "0" - ); + setMem(args[0], "0x" + BigInt(getValue("$" + args[0])) !== BigInt(getValue(args[1])) ? "0x1" : "0x0"); break; @@ -169,13 +120,8 @@ async function jelscript(input, gas, stateDB, block, txInfo, contractInfo, enabl // Flow control case "jump": // Command to jump to labels conditionally - if (getValue(args[0]) === "1") { - ptr = instructions.indexOf( - instructions.find( - line => line.startsWith("label " + getValue(args[1])) - ) - ); - } + if (getValue(args[0]) === "1") + ptr = instructions.indexOf(instructions.find(line => line.startsWith("label " + getValue(args[1])))); break; @@ -196,29 +142,29 @@ async function jelscript(input, gas, stateDB, block, txInfo, contractInfo, enabl // Block info case "timestamp": // Block's timestamp - setMem(args[0], block.timestamp.toString()); + setMem(args[0], "0x" + block.timestamp.toString(16)); break; case "blocknumber": // Block's number - setMem(args[0], block.blockNumber.toString()); + setMem(args[0], "0x" + block.blockNumber.toString(16)); break; case "blockhash": // Block's hash - setMem(args[0], block.hash); + setMem(args[0], "0x" + block.hash); break; case "difficulty": // Block's difficulty - setMem(args[0], block.difficulty.toString()); + setMem(args[0], "0x" + block.difficulty.toString(16)); break; // Transaction info case "txvalue": // Amount of tokens sent in transaction - setMem(args[0], txInfo.amount.toString()); + setMem(args[0], "0x" + txInfo.amount.toString(16)); break; @@ -231,12 +177,12 @@ async function jelscript(input, gas, stateDB, block, txInfo, contractInfo, enabl break; case "txgas": // Transaction gas - setMem(args[0], txInfo.gas.toString()); + setMem(args[0], "0x" + txInfo.gas.toString(16)); break; case "txexecgas": // Contract execution gas - setMem(args[0], txInfo.additionalData.contractGas.toString()); + setMem(args[0], "0x" + txInfo.additionalData.contractGas.toString(16)); break; @@ -248,56 +194,64 @@ async function jelscript(input, gas, stateDB, block, txInfo, contractInfo, enabl break; case "selfbalance": // Contract's balance - const contractState = await stateDB.get(contractInfo.address); + if (!state[contractInfo.address]) { + const contractState = await stateDB.get(contractInfo.address); + state[contractInfo.address] = contractState; + } - setMem(args[0], contractState.balance); + setMem(args[0], state[contractInfo.address].balance); break; // Interactions with others case "balance": // Get balance from address - const address = getValue(args[1]); + const address = getValue(args[1]); // Get address + + const existedAddresses = await stateDB.keys().all(); - if (!(await stateDB.keys().all()).includes(address)) { - setMem(getValue(args[0]), "0"); - break; + if (!existedAddresses.includes(address) && !state[address]) { + setMem(getValue(args[0]), "0x0"); } - const targetState = await stateDB.get(address); - const targetBalance = targetState.balance; + if (existedAddresses.includes(address) && !state[address]) { + setMem(args[0], "0x" + (await stateDB.get(address)).balance.toString(16)); + } - setMem(args[0], targetBalance.toString()); + if (!existedAddresses.includes(address) && state[address]) { + setMem(args[0], "0x" + state[address].balance.toString(16)); + } break; case "send": // Send tokens to address const target = getValue(args[0]); const amount = BigInt(getValue(args[1])); - const state = await stateDB.get(contractInfo.address); - const balance = state.balance; - if (BigInt(balance) >= amount) { - const existedAddresses = await stateDB.keys().all(); + if (!state[contractInfo.address]) { + const contractState = await stateDB.get(contractInfo.address); + state[contractInfo.address] = contractState; + } - if (!existedAddresses.includes(target)) { - await stateDB.put(target, { + const balance = state[contractInfo.address].balance; + + if (BigInt(balance) >= amount) { + if (!await stateDB.keys().all().includes(target) && !state[target]) { + state[target] = { balance: amount.toString(), body: "", - timestamps: [], + nonce: 0, storage: {} - }); + } } else { - const targetState = await stateDB.get(target); + if (!state[target]) { + state[target] = await stateDB.get(target); + } - targetState.balance = BigInt(targetState.balance) + amount; - - await stateDB.put(target, targetState); + state[target].balance = BigInt(targetState.balance) + amount; } - state.balance = BigInt(state.balance) - amount; - - await stateDB.put(contractInfo.address, state); + state[contractInfo.address].balance = BigInt(state.balance) - amount; } break; @@ -312,7 +266,7 @@ async function jelscript(input, gas, stateDB, block, txInfo, contractInfo, enabl // Others case "sha256": // Generate sha256 hash of value and assign to variable - setMem(args[0], SHA256(getValue(args[1]))); + setMem(args[0], "0x" + SHA256(getValue(args[1]))); break; @@ -322,50 +276,62 @@ async function jelscript(input, gas, stateDB, block, txInfo, contractInfo, enabl break; case "gas": // Show current available gas - setMem(args[0], gas.toString()); + setMem(args[0], "0x" + gas.toString(16)); break; } ptr++; - gas-=BigInt("10000000"); + gas-=10000000n; } + if (ptr < instructions.length && instructions[ptr].trim() === "revert") return originalState; // Revert all changes made to state + function getValue(token) { if (token.startsWith("$")) { token = token.replace("$", ""); if (typeof memory[token] === "undefined") { - memory[token] = "0"; + memory[token] = "0x0"; } return memory[token]; } else if (token.startsWith("%")) { token = token.replace("%", ""); - return typeof userArgs[BigInt(token)] === "undefined" ? "0" : userArgs[BigInt(token)]; + if (typeof userArgs[parseInt(token)] === "undefined") { + return "0x0"; + } else { + return bigIntable(userArgs[parseInt(token)]) ? "0x" + BigInt(userArgs[parseInt(token)]).toString(16) : "0x0"; + } } else { return token; } } function setMem(key, value) { - memory[key] = value; + memory[key] = bigIntable(value) ? "0x" + BigInt(value).toString(16) : "0x0"; } async function setStorage(key, value) { - const contractState = await stateDB.get(contractInfo.address); - - contractState.storage[key] = value; + if (!state[contractInfo.address]) { + const contractState = await stateDB.get(contractInfo.address); + state[contractInfo.address] = contractState; + } - stateDB.put(contractInfo.address, contractState); + state[contractInfo.address].storage[key] = bigIntable(value) ? "0x" + BigInt(value).toString(16) : "0x0"; } async function getStorage(key) { - const contractState = await stateDB.get(contractInfo.address); + if (!state[contractInfo.address]) { + const contractState = await stateDB.get(contractInfo.address); + state[contractInfo.address] = contractState; + } - return contractState.storage[key] ? contractState.storage[key] : "0"; + return state[contractInfo.address].storage[key] ? state[contractInfo.address].storage[key] : "0x0"; } + + return state; } module.exports = jelscript; diff --git a/src/core/state.js b/src/core/state.js index 4e929f5..3a2da92 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -16,12 +16,7 @@ async function changeState(newBlock, stateDB, enableLogging = false) { for (const tx of newBlock.transactions) { // If the address doesn't already exist in the chain state, we will create a new empty one. if (!existedAddresses.includes(tx.recipient)) { - await stateDB.put(tx.recipient, { - balance: "0", - body: "", - nonce: 0, - storage: {} - }); + await stateDB.put(tx.recipient, { balance: "0", body: "", nonce: 0, storage: {} }); } // Get sender's public key and address @@ -30,29 +25,25 @@ async function changeState(newBlock, stateDB, enableLogging = false) { // If the address doesn't already exist in the chain state, we will create a new empty one. if (!existedAddresses.includes(txSenderAddress)) { - await stateDB.put(txSenderAddress, { - balance: "0", - body: "", - nonce: 0, - storage: {} - }); - } else if (typeof tx.additionalData.scBody === "string") { + await stateDB.put(txSenderAddress, { balance: "0", body: "", nonce: 0, storage: {} }); + } else if (typeof tx.additionalData.scBody === "string") { // Contract deployment const dataFromSender = await stateDB.get(txSenderAddress); - if (dataFromSender.body === "") { + if (dataFromSender.body === "" && txSenderPubkey !== MINT_PUBLIC_ADDRESS) { dataFromSender.body = tx.additionalData.scBody; await stateDB.put(txSenderAddress, dataFromSender); } } + // Normal transfer const dataFromSender = await stateDB.get(txSenderAddress); const dataFromRecipient = await stateDB.get(tx.recipient); await stateDB.put(txSenderAddress, { balance: (BigInt(dataFromSender.balance) - BigInt(tx.amount) - BigInt(tx.gas) - BigInt((tx.additionalData.contractGas || 0))).toString(), body: dataFromSender.body, - nonce: dataFromSender.nonce + 1, + nonce: dataFromSender.nonce + 1, // Update nonce storage: dataFromSender.storage }); @@ -62,16 +53,8 @@ async function changeState(newBlock, stateDB, enableLogging = false) { nonce: dataFromRecipient.nonce, storage: dataFromRecipient.storage }); - } - - // Separate contract execution from normal transfers. - // EXTREMELY stupud decision but works for now lmao, should be fixed soon. - - for (const tx of newBlock.transactions) { - const txSenderPubkey = Transaction.getPubKey(tx); - - const dataFromRecipient = await stateDB.get(tx.recipient); + // Contract execution if ( txSenderPubkey !== MINT_PUBLIC_ADDRESS && typeof dataFromRecipient.body === "string" && @@ -79,7 +62,11 @@ async function changeState(newBlock, stateDB, enableLogging = false) { ) { const contractInfo = { address: tx.recipient }; - await jelscript(dataFromRecipient.body, BigInt(tx.additionalData.contractGas || 0), stateDB, newBlock, tx, contractInfo, enableLogging); + const newState = await jelscript(dataFromRecipient.body, {}, BigInt(tx.additionalData.contractGas || 0), stateDB, newBlock, tx, contractInfo, enableLogging); + + for (const account of Object.keys(newState)) { + await stateDB.put(account, newState[account]); + } } } } diff --git a/src/core/txPool.js b/src/core/txPool.js index 7004031..cf3c2e6 100644 --- a/src/core/txPool.js +++ b/src/core/txPool.js @@ -1,19 +1,30 @@ 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 jelscript = require("./runtime"); -async function addTransaction(transaction, txPool, stateDB) { +const { BLOCK_GAS_LIMIT } = require("../config.json"); + +const MINT_PRIVATE_ADDRESS = "0700a1ad28a20e5b2a517c00242d3e25a88d84bf54dce9e1733e6096e6d6495e"; +const MINT_KEY_PAIR = ec.keyFromPrivate(MINT_PRIVATE_ADDRESS, "hex"); +const MINT_PUBLIC_ADDRESS = MINT_KEY_PAIR.getPublic("hex"); + +async function addTransaction(transaction, chainInfo, stateDB) { // Transactions are added into "this.transactions", which is the transaction pool. // To be added, transactions must be valid, and they are valid under these criterias: // - They are valid based on Transaction.isValid // - The balance of the sender is enough to make the transaction (based his transactions the pool). // - It has a correct nonce. - if (!(await Transaction.isValid(transaction, stateDB))) { + if (!(await Transaction.isValid(transaction, stateDB)) || BigInt(transaction.additionalData.contractGas || 0) > BigInt(BLOCK_GAS_LIMIT)) { console.log("LOG :: Failed to add one transaction to pool."); return; } + const txPool = chainInfo.transactionPool; + const latestBlock = chainInfo.latestBlock; + // Get public key and address from sender const txSenderPubkey = Transaction.getPubKey(transaction); const txSenderAddress = SHA256(txSenderPubkey); @@ -23,69 +34,180 @@ async function addTransaction(transaction, txPool, stateDB) { return; } - // Fetch sender's state object - const dataFromSender = await stateDB.get(txSenderAddress); - // Get sender's balance - let balance = BigInt(dataFromSender.balance) - BigInt(transaction.amount) - BigInt(transaction.gas) - BigInt(transaction.additionalData.contractGas || 0); + // Emulate state + const states = {}; - txPool.forEach(tx => { - const _txSenderPubkey = Transaction.getPubKey(tx); - const _txSenderAddress = SHA256(_txSenderPubkey); + const existedAddresses = await stateDB.keys().all(); - if (_txSenderAddress === txSenderAddress) { - balance -= BigInt(tx.amount) + BigInt(tx.gas) + BigInt(tx.additionalData.contractGas || 0); - } - }); - - if (balance >= 0) { - // Check nonce - let maxNonce = 0; + for (const tx of txPool.filter(tx => Transaction.getPubKey(tx) === txSenderPubkey)) { + if (!states[txSenderAddress]) { + const senderState = await stateDB.get(txSenderAddress); - for (const tx of txPool) { - const poolTxSenderPubkey = Transaction.getPubKey(transaction); - const poolTxSenderAddress = SHA256(poolTxSenderPubkey); + states[txSenderAddress] = senderState; - if (poolTxSenderAddress === txSenderAddress && tx.nonce > maxNonce) { - maxNonce = tx.nonce; + if (senderState.body !== "") { + console.log("LOG :: Failed to add one transaction to pool."); + return; + } + + states[txSenderAddress].balance = (BigInt(senderState.balance) - BigInt(tx.amount) - BigInt(tx.gas) - BigInt(tx.additionalData.contractGas || 0)).toString(); + } else { + if (states[txSenderAddress].body !== "") { + console.log("LOG :: Failed to add one transaction to pool."); + return; } + + states[txSenderAddress].balance = (BigInt(states[txSenderAddress].balance) - BigInt(tx.amount) - BigInt(tx.gas) - BigInt(tx.additionalData.contractGas || 0)).toString(); + } + + // Contract deployment + if ( + states[txSenderAddress].body === "" && + typeof tx.additionalData.scBody === "string" && + txSenderPubkey !== MINT_PUBLIC_ADDRESS + ) { + states[txSenderAddress].body = tx.additionalData.scBody; } - if (maxNonce + 1 !== transaction.nonce) { - console.log(transaction, maxNonce); + // Update nonce + states[txSenderAddress].nonce += 1; + + if (states[txSenderAddress].balance < 0) { console.log("LOG :: Failed to add one transaction to pool."); return; } + + if (!existedAddresses.includes(tx.recipient) && !states[tx.recipient]) { + states[tx.recipient] = { balance: "0", body: "", nonce: 0, storage: {} } + } + + if (existedAddresses.includes(tx.recipient) && !states[tx.recipient]) { + states[tx.recipient] = await stateDB.get(tx.recipient); + } + + states[tx.recipient].balance = BigInt(states[tx.recipient].balance) + BigInt(tx.amount); + + // Contract execution + if ( + txSenderPubkey !== MINT_PUBLIC_ADDRESS && + typeof states[tx.recipient].body === "string" && + states[tx.recipient].body !== "" + ) { + const contractInfo = { address: tx.recipient }; + + const newState = await jelscript(states[tx.recipient].body, states, BigInt(tx.additionalData.contractGas || 0), stateDB, latestBlock, tx, contractInfo, enableLogging); + + for (const account of Object.keys(newState)) { + states[account] = newState[account]; + } + } + } - txPool.push(transaction); + // Check nonce + let maxNonce = 0; - console.log("LOG :: Added one transaction to pool."); - } else { + for (const tx of txPool) { + const poolTxSenderPubkey = Transaction.getPubKey(transaction); + const poolTxSenderAddress = SHA256(poolTxSenderPubkey); + + if (poolTxSenderAddress === txSenderAddress && tx.nonce > maxNonce) { + maxNonce = tx.nonce; + } + } + + if (maxNonce + 1 !== transaction.nonce) { console.log("LOG :: Failed to add one transaction to pool."); + return; } + + txPool.push(transaction); + + console.log("LOG :: Added one transaction to pool."); } -async function clearDepreciatedTxns(txPool, stateDB) { - const newTxPool = []; +async function clearDepreciatedTxns(chainInfo, stateDB) { + const txPool = chainInfo.transactionPool; + const latestBlock = chainInfo.latestBlock; + + const newTxPool = [], states = {}, skipped = {}, maxNonce = {}; - const balances = {}; + const existedAddresses = await stateDB.keys().all(); for (const tx of txPool) { const txSenderPubkey = Transaction.getPubKey(tx); const txSenderAddress = SHA256(txSenderPubkey); + if (skipped[txSenderAddress]) continue; + const senderState = await stateDB.get(txSenderAddress); - - if (!balances[txSenderAddress]) { - balances[txSenderAddress] = BigInt(senderState.balance); + + if (!maxNonce[txSenderAddress]) { + maxNonce[txSenderAddress] = senderState.nonce; } + + if (!states[txSenderAddress]) { + const senderState = await stateDB.get(txSenderAddress); + + states[txSenderAddress] = senderState; + + if (senderState.body !== "") { + 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].body !== "") { + skipped[txSenderAddress] = true; + continue; + } - balances[txSenderAddress] = balances[txSenderAddress] - BigInt(tx.amount) - BigInt(tx.gas) - BigInt(tx.additionalData.contractGas || 0); + states[txSenderAddress].balance = (BigInt(states[txSenderAddress].balance) - BigInt(tx.amount) - BigInt(tx.gas) - BigInt(tx.additionalData.contractGas || 0)).toString(); + } + // Contract deployment if ( - tx.nonce > senderState.nonce && - balances[txSenderAddress] >= 0 + states[txSenderAddress].body === "" && + typeof tx.additionalData.scBody === "string" && + txSenderPubkey !== MINT_PUBLIC_ADDRESS ) { + states[txSenderAddress].body = tx.additionalData.scBody; + } + + // Update nonce + states[txSenderAddress].nonce += 1; + + if (states[txSenderAddress].balance < 0) { + skipped[txSenderAddress] = true; + continue; + } else if (tx.nonce - 1 === maxNonce[txSenderAddress]) { newTxPool.push(tx); + maxNonce[txSenderAddress] = tx.nonce; + } + + if (!existedAddresses.includes(tx.recipient) && !states[tx.recipient]) { + states[tx.recipient] = { balance: "0", body: "", nonce: 0, storage: {} } + } + + if (existedAddresses.includes(tx.recipient) && !states[tx.recipient]) { + states[tx.recipient] = await stateDB.get(tx.recipient); + } + + states[tx.recipient].balance = BigInt(states[tx.recipient].balance) + BigInt(tx.amount); + + // Contract execution + if ( + txSenderPubkey !== MINT_PUBLIC_ADDRESS && + typeof states[tx.recipient].body === "string" && + states[tx.recipient].body !== "" + ) { + const contractInfo = { address: tx.recipient }; + + const newState = await jelscript(states[tx.recipient].body, states, BigInt(tx.additionalData.contractGas || 0), stateDB, latestBlock, tx, contractInfo, false); + + for (const account of Object.keys(newState)) { + states[account] = newState[account]; + } } } diff --git a/src/node/server.js b/src/node/server.js index 5d51638..309810e 100644 --- a/src/node/server.js +++ b/src/node/server.js @@ -17,6 +17,7 @@ const rpc = require("../rpc/rpc"); const TYPE = require("./message-types"); const { verifyBlock, updateDifficulty } = require("../consensus/consensus"); const { parseJSON } = require("../utils/utils"); +const jelscript = require("../core/runtime"); const MINT_PRIVATE_ADDRESS = "0700a1ad28a20e5b2a517c00242d3e25a88d84bf54dce9e1733e6096e6d6495e"; const MINT_KEY_PAIR = ec.keyFromPrivate(MINT_PRIVATE_ADDRESS, "hex"); @@ -35,6 +36,7 @@ const chainInfo = { transactionPool: [], latestBlock: generateGenesisBlock(), latestSyncBlock: null, + checkedBlock: {}, difficulty: 1 }; @@ -79,12 +81,18 @@ async function startServer(options) { // We will only continue checking the block if its parentHash is not the same as the latest block's hash. // This is because the block sent to us is likely duplicated or from a node that has lost and should be discarded. + if (!chainInfo.checkedBlock[newBlock.hash]) { + chainInfo.checkedBlock[newBlock.hash] = true; + } else { return; } + if ( newBlock.parentHash !== chainInfo.latestBlock.parentHash && (!ENABLE_CHAIN_REQUEST || (ENABLE_CHAIN_REQUEST && currentSyncBlock > 1)) // Only proceed if syncing is disabled or enabled but already synced at least the genesis block ) { - if (await verifyBlock(newBlock, chainInfo, stateDB)) { + chainInfo.checkedBlock[newBlock.hash] = true; + + if (await verifyBlock(newBlock, chainInfo, stateDB, ENABLE_LOGGING)) { console.log("LOG :: New block received."); // If mining is enabled, we will set mined to true, informing that another node has mined before us. @@ -102,10 +110,8 @@ async function startServer(options) { chainInfo.latestBlock = newBlock; // Update chain info - await changeState(newBlock, stateDB, ENABLE_LOGGING); // Transit state - // Update the new transaction pool (remove all the transactions that are no longer valid). - chainInfo.transactionPool = await clearDepreciatedTxns(chainInfo.transactionPool, stateDB); + chainInfo.transactionPool = await clearDepreciatedTxns(chainInfo, stateDB); console.log(`LOG :: Block #${newBlock.blockNumber} synced, state transited.`); @@ -145,41 +151,84 @@ async function startServer(options) { // This is pretty much the same as addTransaction, but we will send the transaction to other connected nodes if it's valid. - const dataFromSender = await stateDB.get(txSenderAddress); // Fetch sender's state object - const senderBalance = dataFromSender.balance; // Get sender's balance + // Emulate state + const states = {}; + + const existedAddresses = await stateDB.keys().all(); + + for (const tx of chainInfo.transactionPool.filter(tx => Transaction.getPubKey(tx) === txSenderPubkey)) { + if (!states[txSenderAddress]) { + const senderState = await stateDB.get(txSenderAddress); + + states[txSenderAddress] = senderState; + + if (senderState.body !== "") return; - let balance = BigInt(senderBalance) - BigInt(transaction.amount) - BigInt(transaction.gas) - BigInt(transaction.additionalData.contractGas || 0); - - chainInfo.transactionPool.forEach(tx => { - const _txSenderPubkey = Transaction.getPubKey(tx); - const _txSenderAddress = SHA256(_txSenderPubkey); + states[txSenderAddress].balance = (BigInt(senderState.balance) - BigInt(tx.amount) - BigInt(tx.gas) - BigInt(tx.additionalData.contractGas || 0)).toString(); + } else { + if (states[txSenderAddress].body !== "") return; - if (_txSenderAddress === txSenderAddress) { - balance -= BigInt(tx.amount) + BigInt(tx.gas) + BigInt(transaction.additionalData.contractGas || 0); + states[txSenderAddress].balance = (BigInt(states[txSenderAddress].balance) - BigInt(tx.amount) - BigInt(tx.gas) - BigInt(tx.additionalData.contractGas || 0)).toString(); } - }); - - if (balance >= 0) { - // Check nonce - let maxNonce = 0; - for (const tx of chainInfo.transactionPool) { - const poolTxSenderPubkey = Transaction.getPubKey(transaction); - const poolTxSenderAddress = SHA256(poolTxSenderPubkey); + // Contract deployment + if ( + states[txSenderAddress].body === "" && + typeof tx.additionalData.scBody === "string" && + txSenderPubkey !== MINT_PUBLIC_ADDRESS + ) { + states[txSenderAddress].body = tx.additionalData.scBody; + } - if (poolTxSenderAddress === txSenderAddress && tx.nonce > maxNonce) { - maxNonce = tx.nonce; + // Update nonce + states[txSenderAddress].nonce += 1; + + if (states[txSenderAddress].balance < 0) return; + + if (!existedAddresses.includes(tx.recipient) && !states[tx.recipient]) { + states[tx.recipient] = { balance: "0", body: "", nonce: 0, storage: {} } + } + + if (existedAddresses.includes(tx.recipient) && !states[tx.recipient]) { + states[tx.recipient] = await stateDB.get(tx.recipient); + } + + states[tx.recipient].balance = BigInt(states[tx.recipient].balance) + BigInt(tx.amount); + + if ( + txSenderPubkey !== MINT_PUBLIC_ADDRESS && + typeof states[tx.recipient].body === "string" && + states[tx.recipient].body !== "" + ) { + const contractInfo = { address: tx.recipient }; + + const newState = await jelscript(states[tx.recipient].body, states, BigInt(tx.additionalData.contractGas || 0), stateDB, chainInfo.latestBlock, tx, contractInfo, ENABLE_LOGGING); + + for (const account of Object.keys(newState)) { + states[account] = newState[account]; } } + } + + // Check nonce + let maxNonce = 0; - if (maxNonce + 1 !== transaction.nonce) return; + for (const tx of chainInfo.transactionPool) { + const poolTxSenderPubkey = Transaction.getPubKey(transaction); + const poolTxSenderAddress = SHA256(poolTxSenderPubkey); - console.log("LOG :: New transaction received and added to pool."); - - chainInfo.transactionPool.push(transaction); - // Broadcast the transaction - sendMessage(message, opened); + if (poolTxSenderAddress === txSenderAddress && tx.nonce > maxNonce) { + maxNonce = tx.nonce; + } } + + if (maxNonce + 1 !== transaction.nonce) return; + + console.log("LOG :: New transaction received and added to pool."); + + chainInfo.transactionPool.push(transaction); + // Broadcast the transaction + sendMessage(message, opened); break; @@ -209,7 +258,7 @@ async function startServer(options) { if ( chainInfo.latestSyncBlock === null // If latest synced block is null then we immediately add the block into the chain without verification. || // This happens due to the fact that the genesis block can discard every possible set rule ¯\_(ツ)_/¯ - await verifyBlock(block, chainInfo, stateDB) + await verifyBlock(block, chainInfo, stateDB, ENABLE_LOGGING) ) { currentSyncBlock += 1; @@ -217,11 +266,10 @@ async function startServer(options) { if (!chainInfo.latestSyncBlock) { chainInfo.latestSyncBlock = block; // Update latest synced block. + await changeState(block, stateDB, ENABLE_LOGGING); // Transit state } chainInfo.latestBlock = block; // Update latest block. - - await changeState(block, stateDB); // Transit state await updateDifficulty(block, chainInfo, blockDB); // Update difficulty. @@ -336,7 +384,7 @@ async function sendTransaction(transaction) { console.log("LOG :: Sent one transaction."); - await addTransaction(transaction, chainInfo.transactionPool, stateDB); + await addTransaction(transaction, chainInfo, stateDB); } function mine(publicKey, ENABLE_LOGGING) { @@ -388,7 +436,7 @@ function mine(publicKey, ENABLE_LOGGING) { await changeState(chainInfo.latestBlock, stateDB, ENABLE_LOGGING); // Transit state // Update the new transaction pool (remove all the transactions that are no longer valid). - chainInfo.transactionPool = await clearDepreciatedTxns(chainInfo.transactionPool, stateDB); + chainInfo.transactionPool = await clearDepreciatedTxns(chainInfo, stateDB); sendMessage(produceMessage(TYPE.NEW_BLOCK, chainInfo.latestBlock), opened); // Broadcast the new block diff --git a/src/utils/utils.js b/src/utils/utils.js index 48cc0c3..71b3705 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -8,6 +8,16 @@ function isNumber(str) { return str.split("").every(char => "0123456789".includes(char)); } +function bigIntable(str) { + try { + BigInt(str); + + return true; + } catch (e) { + return false; + } +} + function parseJSON(value) { let parsed; @@ -20,4 +30,4 @@ function parseJSON(value) { return parsed; } -module.exports = { log16, isNumber, parseJSON }; +module.exports = { log16, isNumber, parseJSON, bigIntable }; From ee0d0e0b2cd90322bc0bbf03f59b2523dd53f100 Mon Sep 17 00:00:00 2001 From: Phu Minh <53084840+nguyenphuminh@users.noreply.github.com> Date: Thu, 27 Oct 2022 01:30:03 +0700 Subject: [PATCH 2/2] Extra fix --- src/core/runtime.js | 3 ++- src/node/server.js | 2 +- src/rpc/rpc.js | 20 +++++++++++++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/core/runtime.js b/src/core/runtime.js index 59b809a..a550714 100644 --- a/src/core/runtime.js +++ b/src/core/runtime.js @@ -120,8 +120,9 @@ async function jelscript(input, originalState = {}, gas, stateDB, block, txInfo, // Flow control case "jump": // Command to jump to labels conditionally - if (getValue(args[0]) === "1") + if (BigInt(getValue(args[0])) === 1n) { ptr = instructions.indexOf(instructions.find(line => line.startsWith("label " + getValue(args[1])))); + } break; diff --git a/src/node/server.js b/src/node/server.js index 309810e..2955be0 100644 --- a/src/node/server.js +++ b/src/node/server.js @@ -339,7 +339,7 @@ async function startServer(options) { } if (ENABLE_MINING) loopMine(publicKey, ENABLE_CHAIN_REQUEST, ENABLE_LOGGING); - if (ENABLE_RPC) rpc(RPC_PORT, { publicKey, mining: ENABLE_MINING }, sendTransaction, stateDB, blockDB); + if (ENABLE_RPC) rpc(RPC_PORT, { publicKey, mining: ENABLE_MINING }, sendTransaction, keyPair, stateDB, blockDB); } // Function to connect to a node. diff --git a/src/rpc/rpc.js b/src/rpc/rpc.js index d8101a9..c310ea1 100644 --- a/src/rpc/rpc.js +++ b/src/rpc/rpc.js @@ -2,9 +2,11 @@ "use strict"; +const Transaction = require("../core/transaction"); + const fastify = require("fastify")(); -function rpc(PORT, client, transactionHandler, stateDB, blockDB) { +function rpc(PORT, client, transactionHandler, keyPair, stateDB, blockDB) { process.on("uncaughtException", err => console.log("LOG ::", err)); @@ -270,6 +272,22 @@ function rpc(PORT, client, transactionHandler, stateDB, blockDB) { break; + case "signTransaction": + if ( + typeof req.body.params !== "object" || + typeof req.body.params.transaction !== "object" + ) { + throwError("Invalid request.", 400); + } else { + const transaction = req.body.params.transaction; + + Transaction.sign(transaction, keyPair); + + respond({ transaction }); + } + + break; + default: throwError("Invalid option.", 404); }