diff --git a/js/batchbuilder.js b/js/batchbuilder.js index df3952e5..3d6fd6d1 100644 --- a/js/batchbuilder.js +++ b/js/batchbuilder.js @@ -26,6 +26,7 @@ module.exports = class BatchBuilder { this.feePlan = Array(16).fill([0, bigInt(0)]); this.counters = Array(16).fill(0); this.nCoins = 0; + this.newBatchNumberDb = bigInt(batchNumber); } _addNopTx() { @@ -94,7 +95,7 @@ module.exports = class BatchBuilder { return bigInt(0); } - async _addTx(tx) { + async _addTx(tx, numTx) { const i = this.input.txData.length; const amountF = utils.fix2float(tx.amount || 0); @@ -202,7 +203,6 @@ module.exports = class BatchBuilder { } const newState2 = Object.assign({}, oldState2); newState2.amount = oldState2.amount.add(effectiveAmount); - if (op1=="INSERT") { const newValue = utils.hashState(newState1); @@ -223,31 +223,104 @@ module.exports = class BatchBuilder { this.input.oldKey1[i]= res.isOld0 ? 0 : res.oldKey; this.input.oldValue1[i]= res.isOld0 ? 0 : res.oldValue; + // Database AxAy const keyAxAy = Constants.DB_AxAy.add(this.input.ax[i]).add(this.input.ay[i]); - const keyEthAddr = Constants.DB_EthAddr.add(this.input.ethAddr[i]); - const lastVals = await this.dbState.multiGet([ - keyAxAy, - keyEthAddr + const lastAxAyStates = await this.dbState.get(keyAxAy); + + // get last state and add last batch number + let valStatesAxAy; + let lastAxAyState; + if (!lastAxAyStates) { + lastAxAyState = null; + valStatesAxAy = []; + valStatesAxAy.push(this.newBatchNumberDb); + } + else { + valStatesAxAy = [...lastAxAyStates]; + await this._purgeStates(valStatesAxAy); + lastAxAyState = valStatesAxAy.slice(-1)[0]; + valStatesAxAy.push(this.newBatchNumberDb); + } + if (numTx) lastAxAyState = valStatesAxAy.slice(-1)[0]; + + // get last state + let valOldAxAy = null; + if (lastAxAyState){ + const keyOldAxAyBatch = poseidonHash([keyAxAy, lastAxAyState]); + valOldAxAy = await this.dbState.get(keyOldAxAyBatch); + } + + let newValAxAy; + if (!valOldAxAy) newValAxAy = []; + else newValAxAy = [...valOldAxAy]; + newValAxAy.push(bigInt(tx.fromIdx)); + // new key newValAxAy + const newKeyAxAyBatch = poseidonHash([keyAxAy, this.newBatchNumberDb]); + await this.dbState.multiIns([ + [keyAxAy, valStatesAxAy], + [newKeyAxAyBatch, newValAxAy], ]); - let valAxAy; - if (!lastVals[0]) valAxAy = []; - else valAxAy = [...lastVals[0]]; - valAxAy.push(bigInt(tx.fromIdx)); + // Database Ether address + const keyEth = Constants.DB_EthAddr.add(this.input.ethAddr[i]); + const lastEthStates = await this.dbState.get(keyEth); + + // get last state and add last batch number + let valStatesEth; + let lastEthState; + if (!lastEthStates) { + lastEthState = null; + valStatesEth = []; + valStatesEth.push(this.newBatchNumberDb); + } + else { + valStatesEth = [...lastEthStates]; + this._purgeStates(valStatesEth); + lastEthState = valStatesEth.slice(-1)[0]; + valStatesEth.push(this.newBatchNumberDb); + } + if (numTx) lastEthState = valStatesEth.slice(-1)[0]; + + // get last state + let valOldEth = null; + if (lastEthState){ + const keyOldEthBatch = poseidonHash([keyEth, lastEthState]); + valOldEth = await this.dbState.get(keyOldEthBatch); + } + + let newValEth; + if (!valOldEth) newValEth = []; + else newValEth = [...valOldEth]; + newValEth.push(bigInt(tx.fromIdx)); + + // new key newValEth + const newKeyEthBatch = poseidonHash([keyEth, this.newBatchNumberDb]); + + await this.dbState.multiIns([ + [keyEth, valStatesEth], + [newKeyEthBatch, newValEth], + ]); - let valEthAddr; - if (!lastVals[1]) valEthAddr = []; - else valEthAddr = [...lastVals[1]]; - valEthAddr.push(bigInt(tx.fromIdx)); + // Database Idx + // get array of states saved by batch + const lastIdStates = await this.dbState.get(Constants.DB_Idx.add(bigInt(tx.fromIdx))); + // add last batch number + let valStatesId; + if (!lastIdStates) valStatesId = []; + else valStatesId = [...lastIdStates]; + if (!valStatesId.includes(this.newBatchNumberDb)) valStatesId.push(this.newBatchNumberDb); + // new state for idx const newValueId = poseidonHash([newValue, tx.fromIdx]); + + // new entry according idx and batchNumber + const keyIdBatch = poseidonHash([tx.fromIdx, this.newBatchNumberDb]); + await this.dbState.multiIns([ [newValueId, utils.state2array(newState1)], - [Constants.DB_Idx.add(bigInt(tx.fromIdx)), newValueId], - [keyAxAy, valAxAy], - [keyEthAddr, valEthAddr] + [keyIdBatch, newValueId], + [Constants.DB_Idx.add(bigInt(tx.fromIdx)), valStatesId], ]); - } else if (op1 == "UPDATE") { const newValue = utils.hashState(newState1); @@ -269,12 +342,24 @@ module.exports = class BatchBuilder { this.input.oldKey1[i]= 0x1234; // It should not matter this.input.oldValue1[i]= 0x1234; // It should not matter + // get array of states saved by batch + const lastIdStates = await this.dbState.get(Constants.DB_Idx.add(bigInt(tx.fromIdx))); + // add last batch number + let valStatesId; + if (!lastIdStates) valStatesId = []; + else valStatesId = [...lastIdStates]; + if (!valStatesId.includes(this.newBatchNumberDb)) valStatesId.push(this.newBatchNumberDb); + + // new state for idx const newValueId = poseidonHash([newValue, tx.fromIdx]); - const oldValueId = poseidonHash([resFind1.foundValue, tx.fromIdx]); - await this.dbState.multiDel([oldValueId]); + + // new entry according idx and batchNumber + const keyIdBatch = poseidonHash([tx.fromIdx, this.newBatchNumberDb]); + await this.dbState.multiIns([ [newValueId, utils.state2array(newState1)], - [Constants.DB_Idx.add(bigInt(tx.fromIdx)), newValueId] + [keyIdBatch, newValueId], + [Constants.DB_Idx.add(bigInt(tx.fromIdx)), valStatesId] ]); } @@ -349,12 +434,24 @@ module.exports = class BatchBuilder { this.input.oldKey2[i]= 0x1234; // It should not matter this.input.oldValue2[i]= 0x1234; // It should not matter + // get array of states saved by batch + const lastIdStates = await this.dbState.get(Constants.DB_Idx.add(bigInt(tx.toIdx))); + // add last batch number + let valStatesId; + if (!lastIdStates) valStatesId = []; + else valStatesId = [...lastIdStates]; + if (!valStatesId.includes(this.newBatchNumberDb)) valStatesId.push(this.newBatchNumberDb); + + // new state for idx const newValueId = poseidonHash([newValue, tx.toIdx]); - const oldValueId = poseidonHash([resFind2.foundValue, tx.toIdx]); - await this.dbState.multiDel([oldValueId]); + + // new entry according idx and batchNumber + const keyIdBatch = poseidonHash([tx.toIdx, this.newBatchNumberDb]); + await this.dbState.multiIns([ [newValueId, utils.state2array(newState2)], - [Constants.DB_Idx.add(bigInt(tx.toIdx)), newValueId] + [keyIdBatch, newValueId], + [Constants.DB_Idx.add(bigInt(tx.toIdx)), valStatesId] ]); } } else if (op2=="NOP") { @@ -372,6 +469,59 @@ module.exports = class BatchBuilder { this.input.oldKey2[i]= 0; this.input.oldValue2[i]= 0; } + + // Database numBatch - Idx + const keyNumBatchIdx = Constants.DB_NumBatch_Idx.add(this.newBatchNumberDb); + let lastBatchIdx = await this.dbState.get(keyNumBatchIdx); + + // get last state and add last batch number + let newBatchIdx; + if (!lastBatchIdx) lastBatchIdx = []; + newBatchIdx = [...lastBatchIdx]; + + if (op1 == "INSERT" || op1 == "UPDATE") { + if (!newBatchIdx.includes(tx.fromIdx)) newBatchIdx.push(tx.fromIdx); + } + + if (op2 == "UPDATE" && !isExit) { + if (!newBatchIdx.includes(tx.toIdx)) newBatchIdx.push(tx.toIdx); + } + await this.dbState.multiIns([ + [keyNumBatchIdx, newBatchIdx], + ]); + + // Database NumBatch - AxAy + if (op1 == "INSERT") { + const encodeAxAy = this.input.ay[i].add(this.input.ax[i].shl(256)); + const keyNumBatchAxAy = Constants.DB_NumBatch_AxAy.add(this.newBatchNumberDb); + let oldStatesAxAy = await this.dbState.get(keyNumBatchAxAy); + let newStatesAxAy; + if (!oldStatesAxAy) oldStatesAxAy = []; + newStatesAxAy = [...oldStatesAxAy]; + if (!newStatesAxAy.includes(encodeAxAy)) { + newStatesAxAy.push(encodeAxAy); + await this.dbState.multiIns([ + [keyNumBatchAxAy, newStatesAxAy], + ]); + } + } + } + + async _purgeStates(states){ + if (states.length === 0) return; + if (states.slice(-1)[0].lesser(this.newBatchNumberDb)) return; + let indexFound = null; + for (let i = states.length - 1; i >= 0; i--){ + if (states[i].lesserOrEquals(this.newBatchNumberDb)){ + indexFound = i+1; + if (states[i].equals(this.newBatchNumberDb)) + indexFound = i; + break; + } + } + if (indexFound !== null){ + states.splice(indexFound); + } } _incCounter(coin, step) { @@ -463,13 +613,13 @@ module.exports = class BatchBuilder { if (this.builded) throw new Error("Batch already builded"); for (let i=0; i this.lastBatch) + throw new Error("Cannot rollback to future state"); + + // update Idx database + await this._updateIdx(numBatch); + // update AxAy database + await this._updateAxAy(numBatch); + // update num batch and root + await this.db.multiIns([ + [Constants.DB_Master, numBatch] + ]); + const roots = await this.db.get(Constants.DB_Batch.add(bigInt(numBatch))); + this.lastBatch = numBatch; + if (numBatch === 0) + this.stateRoot = bigInt(0); + else + this.stateRoot = roots[0]; + } + async getStateByIdx(idx) { const key = Constants.DB_Idx.add(bigInt(idx)); - const valueState = await this.db.get(key); - if (!valueState) return null; - const stateArray = await this.db.get(valueState); + const valStates = await this.db.get(key); + if (!valStates) return null; + // get last state + const lastState = this._findLastState(valStates); + if (!lastState) return null; + // last state key + const keyLastState = poseidonHash([idx, lastState]); + const keyValueState = await this.db.get(keyLastState); + if (!keyValueState) return null; + const stateArray = await this.db.get(keyValueState); if (!stateArray) return null; const st = utils.array2state(stateArray); st.idx = Number(idx); @@ -55,27 +82,39 @@ class RollupDB { async getStateByAxAy(ax, ay) { const keyAxAy = Constants.DB_AxAy.add(bigInt("0x" + ax)).add(bigInt("0x" + ay)); + const valStates = await this.db.get(keyAxAy); + if (!valStates) return null; + // get last state + const lastState = this._findLastState(valStates); + if (!lastState) return null; + // last state key + const keyLastState = poseidonHash([keyAxAy, lastState]); - const idxs = await this.db.get(keyAxAy); + const idxs = await this.db.get(keyLastState); if (!idxs) return null; const promises = []; for (let i=0; i= 0; i--){ + if (valueStates[i].lesserOrEquals(lastBatch)) + return valueStates[i]; + } + return null; + } + getLastBatchId(){ return this.lastBatch; } + + getRoot(){ + return this.stateRoot; + } + + async _updateIdx(numBatch) { + // update idx states + const alreadyUpdated = []; + for (let i = this.lastBatch; i > numBatch; i--){ + const keyNumBatchIdx = Constants.DB_NumBatch_Idx.add(bigInt(i)); + const idxToUpdate = await this.db.get(keyNumBatchIdx); + if (!idxToUpdate) continue; + for (const idx of idxToUpdate) { + if (!alreadyUpdated.includes(idx)){ + const keyIdx = Constants.DB_Idx.add(bigInt(idx)); + const states = await this.db.get(keyIdx); + this._purgeStates(states, numBatch); + await this.db.multiIns([ + [keyIdx, states], + ]); + alreadyUpdated.push(idx); + } + } + } + + // reset num batch - idx for future states + const keysToDel = []; + for (let i = this.lastBatch; i > numBatch; i--){ + const keyNumBatchIdx = Constants.DB_NumBatch_Idx.add(bigInt(i)); + keysToDel.push(keyNumBatchIdx); + } + await this.db.multiDel(keysToDel); + } + + async _updateAxAy(numBatch) { + // update idx states + const alreadyUpdated = []; + for (let i = this.lastBatch; i > numBatch; i--){ + const keyNumBatchAxAy = Constants.DB_NumBatch_AxAy.add(bigInt(i)); + const axAyToUpdate = await this.db.get(keyNumBatchAxAy); + if (!axAyToUpdate) continue; + for (const encodedAxAy of axAyToUpdate) { + if (!alreadyUpdated.includes(encodedAxAy)){ + const ax = encodedAxAy.shr(256); + const ay = encodedAxAy.and(bigInt(1).shl(256).sub(bigInt(1))); + const keyAxAy = Constants.DB_AxAy.add(ax).add(ay); + const states = await this.db.get(keyAxAy); + this._purgeStates(states, numBatch); + await this.db.multiIns([ + [keyAxAy, states], + ]); + alreadyUpdated.push(encodedAxAy); + } + } + } + + // reset num batch - AxAy for future states + const keysToDel = []; + for (let i = this.lastBatch; i > numBatch; i--){ + const keyNumBatchAxAy = Constants.DB_NumBatch_AxAy.add(bigInt(i)); + keysToDel.push(keyNumBatchAxAy); + } + await this.db.multiDel(keysToDel); + } + + async _purgeStates(states, numBatch){ + let indexFound = null; + for (let i = states.length - 1; i >= 0; i--){ + if (states[i].lesserOrEquals(numBatch)){ + indexFound = i+1; + break; + } + } + if (indexFound !== null){ + states.splice(indexFound); + } + } + + async test(numBatch) { + const keyNumBatchAxAy = Constants.DB_NumBatch_AxAy.add(bigInt(numBatch)); + return await this.db.get(keyNumBatchAxAy); + } } module.exports = async function(db) { @@ -110,9 +240,9 @@ module.exports = async function(db) { if (!master) { return new RollupDB(db, 0, bigInt(0)); } - const batch = await db.get(Constants.DB_Batch.add(bigInt(master))); - if (!batch) { + const roots = await db.get(Constants.DB_Batch.add(bigInt(master))); + if (!roots) { throw new Error("Database corrupted"); } - return new RollupDB(db, master, batch[0]); + return new RollupDB(db, master, roots[0]); }; diff --git a/rollup-operator/src/server/operator.js b/rollup-operator/src/server/operator.js index 2b15a451..c5befee6 100644 --- a/rollup-operator/src/server/operator.js +++ b/rollup-operator/src/server/operator.js @@ -194,7 +194,7 @@ let pool; synchConfig.rollup.abi, synchConfig.rollupPoS.address, synchConfig.rollupPoS.abi, - synchConfig.creationHash, + synchConfig.rollup.creationHash, synchConfig.ethAddress, loggerLevel, operatorMode, diff --git a/rollup-operator/src/synch.js b/rollup-operator/src/synch.js index 4ae3ef60..b69faf5d 100644 --- a/rollup-operator/src/synch.js +++ b/rollup-operator/src/synch.js @@ -14,6 +14,8 @@ const Constants = require("./constants"); const bytesOffChainTx = 3*2 + 2; // db keys +const batchStateKey = "batch-state"; + const lastBlockKey = "last-block-synch"; const lastBatchKey = "last-state-batch"; const exitInfoKey = "exit"; @@ -138,25 +140,53 @@ class Synchronizer { // eslint-disable-next-line no-constant-condition while (true) { try { - // get last block synched and current blockchain block + // get last block synched, current block, last batch synched let totalSynch = 0; - let lastSynchBlock = await this.getLastSynchBlock(); + let lastBatchSaved = await this.getLastBatch(); const currentBlock = await this.web3.eth.getBlockNumber(); - if (lastSynchBlock < this.creationBlock) { - await this.db.insert(lastBlockKey, this._toString(lastSynchBlock)); - lastSynchBlock = this.creationBlock; + // get last state saved + const stateSaved = await this.getStateFromBatch(lastBatchSaved); + + // check last batch number matches. Last state saved should match state in contract. + const stateDepth = parseInt(await this.rollupContract.methods.getStateDepth() + .call({from: this.ethAddress}, stateSaved.blockNumber)); + + // console.log("last batch saved: ", lastBatchSaved); + // console.log("state depth - 1: ", stateDepth - 1); + // console.log("stateSaved:", stateSaved); + + if (stateSaved.root && (stateDepth - 1) !== lastBatchSaved){ + console.log("+++++++++++ROLL BACK 1"); + await this._rollback(lastBatchSaved); + continue; + } + + // Check root matches with the one saved + const stateRoot = bigInt(await this.rollupContract.methods.getStateRoot(stateDepth) + .call({ from: this.ethAddress }, stateSaved.blockNumber)); + + const stateRootHex = `0x${bigInt(stateRoot).toString(16)}`; + + if (stateSaved.root && (stateRootHex !== stateSaved.root)) { + console.log("+++++++++++ROLL BACK 2"); + await this._rollback(lastBatchSaved); + continue; } const currentBatchDepth = await this.rollupContract.methods.getStateDepth() .call({from: this.ethAddress}, currentBlock); - let lastBatchSaved = await this.getLastBatch(); if (currentBatchDepth - 1 > lastBatchSaved) { - const targetBlockNumber = await this._getTargetBlock(lastBatchSaved, lastSynchBlock); + const targetBlockNumber = await this._getTargetBlock(lastBatchSaved, stateSaved.blockNumber); + // If no event is found, no tree is updated + if (!targetBlockNumber) { + console.log("Undefined targetBlockNumber"); + continue; + } // get all logs from last batch const logs = await this.rollupContract.getPastEvents("allEvents", { - fromBlock: lastSynchBlock + 1, + fromBlock: stateSaved.blockNumber + 1, toBlock: targetBlockNumber, }); // update events @@ -167,9 +197,9 @@ class Synchronizer { totalSynch = (currentBatchDepth == 0) ? 100 : (((lastBatchSaved + 1) / currentBatchDepth) * 100); this.totalSynch = totalSynch.toFixed(2); - this._fillInfo(currentBlock, lastSynchBlock, currentBatchDepth, lastBatchSaved); + this._fillInfo(currentBlock, stateSaved.blockNumber, currentBatchDepth, lastBatchSaved); - if (totalSynch === 100) await timeout(this.timeouts.NEXT_LOOP); + if (lastBatchSaved >= currentBatchDepth - 1) await timeout(this.timeouts.NEXT_LOOP); } catch (e) { this.logger.error(`SYNCH Message error: ${e.message}`); this.logger.debug(`SYNCH Message error: ${e.stack}`); @@ -188,30 +218,37 @@ class Synchronizer { } async _getTargetBlock(lastBatchSaved, lastSynchBlock){ - // Check if next target block number is in cache memory - let targetBlockNumber = this.forgeEventsCache.get(lastBatchSaved + 1); - if (!targetBlockNumber){ - // read events to get block number for each batch forged - const logsForge = await this.rollupContract.getPastEvents("ForgeBatch", { - fromBlock: lastSynchBlock + 1, - toBlock: "latest", - }); - for (const log of logsForge) { - const key = Number(log.returnValues.batchNumber); - const value = Number(log.returnValues.blockNumber); - this.forgeEventsCache.set(key, value); + // read events to get block number for each batch forged + let targetBlockNumber = undefined; + const logsForge = await this.rollupContract.getPastEvents("ForgeBatch", { + fromBlock: lastSynchBlock + 1, + toBlock: "latest", + }); + + for (const log of logsForge){ + const batchNumber = Number(log.returnValues.batchNumber); + if (batchNumber === lastBatchSaved + 1){ + targetBlockNumber = Number(log.returnValues.blockNumber); + break; } } - // purge cache memory - const lastEventPurged = this.forgeEventsCache.get(lastPurgedKey); - for (let i = lastBatchSaved; i > lastEventPurged; i--) { - this.forgeEventsCache.delete(i); - } - this.forgeEventsCache.set(lastPurgedKey, lastBatchSaved); - // return block number according batch forged - return this.forgeEventsCache.get(lastBatchSaved + 1); + return targetBlockNumber; + } + + async _rollback(batchNumber) { + const rollbackBatch = batchNumber - 1; + const state = this.getStateFromBatch(rollbackBatch); + if (state) { + await this.treeDb.rollbackToBatch(batchNumber); + await this.db.insert(lastBatchKey, this._toString(rollbackBatch)); + } else + throw new Error("can not rollback to a non-existent state"); } + async getStateFromBatch(numBatch) { + const key = `${batchStateKey}${separator}${numBatch}`; + return this._fromString(await this.db.getOrDefault(key, this._toString({root: false, blockNumber: this.creationBlock}))); + } async getLastSynchBlock() { return this._fromString(await this.db.getOrDefault(lastBlockKey, this.creationBlock.toString())); @@ -223,25 +260,19 @@ class Synchronizer { async _updateEvents(logs, nextBatchSynched, blockNumber){ // save events on database - let index = 0; + await this._saveEvents(logs, nextBatchSynched); + + const tmpForgeArray = await this.db.getOrDefault(`${eventForgeBatchKey}${separator}${nextBatchSynched}`); + const tmpOnChainArray = await this.db.getOrDefault(`${eventOnChainKey}${separator}${nextBatchSynched-1}`); + + let eventOnChain = []; + let eventForge = []; + if (tmpForgeArray) + eventForge = this._fromString(tmpForgeArray); + if (tmpOnChainArray) + eventOnChain = this._fromString(tmpOnChainArray); - for (const event of logs){ - await this._saveEvents(event, index); - index += 1; - } // Update rollupTree and last batch synched - const eventForge = []; - const eventOnChain = []; - // add off-chain tx - const keysForge = await this.db.listKeys(`${eventForgeBatchKey}${separator}${nextBatchSynched}`); - for (const key of keysForge) { - eventForge.push(this._fromString(await this.db.get(key))); - } - // add on-chain tx - const keysOnChain = await this.db.listKeys(`${eventOnChainKey}${separator}${nextBatchSynched-1}`); - for (const key of keysOnChain) { - eventOnChain.push(this._fromString(await this.db.get(key))); - } // Add events to rollup-tree if ((eventForge.length > 0) || (eventOnChain.length > 0)) await this._updateTree(eventForge, eventOnChain); @@ -249,6 +280,11 @@ class Synchronizer { if (this.mode !== Constants.mode.archive) await this._purgeEvents(nextBatchSynched); + const root = `0x${this.treeDb.getRoot().toString(16)}`; + // console.log("KEY ADDED: ", `${batchStateKey}${separator}${nextBatchSynched}`); + // console.log("VALUE ROOT: ", root); + // console.log("VALUE BLOCK: ", blockNumber); + await this.db.insert(`${batchStateKey}${separator}${nextBatchSynched}`, this._toString({root, blockNumber})); await this.db.insert(lastBlockKey, this._toString(blockNumber)); await this.db.insert(lastBatchKey, this._toString(nextBatchSynched)); } @@ -258,33 +294,32 @@ class Synchronizer { const lastEventPurged = Number(await this.db.getOrDefault(lastPurgedEventKey, "-1")); // purge off-chain for (let i = batchKey; i > lastEventPurged; i--) { - const keysForge = await this.db.listKeys(`${eventForgeBatchKey}${separator}${i}`); - for (const key of keysForge) { - await this.db.delete(key); - } + await this.db.delete(`${eventForgeBatchKey}${separator}${i}`); + } // purge on-chain for (let i = (batchKey - 1); i > (lastEventPurged - 1); i--) { - const keysOnChain = await this.db.listKeys(`${eventOnChainKey}${separator}${i}`); - for (const key of keysOnChain) { - await this.db.delete(key); - } + await this.db.delete(`${eventOnChainKey}${separator}${i}`); } // update last batch purged await this.db.insert(lastPurgedEventKey, this._toString(batchKey)); } - async _saveEvents(event, index) { - if (event.event == "OnChainTx") { - const eventOnChainData = this._getOnChainEventData(event.returnValues); - const batchNumber = eventOnChainData.batchNumber; - await this.db.insert(`${eventOnChainKey}${separator}${batchNumber}${separator}${index}`, - this._toString(eventOnChainData)); - } else if (event.event == "ForgeBatch") { - const batchNumber = event.returnValues.batchNumber; - await this.db.insert(`${eventForgeBatchKey}${separator}${batchNumber}${separator}${index}`, - this._toString(event.transactionHash)); + async _saveEvents(logs, numBatch) { + const eventsOnChain = []; + const eventsForge = []; + for (const event of logs){ + if (event.event == "OnChainTx") { + const eventOnChainData = this._getOnChainEventData(event.returnValues); + eventsOnChain.push(eventOnChainData); + } else if (event.event == "ForgeBatch") { + eventsForge.push(event.transactionHash); + } } + await this.db.insert(`${eventOnChainKey}${separator}${numBatch}`, + this._toString(eventsOnChain)); + await this.db.insert(`${eventForgeBatchKey}${separator}${numBatch}`, + this._toString(eventsForge)); } _getOnChainEventData(onChainData) { @@ -387,6 +422,8 @@ class Synchronizer { const fromBlock = txForge.blockNumber - this.blocksPerSlot; const toBlock = txForge.blockNumber; + // console.log("update tree from: ", fromBlock); + // console.log("update tree to: ", toBlock); const logs = await this.rollupPoSContract.getPastEvents("dataCommitted", { fromBlock: fromBlock, // previous slot toBlock: toBlock, // current slot @@ -398,6 +435,7 @@ class Synchronizer { break; } } + // console.log("getting data committed..."); const txDataCommitted = await this.web3.eth.getTransaction(txHash); const decodedData2 = abiDecoder.decodeMethod(txDataCommitted.input); let compressedTx; @@ -406,7 +444,7 @@ class Synchronizer { compressedTx = elem.value; } }); - + // console.log("finish getting data commited"); const headerBytes = Math.ceil(this.maxTx/8); const txs = []; const buffCompressedTxs = Buffer.from(compressedTx.slice(2), "hex"); @@ -439,8 +477,6 @@ class Synchronizer { async _setUserFee(bb, txs){ for (const tx of txs) { const stateId = await this.getStateById(tx.fromIdx); - // const userFee = await bb.getOperatorFee(stateId.coin, tx.step); - // tx.userFee = Number(userFee); tx.coin = Number(stateId.coin); } } @@ -471,9 +507,13 @@ class Synchronizer { const currentBlock = await this.web3.eth.getBlockNumber(); const currentBatchDepth = await this.rollupContract.methods.getStateDepth().call({from: this.ethAddress}, currentBlock); // add on-chain txs - const keysOnChain = await this.db.listKeys(`${eventOnChainKey}${separator}${currentBatchDepth-1}`); - for (const key of keysOnChain) { - bb.addTx(await this._getTxOnChain(this._fromString(await this.db.get(key)))); + let eventsOnChain = []; + const tmpEventsOnChain = await this.db.getOrDefault(`${eventOnChainKey}${separator}${currentBatchDepth-1}`); + if (tmpEventsOnChain) + eventsOnChain = this._fromString(tmpEventsOnChain); + + for (const event of eventsOnChain) { + bb.addTx(await this._getTxOnChain(event)); } return bb; } @@ -483,10 +523,13 @@ class Synchronizer { // add off-chain tx if (this.mode === Constants.mode.archive){ const bb = await this.treeDb.buildBatch(this.maxTx, this.nLevels); - const keysForge = await this.db.listKeys(`${eventForgeBatchKey}${separator}${numBatch}`); - for (const key of keysForge) { - const tmp = this._fromString(await this.db.get(key)); - const offChainTxs = await this._getTxOffChain(tmp); + const tmpForgeArray = await this.db.getOrDefault(`${eventForgeBatchKey}${separator}${numBatch}`); + let eventForge = []; + if (tmpForgeArray) + eventForge = this._fromString(tmpForgeArray); + + for (const hashTx of eventForge) { + const offChainTxs = await this._getTxOffChain(hashTx); await this._addFeePlan(bb, offChainTxs.inputFeePlanCoin, offChainTxs.inputFeePlanFee); await this._setUserFee(bb, offChainTxs.txs); for (const tx of offChainTxs.txs) res.push(tx); diff --git a/rollup-operator/test/config/config.env-example b/rollup-operator/test/config/config.env-example index 7178e132..2152476e 100644 --- a/rollup-operator/test/config/config.env-example +++ b/rollup-operator/test/config/config.env-example @@ -2,7 +2,7 @@ WALLET_PATH = ${HOME}/rollup/rollup-operator/test/config/wallet-test.json CONFIG_SYNCH = ${HOME}/rollup/rollup-operator/test/config/synch-config-test.json CONFIG_POOL = ${HOME}/rollup/rollup-operator/test/config/pool-config-test.json EXPOSE_API_SERVER = true -OPERATOR_PORT_EXTERNAL = 9001 +OPERATOR_PORT_EXTERNAL = 9000 URL_SERVER_PROOF = http://127.0.0.1:10001 LOG_LEVEL = info OPERATOR_MODE = archive diff --git a/rollup-operator/test/server/operator-server.test.js b/rollup-operator/test/server/operator-server.test.js index 53716a10..1e25e24c 100644 --- a/rollup-operator/test/server/operator-server.test.js +++ b/rollup-operator/test/server/operator-server.test.js @@ -29,8 +29,7 @@ const timeoutLoop = 10000; // This test assumes 'server-proof' is running locally on port 10001 -// This test assumes 'operator' api-admin is running locally on port 9000 -// This test assumes 'operator' api-external is running locally on port 9001 +// This test assumes 'operator' api-external is running locally on port 9000 contract("Operator", (accounts) => { @@ -81,7 +80,7 @@ contract("Operator", (accounts) => { let cliExternalOp; // Url - const urlExternalOp = "http://127.0.0.1:9001"; + const urlExternalOp = "http://127.0.0.1:9000"; // Constants to move to a specific era const slotPerEra = 20; diff --git a/rollup-operator/test/synch-light.test.js b/rollup-operator/test/synch-light.test.js index 7aabfce1..b19b22be 100644 --- a/rollup-operator/test/synch-light.test.js +++ b/rollup-operator/test/synch-light.test.js @@ -301,7 +301,7 @@ contract("Synchronizer - light mode", (accounts) => { expect(resEthAddress3[0].ay).to.be.equal(ayStr); }); - it("Should add off-chain withdraw tx and synch", async () => { + it("Should add off-chain tx and synch", async () => { const events = []; const tx = { fromIdx: 1, diff --git a/simple-webapp/src/utils/config-example.json b/simple-webapp/src/utils/config-example.json index 141a95fb..401285e3 100644 --- a/simple-webapp/src/utils/config-example.json +++ b/simple-webapp/src/utils/config-example.json @@ -1,5 +1,5 @@ { - "operator": "http://127.0.0.1:9001", + "operator": "http://127.0.0.1:9000", "tokensAddress": "0xaFF4481D10270F50f203E0763e2597776068CBc5", "address": "0x9A1D1FaD8F2Db1f608D03Da3BF482059e59c0890", "nodeEth": "ropsten.infura.io/v3/39753c9df15e15ee844e2c19a928bbc1", diff --git a/test/rollup-utils/rollupDB.test.js b/test/rollup-utils/rollupDB.test.js index f9d5bf0e..c485a2f9 100644 --- a/test/rollup-utils/rollupDB.test.js +++ b/test/rollup-utils/rollupDB.test.js @@ -48,7 +48,6 @@ async function checkDb(memDb, toCheckDb){ } } - describe("RollupDb", async function () { let rollupMemDb; let rollupLevelDb; @@ -75,3 +74,347 @@ describe("RollupDb", async function () { await exec(`rm -rf ${pathDb}`); }); }); + +describe("RollupDb - rollback functionality", async function () { + let rollupDb; + + const account1 = new RollupAccount(1); + const account2 = new RollupAccount(2); + const account3 = new RollupAccount(3); + + it("should initialize with memory database", async () => { + const db = new SMTMemDB(); + rollupDb = await RollupDB(db); + }); + + it("should add one deposit", async () => { + const bb = await rollupDb.buildBatch(4, 8); + + bb.addTx({ fromIdx: 1, loadAmount: 10, coin: 0, ax: account1.ax, ay: account1.ay, + ethAddress: account1.ethAddress, onChain: true }); + + await bb.build(); + await rollupDb.consolidate(bb); + }); + + it("should forge empty batch", async () => { + const bb = await rollupDb.buildBatch(4, 8); + await bb.build(); + await rollupDb.consolidate(bb); + }); + + it("should add one deposit", async () => { + const bb = await rollupDb.buildBatch(4, 8); + + bb.addTx({ fromIdx: 2, loadAmount: 10, coin: 0, ax: account1.ax, ay: account1.ay, + ethAddress: account2.ethAddress, onChain: true }); + + await bb.build(); + await rollupDb.consolidate(bb); + }); + + it("Check rollup database", async () => { + // get info by Id + const resId = await rollupDb.getStateByIdx(1); + // check leaf info matches deposit + expect(resId.ax).to.be.equal(account1.ax); + expect(resId.ay).to.be.equal(account1.ay); + expect(resId.ethAddress).to.be.equal(account1.ethAddress.toString().toLowerCase()); + + // get leafs info by AxAy + const resAxAy = await rollupDb.getStateByAxAy(account1.ax, account1.ay); + // check leaf info matches deposits + expect(resAxAy.length).to.be.equal(2); // 2 deposits with equal Ax, Ay + expect(resAxAy[0].ethAddress).to.be.equal(account1.ethAddress.toString().toLowerCase()); + expect(resAxAy[1].ethAddress).to.be.equal(account2.ethAddress.toString().toLowerCase()); + + // get leaf info by ethAddress + const resEthAddress = await rollupDb.getStateByEthAddr(account1.ethAddress.toString()); + // check leaf info matches deposit + expect(resEthAddress[0].ax).to.be.equal(account1.ax); + expect(resEthAddress[0].ay).to.be.equal(account1.ay); + + // get leaf info by ethAddress + const resEthAddress2 = await rollupDb.getStateByEthAddr(account2.ethAddress.toString()); + // check leaf info matches deposit + expect(resEthAddress2[0].ax).to.be.equal(account1.ax); + expect(resEthAddress2[0].ay).to.be.equal(account1.ay); + }); + + it("should rollback last off-chain transaction", async () => { + // old states + const oldNumBatch = rollupDb.lastBatch; + const oldStateId1 = await rollupDb.getStateByIdx(1); + const oldStateId2 = await rollupDb.getStateByIdx(2); + + // add off-chain transaction + const tx = { + fromIdx: 1, + toIdx: 2, + amount: 3, + }; + const bb = await rollupDb.buildBatch(4, 8); + bb.addTx(tx); + await bb.build(); + await rollupDb.consolidate(bb); + + // check current state database + const stateId1 = await rollupDb.getStateByIdx(1); + expect(stateId1.amount.toJSNumber()).to.be.equal(oldStateId1.amount.toJSNumber() - tx.amount); + const stateId2 = await rollupDb.getStateByIdx(2); + expect(stateId2.amount.toJSNumber()).to.be.equal(oldStateId2.amount.toJSNumber() + tx.amount); + + // rollback database + await rollupDb.rollbackToBatch(oldNumBatch); + + // check states + const newStateId1 = await rollupDb.getStateByIdx(1); + const newStateId2 = await rollupDb.getStateByIdx(2); + expect(lodash.isEqual(newStateId1, oldStateId1)).to.be.equal(true); + expect(lodash.isEqual(newStateId2, oldStateId2)).to.be.equal(true); + }); + + it("should rollback last deposit on-chain transaction", async () => { + // old states + const oldNumBatch = rollupDb.lastBatch; + const oldStateId1 = await rollupDb.getStateByIdx(1); + const oldStateId2 = await rollupDb.getStateByIdx(2); + const oldStateId3 = await rollupDb.getStateByIdx(3); + const oldStatesAxAy = await rollupDb.getStateByAxAy(account1.ax, account1.ay); + const oldStateEthAdd1 = await rollupDb.getStateByEthAddr(account1.ethAddress.toString()); + const oldStateEthAdd2 = await rollupDb.getStateByEthAddr(account2.ethAddress.toString()); + const oldStateEthAdd3 = await rollupDb.getStateByEthAddr(account3.ethAddress.toString()); + + // add deposit on-chain transaction + const bb = await rollupDb.buildBatch(4, 8); + const tx = { + fromIdx: 3, + loadAmount: 10, + coin: 0, + ax: account1.ax, + ay: account1.ay, + ethAddress: account3.ethAddress, + onChain: true, + }; + bb.addTx(tx); + await bb.build(); + await rollupDb.consolidate(bb); + + // check current state database + const stateId3 = await rollupDb.getStateByIdx(3); + expect(stateId3.amount.toJSNumber()).to.be.equal(tx.loadAmount); + + const stateAxAy = await rollupDb.getStateByAxAy(account1.ax, account1.ay); + expect(stateAxAy.length).to.be.equal(3); + + const stateEthAdd3 = await rollupDb.getStateByEthAddr(account3.ethAddress.toString()); + expect(stateEthAdd3.length).to.be.equal(1); + // rollback database + await rollupDb.rollbackToBatch(oldNumBatch); + + // check states + const newStateId1 = await rollupDb.getStateByIdx(1); + const newStateId2 = await rollupDb.getStateByIdx(2); + const newStateId3 = await rollupDb.getStateByIdx(3); + const newStatesAxAy = await rollupDb.getStateByAxAy(account1.ax, account1.ay); + const newStateEthAdd1 = await rollupDb.getStateByEthAddr(account1.ethAddress.toString()); + const newStateEthAdd2 = await rollupDb.getStateByEthAddr(account2.ethAddress.toString()); + const newStateEthAdd3 = await rollupDb.getStateByEthAddr(account3.ethAddress.toString()); + + expect(lodash.isEqual(newStateId1, oldStateId1)).to.be.equal(true); + expect(lodash.isEqual(newStateId2, oldStateId2)).to.be.equal(true); + expect(lodash.isEqual(newStateId3, oldStateId3)).to.be.equal(true); + expect(lodash.isEqual(newStatesAxAy, oldStatesAxAy)).to.be.equal(true); + expect(lodash.isEqual(newStateEthAdd1, oldStateEthAdd1)).to.be.equal(true); + expect(lodash.isEqual(newStateEthAdd2, oldStateEthAdd2)).to.be.equal(true); + expect(lodash.isEqual(newStateEthAdd3, oldStateEthAdd3)).to.be.equal(true); + }); + + it("should rollback two batches", async () => { + // old states + const oldNumBatch = rollupDb.lastBatch; + const oldStateId1 = await rollupDb.getStateByIdx(1); + const oldStateId2 = await rollupDb.getStateByIdx(2); + const oldStateId3 = await rollupDb.getStateByIdx(3); + const oldStatesAxAy = await rollupDb.getStateByAxAy(account1.ax, account1.ay); + const oldStateEthAdd1 = await rollupDb.getStateByEthAddr(account1.ethAddress.toString()); + const oldStateEthAdd2 = await rollupDb.getStateByEthAddr(account2.ethAddress.toString()); + const oldStateEthAdd3 = await rollupDb.getStateByEthAddr(account3.ethAddress.toString()); + // add deposit on-chain transaction + const bb = await rollupDb.buildBatch(4, 8); + const tx = { + fromIdx: 3, + loadAmount: 10, + coin: 0, + ax: account1.ax, + ay: account1.ay, + ethAddress: account3.ethAddress, + onChain: true, + }; + bb.addTx(tx); + await bb.build(); + await rollupDb.consolidate(bb); + + // check current state database + const stateId3 = await rollupDb.getStateByIdx(3); + expect(stateId3.amount.toJSNumber()).to.be.equal(tx.loadAmount); + + const stateAxAy = await rollupDb.getStateByAxAy(account1.ax, account1.ay); + expect(stateAxAy.length).to.be.equal(3); + const stateEthAdd3 = await rollupDb.getStateByEthAddr(account3.ethAddress.toString()); + expect(stateEthAdd3.length).to.be.equal(1); + + // add off-chain transaction + const tx2 = { + fromIdx: 1, + toIdx: 2, + amount: 3, + }; + const bb2 = await rollupDb.buildBatch(4, 8); + bb2.addTx(tx2); + await bb2.build(); + await rollupDb.consolidate(bb2); + + // check current state database + const stateId1 = await rollupDb.getStateByIdx(1); + expect(stateId1.amount.toJSNumber()).to.be.equal(oldStateId1.amount.toJSNumber() - tx2.amount); + const stateId2 = await rollupDb.getStateByIdx(2); + expect(stateId2.amount.toJSNumber()).to.be.equal(oldStateId2.amount.toJSNumber() + tx2.amount); + + // rollback database + await rollupDb.rollbackToBatch(oldNumBatch); + + // check states + const newStateId1 = await rollupDb.getStateByIdx(1); + const newStateId2 = await rollupDb.getStateByIdx(2); + const newStateId3 = await rollupDb.getStateByIdx(3); + const newStatesAxAy = await rollupDb.getStateByAxAy(account1.ax, account1.ay); + const newStateEthAdd1 = await rollupDb.getStateByEthAddr(account1.ethAddress.toString()); + const newStateEthAdd2 = await rollupDb.getStateByEthAddr(account2.ethAddress.toString()); + const newStateEthAdd3 = await rollupDb.getStateByEthAddr(account3.ethAddress.toString()); + + expect(lodash.isEqual(newStateId1, oldStateId1)).to.be.equal(true); + expect(lodash.isEqual(newStateId2, oldStateId2)).to.be.equal(true); + expect(lodash.isEqual(newStateId3, oldStateId3)).to.be.equal(true); + expect(lodash.isEqual(newStatesAxAy, oldStatesAxAy)).to.be.equal(true); + expect(lodash.isEqual(newStateEthAdd1, oldStateEthAdd1)).to.be.equal(true); + expect(lodash.isEqual(newStateEthAdd2, oldStateEthAdd2)).to.be.equal(true); + expect(lodash.isEqual(newStateEthAdd3, oldStateEthAdd3)).to.be.equal(true); + }); + + it("should rollback to genesis state", async () => { + const futureState = rollupDb.lastBatch + 1; + try { + await rollupDb.rollbackToBatch(futureState); + expect(true).to.be.equal(false); + } catch(error){ + const flagError = error.message.includes("Cannot rollback to future state"); + expect(flagError).to.be.equal(true); + } + }); + + it("should rollback to genesis state", async () => { + const genesisBatch = 0; + await rollupDb.rollbackToBatch(genesisBatch); + + // check states + const newStateId1 = await rollupDb.getStateByIdx(1); + const newStateId2 = await rollupDb.getStateByIdx(2); + const newStateId3 = await rollupDb.getStateByIdx(3); + const newStatesAxAy = await rollupDb.getStateByAxAy(account1.ax, account1.ay); + const newStateEthAdd1 = await rollupDb.getStateByEthAddr(account1.ethAddress.toString()); + const newStateEthAdd2 = await rollupDb.getStateByEthAddr(account2.ethAddress.toString()); + const newStateEthAdd3 = await rollupDb.getStateByEthAddr(account3.ethAddress.toString()); + + expect(newStateId1).to.be.equal(null); + expect(newStateId2).to.be.equal(null); + expect(newStateId3).to.be.equal(null); + expect(newStatesAxAy).to.be.equal(null); + expect(newStateEthAdd1).to.be.equal(null); + expect(newStateEthAdd2).to.be.equal(null); + expect(newStateEthAdd3).to.be.equal(null); + }); + + it("should start new rollupdb state", async () => { + const db = new SMTMemDB(); + rollupDb = await RollupDB(db); + }); + + it("should add three deposit", async () => { + const bb = await rollupDb.buildBatch(4, 8); + + bb.addTx({ fromIdx: 1, loadAmount: 10, coin: 0, ax: account1.ax, ay: account1.ay, + ethAddress: account2.ethAddress, onChain: true }); + bb.addTx({ fromIdx: 2, loadAmount: 10, coin: 0, ax: account1.ax, ay: account1.ay, + ethAddress: account2.ethAddress, onChain: true }); + bb.addTx({ fromIdx: 3, loadAmount: 10, coin: 0, ax: account1.ax, ay: account1.ay, + ethAddress: account2.ethAddress, onChain: true }); + + await bb.build(); + await rollupDb.consolidate(bb); + }); + + it("should add three deposit", async () => { + const lastBatch = 6; + const numBatchToForge = 4; + // move forward 'numBatchToForge' batch + for (let i = 0; i { + const initialAmount =10; + const oldStateId1 = await rollupDb.getStateByIdx(1); + const oldStateId2 = await rollupDb.getStateByIdx(2); + const oldStateId3 = await rollupDb.getStateByIdx(3); + + // rollback database + await rollupDb.rollbackToBatch(5); + + const newStateId1 = await rollupDb.getStateByIdx(1); + const newStateId2 = await rollupDb.getStateByIdx(2); + const newStateId3 = await rollupDb.getStateByIdx(3); + + expect(newStateId1.amount.toString()).to.be.equal(initialAmount.toString()); + expect(newStateId2.amount.toString()).to.be.equal(initialAmount.toString()); + expect(newStateId3.amount.toString()).to.be.equal(initialAmount.toString()); + + // add off-chain transaction + const amountToWithdraw = 1; + const tx = { + fromIdx: 1, + toIdx: 0, + coin: 0, + amount: amountToWithdraw, + }; + const bb = await rollupDb.buildBatch(4, 8); + bb.addTx(tx); + await bb.build(); + await rollupDb.consolidate(bb); + + const finalStateId1 = await rollupDb.getStateByIdx(1); + const finalStateId2 = await rollupDb.getStateByIdx(2); + const finalStateId3 = await rollupDb.getStateByIdx(3); + + // expect(finalStateId1.amount.toString()).to.be.equal((initialAmount - amountToWithdraw).toString()); + // expect(finalStateId2.amount.toString()).to.be.equal(initialAmount.toString()); + // expect(finalStateId3.amount.toString()).to.be.equal(initialAmount.toString()); + }); +}); \ No newline at end of file