diff --git a/node/build.sbt b/node/build.sbt index f6739d6..be63d5a 100644 --- a/node/build.sbt +++ b/node/build.sbt @@ -98,7 +98,8 @@ libraryDependencies ++= Dependencies.awsDependencies ++ Dependencies.javaplot ++ Dependencies.pureConfig ++ - Dependencies.silencer + Dependencies.silencer ++ + Dependencies.bouncyCastle dependencyOverrides ++= Seq(Dependencies.AkkaHTTP) ++ diff --git a/node/src/main/scala/com/wavesenterprise/api/ValidLong.scala b/node/src/main/scala/com/wavesenterprise/api/ValidLong.scala new file mode 100644 index 0000000..1f65fd6 --- /dev/null +++ b/node/src/main/scala/com/wavesenterprise/api/ValidLong.scala @@ -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) + } + } + } +} diff --git a/node/src/main/scala/com/wavesenterprise/api/http/AddressApiRoute.scala b/node/src/main/scala/com/wavesenterprise/api/http/AddressApiRoute.scala index 95166c7..e8cc43a 100644 --- a/node/src/main/scala/com/wavesenterprise/api/http/AddressApiRoute.scala +++ b/node/src/main/scala/com/wavesenterprise/api/http/AddressApiRoute.scala @@ -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}") } } } diff --git a/node/src/main/scala/com/wavesenterprise/api/http/consensus/ConsensusApiRoute.scala b/node/src/main/scala/com/wavesenterprise/api/http/consensus/ConsensusApiRoute.scala index c4bd1a5..dc1d923 100644 --- a/node/src/main/scala/com/wavesenterprise/api/http/consensus/ConsensusApiRoute.scala +++ b/node/src/main/scala/com/wavesenterprise/api/http/consensus/ConsensusApiRoute.scala @@ -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 @@ -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) + } + } } } diff --git a/node/src/main/scala/com/wavesenterprise/database/migration/MainnetMigration.scala b/node/src/main/scala/com/wavesenterprise/database/migration/MainnetMigration.scala new file mode 100644 index 0000000..8501e5b --- /dev/null +++ b/node/src/main/scala/com/wavesenterprise/database/migration/MainnetMigration.scala @@ -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) + } +} diff --git a/node/src/main/scala/com/wavesenterprise/database/migration/MigrationV11.scala b/node/src/main/scala/com/wavesenterprise/database/migration/MigrationV11.scala index f1f3671..85e85d5 100644 --- a/node/src/main/scala/com/wavesenterprise/database/migration/MigrationV11.scala +++ b/node/src/main/scala/com/wavesenterprise/database/migration/MigrationV11.scala @@ -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() diff --git a/node/src/main/scala/com/wavesenterprise/database/rocksdb/RocksDBWriter.scala b/node/src/main/scala/com/wavesenterprise/database/rocksdb/RocksDBWriter.scala index 06e0ea4..1ec7b8d 100644 --- a/node/src/main/scala/com/wavesenterprise/database/rocksdb/RocksDBWriter.scala +++ b/node/src/main/scala/com/wavesenterprise/database/rocksdb/RocksDBWriter.scala @@ -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._ @@ -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, @@ -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 = { diff --git a/node/src/main/scala/com/wavesenterprise/http/NodeApiRoute.scala b/node/src/main/scala/com/wavesenterprise/http/NodeApiRoute.scala index 9be41aa..87e96ed 100644 --- a/node/src/main/scala/com/wavesenterprise/http/NodeApiRoute.scala +++ b/node/src/main/scala/com/wavesenterprise/http/NodeApiRoute.scala @@ -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") + } + } } diff --git a/node/src/test/scala/com/wavesenterprise/http/AddressRouteSpec.scala b/node/src/test/scala/com/wavesenterprise/http/AddressRouteSpec.scala index da43c94..cf91423 100644 --- a/node/src/test/scala/com/wavesenterprise/http/AddressRouteSpec.scala +++ b/node/src/test/scala/com/wavesenterprise/http/AddressRouteSpec.scala @@ -180,9 +180,9 @@ class AddressRouteSpec (blockchain.resolveAlias(_: Alias)).when(alias).returns(Left(AliasDoesNotExist(alias))).once Get(routePath(s"/validate/$aliasStr")) ~> route ~> check { - val r = responseAs[AddressApiRoute.ValiditySingle] - r.addressOrAlias shouldEqual aliasStr - r.valid shouldBe false + status shouldEqual StatusCodes.BadRequest + val error = responseAs[String] + error should include("Invalid") } } } @@ -190,9 +190,9 @@ class AddressRouteSpec routePath("/validate/{addressOrAlias} for invalid aliases") in { forAll(invalidAliasStringGen.map(_.filterNot(c => "`%#&=?".contains(c)))) { aliasStr => Get(routePath(s"/validate/$aliasStr")) ~> route ~> check { - val r = responseAs[AddressApiRoute.ValiditySingle] - r.addressOrAlias shouldEqual aliasStr - r.valid shouldBe false + status shouldEqual StatusCodes.BadRequest + val error = responseAs[String] + error should include("Invalid") } } } diff --git a/node/src/test/scala/com/wavesenterprise/http/ConsensusRouteSpec.scala b/node/src/test/scala/com/wavesenterprise/http/ConsensusRouteSpec.scala index bd2a2c5..7ab0be6 100644 --- a/node/src/test/scala/com/wavesenterprise/http/ConsensusRouteSpec.scala +++ b/node/src/test/scala/com/wavesenterprise/http/ConsensusRouteSpec.scala @@ -218,6 +218,44 @@ class ConsensusRouteSpec } } } + + "return incorrect parameter timestamp" in { + forAll(Gen.choose(1, 20)) { numMiners => + val addressToPermMap: Map[Address, PermissionOp] = (1 to numMiners).map { num => + Wallet.generateNewAccount().toAddress -> PermissionOp(OpType.Add, Role.Miner, num.toLong, None) + }.toMap + val bc = mockMyBlockchain(Wallet.generateNewAccount(), addressToPermMap) + + val roundDuration = 60.seconds + val syncDuration = 15.seconds + val banDurationBlocks = 100 + val warningsForBan = 3 + val maxBansPercentage = 50 + val route = + new ConsensusApiRoute( + restAPISettings, + time, + bc, + FunctionalitySettings.TESTNET, + ConsensusSettings.PoASettings(roundDuration, syncDuration, banDurationBlocks, warningsForBan, maxBansPercentage), + ownerAddress, + apiComputationsScheduler + ).route + + Get(routePath(s"/miners/${Long.MinValue}")) ~> route ~> check { + status shouldEqual StatusCodes.BadRequest + val error = responseAs[String] + error should include("Invalid parameter") + } + + val str = "test" + Get(routePath(s"/miners/$str")) ~> route ~> check { + status shouldEqual StatusCodes.BadRequest + val error = responseAs[String] + error should include("Unable to parse Long from") + } + } + } } routePath("/bannedMiners/{height}") - {