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

com.wavesplatform.Exporter.scala Maven / Gradle / Ivy

The newest version!
package com.wavesplatform

import com.google.common.collect.AbstractIterator
import com.google.common.primitives.Ints
import com.wavesplatform.block.Block
import com.wavesplatform.database.protobuf.BlockMeta
import com.wavesplatform.database.{KeyTags, RDB, createBlock, readBlockMeta, readTransaction}
import com.wavesplatform.events.BlockchainUpdateTriggers
import com.wavesplatform.history.StorageFactory
import com.wavesplatform.metrics.Metrics
import com.wavesplatform.protobuf.ByteStringExt
import com.wavesplatform.protobuf.block.PBBlocks
import com.wavesplatform.state.Height
import com.wavesplatform.transaction.Transaction
import com.wavesplatform.utils.*
import kamon.Kamon
import org.rocksdb.{ColumnFamilyHandle, ReadOptions, RocksDB}
import scopt.OParser

import java.io.{BufferedOutputStream, File, FileOutputStream, OutputStream}
import scala.annotation.tailrec
import scala.concurrent.Await
import scala.concurrent.duration.*
import scala.jdk.CollectionConverters.*
import scala.util.Using.Releasable
import scala.util.{Failure, Success, Try, Using}

object Exporter extends ScorexLogging {
  private[wavesplatform] object Formats {
    val Binary   = "BINARY"
    val Protobuf = "PROTOBUF"

    def list: Seq[String] = Seq(Binary, Protobuf)
    def default: String   = Binary

    def isSupported(f: String): Boolean = list.contains(f.toUpperCase)
  }

  // noinspection ScalaStyle
  def main(args: Array[String]): Unit = {
    OParser.parse(commandParser, args, ExporterOptions()).foreach {
      case ExporterOptions(configFile, blocksOutputFileNamePrefix, snapshotsOutputFileNamePrefix, exportHeight, format) =>
        val settings = Application.loadApplicationConfig(configFile)

        Using.resources(
          new NTP(settings.ntpServer),
          RDB.open(settings.dbSettings)
        ) { (time, rdb) =>
          val (blockchain, rdbWriter) = StorageFactory(settings, rdb, time, BlockchainUpdateTriggers.noop)
          val blockchainHeight        = blockchain.height
          val height                  = Math.min(blockchainHeight, exportHeight.getOrElse(blockchainHeight))
          log.info(s"Blockchain height is $blockchainHeight exporting to $height")
          val blocksOutputFilename = s"$blocksOutputFileNamePrefix-$height"
          log.info(s"Blocks output file: $blocksOutputFilename")

          val exportSnapshots = snapshotsOutputFileNamePrefix.isDefined
          val snapshotsOutputFilename = if (exportSnapshots) {
            val filename = s"${snapshotsOutputFileNamePrefix.get}-$height"
            log.info(s"Snapshots output file: $filename")
            Some(filename)
          } else None

          implicit def optReleasable[A](implicit ev: Releasable[A]): Releasable[Option[A]] = {
            case Some(r) => ev.release(r)
            case None    => ()
          }

          Using.resources(
            createOutputFile(blocksOutputFilename),
            snapshotsOutputFilename.map(createOutputFile),
            rdbWriter
          ) { case (blocksOutput, snapshotsOutput, _) =>
            Using.resources(createBufferedOutputStream(blocksOutput, 10), snapshotsOutput.map(createBufferedOutputStream(_, 100))) {
              case (blocksStream, snapshotsStream) =>
                var exportedBlocksBytes    = 0L
                var exportedSnapshotsBytes = 0L
                val start                  = System.currentTimeMillis()

                new BlockSnapshotIterator(rdb, height, exportSnapshots).asScala.foreach { case (h, block, txSnapshots) =>
                  val txCount = block.transactionData.length
                  if (exportSnapshots && txCount != txSnapshots.length)
                    throw new RuntimeException(
                      s"${txSnapshots.length} snapshot(s) don't match $txCount transaction(s) on height $h, data is corrupted"
                    )
                  exportedBlocksBytes += IO.exportBlock(blocksStream, Some(block), format == Formats.Binary)
                  snapshotsStream.foreach { output =>
                    exportedSnapshotsBytes += IO.exportBlockTxSnapshots(output, txSnapshots)
                  }

                  if (h % (height / 10) == 0) {
                    log.info(
                      s"$h blocks exported, ${humanReadableSize(exportedBlocksBytes)} written for blocks${snapshotsLogInfo(exportSnapshots, exportedSnapshotsBytes)}"
                    )
                  }
                }
                val duration = System.currentTimeMillis() - start
                log
                  .info(
                    s"Finished exporting $height blocks in ${java.time.Duration.ofMillis(duration)}, ${humanReadableSize(exportedBlocksBytes)} written for blocks${snapshotsLogInfo(exportSnapshots, exportedSnapshotsBytes)}"
                  )
            }
          }
        }

        Try(Await.result(Kamon.stopModules(), 10.seconds))
        Metrics.shutdown()
    }
  }

  private class BlockSnapshotIterator(rdb: RDB, targetHeight: Int, exportSnapshots: Boolean)
      extends AbstractIterator[(Int, Block, Seq[Array[Byte]])] {
    var nextTxEntry: Option[(Int, Transaction)]       = None
    var nextSnapshotEntry: Option[(Int, Array[Byte])] = None

    val blockMetaIterator: DataIterator[BlockMeta] =
      new DataIterator[BlockMeta](
        rdb.db,
        rdb.db.getDefaultColumnFamily,
        KeyTags.BlockInfoAtHeight.prefixBytes,
        _.takeRight(Ints.BYTES),
        _ => readBlockMeta
      )
    val txIterator: DataIterator[Transaction] = {
      val prefixBytes = KeyTags.NthTransactionInfoAtHeight.prefixBytes
      new DataIterator(
        rdb.db,
        rdb.txHandle.handle,
        prefixBytes,
        _.slice(prefixBytes.length, prefixBytes.length + Ints.BYTES),
        h => readTransaction(Height(h))(_)._2
      )
    }
    val snapshotIterator: DataIterator[Array[Byte]] = {
      val prefixBytes = KeyTags.NthTransactionStateSnapshotAtHeight.prefixBytes
      new DataIterator(
        rdb.db,
        rdb.txSnapshotHandle.handle,
        prefixBytes,
        _.slice(prefixBytes.length, prefixBytes.length + Ints.BYTES),
        _ => identity
      )
    }

    @tailrec
    private def loadTxData[A](acc: Seq[A], height: Int, iterator: DataIterator[A], updateNextEntryF: (Int, A) => Unit): Seq[A] = {
      if (iterator.hasNext) {
        val (h, txData) = iterator.next()
        if (h == height) {
          loadTxData(txData +: acc, height, iterator, updateNextEntryF)
        } else {
          updateNextEntryF(h, txData)
          acc.reverse
        }
      } else acc.reverse
    }

    @tailrec
    override final def computeNext(): (Int, Block, Seq[Array[Byte]]) = {
      if (blockMetaIterator.hasNext) {
        val (h, meta) = blockMetaIterator.next()
        if (h <= targetHeight) {
          val txs = nextTxEntry match {
            case Some((txHeight, tx)) if txHeight == h =>
              nextTxEntry = None
              loadTxData[Transaction](Seq(tx), h, txIterator, (h, tx) => nextTxEntry = Some(h -> tx))
            case Some(_) => Seq.empty
            case _       => loadTxData[Transaction](Seq.empty, h, txIterator, (h, tx) => nextTxEntry = Some(h -> tx))
          }
          val snapshots = if (exportSnapshots) {
            nextSnapshotEntry match {
              case Some((snapshotHeight, txSnapshot)) if snapshotHeight == h =>
                nextSnapshotEntry = None
                loadTxData[Array[Byte]](Seq(txSnapshot), h, snapshotIterator, (h, sn) => nextSnapshotEntry = Some(h -> sn))
              case Some(_) => Seq.empty
              case _       => loadTxData[Array[Byte]](Seq.empty, h, snapshotIterator, (h, sn) => nextSnapshotEntry = Some(h -> sn))
            }
          } else Seq.empty
          createBlock(PBBlocks.vanilla(meta.getHeader), meta.signature.toByteStr, txs).toOption
            .map(block => (h, block, snapshots)) match {
            case Some(r) => r
            case None    => computeNext()
          }
        } else {
          closeResources()
          endOfData()
        }
      } else {
        closeResources()
        endOfData()
      }
    }

    def closeResources(): Unit = {
      txIterator.closeResources()
      snapshotIterator.closeResources()
      blockMetaIterator.closeResources()
    }
  }

  private class DataIterator[A](
      db: RocksDB,
      cfHandle: ColumnFamilyHandle,
      prefixBytes: Array[Byte],
      heightFromKeyF: Array[Byte] => Array[Byte],
      parseDataF: Int => Array[Byte] => A
  ) extends AbstractIterator[(Int, A)] {
    private val snapshot    = db.getSnapshot
    private val readOptions = new ReadOptions().setSnapshot(snapshot).setVerifyChecksums(false)
    private val dbIterator  = db.newIterator(cfHandle, readOptions.setTotalOrderSeek(true))

    dbIterator.seek(prefixBytes)

    @tailrec
    override final def computeNext(): (Int, A) = {
      if (dbIterator.isValid && dbIterator.key().startsWith(prefixBytes)) {
        val h = Ints.fromByteArray(heightFromKeyF(dbIterator.key()))
        if (h > 1) {
          val txData = parseDataF(h)(dbIterator.value())
          dbIterator.next()
          h -> txData
        } else {
          dbIterator.next()
          computeNext()
        }
      } else {
        closeResources()
        endOfData()
      }
    }

    def closeResources(): Unit = {
      snapshot.close()
      readOptions.close()
      dbIterator.close()
    }
  }

  object IO {
    def createOutputStream(filename: String): Try[FileOutputStream] =
      Try(new FileOutputStream(filename))

    def exportBlock(stream: OutputStream, maybeBlock: Option[Block], legacy: Boolean): Int = {
      val maybeBlockBytes = maybeBlock.map(_.bytes())
      maybeBlockBytes
        .map { oldBytes =>
          val bytes       = if (legacy) oldBytes else PBBlocks.clearChainId(PBBlocks.protobuf(Block.parseBytes(oldBytes).get)).toByteArray
          val bytesLength = bytes.length

          stream.write(Ints.toByteArray(bytesLength))
          stream.write(bytes)

          Ints.BYTES + bytesLength
        }
        .getOrElse(0)
    }

    def exportBlockTxSnapshots(stream: OutputStream, snapshots: Seq[Array[Byte]]): Int = {
      val snapshotBytesWithSizes = snapshots.map { snapshot =>
        snapshot -> snapshot.length
      }

      val fullSize = snapshotBytesWithSizes.map(_._2 + Ints.BYTES).sum
      stream.write(Ints.toByteArray(fullSize))

      snapshotBytesWithSizes.foreach { case (snapshotBytes, size) =>
        stream.write(Ints.toByteArray(size))
        stream.write(snapshotBytes)
      }

      fullSize + Ints.BYTES
    }

    def writeString(stream: OutputStream, str: String): Int = {
      val bytes = str.utf8Bytes
      stream.write(bytes)
      bytes.length
    }
  }

  private[this] final case class ExporterOptions(
      configFileName: Option[File] = None,
      blocksOutputFileNamePrefix: String = "blockchain",
      snapshotsFileNamePrefix: Option[String] = None,
      exportHeight: Option[Int] = None,
      format: String = Formats.Binary
  )

  private[this] lazy val commandParser = {
    import scopt.OParser

    val builder = OParser.builder[ExporterOptions]
    import builder.*

    OParser.sequence(
      programName("waves export"),
      head("Waves Blockchain Exporter", Version.VersionString),
      opt[File]('c', "config")
        .text("Node config file path")
        .action((f, c) => c.copy(configFileName = Some(f))),
      opt[String]('o', "output-prefix")
        .text("Blocks output file name prefix")
        .action((p, c) => c.copy(blocksOutputFileNamePrefix = p)),
      opt[String]('s', "snapshot-output-prefix")
        .text("Snapshots output file name prefix")
        .action((p, c) => c.copy(snapshotsFileNamePrefix = Some(p))),
      opt[Int]('h', "height")
        .text("Export to height")
        .action((h, c) => c.copy(exportHeight = Some(h)))
        .validate(h => if (h > 0) success else failure("Export height must be > 0")),
      opt[String]('f', "format")
        .hidden()
        .text("Output file format")
        .valueName(s"<${Formats.list.mkString("|")}> (default is ${Formats.default})")
        .action { (f, c) =>
          log.warn("Export file format option is deprecated and will be removed eventually")
          c.copy(format = f)
        }
        .validate {
          case f if Formats.isSupported(f.toUpperCase) => success
          case f                                       => failure(s"Unsupported format: $f")
        },
      opt[Int]('h', "height")
        .text("Export to height")
        .action((h, c) => c.copy(exportHeight = Some(h)))
        .validate(h => if (h > 0) success else failure("Export height must be > 0")),
      help("help").hidden()
    )
  }

  private def createOutputFile(outputFilename: String): FileOutputStream =
    IO.createOutputStream(outputFilename) match {
      case Success(output) => output
      case Failure(ex) =>
        log.error(s"Failed to create file '$outputFilename': $ex")
        throw ex
    }

  private def createBufferedOutputStream(fileOutputStream: FileOutputStream, sizeInMb: Int) =
    new BufferedOutputStream(fileOutputStream, sizeInMb * 1024 * 1024)

  private def snapshotsLogInfo(exportSnapshots: Boolean, exportedSnapshotsBytes: Long): String =
    if (exportSnapshots) {
      s", ${humanReadableSize(exportedSnapshotsBytes)} for snapshots"
    } else ""
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy