Skip to content

Commit

Permalink
Merge branch 'WE-8753-invalid-block-fix' into 'release-1.13'
Browse files Browse the repository at this point in the history
WE-8753 - Invalid block fix

Closes WE-8756

See merge request development/we/node/open-source-node!25
  • Loading branch information
apospelov committed Nov 17, 2023
2 parents a06a534 + 4463d8c commit c2e81e4
Show file tree
Hide file tree
Showing 10 changed files with 329 additions and 26 deletions.
3 changes: 2 additions & 1 deletion node/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ libraryDependencies ++=
Dependencies.awsDependencies ++
Dependencies.javaplot ++
Dependencies.pureConfig ++
Dependencies.silencer
Dependencies.silencer ++
Dependencies.bouncyCastle

dependencyOverrides ++=
Seq(Dependencies.AkkaHTTP) ++
Expand Down
76 changes: 76 additions & 0 deletions node/src/main/scala/com/wavesenterprise/api/ValidLong.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.wavesenterprise.api

import akka.http.scaladsl.server.Directives.complete
import akka.http.scaladsl.server.StandardRoute
import cats.data.{NonEmptyList, Validated}
import cats.implicits.catsSyntaxEither
import cats.instances.list.catsStdInstancesForList
import cats.syntax.traverse.toTraverseOps
import com.wavesenterprise.api.http.ApiError
import enumeratum.{Enum, EnumEntry}

import scala.collection.immutable

sealed trait ValidLong extends EnumEntry {
protected def validation: Long => Boolean
protected def description: String

protected final def validate(i: Long): Validated[String, Long] = {
Validated.cond(validation(i), i, s"'$i' must be $description")
}

protected final def validateStr(str: String): Validated[String, Long] = {
Validated
.catchOnly[NumberFormatException](str.toLong)
.leftMap(_ => s"Unable to parse Long from '$str'")
.andThen(validate)
}
}

object ValidLong extends Enum[ValidLong] {
override val values: immutable.IndexedSeq[ValidLong] = findValues

case object PositiveLong extends ValidLong {
override protected val validation: Long => Boolean = _ > 0
override protected val description: String = "positive"

def apply(str: String): Validated[String, Long] = validateStr(str)
def apply(i: Long): Validated[String, Long] = validate(i)
}

case object NonNegativeLong extends ValidLong {
override protected val validation: Long => Boolean = _ >= 0
override protected val description: String = "non-negative"

def apply(str: String): Validated[String, Long] = validateStr(str)
def apply(i: Long): Validated[String, Long] = validate(i)
}

implicit class ValidatedLongListExt(private val v: List[Validated[String, Long]]) extends AnyVal {
def toApiError: Either[ApiError, List[Long]] = {
v.traverse(_.leftMap(NonEmptyList.of(_))).toEither.leftMap { errors =>
ApiError.CustomValidationError(s"Invalid parameters: [${errors.toList.mkString(", ")}]")
}
}

def processRoute(f: List[Long] => StandardRoute): StandardRoute = {
v.toApiError match {
case Right(validLongs) => f(validLongs)
case Left(apiError) => complete(apiError)
}
}
}

implicit class ValidatedLongExt(private val v: Validated[String, Long]) extends AnyVal {
def toApiError[T]: Either[ApiError, Long] = {
v.toEither.leftMap(err => ApiError.CustomValidationError(s"Invalid parameter: $err"))
}

def processRoute(f: Long => StandardRoute): StandardRoute = {
v.toApiError match {
case Right(validLong) => f(validLong)
case Left(apiError) => complete(apiError)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ class AddressApiRoute(addressApiService: AddressApiService,
complete {
checkAddressOrAliasValid(addressOrAliasStr) match {
case Right(_) => ValiditySingle(addressOrAliasStr, valid = true, None)
case Left(err) => ValiditySingle(addressOrAliasStr, valid = false, Some(err.toString))
case Left(err) => CustomValidationError(s"Invalid address or alias: ${err.toString}")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.wavesenterprise.api.http.consensus
import akka.http.scaladsl.server.Route
import com.wavesenterprise.account.Address
import com.wavesenterprise.api.ValidInt._
import com.wavesenterprise.api.ValidLong._
import com.wavesenterprise.api.http.ApiError.RequestedHeightDoesntExist
import com.wavesenterprise.api.http._
import com.wavesenterprise.api.http.auth.ApiProtectionLevel.ApiKeyProtection
import com.wavesenterprise.api.http.auth.AuthRole.Administrator
Expand Down Expand Up @@ -171,32 +173,36 @@ class ConsensusApiRoute(val settings: ApiSettings,
* Retrieves list of miners for timestamp of a block at given height or
* error with negative height
**/
def minersAtHeight: Route = (path("minersAtHeight" / Segment) & get)(heightStr => {
PositiveInt(heightStr).processRoute {
height =>
withExecutionContext(scheduler) {
def minersAtHeight: Route = (path("minersAtHeight" / Segment) & get) { heightStr =>
withExecutionContext(scheduler) {
PositiveInt(heightStr).processRoute {
height =>
complete {
for {
blockHeader <- blockchain.blockHeaderAt(height)
blockHeader <- blockchain.blockHeaderAt(height).toRight[ApiError](RequestedHeightDoesntExist(height, blockchain.height))
requestedTimestamp = blockHeader.timestamp
minerAddresses = blockchain.miners.currentMinersSet(requestedTimestamp).map(_.toString)
} yield MinersAtHeight(minerAddresses.toSeq, height)
}
}
}

}
})
}

/**
* GET /consensus/miners/{timestamp}
*
* Retrieves list of miners at given timestamp
**/
def minersAtTimestamp: Route = (path("miners" / LongNumber) & get) { atTimestamp =>
withExecutionContext(scheduler) {
val minerAddresses = blockchain.miners.currentMinersSet(atTimestamp).map(_.toString)
complete {
MinersAtTimestamp(minerAddresses.toSeq, atTimestamp)
}
def minersAtTimestamp: Route = (path("miners" / Segment) & get) { atTimestampStr =>
PositiveLong(atTimestampStr).processRoute {
atTimestamp =>
withExecutionContext(scheduler) {
val minerAddresses = blockchain.miners.currentMinersSet(atTimestamp).map(_.toString)
complete {
MinersAtTimestamp(minerAddresses.toSeq, atTimestamp)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package com.wavesenterprise.database.migration

import com.google.common.io.ByteArrayDataOutput
import com.google.common.io.ByteStreams.newDataOutput
import com.google.common.primitives.{Ints, Shorts}
import com.wavesenterprise.account.{Address, PublicKeyAccount}
import com.wavesenterprise.crypto
import com.wavesenterprise.database.KeyHelpers.hBytes
import com.wavesenterprise.database.keys.ContractCFKeys.{ContractIdsPrefix, ContractPrefix}
import com.wavesenterprise.database.rocksdb.MainDBColumnFamily.ContractCF
import com.wavesenterprise.database.rocksdb.{MainDBColumnFamily, MainReadWriteDB}
import com.wavesenterprise.database.{InternalRocksDBSet, MainDBKey, WEKeys}
import com.wavesenterprise.docker.ContractApiVersion
import com.wavesenterprise.docker.validator.ValidationPolicy
import com.wavesenterprise.serialization.{BinarySerializer, ModelsBinarySerializer}
import com.wavesenterprise.state.ByteStr
import com.wavesenterprise.utils.DatabaseUtils.ByteArrayDataOutputExt

object MainnetMigration {

object KeysInfo {
def legacyContractInfoKey(contractId: ByteStr)(height: Int): MainDBKey[Option[LegacyContractInfo]] =
MainDBKey.opt("contract", ContractCF, hBytes(ContractPrefix, height, contractId.arr), parseLegacyContractInfo, writeLegacyContractInfo)

def modernContractInfoKey(contractId: ByteStr)(height: Int): MainDBKey[Option[ModernContractInfo]] =
MainDBKey.opt("contract", ContractCF, hBytes(ContractPrefix, height, contractId.arr), parseModernContractInfo, writeModernContractInfo)
}

private val ContractsIdSet = new InternalRocksDBSet[ByteStr, MainDBColumnFamily](
name = "contract-ids",
columnFamily = ContractCF,
prefix = Shorts.toByteArray(ContractIdsPrefix),
itemEncoder = (_: ByteStr).arr,
itemDecoder = ByteStr(_),
keyConstructors = MainDBKey
)

def apply(rw: MainReadWriteDB): Unit = {
for {
contractId <- ContractsIdSet.members(rw)
contractHistory = rw.get(WEKeys.contractHistory(contractId))
height <- contractHistory
oldContractInfo <- rw.get(KeysInfo.legacyContractInfoKey(contractId)(height)).toSeq
} yield {
val newContractInfo = ModernContractInfo(
creator = oldContractInfo.creator,
contractId = oldContractInfo.contractId,
image = oldContractInfo.image,
imageHash = oldContractInfo.imageHash,
version = oldContractInfo.version,
active = oldContractInfo.active,
validationPolicy = ValidationPolicy.Default,
apiVersion = ContractApiVersion.Initial,
isConfidential = false,
groupParticipants = Set(),
groupOwners = Set()
)
rw.put(KeysInfo.modernContractInfoKey(contractId)(height), Some(newContractInfo))
}
}

case class LegacyContractInfo(creator: PublicKeyAccount,
contractId: ByteStr,
image: String,
imageHash: String,
version: Int,
active: Boolean,
validationPolicy: ValidationPolicy,
apiVersion: ContractApiVersion)

case class ModernContractInfo(creator: PublicKeyAccount,
contractId: ByteStr,
image: String,
imageHash: String,
version: Int,
active: Boolean,
validationPolicy: ValidationPolicy,
apiVersion: ContractApiVersion,
isConfidential: Boolean,
groupParticipants: Set[Address],
groupOwners: Set[Address])

def writeLegacyContractInfo(contractInfo: LegacyContractInfo): Array[Byte] = {
import contractInfo._
val ndo = newDataOutput()
ndo.writePublicKey(creator)
ndo.writeBytes(contractId.arr)
ndo.writeString(image)
ndo.writeString(imageHash)
ndo.writeInt(version)
ndo.writeBoolean(active)
ndo.write(contractInfo.validationPolicy.bytes)
ndo.write(contractInfo.apiVersion.bytes)
ndo.toByteArray
}

def parseLegacyContractInfo(bytes: Array[Byte]): LegacyContractInfo = {
val (creatorBytes, creatorEnd) = bytes.take(crypto.KeyLength) -> crypto.KeyLength
val (contractId, contractIdEnd) = BinarySerializer.parseShortByteStr(bytes, creatorEnd)
val (image, imageEnd) = BinarySerializer.parseShortString(bytes, contractIdEnd)
val (imageHash, imageHashEnd) = BinarySerializer.parseShortString(bytes, imageEnd)
val (version, versionEnd) = Ints.fromByteArray(bytes.slice(imageHashEnd, imageHashEnd + Ints.BYTES)) -> (imageHashEnd + Ints.BYTES)
val (active, activeEnd) = (bytes(versionEnd) == 1) -> (versionEnd + 1)
val (validationPolicy, validationPolicyEnd) = ValidationPolicy.fromBytesUnsafe(bytes, activeEnd)
val (apiVersion, _) = ContractApiVersion.fromBytesUnsafe(bytes, validationPolicyEnd)

LegacyContractInfo(PublicKeyAccount(creatorBytes), contractId, image, imageHash, version, active, validationPolicy, apiVersion)
}

def writeModernContractInfo(contractInfo: ModernContractInfo): Array[Byte] = {
def addressWriter(address: Address, output: ByteArrayDataOutput): Unit = {
output.write(address.bytes.arr)
}

import contractInfo._
val ndo = newDataOutput()
ndo.writePublicKey(creator)
ndo.writeBytes(contractId.arr)
ndo.writeString(image)
ndo.writeString(imageHash)
ndo.writeInt(version)
ndo.writeBoolean(active)
ndo.write(contractInfo.validationPolicy.bytes)
ndo.write(contractInfo.apiVersion.bytes)
ndo.writeBoolean(isConfidential)
BinarySerializer.writeShortIterable(contractInfo.groupParticipants, addressWriter, ndo)
BinarySerializer.writeShortIterable(contractInfo.groupOwners, addressWriter, ndo)

ndo.toByteArray
}

def parseModernContractInfo(bytes: Array[Byte]): ModernContractInfo = {

val (creatorBytes, creatorEnd) = bytes.take(crypto.KeyLength) -> crypto.KeyLength
val (contractId, contractIdEnd) = BinarySerializer.parseShortByteStr(bytes, creatorEnd)
val (image, imageEnd) = BinarySerializer.parseShortString(bytes, contractIdEnd)
val (imageHash, imageHashEnd) = BinarySerializer.parseShortString(bytes, imageEnd)
val (version, versionEnd) = Ints.fromByteArray(bytes.slice(imageHashEnd, imageHashEnd + Ints.BYTES)) -> (imageHashEnd + Ints.BYTES)
val (active, activeEnd) = (bytes(versionEnd) == 1) -> (versionEnd + 1)
val (validationPolicy, validationPolicyEnd) = ValidationPolicy.fromBytesUnsafe(bytes, activeEnd)
val (apiVersion, apiVersionEnd) = ContractApiVersion.fromBytesUnsafe(bytes, validationPolicyEnd)
val (isConfidential, isConfidentialEnd) = (bytes(apiVersionEnd) == 1) -> (apiVersionEnd + 1)
val (groupParticipants, groupParticipantsEnd) = ModelsBinarySerializer.parseAddressesSet(bytes, isConfidentialEnd)
val (groupOwners, _) = ModelsBinarySerializer.parseAddressesSet(bytes, groupParticipantsEnd)

ModernContractInfo(PublicKeyAccount(creatorBytes),
contractId,
image,
imageHash,
version,
active,
validationPolicy,
apiVersion,
isConfidential,
groupParticipants,
groupOwners)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ object MigrationV11 {
imageHash = oldContractInfo.imageHash,
version = oldContractInfo.version,
active = oldContractInfo.active,
validationPolicy = ValidationPolicy.Default,
apiVersion = ContractApiVersion.Initial,
validationPolicy = oldContractInfo.validationPolicy,
apiVersion = oldContractInfo.apiVersion,
isConfidential = false,
groupParticipants = Set(),
groupOwners = Set()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.wavesenterprise.database.keys.CrlKey
import com.wavesenterprise.database.rocksdb.MainDBColumnFamily.CertsCF
import com.wavesenterprise.database.docker.{KeysPagination, KeysRequest}
import com.wavesenterprise.database.keys.{ContractCFKeys, LeaseCFKeys}
import com.wavesenterprise.database.migration.MainnetMigration
import com.wavesenterprise.docker.ContractInfo
import com.wavesenterprise.features.BlockchainFeature
import com.wavesenterprise.privacy._
Expand Down Expand Up @@ -358,6 +359,14 @@ class RocksDBWriter(val storage: MainRocksDBStorage,

private[this] val crlIssuersSet = WEKeys.crlIssuers(storage)

/**
* Do not delete, important fix
* Block on which an unsuccessful update occurred
*/
private[this] val blockMigration = List(
3550139 -> ByteStr.decodeBase58("4jDmMrRn17w3BpSjN156SUvAwiTuhgheKynF6yftfKFbTvqTe67anLuui4nKBvRBHZDFcdmsGq4jSsVTNKzbAjuv").get
)

// noinspection ScalaStyle
override protected def doAppend(
block: Block,
Expand Down Expand Up @@ -736,6 +745,15 @@ class RocksDBWriter(val storage: MainRocksDBStorage,
leasesForAssetHolderDB.addLastN(rw, leases)
}

/**
* Only on a specific network and at a specific height does
* a special migration apply that fixes a failed update
*
* WE-8755 & WE-8756
*/
if (blockMigration.contains((height, block.uniqueId))) {
MainnetMigration.apply(rw)
}
}

private def addressIdUnsafe(address: Address): BigInt = {
Expand Down
12 changes: 9 additions & 3 deletions node/src/main/scala/com/wavesenterprise/http/NodeApiRoute.scala
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,15 @@ class NodeApiRoute(nodeSetting: WESettings,
**/
def loggingEditRoute: Route = (path("logging") & post) {
json[ChangeLoggerLevelRequest] { req =>
val lc = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]
lc.getLogger(req.logger).setLevel(Level.valueOf(req.level))
Response.OK + (req.logger -> req.level)
req.level match {
case "ALL" | "DEBUG" | "TRACE" | "INFO" | "WARN" | "ERROR" => {
val lc = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]
lc.getLogger(req.logger).setLevel(Level.valueOf(req.level))
Response.OK + (req.logger -> req.level)
}
case _ => CustomValidationError(s"This name level ${req.level} is not correct")
}

}
}

Expand Down
Loading

0 comments on commit c2e81e4

Please sign in to comment.