diff --git a/package.json b/package.json index 22eab35..589ea23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jechain", - "version": "0.28.2", + "version": "0.29.0", "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 da64ba0..5d9bfd5 100644 --- a/src/consensus/consensus.js +++ b/src/consensus/consensus.js @@ -1,9 +1,8 @@ const crypto = require("crypto"), SHA256 = message => crypto.createHash("sha256").update(message).digest("hex"); const Block = require("../core/block"); const { log16 } = require("../utils/utils"); -const { buildMerkleTree } = require("../core/merkle"); +const Merkle = require("../core/merkle"); const { BLOCK_TIME } = require("../config.json"); -const { indexTxns } = require("../utils/utils"); async function verifyBlock(newBlock, chainInfo, stateDB, codeDB, 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. @@ -32,11 +31,11 @@ async function verifyBlock(newBlock, chainInfo, stateDB, codeDB, enableLogging = newBlock.hash.startsWith("00000" + Array(Math.floor(log16(chainInfo.difficulty)) + 1).join("0")) && newBlock.difficulty === chainInfo.difficulty && - // Check transaction hash - buildMerkleTree(indexTxns(newBlock.transactions)).val === newBlock.txRoot && - // Check transactions ordering await Block.hasValidTxOrder(newBlock, stateDB) && + + // Check transaction trie root + Merkle.buildTxTrie(newBlock.transactions).root === newBlock.txRoot && // Check timestamp newBlock.timestamp > chainInfo.latestBlock.timestamp && diff --git a/src/core/block.js b/src/core/block.js index bafb093..b858ef7 100644 --- a/src/core/block.js +++ b/src/core/block.js @@ -4,10 +4,10 @@ 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 { buildMerkleTree } = require("./merkle"); +const Merkle = require("./merkle"); const { BLOCK_REWARD, BLOCK_GAS_LIMIT, EMPTY_HASH } = require("../config.json"); const jelscript = require("./runtime"); -const { indexTxns, serializeState, deserializeState } = require("../utils/utils"); +const { serializeState, deserializeState } = require("../utils/utils"); class Block { constructor(blockNumber = 1, timestamp = Date.now(), transactions = [], difficulty = 1, parentHash = "", coinbase = "") { @@ -19,9 +19,10 @@ class Block { this.difficulty = difficulty; // Difficulty to mine block this.parentHash = parentHash; // Parent (previous) block's hash this.nonce = 0; // Nonce - this.txRoot = buildMerkleTree(indexTxns(transactions)).val; // Merkle root of transactions this.coinbase = coinbase; // Address to receive reward this.hash = Block.getHash(this); // Hash of the block + // Merkle root of transactions + this.txRoot = Merkle.buildTxTrie(transactions.map(tx => Transaction.deserialize(tx))).root; } static serialize(block) { @@ -226,7 +227,7 @@ class Block { const storageDB = new Level(__dirname + "/../../log/accountStore/" + address); const keys = Object.keys(storage[address]); - states[address].storageRoot = buildMerkleTree(keys.map(key => key + " " + storage[address][key])).val; + states[address].storageRoot = Merkle.buildTxTrie(keys.map(key => key + " " + storage[address][key]), false).root; for (const key of keys) { await storageDB.put(key, storage[address][key]); diff --git a/src/core/merkle.js b/src/core/merkle.js index f8750bd..6aecf65 100644 --- a/src/core/merkle.js +++ b/src/core/merkle.js @@ -1,73 +1,104 @@ "use strict"; +const Transaction = require("./transaction"); +const { EMPTY_HASH } = require("../config.json"); + const crypto = require("crypto"), SHA256 = message => crypto.createHash("sha256").update(message).digest("hex"); -function Node(val, left = null, right = null) { - return { val, left, right }; +class Node { + constructor(leftHash = EMPTY_HASH, rightHash = EMPTY_HASH, parentHash = EMPTY_HASH) { + this.leftHash = leftHash; + this.rightHash = rightHash; + this.parentHash = parentHash; + } } -function getMerklePath(node, target, path = []) { - if (node.val === target) return [...path, target]; - if (node.left === null) return []; - - const path1 = getMerklePath(node.left, target, [...path, node.right.val]); - const path2 = getMerklePath(node.right, target, [...path, node.left.val]); +class TxTrie { + constructor(root, trieMap) { + this.root = root; + this.trieMap = trieMap; + } +} - if (path1.length !== 0) return path1; - if (path2.length !== 0) return path2; +class Merkle { + static buildTxTrie(transactionList = [], indexed = true) { + let hashList = []; + const trieMap = []; + + // Hash transactions + for (let index = 0; index < transactionList.length; index++) { + const tx = transactionList[index]; - return []; -} + const hash = indexed ? SHA256(`${index} ` + Transaction.getHash(tx)) : SHA256(tx); -function verifyMerkleProof(leaves, root) { - let genHash = leaves[0]; + hashList.push(hash); + trieMap[hash] = new Node(); + } - for (let i = 1; i < leaves.length; i++) { - if (BigInt("0x" + genHash) < BigInt("0x" + leaves[i])) { - genHash = SHA256(genHash + leaves[i]); - } else { - genHash = SHA256(leaves[i] + genHash); + // If there are no transaction, supply an empty hash so there would not be an error + if (transactionList.length === 0) { + hashList.push(EMPTY_HASH); + trieMap[EMPTY_HASH] = new Node(); } - } - return genHash === root; -} + // Build the tree up continuously + while (true) { + // If the hash list only have one hash left, it is the root and we have finished building the tree + if (hashList.length === 1) return new TxTrie(hashList[0], trieMap); -function buildMerkleTree(items) { - if (items.length === 0) return Node(SHA256("0")); + // If hash amount is odd, then we duplicate the latest hash + if (hashList.length % 2 !== 0) { + hashList.push(hashList.at(-1)); + } - let hashList = items.map(item => Node(SHA256(item))); - - if (hashList.length % 2 !== 0 && hashList.length !== 1) { - hashList.push(hashList[hashList.length-1]); - } + const newHashList = []; - while (hashList.length !== 1) { - const newRow = []; + // Generate hashes at current depth + while (hashList.length !== 0) { - while (hashList.length !== 0) { - if (hashList.length % 2 !== 0 && hashList.length !== 1) { - hashList.push(hashList[hashList.length-1]); - } - - const left = hashList.shift(); - const right = hashList.shift(); + const leftHash = hashList.shift(); + const rightHash = hashList.shift(); - if (BigInt("0x" + left.val) < BigInt("0x" + right.val)) { - const node = Node(SHA256(left.val + right.val), left, right); + let hash = EMPTY_HASH; - newRow.push(node); - } else { - const node = Node(SHA256(right.val + left.val), right, left); + if (BigInt("0x" + leftHash) > BigInt("0x" + rightHash)) { + hash = SHA256(leftHash + rightHash); + } else { + hash = SHA256(rightHash + leftHash); + } - newRow.push(node); + // Push hash to hash list + newHashList.push(hash); + // Update nodes in trie + trieMap[hash] = new Node(leftHash, rightHash); + trieMap[leftHash].parentHash = hash; + trieMap[rightHash].parentHash = hash; } + + hashList = newHashList; } + } + + static getTxTriePath(trieMap, rootHash, target) { + const path = []; + + let currentHash = target; - hashList = newRow; + while (true) { + if (currentHash === rootHash) return path; + + const currentNode = trieMap[currentHash]; + const parentNode = trieMap[currentNode.parentHash]; + + if (parentNode.leftHash === currentHash) { + path.push(parentNode.rightHash); + } else { + path.push(parentNode.leftHash); + } + + currentHash = currentNode.parentHash; + } } - - return hashList[0]; } -module.exports = { getMerklePath, verifyMerkleProof, buildMerkleTree }; +module.exports = Merkle; diff --git a/src/core/runtime.js b/src/core/runtime.js index 17a0b9c..54fb961 100644 --- a/src/core/runtime.js +++ b/src/core/runtime.js @@ -10,8 +10,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) { - const storageDB = new Level(__dirname + "/../../log/accountStore/" + contractInfo.address); - + // Prepare code, memory, state, storage placeholder const instructions = input.trim().replace(/\t/g, "").split("\n").map(ins => ins.trim()).filter(ins => ins !== ""); const memory = {}, state = { ...originalState }, storage = {}; @@ -20,6 +19,18 @@ async function jelscript(input, originalState = {}, gas, stateDB, block, txInfo, let ptr = 0; + + // Get contract state and storage + const storageDB = new Level(__dirname + "/../../log/accountStore/" + contractInfo.address); + + for (const key of (await storageDB.keys().all())) { + storage[key] = await storageDB.get(key); + } + + const contractState = deserializeState(await stateDB.get(contractInfo.address)); + state[contractInfo.address] = contractState; + + while ( ptr < instructions.length && instructions[ptr].trim() !== "stop" && @@ -217,11 +228,6 @@ async function jelscript(input, originalState = {}, gas, stateDB, block, txInfo, break; case "selfbalance": // Contract's balance - if (!state[contractInfo.address]) { - const contractState = deserializeState(await stateDB.get(contractInfo.address)); - state[contractInfo.address] = contractState; - } - setMem(c, "0x" + BigInt(state[contractInfo.address].balance).toString(16)); break; @@ -250,12 +256,6 @@ async function jelscript(input, originalState = {}, gas, stateDB, block, txInfo, case "send": // Send tokens to address const target = getValue(c).slice(2); const amount = BigInt(getValue(args[1])); - - if (!state[contractInfo.address]) { - const contractState = deserializeState(await stateDB.get(contractInfo.address)); - state[contractInfo.address] = contractState; - } - const balance = state[contractInfo.address].balance; if (BigInt(balance) >= amount) { @@ -338,28 +338,10 @@ async function jelscript(input, originalState = {}, gas, stateDB, block, txInfo, } async function setStorage(key, value) { - if (!state[contractInfo.address]) { - const contractState = deserializeState(await stateDB.get(contractInfo.address)); - state[contractInfo.address] = contractState; - } - - for (const key of (await storageDB.keys().all())) { - storage[key] = await storageDB.get(key); - } - storage[key] = value; } async function getStorage(key) { - if (!state[contractInfo.address]) { - const contractState = deserializeState(await stateDB.get(contractInfo.address)); - state[contractInfo.address] = contractState; - } - - for (const key of (await storageDB.keys().all())) { - storage[key] = await storageDB.get(key); - } - return storage[key] ? storage[key] : "0x0"; } diff --git a/src/core/state.js b/src/core/state.js index 5c37484..aa6d20d 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -4,6 +4,7 @@ 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 Merkle = require("./merkle"); const jelscript = require("./runtime"); const Transaction = require("./transaction"); @@ -69,7 +70,7 @@ async function changeState(newBlock, stateDB, codeDB, enableLogging = false) { / const storageDB = new Level(__dirname + "/../../log/accountStore/" + tx.recipient); const keys = Object.keys(newStorage); - newState[tx.recipient].storageRoot = buildMerkleTree(keys.map(key => key + " " + newStorage[key])).val; + newState[tx.recipient].storageRoot = Merkle.buildTxTrie(keys.map(key => key + " " + newStorage[key]), false).root; for (const key in newStorage) { await storageDB.put(key, newStorage[key]); diff --git a/src/node/server.js b/src/node/server.js index 4258a4f..67a6951 100644 --- a/src/node/server.js +++ b/src/node/server.js @@ -16,9 +16,9 @@ const { addTransaction, clearDepreciatedTxns }= require("../core/txPool"); const rpc = require("../rpc/rpc"); const TYPE = require("./message-types"); const { verifyBlock, updateDifficulty } = require("../consensus/consensus"); -const { parseJSON, indexTxns, numToBuffer, serializeState, deserializeState } = require("../utils/utils"); +const { parseJSON, numToBuffer, serializeState, deserializeState } = require("../utils/utils"); const jelscript = require("../core/runtime"); -const { buildMerkleTree } = require("../core/merkle"); +const Merkle = require("../core/merkle"); const { SyncQueue } = require("./queue"); const opened = []; // Addresses and sockets from connected nodes. @@ -524,7 +524,7 @@ async function mine(publicKey, ENABLE_LOGGING) { block.transactions = transactionsToMine.map(tx => Transaction.serialize(tx)); // Add transactions to block block.hash = Block.getHash(block); // Re-hash with new transactions - block.txRoot = buildMerkleTree(indexTxns(block.transactions)).val; // Re-gen transaction root with new transactions + block.txRoot = Merkle.buildTxTrie(transactionsAsObj).root; // Re-gen transaction root with new transactions // Mine the block. mine(block, chainInfo.difficulty) @@ -568,8 +568,8 @@ async function mine(publicKey, ENABLE_LOGGING) { for (const address in storage) { const storageDB = new Level(__dirname + "/../../log/accountStore/" + address); const keys = Object.keys(storage[address]); - - states[address].storageRoot = buildMerkleTree(keys.map(key => key + " " + storage[address][key])).val; + + states[address].storageRoot = Merkle.buildTxTrie(keys.map(key => key + " " + storage[address][key]), false).root; for (const key of keys) { await storageDB.put(key, storage[address][key]); diff --git a/src/utils/utils.js b/src/utils/utils.js index 86599c2..5e881ff 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -44,10 +44,6 @@ function numToBuffer(value) { return Buffer.from(hexValue.padStart(hexLength, "0"), "hex"); } -function indexTxns(transactions) { - return transactions.map((txn, index) => index.toString() + JSON.stringify(txn)); -} - function serializeState(stateHeader) { let hexState = ""; @@ -78,4 +74,4 @@ function deserializeState(stateInBytes) { return stateHeader; } -module.exports = { log16, isNumber, isHex, parseJSON, bigIntable, indexTxns, numToBuffer, serializeState, deserializeState }; +module.exports = { log16, isNumber, isHex, parseJSON, bigIntable, numToBuffer, serializeState, deserializeState };