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

scorex.transaction.SimpleTransactionModule.scala Maven / Gradle / Ivy

The newest version!
package scorex.transaction

import com.google.common.base.Charsets
import com.google.common.primitives.{Bytes, Ints}
import play.api.libs.json.{JsArray, JsObject, Json}
import scorex.account.{Account, PrivateKeyAccount, PublicKeyAccount}
import scorex.app.Application
import scorex.block.{Block, BlockField}
import scorex.consensus.TransactionsOrdering
import scorex.crypto.encode.Base58
import scorex.network.message.Message
import scorex.network.{Broadcast, NetworkController, TransactionalMessagesRepo}
import scorex.settings.{Settings, WavesHardForkParameters}
import scorex.transaction.SimpleTransactionModule.StoredInBlock
import scorex.transaction.ValidationResult.ValidationResult
import scorex.transaction.assets._
import scorex.transaction.assets.exchange.{Order, OrderMatch}
import scorex.transaction.state.database.{BlockStorageImpl, UnconfirmedTransactionsDatabaseImpl}
import scorex.transaction.state.wallet._
import scorex.utils._
import scorex.wallet.Wallet

import scala.concurrent.duration._
import scala.util.{Failure, Success, Try}
import scala.util.control.NonFatal

@SerialVersionUID(3044437555808662124L)
case class TransactionsBlockField(override val value: Seq[Transaction])
  extends BlockField[Seq[Transaction]] {

  import SimpleTransactionModule.MaxTransactionsPerBlock

  override val name = "transactions"

  override lazy val json: JsObject = Json.obj(name -> JsArray(value.map(_.json)))

  override lazy val bytes: Array[Byte] = {
    val txCount = value.size.ensuring(_ <= MaxTransactionsPerBlock).toByte
    value.foldLeft(Array(txCount)) { case (bs, tx) =>
      val txBytes = tx.bytes
      bs ++ Bytes.ensureCapacity(Ints.toByteArray(txBytes.length), 4, 0) ++ txBytes
    }
  }
}


class SimpleTransactionModule(hardForkParams: WavesHardForkParameters)(implicit val settings: TransactionSettings with Settings,
                                                                       application: Application)
  extends TransactionModule[StoredInBlock] with ScorexLogging {

  import SimpleTransactionModule._

  val networkController = application.networkController
  private val feeCalculator = new FeeCalculator(settings)

  val TransactionSizeLength = 4
  val InitialBalance = 10000000000000L

  override val utxStorage: UnconfirmedTransactionsStorage = new UnconfirmedTransactionsDatabaseImpl

  override val blockStorage = new BlockStorageImpl(settings, hardForkParams)(application.consensusModule, this)

  /**
    * In Lagonaki, transaction-related data is just sequence of transactions. No Merkle-tree root of txs / state etc
    *
    * @param bytes - serialized sequence of transaction
    * @return
    */
  override def parseBytes(bytes: Array[Byte]): Try[TransactionsBlockField] = Try {
    bytes.isEmpty match {
      case true => TransactionsBlockField(Seq())
      case false =>
        val txData = bytes.tail
        val txCount = bytes.head // so 255 txs max
        formBlockData((1 to txCount).foldLeft((0: Int, Seq[TypedTransaction]())) { case ((pos, txs), _) =>
          val transactionLengthBytes = txData.slice(pos, pos + TransactionSizeLength)
          val transactionLength = Ints.fromByteArray(transactionLengthBytes)
          val transactionBytes = txData.slice(pos + TransactionSizeLength, pos + TransactionSizeLength + transactionLength)
          val transaction = TypedTransaction.parseBytes(transactionBytes).get

          (pos + TransactionSizeLength + transactionLength, txs :+ transaction)
        }._2)
    }
  }

  override def formBlockData(transactions: StoredInBlock): TransactionsBlockField = TransactionsBlockField(transactions)

  //TODO asInstanceOf
  override def transactions(block: Block): StoredInBlock =
    block.transactionDataField.asInstanceOf[TransactionsBlockField].value

  override def unconfirmedTxs: Seq[Transaction] = utxStorage.all()

  override def putUnconfirmedIfNew(tx: Transaction): Boolean = synchronized {
    if (feeCalculator.enoughFee(tx)) {
      utxStorage.putIfNew(tx, isValid(_, tx.timestamp))
    } else false
  }

  override def packUnconfirmed(heightOpt: Option[Int]): StoredInBlock = synchronized {
    clearIncorrectTransactions()

    val txs = utxStorage.all().sorted(TransactionsOrdering).take(MaxTransactionsPerBlock)
    val valid = blockStorage.state.validate(txs, heightOpt, NTP.correctedTime())

    if (valid.size != txs.size) {
      log.debug(s"Txs for new block do not match: valid=${valid.size} vs all=${txs.size}")
    }

    valid
  }

  override def clearFromUnconfirmed(data: StoredInBlock): Unit = synchronized {
    data.foreach(tx => utxStorage.getBySignature(tx.id) match {
      case Some(unconfirmedTx) => utxStorage.remove(unconfirmedTx)
      case None =>
    })

    clearIncorrectTransactions() // todo makes sence to remove expired only at this point
  }

  /**
    * Removes too old or invalid transactions from UnconfirmedTransactionsPool
    */
  def clearIncorrectTransactions(): Unit = {
    val currentTime = NTP.correctedTime()
    val txs = utxStorage.all()
    val notExpired = txs.filter { tx => (currentTime - tx.timestamp).millis <= MaxTimeForUnconfirmed }
    val notFromFuture = notExpired.filter { tx => (tx.timestamp - currentTime).millis <= MaxTimeDrift }
    val valid = blockStorage.state.validate(notFromFuture, blockTime = currentTime)
    // remove non valid or expired from storage
    txs.diff(valid).foreach(utxStorage.remove)
  }

  override def onNewOffchainTransaction(transaction: Transaction): Unit =
    if (putUnconfirmedIfNew(transaction)) {
      val spec = TransactionalMessagesRepo.TransactionMessageSpec
      val ntwMsg = Message(spec, Right(transaction), None)
      networkController ! NetworkController.SendToNetwork(ntwMsg, Broadcast)
    }

  @deprecated("Use transferAsset()")
  def createPayment(payment: Payment, wallet: Wallet): Option[PaymentTransaction] = {
    wallet.privateKeyAccount(payment.sender).map { sender =>
      createPayment(sender, new Account(payment.recipient), payment.amount, payment.fee)
    }
  }

  def transferAsset(request: TransferRequest, wallet: Wallet): Try[TransferTransaction] = {
    for {
      transfer <- transferRequestToTransaction(request, wallet)
    } yield {
      if (isValid(transfer, transfer.timestamp)) {
        onNewOffchainTransaction(transfer)
      } else {
        throw new StateCheckFailed("Invalid transfer transaction generated: " + transfer.json)
      }
      transfer
    }
  }

  private def transferRequestToTransaction(request: TransferRequest, wallet: Wallet): Try[TransferTransaction] = Try {
    val sender = wallet.privateKeyAccount(request.sender).get

    val transfer: TransferTransaction = TransferTransaction.create(request.assetId.map(s => Base58.decode(s).get),
      sender: PrivateKeyAccount,
      new Account(request.recipient),
      request.amount,
      getTimestamp,
      request.feeAssetId.map(s => Base58.decode(s).get),
      request.fee,
      Option(request.attachment).filter(_.nonEmpty).map(Base58.decode(_).get).getOrElse(Array.emptyByteArray))

    transfer
  }

  def issueAsset(request: IssueRequest, wallet: Wallet): Try[IssueTransaction] = Try {
    val sender = wallet.privateKeyAccount(request.sender).get
    val issue = IssueTransaction.create(sender,
      request.name.getBytes(Charsets.UTF_8),
      request.description.getBytes(Charsets.UTF_8),
      request.quantity,
      request.decimals,
      request.reissuable,
      request.fee,
      getTimestamp)
    if (isValid(issue, issue.timestamp)) onNewOffchainTransaction(issue)
    else throw new StateCheckFailed("Invalid issue transaction generated: " + issue.json)
    issue
  }

  /**
    * Validate transaction according to the State and send it to network
    */
  def broadcastTransaction(tx: SignedTransaction): ValidationResult = {
    tx.validate match {
      case ValidationResult.ValidateOke =>
        if (isValid(tx, tx.timestamp)) {
          onNewOffchainTransaction(tx)
          ValidationResult.ValidateOke
        } else ValidationResult.StateCheckFailed
      case error: ValidationResult => error
    }
  }

  /**
    * Validate transactions according to the State and send it to network
    */
  def broadcastTransactions(txs: Seq[SignedTransaction]): ValidationResult = {
    if(txs.nonEmpty && isValid(txs, txs.map(_.timestamp).max)) {
      txs.foreach(onNewOffchainTransaction)
      ValidationResult.ValidateOke
    } else {
      ValidationResult.StateCheckFailed
    }
  }

  def reissueAsset(request: ReissueRequest, wallet: Wallet): Try[ReissueTransaction] = Try {
    val sender = wallet.privateKeyAccount(request.sender).get
    val reissue = ReissueTransaction.create(sender,
      Base58.decode(request.assetId).get,
      request.quantity,
      request.reissuable,
      request.fee,
      getTimestamp)
    if (isValid(reissue, reissue.timestamp)) onNewOffchainTransaction(reissue)
    else throw new StateCheckFailed("Invalid reissue transaction generated: " + reissue.json)
    reissue
  }

  def burnAsset(request: BurnRequest, wallet: Wallet): Try[BurnTransaction] = Try {
    val sender = wallet.privateKeyAccount(request.sender).get
    val tx = BurnTransaction.create(sender,
      Base58.decode(request.assetId).get,
      request.quantity,
      request.fee,
      getTimestamp)
    if (isValid(tx, tx.timestamp)) onNewOffchainTransaction(tx)
    else throw new StateCheckFailed("Invalid burn transaction generated: " + tx.json)
    tx
  }

  private var txTime: Long = 0

  private def getTimestamp: Long = synchronized {
    txTime = Math.max(NTP.correctedTime(), txTime + 1)
    txTime
  }

  def createPayment(sender: PrivateKeyAccount, recipient: Account, amount: Long, fee: Long): PaymentTransaction = {
    val time = getTimestamp
    val sig = PaymentTransaction.generateSignature(sender, recipient, amount, fee, time)
    val payment = new PaymentTransaction(new PublicKeyAccount(sender.publicKey), recipient, amount, fee, time, sig)
    if (isValid(payment, payment.timestamp)) onNewOffchainTransaction(payment)
    payment
  }

  def createOrderMatch(buyOrder: Order, sellOrder: Order, price: Long, amount: Long,
                       buyMatcherFee: Long, sellMatcherFee: Long, fee: Long, wallet: Wallet): Try[OrderMatch] = Try {
    val matcher = wallet.privateKeyAccount(buyOrder.matcher.address).get
    val om = OrderMatch.create(matcher, buyOrder, sellOrder, price, amount, buyMatcherFee, sellMatcherFee, fee, getTimestamp)
    if (isValid(om, om.timestamp)) onNewOffchainTransaction(om)
    else throw new StateCheckFailed("Invalid ordermatch transaction  generated: " + om.json)
    om
  }

  override def genesisData: BlockField[StoredInBlock] = {
    val ipoMembers = List(
      "3Mb4mR4taeYS3wci78SntztFwLoaS6Wbg81",
      "3MbWTyn6Tg7zL6XbdN8TLcFMfhWX77hKcmc",
      "3Mn3UAtrpGY3cwiqLYf973q29oDR2LpnMYv"
    )

    val timestamp = 0L
    val totalBalance = InitialBalance

    val txs = ipoMembers.map { addr =>
      val recipient = new Account(addr)
      GenesisTransaction(recipient, totalBalance / ipoMembers.length, timestamp)
    }

    TransactionsBlockField(txs)
  }

  /** Check whether tx is valid on current state and not expired yet
    */
  override def isValid(tx: Transaction, blockTime: Long): Boolean = try {
    val lastBlockTs = blockStorage.history.lastBlock.timestampField.value
    val notExpired = (lastBlockTs - tx.timestamp).millis <= MaxTimeForUnconfirmed
    notExpired && blockStorage.state.isValid(tx, blockTime)
  } catch {
    case e: UnsupportedOperationException =>
      log.debug(s"DB can't find last block because of unexpected modification")
      false
    case NonFatal(t) =>
      log.error(s"Unexpected error during validation", t)
      throw t
  }

  /** Check whether txs is valid on current state and not expired yet
    */
  def isValid(txs: Seq[Transaction], blockTime: Long): Boolean = try {
    val notExpiredForAll = txs.forall(tx => {
      val lastBlockTs = blockStorage.history.lastBlock.timestampField.value
      (lastBlockTs - tx.timestamp).millis <= MaxTimeForUnconfirmed
    })
    notExpiredForAll && blockStorage.state.isValid(txs, None, blockTime)
  } catch {
    case e: UnsupportedOperationException =>
      log.debug(s"DB can't find last block because of unexpected modification")
      false
    case NonFatal(t) =>
      log.error(s"Unexpected error during validation", t)
      throw t
  }

  override def isValid(block: Block): Boolean = try {
    val lastBlockTs = blockStorage.history.lastBlock.timestampField.value
    lazy val txsAreNew = block.transactions.forall { tx => (lastBlockTs - tx.timestamp).millis <= MaxTxAndBlockDiff }
    lazy val blockIsValid = blockStorage.state.isValid(block.transactions, blockStorage.history.heightOf(block), block.timestampField.value)
    if (!txsAreNew) log.debug(s"Invalid txs in block ${block.encodedId}: txs from the past")
    if (!blockIsValid) log.debug(s"Invalid txs in block ${block.encodedId}: not valid txs")
    txsAreNew && blockIsValid
  } catch {
    case e: UnsupportedOperationException =>
      log.debug(s"DB can't find last block because of unexpected modification")
      false
    case NonFatal(t) =>
      log.error(s"Unexpected error during validation", t)
      throw t
  }
}

object SimpleTransactionModule {
  type StoredInBlock = Seq[Transaction]

  val MaxTimeDrift = 15.seconds
  val MaxTimeForUnconfirmed = 90.minutes
  val MaxTxAndBlockDiff = 2.hour
  val MaxTransactionsPerBlock = 100
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy