All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.wavesplatform.utils.generator.MinerChallengeSimulator.scala Maven / Gradle / Ivy

The newest version!
package com.wavesplatform.utils.generator

import com.typesafe.config.{ConfigFactory, ConfigParseOptions}
import com.wavesplatform.GenesisBlockGenerator
import com.wavesplatform.account.{Address, SeedKeyPair}
import com.wavesplatform.block.Block
import com.wavesplatform.block.Block.BlockId
import com.wavesplatform.consensus.PoSSelector
import com.wavesplatform.database.{RDB, RocksDBWriter, loadActiveLeases}
import com.wavesplatform.events.{BlockchainUpdateTriggers, UtxEvent}
import com.wavesplatform.features.BlockchainFeatures
import com.wavesplatform.history.StorageFactory
import com.wavesplatform.lang.ValidationError
import com.wavesplatform.mining.{Miner, MinerImpl}
import com.wavesplatform.network.BlockSnapshotResponse
import com.wavesplatform.settings.*
import com.wavesplatform.state.BlockchainUpdaterImpl.BlockApplyResult
import com.wavesplatform.state.appender.BlockAppender
import com.wavesplatform.state.{BalanceSnapshot, BlockchainUpdaterImpl}
import com.wavesplatform.transaction.TxValidationError.GenericError
import com.wavesplatform.utils.{Schedulers, Time}
import com.wavesplatform.utx.UtxPoolImpl
import com.wavesplatform.wallet.Wallet
import io.netty.channel.group.DefaultChannelGroup
import monix.eval.Task
import monix.execution.schedulers.SchedulerService
import monix.reactive.subjects.ConcurrentSubject
import net.ceedubs.ficus.Ficus.*
import net.ceedubs.ficus.readers.ArbitraryTypeReader.*
import org.apache.commons.io.FileUtils

import java.io.{File, FileNotFoundException}
import scala.concurrent.duration.*
import scala.language.reflectiveCalls

object MinerChallengeSimulator {
  implicit val scheduler: SchedulerService = Schedulers.singleThread("miner-challenge-simulator")
  sys.addShutdownHook(synchronized {
    scheduler.shutdown()
    scheduler.awaitTermination(10 seconds)
  })

  val forkHeight = 1000000

  var quit = false
  sys.addShutdownHook {
    quit = true
  }

  def main(args: Array[String]): Unit = {
    val genesisConfFile = new File(args.headOption.getOrElse(throw new IllegalArgumentException("Specify a path to genesis generator config file")))
    if (!genesisConfFile.exists()) throw new FileNotFoundException(genesisConfFile.getCanonicalPath)
    val nodeConfFile = new File(args.tail.headOption.getOrElse(throw new IllegalArgumentException("Specify a path to node config file")))
    if (!nodeConfFile.exists()) throw new FileNotFoundException(nodeConfFile.getCanonicalPath)
    val challengingMinerIdx = args.drop(2).headOption.map(_.toInt).getOrElse(1)

    val config      = readConfFile(genesisConfFile)
    val genSettings = GenesisBlockGenerator.parseSettings(config)
    val genesis     = ConfigFactory.parseString(GenesisBlockGenerator.createConfig(genSettings)).as[GenesisSettings]("genesis")

    val blockchainSettings = BlockchainSettings(
      genSettings.chainId.toChar,
      genSettings.functionalitySettings.copy(preActivatedFeatures = BlockchainFeatures.implemented.map(_ -> 0).toMap),
      genesis,
      RewardsSettings.MAINNET
    )
    val wavesSettings = {
      val settings = WavesSettings.fromRootConfig(loadConfig(Some(nodeConfFile).map(readConfFile)))
      settings.copy(blockchainSettings = blockchainSettings, minerSettings = settings.minerSettings.copy(quorum = 0))
    }

    val miners = genSettings.distributions.collect {
      case item if item.miner =>
        val info = GenesisBlockGenerator.toFullAddressInfo(item)
        info.account
    }

    val maliciousMiner   = miners.head
    val challengingMiner = miners(challengingMinerIdx)

    val wallet: Wallet = new Wallet {
      private[this] val map                                            = miners.map(kp => kp.toAddress -> kp).toMap
      override def seed: Array[Byte]                                   = Array.emptyByteArray
      override def nonce: Int                                          = miners.length
      override def privateKeyAccounts: Seq[SeedKeyPair]                = miners
      override def generateNewAccounts(howMany: Int): Seq[SeedKeyPair] = ???
      override def generateNewAccount(): Option[SeedKeyPair]           = ???
      override def generateNewAccount(nonce: Int): Option[SeedKeyPair] = ???
      override def deleteAccount(account: SeedKeyPair): Boolean        = ???
      override def privateKeyAccount(account: Address): Either[ValidationError, SeedKeyPair] =
        map.get(account).toRight(GenericError(s"No key for $account"))
    }

    var originalBlockchain = BlockchainObjects.createOriginal(wavesSettings, wallet, genSettings.timestamp.getOrElse(System.currentTimeMillis()))
    var challengingBlockchain: Option[BlockchainObjects] = None

    while (!Thread.currentThread().isInterrupted && !quit) synchronized {
      val prevTime         = originalBlockchain.fakeTime
      val originalScore    = originalBlockchain.forgeAndAppendBlock(miners, challengingMiner, maliciousMiner).get
      val challengingScore = challengingBlockchain.flatMap(_.forgeAndAppendBlock(miners, challengingMiner, maliciousMiner))

      if (originalBlockchain.blockchain.height > forkHeight - 1) {
        if (challengingScore.exists(_ <= originalScore)) {
          println(s"Original score = $originalScore, challenging score = ${challengingScore.get} on height ${originalBlockchain.blockchain.height}")
        } else if (challengingScore.isDefined) {
          println(s"Original score ($originalScore) < challenging score (${challengingScore.get}) on height ${originalBlockchain.blockchain.height}")
          quit = true
        }
      }

      if (originalBlockchain.blockchain.height == forkHeight + 1 && challengingBlockchain.isEmpty) {
        originalBlockchain.blockchain.shutdown()
        originalBlockchain.rdb.close()
        challengingBlockchain = Some(BlockchainObjects.createChallenging(wavesSettings, wallet, challengingMiner, maliciousMiner))
        originalBlockchain = BlockchainObjects.createOriginal(wavesSettings, wallet, prevTime.time)
      }
    }
  }

  case class BlockchainObjects(
      blockchain: BlockchainUpdaterImpl,
      rdb: RDB,
      miner: MinerImpl,
      blockAppender: (Block, Option[BlockSnapshotResponse]) => Task[Either[ValidationError, BlockApplyResult]],
      fakeTime: Time & Object { var time: Long },
      isChallenging: Boolean
  ) {
    def forgeAndAppendBlock(miners: List[SeedKeyPair], challengingMiner: SeedKeyPair, maliciousMiner: SeedKeyPair): Option[BigInt] = {
      val miningTimes = getBlockMiningTimes(miners)
      val (bestMiner, nextTime) = if (blockchain.height == forkHeight && isChallenging) {
        miningTimes.find(_._1 == challengingMiner).get
      } else if (blockchain.height == forkHeight) {
        miningTimes.find(_._1 == maliciousMiner).get
      } else {
        miningTimes.minBy(_._2)
      }
      fakeTime.time = nextTime

      miner.forgeBlock(bestMiner) match {
        case Right((block, _)) =>
          blockAppender(block, None).runSyncUnsafe() match {
            case Right(BlockApplyResult.Applied(_, score)) => Some(score)
            case other =>
              println(s"Error appending block: $other")
              quit = true
              Some(0)
          }

        case Left(err) =>
          println(s"Error generating block: $err")
          quit = true
          Some(0)
      }
    }

    private def getBlockMiningTimes(miners: List[SeedKeyPair]): Seq[(SeedKeyPair, Long)] =
      miners.flatMap { kp =>
        val time = miner.nextBlockGenerationTime(blockchain, blockchain.height, blockchain.lastBlockHeader.get, kp)
        time.toOption.map(kp -> _)
      }
  }

  object BlockchainObjects {
    def createOriginal(wavesSettings: WavesSettings, wallet: Wallet, startTime: Long): BlockchainObjects = {
      val rdb      = RDB.open(wavesSettings.dbSettings)
      val fakeTime = createFakeTime(startTime)
      val (blockchainUpdater, _) =
        StorageFactory(wavesSettings, rdb, fakeTime, BlockchainUpdateTriggers.noop)
      com.wavesplatform.checkGenesis(wavesSettings, blockchainUpdater, Miner.Disabled)
      sys.addShutdownHook(synchronized {
        blockchainUpdater.shutdown()
        rdb.close()
      })
      val (miner, appender) = createMinerAndAppender(blockchainUpdater, fakeTime, wavesSettings, wallet)
      BlockchainObjects(blockchainUpdater, rdb, miner, appender, fakeTime, false)
    }

    def createChallenging(
        wavesSettings: WavesSettings,
        wallet: Wallet,
        challengingMiner: SeedKeyPair,
        maliciousMiner: SeedKeyPair
    ): BlockchainObjects = {
      val correctBlockchainDbDir = wavesSettings.dbSettings.directory + "/../challenged"
      FileUtils.copyDirectory(new File(wavesSettings.dbSettings.directory), new File(correctBlockchainDbDir))
      val dbSettings         = wavesSettings.dbSettings.copy(directory = correctBlockchainDbDir)
      val fixedWavesSettings = wavesSettings.copy(dbSettings = dbSettings)
      val rdb                = RDB.open(dbSettings)
      val rocksDBWriter      = RocksDBWriter(rdb, fixedWavesSettings.blockchainSettings, fixedWavesSettings.dbSettings, false)
      val fakeTime           = createFakeTime(rocksDBWriter.lastBlockTimestamp.get)
      val blockchainUpdater = new BlockchainUpdaterImpl(
        rocksDBWriter,
        fixedWavesSettings,
        fakeTime,
        BlockchainUpdateTriggers.noop,
        (minHeight, maxHeight) => loadActiveLeases(rdb, minHeight, maxHeight)
      ) {
        override def balanceSnapshots(address: Address, from: Int, to: Option[BlockId]): Seq[BalanceSnapshot] = {
          val initSnapshots = super.balanceSnapshots(address, from, to)

          if (address == maliciousMiner.toAddress) {
            initSnapshots.map { bs =>
              bs.copy(leaseOut = bs.regularBalance)
            }
          } else if (address == challengingMiner.toAddress) {
            initSnapshots.map { bs =>
              bs.copy(leaseIn = super.balance(maliciousMiner.toAddress))
            }
          } else {
            initSnapshots
          }
        }
      }

      com.wavesplatform.checkGenesis(fixedWavesSettings, blockchainUpdater, Miner.Disabled)
      sys.addShutdownHook(synchronized {
        blockchainUpdater.shutdown()
        rdb.close()
      })

      val (miner, appender) = createMinerAndAppender(blockchainUpdater, fakeTime, wavesSettings, wallet)
      BlockchainObjects(blockchainUpdater, rdb, miner, appender, fakeTime, true)
    }

    private def createMinerAndAppender(
        blockchain: BlockchainUpdaterImpl,
        fakeTime: Time,
        wavesSettings: WavesSettings,
        wallet: Wallet
    ): (
        MinerImpl,
        (
            com.wavesplatform.block.Block,
            Option[com.wavesplatform.network.BlockSnapshotResponse]
        ) => monix.eval.Task[Either[com.wavesplatform.lang.ValidationError, com.wavesplatform.state.BlockchainUpdaterImpl.BlockApplyResult]]
    ) = {
      val utx = new UtxPoolImpl(fakeTime, blockchain, wavesSettings.utxSettings, wavesSettings.maxTxErrorLogSize, wavesSettings.minerSettings.enable)
      val posSelector = PoSSelector(blockchain, None)
      val utxEvents   = ConcurrentSubject.publish[UtxEvent](scheduler)
      val miner = new MinerImpl(
        new DefaultChannelGroup("", null),
        blockchain,
        wavesSettings,
        fakeTime,
        utx,
        wallet,
        posSelector,
        scheduler,
        scheduler,
        utxEvents.collect { case _: UtxEvent.TxAdded => () }
      )
      val blockAppender = BlockAppender(blockchain, fakeTime, utx, posSelector, scheduler, verify = false) _

      miner -> blockAppender
    }

    private def createFakeTime(startTime: Long) =
      new Time {
        @volatile
        var time: Long = startTime

        override def correctedTime(): Long = time
        override def getTimestamp(): Long  = time
      }
  }

  private def readConfFile(f: File) = ConfigFactory.parseFile(f, ConfigParseOptions.defaults().setAllowMissing(false))
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy