Source: lib/RootChain.js

'use strict';

const Web3 = require("web3");

// const config = require("./config");
const utils = require("./utils");
const Block = require("./Block");
const Transaction = require("./Transaction");

/**
 * RootChain interacting with the Ethereum network
 */
class RootChain {
  /**
   * Create a RootChain to interact with Ethereum root chain
   * @param {Object} config configuration object
   */
  constructor(config, db) {
    if (!config.artifacts) {
      throw "Artifacts path is not specified"
    }
    this.config = config;
    this.db = db;
    // this.init(config, db);
  }

  async init() {
    let config = this.config;
    let db = this.db;
    let artifacts = config.artifacts;
    let provider = config.provider || new Web3.providers.HttpProvider('http://localhost:7545');
    this.web3 = new Web3(provider);
    let block = await this.web3.eth.getBlock("latest");
    let gasPrice = await this.web3.eth.getGasPrice();
    this.gasLimit = config.gas || (block.gasLimit - 1000000);
    this.gasPrice = config.gasPrice || gasPrice;
    console.log("gasPrice", this.gasPrice);
    console.log("gasLimit", this.gasLimit);
    this.plasmaContract = new this.web3.eth.Contract(artifacts.abi, config.plasmaContractAddress, {
      gas: this.gasLimit
    });
    this.config = config;
    // this.queue = [];

    // TODO: move to DB
    this.status = {};
    this.db = db;
    // this.startBlockSubmitter();

    this.alreadyDeposit = {};
  }

  /**
   * Start block submitter, checking to see block header in the queue and then append to the chain
   */
  async startBlockSubmitter() {
    let running = false;
    setInterval(async () => {
      if (running) {
        return;
      }
      running = true;
      let currentBlk = await this.plasmaContract.methods.getCurrentChildBlock().call();
      currentBlk = +currentBlk;

      // Submit unsubmitted blocks
      let blocks = await this.db.getBlocksInRange(currentBlk);
      blocks = blocks.sort((a, b) => +a.blockHeader.blockNumber > +b.blockHeader.blockNumber);
      // console.log("currentBlk", currentBlk, blocks.length);

      if (blocks.length > 0) {
        for (let i = 0; i < blocks.length; i++) {
          const block = blocks[i];
          let blkNum = block.blockHeader.blockNumber;
          let root = block.blockHeader.merkleRoot;
          console.log("submitBlk", blkNum, root);
          let promiEventObj = await this.submitBlockHeader(blkNum, root).catch((err) => {
            console.error("error", err);
          });
          if (!promiEventObj) {
            continue;
          }
          let result = await promiEventObj.promiEvent.catch((err) => {
            console.error("error submitting block header", err);
          });
        }
      }
      running = false;

      // if (this.queue.length == 0) {
      //   return;
      // }
      // let args = this.queue[0];
      // let blkNum = args[0];
      // let root = args[1];
      // let promiEventObj = await this.submitBlockHeader(blkNum, root).catch((err) => {
      //   console.error(err);
      // });
      // if (!promiEventObj) {
      //   return;
      // }
      // promiEventObj.promiEvent.on('confirmation', (confNumber, receipt) => {
      //   this.status[blkNum] = {
      //     confNumber,
      //     receipt
      //   }
      // });
      // let result = await promiEventObj.promiEvent.catch((err) => {
      //   console.error("error submitting block header", err);
      // });
      // if (result && result.status) {
      //   this.queue.shift();
      // }
    }, 1000);
  }

  /**
   * Queue block header to be submit
   * @param {number} blkNum block nubmer
   * @param {string} header merkle root hex string
   */
  queueSubmitBlockHeader(blkNum, header) {
    // this.queue.push([blkNum, header]);
  }

  /**
   * Submit block header
   * @param {number} blkNum block nubmer
   * @param {string} header merkle root hex string
   */
  async submitBlockHeader(blkNum, header) {
    let currentBlk = await this.plasmaContract.methods.getCurrentChildBlock().call();
    if (currentBlk != blkNum) {
      throw `Out of sync, rootChain(${currentBlk}) and childChain(${blkNum}) are out of sync`;
    }
    let result = await this.web3.eth.accounts.signTransaction({
      from: this.config.plasmaOperatorAddress,
      gas: this.gasLimit,
      gasPrice: this.gasPrice,
      to: this.config.plasmaContractAddress,
      data: this.plasmaContract.methods.submitBlock(blkNum, header).encodeABI(),
    }, this.config.plasmaOperatorKey);
    let promiEvent = this.web3.eth.sendSignedTransaction(result.rawTransaction)
    return {
      promiEvent
    };
  };

  /**
   * Sign a block
   * @param {string} message hex string of the message
   */
  signBlock(message) {
    return this.web3.eth.accounts.sign(message, this.config.plasmaOperatorKey).signature;
  };

  /**
   * Sign a transaction with Key
   * @param {string} message hex string of the message
   * @param {string} key hex string of the private key
   */
  signTransactionKey(message, key) {
    // const hash = this.web3.eth.accounts.hashMessage(message);
    return this.web3.eth.accounts.sign(message, key).signature;
  };

  /**
   * Check whether a signature is valid
   * @param {string} message hex string of the message
   * @param {string} signature hex string of the signature
   * @param {string} address hex string of the address
   */
  isValidSignature(message, signature, address) {
    // const hash = await this.web3.eth.accounts.hashMessage(message);
    // TODO: normalize signing and recovery from different ways
    // let signer1 = utils.ecRecover(message, signature);
    console.log("message, signature", message, signature)
    let signer2 = this.web3.eth.accounts.recover(message, signature);
    console.log("signer2", signer2);
    // return utils.removeHexPrefix(address.toLowerCase()) == utils.removeHexPrefix(signer1.toLowerCase()) || 
    return utils.removeHexPrefix(address.toLowerCase()) == utils.removeHexPrefix(signer2.toLowerCase());
  };

  /**
   * Deposit in ether unit
   * @param {string} address 
   * @param {string} token 
   * @param {string} amount 
   * @param {string} key 
   */
  async deposit(address, token, amount, key) {
    amount = Web3.utils.toWei("" + amount, "ether");
    let value = 0;
    if (token == "0x0") {
      value = amount;
    }
    let result = await this.web3.eth.accounts.signTransaction({
      from: address,
      value: value,
      gas: this.gasLimit,
      gasPrice: this.gasPrice,
      to: this.config.plasmaContractAddress,
      data: this.plasmaContract.methods.deposit(token, utils.toBN(amount)).encodeABI(),
    }, key);
    result = await this.web3.eth.sendSignedTransaction(result.rawTransaction)
    // console.log(JSON.stringify(result, null, 4));
    return result;
  };

  /**
   * Create a deposit in ether unit
   * @param {string} address 
   * @param {string} token 
   * @param {string} amount 
   */
  async createDeposit(address, token, amount) {
    amount = Web3.utils.toWei("" + amount, "ether");
    let value = 0;
    if (token == "0x0") {
      value = amount;
    }
    return {
      from: address,
      value: value,
      gas: this.gasLimit,
      gasPrice: this.gasPrice,
      to: this.config.plasmaContractAddress,
      data: this.plasmaContract.methods.deposit(token, utils.toBN(amount)).encodeABI(),
    };
  }

  /**
   * Submit signed transaction to ethereum chain
   * @param {string} signedTransaction hex string of the signed transaction
   */
  async submitSignedTransaction(signedTransaction) {
    let result = await this.web3.eth.sendSignedTransaction(signedTransaction)
    // console.log(JSON.stringify(result, null, 4));
    return result;
  }

  /**
   * Start deposit withdrawal
   * @param {number} depositPos 
   * @param {string} token 
   * @param {number} amount 
   * @param {string} from 
   * @param {string} key 
   */
  async startDepositWithdrawal(depositPos, token, amount, from, key) {
    let result = await this.web3.eth.accounts.signTransaction(createStartDepositWithdrawal(depositPos, token, amount, from), key);
    result = await this.web3.eth.sendSignedTransaction(result.rawTransaction)
    // console.log(JSON.stringify(result, null, 4));
    return result;
  };

  /**
   * Create start deposit withdrawal
   * @param {number} depositPos 
   * @param {string} token 
   * @param {number} amount 
   * @param {string} from 
   */
  async createStartDepositWithdrawal(depositPos, token, amount, from) {
    return {
      from: from,
      gas: this.gasLimit,
      gasPrice: this.gasPrice,
      to: this.config.plasmaContractAddress,
      data: this.plasmaContract.methods.startDepositExit(depositPos, token, utils.toBN(amount)).encodeABI(),
    };
  };

  /**
   * Start Withdrawal
   * @param {number} blkNum 
   * @param {number} txIndex 
   * @param {number} oIndex 
   * @param {string} txData 
   * @param {string} proof 
   * @param {string} confirmSigs 
   * @param {string} from 
   * @param {string} key 
   */
  async startWithdrawal(blkNum, txIndex, oIndex, txData, proof, confirmSigs, from, key) {
    let result = await this.web3.eth.accounts.signTransaction(createStartWithdrawal(blkNum, txIndex, oIndex, txData, proof, confirmSigs, from), key);
    result = await this.web3.eth.sendSignedTransaction(result.rawTransaction)
    // console.log(JSON.stringify(result, null, 4));
    return result;
  };

  /**
   * Create start withdrawal
   * @param {number} blkNum 
   * @param {number} txIndex 
   * @param {number} oIndex 
   * @param {string} txData 
   * @param {string} proof 
   * @param {string} confirmSigs 
   * @param {string} from 
   */
  async createStartWithdrawal(blkNum, txIndex, oIndex, txData, proof, confirmSigs, from) {
    let {
      txBytes,
      sig1,
      sig2
    } = Block.splitTxData(txData);
    let sigs = utils.concatHex(sig1, sig2, confirmSigs);
    let utxoPos = blkNum * 1000000000 + txIndex * 10000 + oIndex;

    console.log("data", utxoPos, txBytes, proof, sigs);
    console.log("fromTxData", Transaction.fromTxData(txData).toJSON());
    console.log("merkleHash", Transaction.fromTxData(txData).merkleHash());

    return {
      from: from,
      gas: this.gasLimit,
      gasPrice: this.gasPrice,
      to: this.config.plasmaContractAddress,
      data: this.plasmaContract.methods.startExit(utxoPos, txBytes, proof, sigs).encodeABI(),
    };
  };

  /**
   * Challenge withdrawal
   * @param {number} eUtxoPos 
   * @param {number} blkNum 
   * @param {number} txIndex 
   * @param {number} oIndex 
   * @param {string} txData 
   * @param {string} proof 
   * @param {string} confirmSig 
   * @param {string} from 
   * @param {string} key 
   */
  async challengeWithdrawal(eUtxoPos, blkNum, txIndex, oIndex, txData, proof, confirmSig, from, key) {
    let result = await this.web3.eth.accounts.signTransaction(createChallengeWithdrawal(eUtxoPos, blkNum, txIndex, oIndex, txData, proof, confirmSig, from), key);
    result = await this.web3.eth.sendSignedTransaction(result.rawTransaction)
    // console.log(JSON.stringify(result, null, 4));
    return result;
  };

  /**
   * Create challenge withdrawal
   * @param {number} eUtxoPos 
   * @param {number} blkNum 
   * @param {number} txIndex 
   * @param {number} oIndex 
   * @param {string} txData 
   * @param {string} proof 
   * @param {string} confirmSig 
   * @param {string} from 
   */
  async createChallengeWithdrawal(eUtxoPos, blkNum, txIndex, oIndex, txData, proof, confirmSig, from) {
    let {
      txBytes,
      sig1,
      sig2
    } = Block.splitTxData(txData);
    let sigs = utils.concatHex(sig1, sig2);
    let utxoPos = blkNum * 1000000000 + txIndex * 10000 + oIndex;

    return {
      from: from,
      gas: this.gasLimit,
      gasPrice: this.gasPrice,
      to: this.config.plasmaContractAddress,
      data: this.plasmaContract.methods.challengeExit(utxoPos, eUtxoPos, txBytes, proof, sigs, confirmSig).encodeABI(),
    };
  };

  /**
   * Finalize withdrawal
   * @param {string} from 
   * @param {string} key 
   */
  async finalizeWithdrawal(from, key) {
    let result = await this.web3.eth.accounts.signTransaction(createFinalizeWithdrawal(from), key);
    result = await this.web3.eth.sendSignedTransaction(result.rawTransaction)
    // console.log(JSON.stringify(result, null, 4));
    return result;
  };

  /**
   * Create finalize withdrawal
   * @param {string} from 
   */
  async createFinalizeWithdrawal(from) {
    return {
      from: from,
      gas: this.gasLimit,
      gasPrice: this.gasPrice,
      to: this.config.plasmaContractAddress,
      data: this.plasmaContract.methods.finalizeExits().encodeABI(),
    };
  };

  /**
   * Get deposits
   * @param {number} blockNumber 
   */
  async getDeposits(blockNumber) {
    let depositEvents = await this.plasmaContract.getPastEvents('Deposit', {
      filter: {
        blockNumber: blockNumber.toString()
      },
      fromBlock: 0,
      toBlock: 'latest'
    });

    let deposits = [];
    depositEvents.forEach(ev => deposits.push(ev.returnValues));
    deposits = deposits.filter((deposit) => !this.alreadyDeposit[deposit.depositBlock])
    deposits.sort((d1, d2) => (+d1.depositBlock - +d2.depositBlock));
    deposits.forEach((deposit) => {
      this.alreadyDeposit[deposit.depositBlock] = deposit;
    })
    return deposits;
  }

  /**
   * Get withdrawals from ExitStarted events.
   * @param {number} blockNumber 
   */
  async getWithdrawals(blockNumber) {
    // let withdrawalEvents = []
    // ExitFinalized
    let withdrawalEvents = await this.plasmaContract.getPastEvents('ExitStarted', {
      filter: {
        blockNumber: blockNumber.toString()
      },
      fromBlock: 0,
      toBlock: 'latest'
    });

    return withdrawalEvents.map(ev => ev.returnValues);
  };

  /**
   * Create a confirmation signature
   * @param {string} txHash 
   * @param {string} root 
   * @param {string} key 
   */
  confirmSig(txHash, root, key) {
    console.log("confirmSigHash", txHash, root, this.web3.utils.soliditySha3(txHash, root));

    // console.log(web3.eth.accounts.sign, utils.concatHex(txHash, root), key);
    let confirmSignature = this.web3.eth.accounts.sign(Web3.utils.soliditySha3(txHash, root), key).signature;
    console.log("confirmSignature", confirmSignature);
    return confirmSignature;
  }

  /**
   * Check if a confirmation signature is valid
   * @param {string} txHash 
   * @param {string} root 
   * @param {string} sig1 
   * @param {string} confSig1 
   * @param {string} sig2
   * @param {string} confSig2
   */
  isValidConfirmSig(txHash, root, sig1, confSig1, sig2, confSig2) {
    try {
      let check1 = true;
      let check2 = true;
      let confirmationHash = Web3.utils.soliditySha3(txHash, root);
      let account1 = this.web3.eth.accounts.recover(confirmationHash, confSig1);
      let account2 = this.web3.eth.accounts.recover(txHash, sig1);
      check1 = (utils.removeHexPrefix(account1.toLowerCase()) == utils.removeHexPrefix(account2.toLowerCase()));
      // var prefix = "\x19Ethereum Signed Message:\n32";
      // return Web3.utils.soliditySha3(prefix, message);
      console.log("check1", check1, root, confSig1, sig2, account1, account2);

      if (sig2 && sig2 != "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") {
        let account1_2 = this.web3.eth.accounts.recover(confirmationHash, confSig2);
        let account2_2 = this.web3.eth.accounts.recover(txHash, sig2);
        check2 = (utils.removeHexPrefix(account1_2.toLowerCase()) == utils.removeHexPrefix(account2_2.toLowerCase()));
        console.log("check2", check2);

      }

      return check1 && check2;
      // let account1ec = utils.ecRecover(confirmationHash, confSig1);
      // let account2ec = utils.ecRecover(txHash, sig1);
      // return utils.removeHexPrefix(account1ec.toLowerCase()) == utils.removeHexPrefix(account2ec.toLowerCase());
    } catch (e) {
      console.log("error", e)
      return false
    }
  }

}

module.exports = RootChain;