diff --git a/.gitignore b/.gitignore index a0b6a0c..f38c6df 100644 --- a/.gitignore +++ b/.gitignore @@ -42,9 +42,6 @@ envfile .metals/ .bloop/ -# State migration app -!state-migration/migration.jar - # vscode .vscode project/metals.sbt diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0705622..b1260ed 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ -image: ${REGISTRY}/weit/docker:19.03.1 +image: ${REGISTRY}/it/docker:19.03.1 services: - - name: ${REGISTRY}/weit/docker:19.03.1-dind + - name: ${REGISTRY}/it/docker:19.03.1-dind alias: docker variables: @@ -14,7 +14,7 @@ stages: - publish .create-environment: &create-environment - image: ${REGISTRY}/weit/hseeberger/scala-sbt:11.0.14.1_1.6.2_2.12.15 + image: ${REGISTRY}/it/hseeberger/scala-sbt:11.0.14.1_1.6.2_2.12.15 tags: - wavesenterprise before_script: @@ -138,39 +138,94 @@ publish-docker-images: stage: publish tags: - wavesenterprise + except: + - /^v[0-9].*$/ needs: - compile - assembly-node-jar - assembly-generator-jar + before_script: + - mkdir -p $HOME/.docker + - echo $DOCKER_AUTH_CONFIG > $HOME/.docker/config.json script: - - docker login -u "${REGISTRY_USER}" -p "${REGISTRY_PASSWORD}" "${REGISTRY}" # Pull the latest image for using cache - - docker pull "${REGISTRY}/we/open-source-node:latest" || true + - docker pull "${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest" || true # open-source-node image - cat Dockerfile | sed "s/\${REGISTRY}/${REGISTRY}/g" | docker build --rm - --cache-from ${REGISTRY}/we/open-source-node:latest - --tag ${REGISTRY}/we/open-source-node:${CI_COMMIT_REF_NAME} - --tag ${REGISTRY}/we/open-source-node:latest + --cache-from ${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest + --tag ${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:${CI_COMMIT_REF_NAME} + --tag ${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest --file - . - - docker push "${REGISTRY}/we/open-source-node:${CI_COMMIT_REF_NAME}" - - docker push "${REGISTRY}/we/open-source-node:latest" + - docker push "${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:${CI_COMMIT_REF_NAME}" + - docker push "${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest" + + # open-source-node scratch image + - docker pull "${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest-scratch" || true + - cat Dockerfile.scratch | docker build + --rm + --cache-from ${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest-scratch + --build-arg REGISTRY=${REGISTRY} + --build-arg NODE_VERSION=${CI_COMMIT_REF_NAME} + --tag ${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:${CI_COMMIT_REF_NAME}-scratch + --tag ${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest-scratch + --file - + . + - docker push "${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:${CI_COMMIT_REF_NAME}-scratch" + - docker push "${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest-scratch" + cache: { } + dependencies: + - compile + - assembly-node-jar + - assembly-generator-jar + +publish-clean-docker-images: + stage: publish + tags: + - wavesenterprise + only: + - /^v[0-9].*$/ + needs: + - compile + - assembly-node-jar + - assembly-generator-jar + before_script: + - mkdir -p $HOME/.docker + - echo $DOCKER_AUTH_CONFIG > $HOME/.docker/config.json + script: + # Pull the latest image for using cache + - docker pull "${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest" || true + # open-source-node image + - cat Dockerfile | sed "s/\${REGISTRY}/${REGISTRY}/g" | docker build + --rm + --cache-from ${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest + --tag ${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:${CI_COMMIT_REF_NAME} + --tag ${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest + --tag registry.wavesenterprise.com/mainnet/$CI_PROJECT_NAME:${CI_COMMIT_REF_NAME} + --file - + . + - docker push "${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:${CI_COMMIT_REF_NAME}" + - docker push "${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest" + - docker push "registry.wavesenterprise.com/mainnet/$CI_PROJECT_NAME:${CI_COMMIT_REF_NAME}" + # open-source-node scratch image - - docker pull "${REGISTRY}/we/open-source-node:latest-scratch" || true + - docker pull "${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest-scratch" || true - cat Dockerfile.scratch | docker build --rm - --cache-from ${REGISTRY}/we/open-source-node:latest-scratch + --cache-from ${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest-scratch --build-arg REGISTRY=${REGISTRY} --build-arg NODE_VERSION=${CI_COMMIT_REF_NAME} - --tag ${REGISTRY}/we/open-source-node:${CI_COMMIT_REF_NAME}-scratch - --tag ${REGISTRY}/we/open-source-node:latest-scratch + --tag ${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:${CI_COMMIT_REF_NAME}-scratch + --tag registry.wavesenterprise.com/mainnet/$CI_PROJECT_NAME:${CI_COMMIT_REF_NAME}-scratch + --tag ${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest-scratch --file - . - - docker push "${REGISTRY}/we/open-source-node:${CI_COMMIT_REF_NAME}-scratch" - - docker push "${REGISTRY}/we/open-source-node:latest-scratch" + - docker push "${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:${CI_COMMIT_REF_NAME}-scratch" + - docker push "registry.wavesenterprise.com/mainnet/$CI_PROJECT_NAME:${CI_COMMIT_REF_NAME}-scratch" + - docker push "${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest-scratch" cache: { } dependencies: - compile @@ -190,20 +245,20 @@ publish-images-dockerhub: - docker login -u "${REGISTRY_USER}" -p "${REGISTRY_PASSWORD}" "${REGISTRY}" # Pull latest image for using cache - - docker pull "${REGISTRY}/we/open-source-node:latest" || true + - docker pull "${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest" || true - cat Dockerfile | docker build --rm - --cache-from ${REGISTRY}/we/open-source-node:latest + --cache-from ${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest --tag wavesenterprise/node:${CI_COMMIT_REF_NAME} --file - . - docker push "wavesenterprise/node:${CI_COMMIT_REF_NAME}" - - docker pull "${REGISTRY}/we/open-source-node:latest-scratch" || true + - docker pull "${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest-scratch" || true - cat Dockerfile.scratch | docker build --rm - --cache-from ${REGISTRY}/we/open-source-node:latest-scratch + --cache-from ${REGISTRY}/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest-scratch --build-arg REGISTRY=${REGISTRY} --build-arg NODE_VERSION=${CI_COMMIT_REF_NAME} --tag wavesenterprise/node:${CI_COMMIT_REF_NAME}-scratch @@ -238,3 +293,4 @@ publish-nexus-artifacts: - assembly-node-jar - assembly-generator-jar - assembly-transactions-signer-jar + diff --git a/Dockerfile.scratch b/Dockerfile.scratch index 931d11b..4941616 100644 --- a/Dockerfile.scratch +++ b/Dockerfile.scratch @@ -1,7 +1,7 @@ ARG REGISTRY ARG NODE_VERSION -FROM ${REGISTRY}/we/open-source-node:${NODE_VERSION} AS NODE +FROM ${REGISTRY}/development/we/node/open-source-node:${NODE_VERSION} AS NODE RUN sed -i 's/openjdk-[[:digit:]]*/openjdk/g' launcher.py RUN cp -r /usr/local/lib/$( \ diff --git a/README.md b/README.md index ace319c..14aca8a 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Our jar file should now be built and available at `./node/target/node-*.jar` ### Build docker image ``` -docker build --tag wavesenterprise/node:v1.12.2 . +docker build --tag wavesenterprise/node:v1.12.3 . ``` ## Usage diff --git a/node/src/docker/docker-compose.yml b/node/src/docker/docker-compose.yml index cf03537..b759a00 100644 --- a/node/src/docker/docker-compose.yml +++ b/node/src/docker/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: node-0: - image: wavesenterprise/node:v1.12.2 + image: wavesenterprise/node:v1.12.3 ports: - "6862:6862" - "6864:6864" @@ -19,7 +19,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock restart: always node-1: - image: wavesenterprise/node:v1.12.2 + image: wavesenterprise/node:v1.12.3 ports: - "6872:6862" - "6874:6864" @@ -37,7 +37,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock restart: always node-2: - image: wavesenterprise/node:v1.12.2 + image: wavesenterprise/node:v1.12.3 ports: - "6882:6862" - "6884:6864" diff --git a/node/src/docker/launcher.py b/node/src/docker/launcher.py index 4c52629..5657fe8 100755 --- a/node/src/docker/launcher.py +++ b/node/src/docker/launcher.py @@ -134,55 +134,6 @@ def move_files(src, dst): shutil.move(src_f, dst) -def migrate_state(data_directory, we_config_path, state_migration_class): - migrated_file_path = '{}/MIGRATED'.format(data_directory) - is_migrated = os.path.exists(migrated_file_path) - - if is_migrated: - logger.info('Data has been already migrated') - return - - # Execution continues in two cases: - # 1. the state is 1.4-ish and requires migration - # 2. the state is made from scratch on 1.5.0 or 1.5.1, and is ok, but doesn't have MIGRATED file - db_backup = os.path.join(data_directory, 'db_backup') - db_backup_exists = os.path.exists(db_backup) - db_exists = os.path.exists(data_directory) - - if not db_backup_exists and db_exists: - logger.info("Backing up current state: copying from '{}' to '{}'".format(data_directory, db_backup)) - move_files(data_directory, db_backup) - - logger.info('Starting or continuing migration...') - - # build command - cmd = ['{}/bin/java'.format(java_home)] - cmd.extend(['-Xmx1024m']) - cmd.extend(['-Dmigration.source-path={}'.format(db_backup)]) - cmd.extend(['-Dmigration.target-path={}'.format(data_directory)]) - cmd.extend(['-Dmigration.config-path={}'.format(we_config_path)]) - cmd.extend(['-cp', '{}:/node/lib/*'.format(jar_file), state_migration_class]) - - logger.info(' '.join(cmd)) - - p = subprocess.Popen(cmd) - p.wait() - - if p.returncode == 0: - # Create data_directory, if it doesn't exist - if not os.path.exists(data_directory): - logger.info("Creating data-directory '{}'".format(data_directory)) - os.makedirs(data_directory) - # create MIGRATED file at data_directory - open(migrated_file_path, 'w').close() - logger.info("Created '{}' file".format(migrated_file_path)) - if os.path.exists(db_backup): - logger.info('Removing obsolete db backup...') - shutil.rmtree(db_backup) - else: - raise RuntimeError("Migration process failed") - - def clean_data_state(data_dir): if os.path.exists(data_dir): try: @@ -311,7 +262,7 @@ def run_snapshot_starter(data_dir, conf): f"No need to launch SnapshotStarterApp. Datadir is empty: {is_datadir_empty}, genesis is snapshot-based: {is_genesis_snapshot_based}") -def prepare_node(app, state_migration_class): +def prepare_node(app): if app == ExecutableApp.node: data_dir = find_data_directory(conf) clean_state = os.getenv('CLEAN_STATE', False) @@ -321,9 +272,6 @@ def prepare_node(app, state_migration_class): run_snapshot_starter(data_dir, conf) - # Migrate RocksDB to new database scheme - migrate_state(data_dir, config_path, state_migration_class) - def run_cmd(exec_app, node_class, generator_class, default_options): if exec_app == ExecutableApp.generator: @@ -407,8 +355,7 @@ def validate_crypto(config, config_file): validate_crypto(conf, config_path) - state_migration_class = 'com.wavesenterprise.StateMigration' - prepare_node(exec_app, state_migration_class) + prepare_node(exec_app) default_options = [ ('-XX:+', 'AlwaysPreTouch'), diff --git a/node/src/main/resources/open-api-base.json b/node/src/main/resources/open-api-base.json index 6409ccc..b8d39cf 100644 --- a/node/src/main/resources/open-api-base.json +++ b/node/src/main/resources/open-api-base.json @@ -3878,6 +3878,42 @@ } } }, + "/contracts/balance/details/{contractId}": { + "get": { + "description": "Get contract balance details", + "summary": "Contract's balance details", + "tags": [ + "contracts" + ], + "operationId": "getContractBalanceDetails", + "deprecated": false, + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "contractId", + "in": "path", + "required": true, + "type": "string", + "description": "Contract id", + "x-example": "CgqRPcPnexY533gCh2SSvBXh5bca1qMs7KFGntawHGww" + } + ], + "responses": { + "200": { + "description": "Returns key values on contract", + "schema": { + "type": "object", + "items": { + "$ref": "#/definitions/ContractBalanceDetails" + } + }, + "headers": {} + } + } + } + }, "/leasing/active/{address}": { "get": { "description": "List of LeaseTransactions", @@ -6850,7 +6886,7 @@ "contractId", "assetIds" ] - }, + }, "ValiditySingle": { "title": "ValiditySingle", "type": "object", @@ -6916,11 +6952,6 @@ "example": 11, "type": "integer", "format": "int64" - }, - "extraFee": { - "example": 10001, - "type": "integer", - "format": "int32" } }, "required": [ @@ -7001,6 +7032,37 @@ "effective" ] }, + "ContractBalanceDetails": { + "title": "ContractBalanceDetails", + "type": "object", + "properties": { + "contractId": { + "example": "DP5MggKC8GJuLZshCVNSYwBtE6WTRtMM1YPPdcmwbuNg", + "type": "string" + }, + "regular": { + "example": 100000, + "type": "integer", + "format": "int64" + }, + "leasedOut": { + "example": 30000, + "type": "integer", + "format": "int64" + }, + "available": { + "example": 70000, + "type": "integer", + "format": "int64" + } + }, + "required": [ + "address", + "regular", + "leasedOut", + "available" + ] + }, "AddressesMessage": { "title": "AddressesMessage", "type": "object", @@ -8704,7 +8766,7 @@ "properties": { "type": { "example": "snapshot-based", - "type": "string" + "type": "string" }, "block-timestamp": { "example": 1614774779523, diff --git a/node/src/main/scala/com/wavesenterprise/Application.scala b/node/src/main/scala/com/wavesenterprise/Application.scala index 081e2c4..2104560 100644 --- a/node/src/main/scala/com/wavesenterprise/Application.scala +++ b/node/src/main/scala/com/wavesenterprise/Application.scala @@ -24,7 +24,7 @@ import com.wavesenterprise.api.http.acl.PermissionApiRoute import com.wavesenterprise.api.http.alias.AliasApiRoute import com.wavesenterprise.api.http.assets.AssetsApiRoute import com.wavesenterprise.api.http.consensus.ConsensusApiRoute -import com.wavesenterprise.api.http.docker.{ContractsApiRoute, InternalContractsApiRoute} +import com.wavesenterprise.api.http.docker.ContractsApiRoute import com.wavesenterprise.api.http.leasing.LeaseApiRoute import com.wavesenterprise.api.http.privacy.PrivacyApiRoute import com.wavesenterprise.api.http.service._ @@ -685,8 +685,7 @@ class Application(val ownerPasswordMode: OwnerPasswordMode, maybeAnchoring.foreach(_.run()) } - val initRestAPI = settings.api.rest.enable || maybeContractExecutionComponents.isDefined - val dockerApiRoutes: Seq[ApiRoute] = maybeContractExecutionComponents.map(_.contractsRoutes).getOrElse(Seq.empty[ApiRoute]) + val initRestAPI = settings.api.rest.enable || maybeContractExecutionComponents.isDefined val restAPIRoutes: Seq[ApiRoute] = { if (settings.api.rest.enable) { val peersApiService = @@ -778,7 +777,7 @@ class Application(val ownerPasswordMode: OwnerPasswordMode, } if (initRestAPI) { - val allRoutes: Seq[ApiRoute] = predefinedRoutes ++ dockerApiRoutes ++ restAPIRoutes ++ snapshotApiRoutes + val allRoutes: Seq[ApiRoute] = predefinedRoutes ++ restAPIRoutes ++ snapshotApiRoutes val combinedRoute = buildCompositeHttpService(allRoutes, settings.api, metricsSettings.httpRequestsCache, customSwaggerRoute).enrichedCompositeRoute @@ -977,14 +976,6 @@ class Application(val ownerPasswordMode: OwnerPasswordMode, new grpc.service.PrivacyServiceImpl(privacyApiService, contractAuthTokenService, dockerExecutorScheduler) } - protected def buildInternalContractsApiRoute(contractsApiService: ContractsApiService, - settings: ApiSettings, - time: Time, - contractAuthTokenServiceParam: ContractAuthTokenService, - externalNodeOwner: Address, - scheduler: SchedulerService) = - new InternalContractsApiRoute(contractsApiService, settings, time, contractAuthTokenServiceParam, externalNodeOwner, scheduler) - protected def buildContractExecutionComponents( wallet: Wallet, utx: UtxPool, @@ -1005,17 +996,6 @@ class Application(val ownerPasswordMode: OwnerPasswordMode, val localDockerHostResolver = new LocalDockerHostResolver(dockerEngine.docker) - val legacyContractExecutor = LegacyContractExecutor( - dockerEngine, - dockerEngineSettings, - metricsSettings.circuitBreakerCache, - contractAuthTokenService, - contractReusedContainers, - dockerExecutorScheduler, - settings.api.rest.port, - localDockerHostResolver - ) - val grpcContractExecutor = GrpcContractExecutor( dockerEngine, dockerEngineSettings, @@ -1040,14 +1020,10 @@ class Application(val ownerPasswordMode: OwnerPasswordMode, wallet, privacyServiceImpl, activePeerConnections, - schedulerService = schedulers.apiComputationsScheduler, - legacyContractExecutor, grpcContractExecutor, dockerEngine, contractAuthTokenService, contractReusedContainers, - buildInternalContractsApiRoute, - keyBlockIdsCache ) } diff --git a/node/src/main/scala/com/wavesenterprise/StateMigration.scala b/node/src/main/scala/com/wavesenterprise/StateMigration.scala deleted file mode 100644 index 096dd92..0000000 --- a/node/src/main/scala/com/wavesenterprise/StateMigration.scala +++ /dev/null @@ -1,344 +0,0 @@ -package com.wavesenterprise - -import com.google.common.io.ByteStreams.newDataOutput -import com.wavesenterprise.StateMigration.BlockNotFoundException -import com.wavesenterprise.account.AddressSchemeHelper -import com.wavesenterprise.block.{Block, BlockHeader, _} -import com.wavesenterprise.consensus.Consensus -import com.wavesenterprise.crypto.CryptoInitializer -import com.wavesenterprise.database.KeyHelpers.{h, hash} -import com.wavesenterprise.database.rocksdb.ColumnFamily.DefaultCF -import com.wavesenterprise.database.rocksdb._ -import com.wavesenterprise.database.{Key, Keys, PrivacyState, readTxIds} -import com.wavesenterprise.history.BlockchainFactory -import com.wavesenterprise.certs.CertChainStore -import com.wavesenterprise.settings.{CryptoSettings, WESettings} -import com.wavesenterprise.state.appender.BaseAppender.BlockType.Hard -import com.wavesenterprise.state.{ByteStr, MiningConstraintsHolder, NG} -import com.wavesenterprise.transaction.BlockchainUpdater -import com.wavesenterprise.utils.NTPUtils.NTPExt -import com.wavesenterprise.utils.{NTP, ScorexLogging, Time} -import monix.eval.{Coeval, Task} -import monix.execution.Scheduler.forkJoin -import monix.execution.schedulers.SchedulerService -import monix.execution.{CancelableFuture, Scheduler} -import monix.reactive.{Observable, OverflowStrategy} -import pureconfig.generic.semiauto.deriveReader -import pureconfig.{ConfigObjectSource, ConfigReader, ConfigSource} - -import java.io.File -import java.nio.file.{Files, NoSuchFileException, Paths, StandardCopyOption} -import java.util.concurrent.atomic.AtomicBoolean -import scala.collection.JavaConverters._ -import scala.concurrent._ -import scala.concurrent.duration._ -import scala.util.control.NoStackTrace -import scala.util.{Failure, Success, Try} - -case class MigrationSettings(sourcePath: String, targetPath: String, configPath: Option[String], bufferSize: Int, backpressure: Int) - -object MigrationSettings { - - val configPath: String = "migration" - - implicit val configReader: ConfigReader[MigrationSettings] = deriveReader -} - -abstract class StateMigrationBase extends App with ScorexLogging { - - case class BlockNotFoundException(height: Int) extends NoStackTrace - - val DefaultSettings: ConfigObjectSource = ConfigSource.string(s"""migration { - | buffer-size = 10 - | backpressure = 30 - |}""".stripMargin) - - val configSource = args.headOption.map(new File(_)) match { - case Some(configFile) if configFile.exists() => ConfigSource.file(configFile) - case Some(configFile) => exitWithError(s"Configuration file '$configFile' does not exist!") - case None => ConfigSource.empty - } - val migrationSettings = ConfigSource.defaultOverrides - .withFallback(configSource) - .withFallback(DefaultSettings) - .at(MigrationSettings.configPath) - .loadOrThrow[MigrationSettings] - - val (config, _) = readConfigOrTerminate(migrationSettings.configPath) - val cryptoSettings = ConfigSource.fromConfig(config).at(WESettings.configPath).loadOrThrow[CryptoSettings] - CryptoInitializer.init(cryptoSettings).left.foreach(error => exitWithError(error.message)) - - AddressSchemeHelper.setAddressSchemaByte(config) - val weSettings = ConfigSource.fromConfig(config).at(WESettings.configPath).loadOrThrow[WESettings] - - val shutdownInitiated = new AtomicBoolean(false) - - var sourceDb: RocksDBStorage = _ - var targetDb: RocksDBStorage = _ - var schedulers: AppSchedulers = _ - var time: NTP = _ - var f: CancelableFuture[Unit] = _ - - implicit val scheduler: SchedulerService = - forkJoin(Runtime.getRuntime.availableProcessors(), 64, "migration-pool", reporter = log.error("Error in migration", _)) - - sys.addShutdownHook { closeAll() } - - try { - // Let's check if source directory exists and is non-empty - Try(Files.list(Paths.get(migrationSettings.sourcePath))) match { - // source directory is non-empty, expecting to find state there - case Success(files) if files.findAny().isPresent => - Try { - sourceDb = RocksDBStorage.openDB(migrationSettings.sourcePath, migrateScheme = false, MigrationSourceParams) - } match { - case Failure(ex) => - log.info("Failed to open source DB. Skipping migration", ex) - case Success(_) if storageIsEmpty(sourceDb) => - log.info("Source DB is empty. Skipping migration") - case Success(_) => - targetDb = RocksDBStorage.openDB(migrationSettings.targetPath, params = MigrationDestinationParams) - schedulers = new AppSchedulers - time = NTP(weSettings.blockchain.consensus.consensusType, weSettings.ntp)(schedulers.ntpTimeScheduler) - - // Open source DB with target parameters to handle a specific case when migration has already been done. - val lazySourceAsTargetDb = Coeval.evalOnce { - sourceDb.close() - sourceDb = RocksDBStorage.openDB(migrationSettings.sourcePath, migrateScheme = false, MigrationDestinationParams) - sourceDb - } - - val migrationService = buildMigrationService(lazySourceAsTargetDb) - - f = migrationService.runMigration(migrationSettings.bufferSize, migrationSettings.backpressure) - - Await.result(f, Duration.Inf) - log.info("Migration finished successfully") - } - case Success(_) => - log.info("Source directory is empty. Skipping migration") - case Failure(_: NoSuchFileException) => - log.info("Source directory doesn't exist. Skipping migration") - case Failure(ex) => - throw ex - } - } catch { - case ex: Throwable => exitWithError("Migration failed with error", Some(ex)) - } finally { - closeAll() - } - - protected def buildMigrationService(lazySourceAsTargetDb: Coeval[RocksDBStorage]) = - new MigrationService( - sourceDb, - targetDb, - lazySourceAsTargetDb, - weSettings, - migrationSettings, - schedulers, - time - ) - - def closeAll(): Unit = { - if (shutdownInitiated.compareAndSet(false, true)) { - Option(f).filterNot(_.isCompleted).foreach(_.cancel()) - AppSchedulers.shutdownAndWait(scheduler, "migration-pool", 2.minute) - Option(time).foreach(_.close()) - Option(schedulers).foreach(_.shutdown()) - Option(sourceDb).foreach(_.close()) - Option(targetDb).foreach(_.close()) - } - } - - def exitWithError(errorMessage: String, ex: Option[Throwable] = None): Nothing = { - log.error(errorMessage, ex.orNull) - sys.exit(1) - } - - private def storageIsEmpty(storage: RocksDBStorage): Boolean = { - val iterator = storage.newIterator() - iterator.seekToFirst() - val hasNext = iterator.isValid - iterator.close() - !hasNext - } -} - -object StateMigration extends StateMigrationBase - -class SourceBlockchain(override val storage: RocksDBStorage) extends ReadWriteDB { - - object LegacyKeys { - def blockHeaderBytesAt(height: Int): Key[Option[Array[Byte]]] = - Key.opt("block-header-bytes-at-height", - DefaultCF, - h(3, height), - _.drop(4), - _ => throw new Exception("Key \"block-header-bytes-at-height\" - is read only!")) - - def blockTransactionsAtHeight(height: Int): Key[Seq[ByteStr]] = - Key("block-transaction-ids-at-height", - DefaultCF, - h(49, height), - readTxIds, - _ => throw new Exception("Key \"block-transaction-ids-at-height\" - is read only!")) - - def transactionBytes(txId: ByteStr): Key[Option[Array[Byte]]] = - Key.opt("transaction-info-bytes", - DefaultCF, - hash(18, txId), - _.drop(4), - _ => throw new Exception("Key \"transaction-info-bytes\" - is read only!")) - } - - def height: Int = readOnly(_.get(Keys.height)) - - def blockAt(height: Int): Option[Block] = loadBlockBytes(height).map(parseBlockBytes) - - private def parseBlockBytes(bb: Array[Byte]): Block = - Block.parseBytes(bb).fold(e => throw new RuntimeException("Can't parse block bytes", e), identity) - - private def loadBlockBytes(h: Int): Option[Array[Byte]] = readOnly { db => - val headerKey = LegacyKeys.blockHeaderBytesAt(h) - db.get(headerKey).map { headerBytes => - val blockHeader = BlockHeader.parse(headerBytes) - val txBytes = readBlockTransactionBytes(h, db) - val out = newDataOutput(headerBytes.length + txBytes.length) - out.writeAsBlockBytes(blockHeader, txBytes) - out.toByteArray - } - } - - private def readBlockTransactionBytes(h: Int, db: ReadOnlyDB): Array[Byte] = { - val out = newDataOutput() - val txIdList = db.get(LegacyKeys.blockTransactionsAtHeight(h)) - for (txId <- txIdList) { - db.get(LegacyKeys.transactionBytes(txId)) - .map { txBytes => - out.writeInt(txBytes.length) - out.write(txBytes) - } - .getOrElse(throw new RuntimeException(s"Cannot parse transaction with id '$txId' in block at height: $h")) - } - out.toByteArray - } -} - -class MigrationService( - sourceDb: RocksDBStorage, - targetDb: RocksDBStorage, - sourceAsTargetDb: Coeval[RocksDBStorage], - weSettings: WESettings, - migrationSettings: MigrationSettings, - schedulers: Schedulers, - time: NTP -)(implicit val scheduler: Scheduler) - extends ScorexLogging { - - private val sourceState = new SourceBlockchain(sourceDb) - protected val (persistentStorage, targetState) = blockchainFactory(weSettings, targetDb, time, schedulers) - - protected def blockchainFactory(settings: WESettings, - storage: RocksDBStorage, - time: Time, - schedulers: Schedulers): (RocksDBWriter, BlockchainUpdater with PrivacyState with NG with MiningConstraintsHolder) = - BlockchainFactory(settings, storage, time, schedulers) - - private val consensus = Consensus(weSettings.blockchain, targetState, time) - - protected def loadCerts(settings: WESettings, rocksDBWriter: RocksDBWriter): CertChainStore = CertChainStore.empty - - def runMigration(bufferSize: Int, backpressure: Int): CancelableFuture[Unit] = { - val certs = loadCerts(weSettings, persistentStorage) - checkGenesis(weSettings, targetState, certs) - - val sourceHeight = sourceState.height - - if (sourceAlreadyMigrated(sourceHeight)) { - Task { - log.warn(s"Migration has already been done, move source to target") - - sourceDb.close() - targetDb.close() - - val targetDir = new File(migrationSettings.targetPath) - - Files - .walk(Paths.get(migrationSettings.sourcePath)) - .iterator() - .asScala - .map(_.toFile) - .filter(_.isFile) - .foreach { sourceFile => - val destinationFile = new File(targetDir, sourceFile.getName) - Files.move(sourceFile.toPath, destinationFile.toPath, StandardCopyOption.REPLACE_EXISTING) - } - }.runToFuture - } else { - val targetHeight = targetState.height - - val startHeight = targetHeight + 1 - log.info(s"Starting migration from height '$startHeight' to '$sourceHeight'...") - - Observable - .range(startHeight, sourceHeight + 1) - .mapParallelOrdered(parallelism = Runtime.getRuntime.availableProcessors()) { i => - Task { - loadBlock(i.toInt) - } - } - .executeAsync - .executeOn(scheduler) - .bufferTumbling(bufferSize) - .asyncBoundary(OverflowStrategy.BackPressure(backpressure)) - .foreachL { batch => - batch.foreach(processBlock) - } - .runAsyncLogErr - } - } - - private def loadBlock(height: Int): (Int, Block) = { - val block = blocking(sourceState.blockAt(height).getOrElse(throw BlockNotFoundException(height))) - block.blockScore() - block.bytes() - // block.signatureValid() - block.feesPortfolio() - block.prevBlockFeePart() - block.transactionData.foreach { tx => - tx.id() - tx.bytes() - } - height -> block - } - - private def processBlock(tuple: (Int, Block)): Unit = { - val Tuple2(height, block) = tuple - - if (height % 1000 == 0) { - log.info(s"Processing height '$height'") - } - blocking { - consensus - .calculatePostAction(block) - .flatMap { postAction => - targetState.processBlock(block, postAction, Hard, isOwn = true, certChainStore = CertChainStore.empty) // TODO: Another task? - } - .fold(ve => throw new RuntimeException(ve.toString), _ => ()) - } - } - - /** - * Checks if the migration invoke by mistake - */ - private def sourceAlreadyMigrated(sourceHeight: Int): Boolean = { - lazy val (_, sourceStateAsTarget) = blockchainFactory(weSettings, sourceAsTargetDb(), time, schedulers) - - try { - loadBlock(sourceHeight) - false - } catch { - case BlockNotFoundException(height) if sourceStateAsTarget.blockAt(height).isDefined => true - } - } -} 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 6d53bd6..95166c7 100644 --- a/node/src/main/scala/com/wavesenterprise/api/http/AddressApiRoute.scala +++ b/node/src/main/scala/com/wavesenterprise/api/http/AddressApiRoute.scala @@ -358,7 +358,7 @@ class AddressApiRoute(addressApiService: AddressApiService, } private def balancesDetails(account: Address): BalanceDetails = { - val portfolio = blockchain.westPortfolio(account) + val portfolio = blockchain.addressWestPortfolio(account) BalanceDetails( address = account.address, regular = portfolio.balance, diff --git a/node/src/main/scala/com/wavesenterprise/api/http/ApiError.scala b/node/src/main/scala/com/wavesenterprise/api/http/ApiError.scala index 68ddfdc..1155cdc 100644 --- a/node/src/main/scala/com/wavesenterprise/api/http/ApiError.scala +++ b/node/src/main/scala/com/wavesenterprise/api/http/ApiError.scala @@ -682,5 +682,11 @@ object ApiError extends IntEnum[ApiError] { override val message: String = s"Couldn't process snapshot because of error: '$reason'" } + case class NoSuchElementError(reason: String) extends ApiError { + override val value: Int = 706 + override val code: StatusCode = StatusCodes.BadRequest + override val message: String = reason + } + override def values: immutable.IndexedSeq[ApiError] = findValues } diff --git a/node/src/main/scala/com/wavesenterprise/api/http/ApiRoute.scala b/node/src/main/scala/com/wavesenterprise/api/http/ApiRoute.scala index 252549f..2d02bfd 100644 --- a/node/src/main/scala/com/wavesenterprise/api/http/ApiRoute.scala +++ b/node/src/main/scala/com/wavesenterprise/api/http/ApiRoute.scala @@ -4,7 +4,7 @@ import akka.dispatch.ExecutionContexts import akka.http.scaladsl.marshalling.ToResponseMarshallable import akka.http.scaladsl.server._ import com.wavesenterprise.account.Address -import com.wavesenterprise.api.http.ApiError.{ApiKeyNotValid, HttpEntityTooBig, PrivacyApiKeyNotValid, SignatureError, WrongJson} +import com.wavesenterprise.api.http.ApiError.{ApiKeyNotValid, HttpEntityTooBig, PrivacyApiKeyNotValid, SignatureError, WrongJson, NoSuchElementError} import com.wavesenterprise.api.http.auth.ApiProtectionLevel._ import com.wavesenterprise.api.http.auth.AuthRole._ import com.wavesenterprise.api.http.auth._ @@ -43,6 +43,9 @@ trait ApiRoute extends Directives with CommonApiFunctions with ApiMarshallers wi case malformed: MalformedRequestContentRejection if malformed.cause.isInstanceOf[akka.http.scaladsl.model.EntityStreamSizeException] => val exception = malformed.cause.asInstanceOf[akka.http.scaladsl.model.EntityStreamSizeException] complete(HttpEntityTooBig(exception.actualSize.getOrElse(-1), exception.limit)) + case malformed: MalformedRequestContentRejection if malformed.cause.isInstanceOf[java.util.NoSuchElementException] => + val exception = malformed.cause.asInstanceOf[java.util.NoSuchElementException] + complete(NoSuchElementError(exception.getMessage)) } .result() diff --git a/node/src/main/scala/com/wavesenterprise/api/http/TxApiFunctions.scala b/node/src/main/scala/com/wavesenterprise/api/http/TxApiFunctions.scala index c568e90..934781c 100644 --- a/node/src/main/scala/com/wavesenterprise/api/http/TxApiFunctions.scala +++ b/node/src/main/scala/com/wavesenterprise/api/http/TxApiFunctions.scala @@ -1,7 +1,7 @@ package com.wavesenterprise.api.http import com.wavesenterprise.block.{Block, BlockHeader} -import com.wavesenterprise.state.Blockchain +import com.wavesenterprise.state.{Blockchain, LeaseId} import com.wavesenterprise.transaction.Transaction import com.wavesenterprise.transaction.lease.LeaseCancelTransaction import play.api.libs.json._ @@ -15,7 +15,7 @@ trait TxApiFunctions { tx match { case lease: LeaseTransaction => import com.wavesenterprise.transaction.lease.LeaseTransactionStatus._ - lease.json() ++ Json.obj("status" -> (if (blockchain.leaseDetails(lease.id()).exists(_.isActive)) Active else Canceled)) + lease.json() ++ Json.obj("status" -> (if (blockchain.leaseDetails(LeaseId(lease.id())).exists(_.isActive)) Active else Canceled)) case leaseCancel: LeaseCancelTransaction => leaseCancel.json() ++ Json.obj("lease" -> blockchain.transactionInfo(leaseCancel.leaseId).map(_._2.json()).getOrElse[JsValue](JsNull)) case t => t.json() diff --git a/node/src/main/scala/com/wavesenterprise/api/http/assets/AssetsApiRoute.scala b/node/src/main/scala/com/wavesenterprise/api/http/assets/AssetsApiRoute.scala index 54fc64c..764ada4 100644 --- a/node/src/main/scala/com/wavesenterprise/api/http/assets/AssetsApiRoute.scala +++ b/node/src/main/scala/com/wavesenterprise/api/http/assets/AssetsApiRoute.scala @@ -194,7 +194,7 @@ class AssetsApiRoute(val settings: ApiSettings, assetId <- ByteStr .decodeBase58(assetIdStr) .toEither - .leftMap(ex => ValidationError.InvalidAddress(s"Asset id '$assetIdStr' is invalid: ${ex.getMessage}")) + .leftMap(ex => ValidationError.InvalidAssetId(s"Asset id '$assetIdStr' is invalid: ${ex.getMessage}")) address <- Address.fromString(addressStr).leftMap(ValidationError.fromCryptoError) } yield { val balance = blockchain.addressBalance(address, Some(assetId)) diff --git a/node/src/main/scala/com/wavesenterprise/api/http/docker/ContractsApiRoute.scala b/node/src/main/scala/com/wavesenterprise/api/http/docker/ContractsApiRoute.scala index 7dc9ddd..1289d72 100644 --- a/node/src/main/scala/com/wavesenterprise/api/http/docker/ContractsApiRoute.scala +++ b/node/src/main/scala/com/wavesenterprise/api/http/docker/ContractsApiRoute.scala @@ -24,7 +24,7 @@ class ContractsApiRoute(val contractsApiService: ContractsApiService, override lazy val route: Route = pathPrefix("contracts") { withAuth() { - executedTransactionFor ~ contractAssetBalance ~ executionStatus ~ contractInfo ~ + executedTransactionFor ~ contractBalanceDetails ~ contractAssetBalance ~ executionStatus ~ contractInfo ~ contractKeys() ~ contractBalance ~ contractAssetBalance ~ contractAssetsBalances ~ contractKey() ~ contractKeysFiltered() ~ contracts ~ contractsState() } @@ -195,6 +195,18 @@ class ContractsApiRoute(val contractsApiService: ContractsApiService, } } } + + /** + * GET /contracts/balance/details/{contractId} + */ + def contractBalanceDetails: Route = (get & path("balance" / "details" / Segment)) { contractId => + withExecutionContext(scheduler) { + complete( + contractsApiService.contractBalanceDetails(contractId) + ) + } + } + } object ContractsApiRoute { diff --git a/node/src/main/scala/com/wavesenterprise/api/http/docker/InternalContractsApiRoute.scala b/node/src/main/scala/com/wavesenterprise/api/http/docker/InternalContractsApiRoute.scala deleted file mode 100644 index fab0185..0000000 --- a/node/src/main/scala/com/wavesenterprise/api/http/docker/InternalContractsApiRoute.scala +++ /dev/null @@ -1,38 +0,0 @@ -package com.wavesenterprise.api.http.docker - -import akka.http.scaladsl.server.Route -import com.wavesenterprise.account.Address -import com.wavesenterprise.api.http.AdditionalDirectiveOps -import com.wavesenterprise.api.http.auth.WithAuthFromContract -import com.wavesenterprise.api.http.service.ContractsApiService -import com.wavesenterprise.docker.ContractAuthTokenService -import com.wavesenterprise.settings.ApiSettings -import com.wavesenterprise.state.ContractBlockchain.ContractReadingContext -import com.wavesenterprise.utils.Time -import monix.execution.schedulers.SchedulerService - -/** - * Contracts API for internal calls from contracts code - */ -class InternalContractsApiRoute(contractsApiService: ContractsApiService, - settings: ApiSettings, - time: Time, - contractAuthTokenServiceParam: ContractAuthTokenService, - externalNodeOwner: Address, - scheduler: SchedulerService) - extends ContractsApiRoute(contractsApiService, settings, time, externalNodeOwner, scheduler) - with AdditionalDirectiveOps - with WithAuthFromContract { - - override val contractAuthTokenService: Option[ContractAuthTokenService] = Some(contractAuthTokenServiceParam) - - override lazy val route: Route = - pathPrefix("internal" / "contracts") { - addedGuard { - withContractAuthClaim { claim => - val readingContext = ContractReadingContext.TransactionExecution(claim.txId) - executedTransactionFor ~ contractKeys(readingContext) ~ contractKey(readingContext) ~ contracts ~ contractsState(readingContext) - } - } - } -} diff --git a/node/src/main/scala/com/wavesenterprise/api/http/leasing/LeaseApiRoute.scala b/node/src/main/scala/com/wavesenterprise/api/http/leasing/LeaseApiRoute.scala index 59b0f4d..7791243 100644 --- a/node/src/main/scala/com/wavesenterprise/api/http/leasing/LeaseApiRoute.scala +++ b/node/src/main/scala/com/wavesenterprise/api/http/leasing/LeaseApiRoute.scala @@ -4,23 +4,29 @@ import akka.http.scaladsl.server.Route import com.wavesenterprise.account.Address import com.wavesenterprise.api.http._ import com.wavesenterprise.settings.ApiSettings -import com.wavesenterprise.state.Blockchain +import com.wavesenterprise.state.{Blockchain, ByteStr} +import com.wavesenterprise.transaction.docker.ExecutedContractTransactionV3 +import com.wavesenterprise.transaction.docker.assets.ContractAssetOperation.{ContractCancelLeaseV1, ContractLeaseV1} +import com.wavesenterprise.state.{Blockchain, LeaseId} import com.wavesenterprise.utils.EitherUtils.EitherExt import com.wavesenterprise.transaction.lease.LeaseTransaction import com.wavesenterprise.utils.Time import com.wavesenterprise.utx.UtxPool import com.wavesenterprise.wallet.Wallet import monix.execution.schedulers.SchedulerService -import play.api.libs.json.JsNumber +import play.api.libs.json.{JsNumber, JsObject} -class LeaseApiRoute(val settings: ApiSettings, - wallet: Wallet, - blockchain: Blockchain, - val utx: UtxPool, - val time: Time, - val nodeOwner: Address, - val scheduler: SchedulerService) - extends ApiRoute { +import scala.collection.mutable + +class LeaseApiRoute( + val settings: ApiSettings, + wallet: Wallet, + blockchain: Blockchain, + val utx: UtxPool, + val time: Time, + val nodeOwner: Address, + val scheduler: SchedulerService +) extends ApiRoute { override val route = pathPrefix("leasing") { withAuth() { @@ -35,17 +41,49 @@ class LeaseApiRoute(val settings: ApiSettings, pathPrefix(Segment) { address => withExecutionContext(scheduler) { complete(Address.fromString(address) match { - case Left(e) => ApiError.fromCryptoError(e) - case Right(a) => - blockchain - .addressTransactions(a, Set(LeaseTransaction.typeId), Int.MaxValue, None) + case Left(error) => ApiError.fromCryptoError(error) + case Right(address) => + val leaseTxs = blockchain + .addressTransactions(address, Set(LeaseTransaction.typeId), Int.MaxValue, None) .explicitGet() .collect { - case (h, lt: LeaseTransaction) if blockchain.leaseDetails(lt.id()).exists(_.isActive) => - lt.json() + ("height" -> JsNumber(h)) + case (height, leaseTx: LeaseTransaction) if blockchain.leaseDetails(LeaseId(leaseTx.id())).exists(_.isActive) => + leaseTx.json() + ("height" -> JsNumber(height)) } + + val assetOperationLeaseTxs = findAssetOperationLease(address) + + leaseTxs ++ assetOperationLeaseTxs }) } } } + + private def findAssetOperationLease(address: Address): Seq[JsObject] = { + val leaseOps = mutable.Map[ByteStr, JsObject]() + val leaseCancelOps = mutable.Set[ByteStr]() + + blockchain + .addressTransactions(address, Set(ExecutedContractTransactionV3.typeId), Int.MaxValue, None) + .explicitGet() + .foreach { + case (height, tx: ExecutedContractTransactionV3) => + tx.assetOperations.foreach { + case leaseOp: ContractLeaseV1 => + val txJson = tx.json() + ("height" -> JsNumber(height)) + leaseOps.put(leaseOp.leaseId, txJson) + + case leaseCancelOp: ContractCancelLeaseV1 => + leaseCancelOps.add(leaseCancelOp.leaseId) + + case _ => () + } + + case _ => () + } + + leaseCancelOps.foreach(leaseOps.remove) + + leaseOps.values.toSeq + } } diff --git a/node/src/main/scala/com/wavesenterprise/api/http/service/AddressApiService.scala b/node/src/main/scala/com/wavesenterprise/api/http/service/AddressApiService.scala index cdabf36..25cd110 100644 --- a/node/src/main/scala/com/wavesenterprise/api/http/service/AddressApiService.scala +++ b/node/src/main/scala/com/wavesenterprise/api/http/service/AddressApiService.scala @@ -3,7 +3,7 @@ package com.wavesenterprise.api.http.service import cats.syntax.either._ import com.wavesenterprise.account.{Address, PublicKeyAccount} import com.wavesenterprise.api.http.AddressApiRoute.{AddressPublicKeyInfo, Signed, VerificationResult} -import com.wavesenterprise.api.http.ApiError.InvalidMessage +import com.wavesenterprise.api.http.ApiError.{InvalidMessage, InvalidPublicKey, InvalidSignature} import com.wavesenterprise.api.http.{ApiError, Message, SignedMessage} import com.wavesenterprise.crypto import com.wavesenterprise.state.{Blockchain, DataEntry, _} @@ -23,17 +23,17 @@ class AddressApiService(val blockchain: Blockchain, wallet: Wallet) { } def verifySignedMessage(m: SignedMessage, address: String, isMessageEncoded: Boolean): Either[ApiError, VerificationResult] = { - def decodeOrInvalidMessage(input: String): Either[ApiError, Array[Byte]] = - Base58.decode(input).toEither.leftMap(_ => InvalidMessage) + def decodeOrInvalidMessage(input: String, error: ApiError): Either[ApiError, Array[Byte]] = + Base58.decode(input).toEither.leftMap(_ => error) for { signerAddress <- Address.fromString(address).leftMap(ApiError.fromCryptoError) msg <- if (isMessageEncoded) - decodeOrInvalidMessage(m.message) + decodeOrInvalidMessage(m.message, InvalidMessage) else Right(m.message.getBytes(StandardCharsets.UTF_8)) - signature <- decodeOrInvalidMessage(m.signature) - publicKeyBytes <- decodeOrInvalidMessage(m.publickey) + signature <- decodeOrInvalidMessage(m.signature, InvalidSignature) + publicKeyBytes <- decodeOrInvalidMessage(m.publickey, InvalidPublicKey(m.publickey)) publicKeyAccount <- PublicKeyAccount.fromBytes(publicKeyBytes).leftMap(ApiError.fromCryptoError) } yield { val isValid = publicKeyAccount.toAddress == signerAddress && crypto.verify(signature, msg, publicKeyAccount.publicKey) diff --git a/node/src/main/scala/com/wavesenterprise/api/http/service/ContractsApiService.scala b/node/src/main/scala/com/wavesenterprise/api/http/service/ContractsApiService.scala index 0892bf3..5496775 100644 --- a/node/src/main/scala/com/wavesenterprise/api/http/service/ContractsApiService.scala +++ b/node/src/main/scala/com/wavesenterprise/api/http/service/ContractsApiService.scala @@ -3,7 +3,7 @@ package com.wavesenterprise.api.http.service import cats.implicits._ import com.wavesenterprise.api.http.ApiError._ import com.wavesenterprise.api.http._ -import com.wavesenterprise.api.http.service.ContractsApiService.ContractAssetBalanceInfo +import com.wavesenterprise.api.http.service.ContractsApiService.{BalanceDetails, ContractAssetBalanceInfo} import com.wavesenterprise.database.docker.KeysRequest import com.wavesenterprise.docker.{ContractExecutionMessage, ContractExecutionMessagesCache, ContractInfo} import com.wavesenterprise.settings.Constants @@ -12,7 +12,7 @@ import com.wavesenterprise.state.{Blockchain, ByteStr, ContractId, DataEntry} import com.wavesenterprise.transaction.ValidationError.{GenericError, InvalidContractKeys} import com.wavesenterprise.utils.StringUtilites.ValidateAsciiAndRussian.notValidMapOrRight import monix.reactive.Observable -import play.api.libs.json.JsObject +import play.api.libs.json.{Format, JsObject, Json} import scala.util.{Failure, Success, Try} @@ -133,6 +133,23 @@ class ContractsApiService(blockchain: Blockchain, messagesCache: ContractExecuti } yield ContractAssetBalanceInfo(blockchain.contractBalance(ContractId(contractIdByteStr), maybeAssetIdByteStr, readingContext), decimals) } + def contractBalanceDetails(contractIdStr: String): Either[ApiError, BalanceDetails] = { + + for { + contractIdByteStr <- ByteStr + .decodeBase58(contractIdStr) + .toEither + .leftMap(_ => ApiError.CustomValidationError(s"Failed to decode base58 contract id value '$contractIdStr'")) + portfolio = blockchain.contractWestPortfolio(ContractId(contractIdByteStr)) + } yield BalanceDetails( + contractId = contractIdStr, + regular = portfolio.balance, + leasedOut = portfolio.lease.out, + available = portfolio.spendableBalance + ) + + } + private def findContract(contractIdStr: String): Either[ContractNotFound, ContractInfo] = { for { contractId <- ByteStr.decodeBase58(contractIdStr).toEither.leftMap(_ => ContractNotFound(contractIdStr)) @@ -151,4 +168,9 @@ class ContractsApiService(blockchain: Blockchain, messagesCache: ContractExecuti object ContractsApiService { case class ContractAssetBalanceInfo(amount: Long, decimals: Int) + + case class BalanceDetails(contractId: String, regular: Long, leasedOut: Long, available: Long) + + implicit val balanceDetailsFormat: Format[BalanceDetails] = Json.format + } diff --git a/node/src/main/scala/com/wavesenterprise/database/Caches.scala b/node/src/main/scala/com/wavesenterprise/database/Caches.scala index 1139ecf..71d5825 100644 --- a/node/src/main/scala/com/wavesenterprise/database/Caches.scala +++ b/node/src/main/scala/com/wavesenterprise/database/Caches.scala @@ -10,6 +10,7 @@ import com.wavesenterprise.consensus.{ConsensusPostActionDiff, MinerBanHistory} import com.wavesenterprise.docker.ContractInfo import com.wavesenterprise.state.AssetHolder._ import com.wavesenterprise.state._ +import com.wavesenterprise.state.reader.LeaseDetails import com.wavesenterprise.transaction.docker.ExecutedContractData import com.wavesenterprise.transaction.smart.script.Script import com.wavesenterprise.transaction.{AssetId, PolicyDataHashTransaction, Transaction} @@ -114,13 +115,21 @@ trait Caches extends Blockchain with ScorexLogging { override def containsTransaction(tx: Transaction): Boolean = containsTransaction(tx.id()) - private val leaseBalanceCache: LoadingCache[Address, LeaseBalance] = cache(maxCacheSize, loadAddressLeaseBalance) + override def addressLeaseBalance(address: Address): LeaseBalance = addressLeaseBalanceCache.get(address) + + private val addressLeaseBalanceCache: LoadingCache[Address, LeaseBalance] = cache(maxCacheSize, loadAddressLeaseBalance) protected def loadAddressLeaseBalance(address: Address): LeaseBalance - protected def discardLeaseBalance(address: Address): Unit = leaseBalanceCache.invalidate(address) + protected def discardAddressLeaseBalance(address: Address): Unit = addressLeaseBalanceCache.invalidate(address) + + override def contractLeaseBalance(address: ContractId): LeaseBalance = contractLeaseBalanceCache.get(address) + + private val contractLeaseBalanceCache: LoadingCache[ContractId, LeaseBalance] = cache(maxCacheSize, loadContractLeaseBalance) + + protected def loadContractLeaseBalance(contractId: ContractId): LeaseBalance - override def addressLeaseBalance(address: Address): LeaseBalance = leaseBalanceCache.get(address) + protected def discardContractLeaseBalance(contractId: ContractId): Unit = contractLeaseBalanceCache.invalidate(contractId) private val addressPortfolioCache: LoadingCache[Address, Portfolio] = cache(maxCacheSize, loadAddressPortfolio) @@ -258,10 +267,12 @@ trait Caches extends Blockchain with ScorexLogging { newNonEmptyRoleAddresses: Map[Address, BigInt], westAddressesBalances: Map[BigInt, Long], assetAddressesBalances: Map[BigInt, Map[ByteStr, Long]], - leaseBalances: Map[BigInt, LeaseBalance], + addressLeaseBalances: Map[BigInt, LeaseBalance], + contractLeaseBalances: Map[BigInt, LeaseBalance], westContractsBalances: Map[BigInt, Long], assetContractsBalances: Map[BigInt, Map[ByteStr, Long]], - leaseStates: Map[ByteStr, Boolean], + leaseMap: Map[LeaseId, LeaseDetails], + leaseCancelMap: Map[LeaseId, LeaseDetails], transactions: Seq[Transaction], addressTransactions: Map[BigInt, List[(Int, ByteStr)]], contractTransactions: Map[BigInt, List[(Int, ByteStr)]], @@ -336,13 +347,15 @@ trait Caches extends Blockchain with ScorexLogging { log.trace(s"CACHE lastAddressId = $lastAddressId") log.trace(s"CACHE lastContractStateId = $lastContractStateId") - val westAccountBalances = Map.newBuilder[BigInt, Long] - val assetAccountBalances = Map.newBuilder[BigInt, Map[ByteStr, Long]] - val leaseBalances = Map.newBuilder[BigInt, LeaseBalance] - val westContractBalances = Map.newBuilder[BigInt, Long] - val assetContractBalances = Map.newBuilder[BigInt, Map[ByteStr, Long]] - val updatedLeaseBalances = Map.newBuilder[Address, LeaseBalance] - val newPortfolios = Map.newBuilder[AssetHolder, Portfolio] + val westAccountBalances = Map.newBuilder[BigInt, Long] + val assetAccountBalances = Map.newBuilder[BigInt, Map[ByteStr, Long]] + val addressLeaseBalances = Map.newBuilder[BigInt, LeaseBalance] + val contractLeaseBalances = Map.newBuilder[BigInt, LeaseBalance] + val westContractBalances = Map.newBuilder[BigInt, Long] + val assetContractBalances = Map.newBuilder[BigInt, Map[ByteStr, Long]] + val updatedAddressLeaseBalances = Map.newBuilder[Address, LeaseBalance] + val updatedContractLeaseBalances = Map.newBuilder[ContractId, LeaseBalance] + val newPortfolios = Map.newBuilder[AssetHolder, Portfolio] for ((owner, portfolioDiff) <- diff.portfolios) { val newPortfolio = owner.product(addressPortfolioCache.get, contractPortfolioCache.get(_)).combine(portfolioDiff) @@ -354,13 +367,11 @@ trait Caches extends Blockchain with ScorexLogging { if (portfolioDiff.lease.nonEmpty) { owner match { case Account(address) => - leaseBalances += getAddressId(address) -> newPortfolio.lease - updatedLeaseBalances += address -> newPortfolio.lease + addressLeaseBalances += getAddressId(address) -> newPortfolio.lease + updatedAddressLeaseBalances += address -> newPortfolio.lease case Contract(contractId) => - val errorMessage = s"Error appending block '${block.uniqueId}'. " + - s"Contract '$contractId' received or sent a Lease, which is not allowed. LeaseBalance: ${newPortfolio.lease}" - log.error(errorMessage) - throw new IllegalStateException(errorMessage) + contractLeaseBalances += getContractStateId(contractId) -> newPortfolio.lease + updatedContractLeaseBalances += contractId -> newPortfolio.lease } } @@ -412,10 +423,12 @@ trait Caches extends Blockchain with ScorexLogging { newNonEmptyRoleAddresses = newNonEmptyRoleAddressIds, westAddressesBalances = westAccountBalances.result(), assetAddressesBalances = assetAccountBalances.result(), - leaseBalances = leaseBalances.result(), + addressLeaseBalances = addressLeaseBalances.result(), + contractLeaseBalances = contractLeaseBalances.result(), westContractsBalances = westContractBalances.result(), assetContractsBalances = assetContractBalances.result(), - leaseStates = diff.leaseState, + leaseMap = diff.leaseMap, + leaseCancelMap = diff.leaseCancelMap, transactions = diff.transactions, addressTransactions = diff.assetHolderTransactionIds.collectAddresses.map({ case (aStateId, txs) => getAddressId(aStateId) -> txs @@ -453,7 +466,8 @@ trait Caches extends Blockchain with ScorexLogging { assetHolder.product(addressPortfolioCache.put(_, portfolio), contractPortfolioCache.put(_, portfolio)) for (id <- diff.assets.keySet ++ diff.assetScripts.keySet ++ diff.sponsorship.keySet) assetDescriptionCache.invalidate(id) - leaseBalanceCache.putAll(updatedLeaseBalances.result().asJava) + addressLeaseBalanceCache.putAll(updatedAddressLeaseBalances.result().asJava) + contractLeaseBalanceCache.putAll(updatedContractLeaseBalances.result().asJava) scriptCache.putAll(diff.scripts.asJava) hasScriptCache.putAll(diff.scripts.mapValues(_.isDefined: java.lang.Boolean).asJava) assetScriptCache.putAll(diff.assetScripts.asJava) diff --git a/node/src/main/scala/com/wavesenterprise/database/Keys.scala b/node/src/main/scala/com/wavesenterprise/database/Keys.scala index f12d90d..06496a6 100644 --- a/node/src/main/scala/com/wavesenterprise/database/Keys.scala +++ b/node/src/main/scala/com/wavesenterprise/database/Keys.scala @@ -87,10 +87,6 @@ object Keys { def leaseBalanceHistory(addressId: BigInt): Key[Seq[Int]] = historyKey("lease-balance-history", LeaseBalanceHistoryPrefix, addressId.toByteArray) def leaseBalance(addressId: BigInt)(height: Int): Key[LeaseBalance] = Key("lease-balance", hAddr(LeaseBalancePrefix, height, addressId), readLeaseBalance, writeLeaseBalance) - def leaseStatusHistory(leaseId: ByteStr): Key[Seq[Int]] = historyKey("lease-status-history", LeaseStatusHistoryPrefix, leaseId.arr) - def leaseStatus(leaseId: ByteStr)(height: Int): Key[Boolean] = - Key("lease-status", hBytes(LeaseStatusPrefix, height, leaseId.arr), _(0) == 1, active => Array[Byte](if (active) 1 else 0)) - def filledVolumeAndFeeHistory(orderId: ByteStr): Key[Seq[Int]] = historyKey("filled-volume-and-fee-history", FilledVolumeAndFeeHistoryPrefix, orderId.arr) def filledVolumeAndFee(orderId: ByteStr)(height: Int): Key[VolumeAndFee] = diff --git a/node/src/main/scala/com/wavesenterprise/database/RocksDBDeque.scala b/node/src/main/scala/com/wavesenterprise/database/RocksDBDeque.scala new file mode 100644 index 0000000..fec314f --- /dev/null +++ b/node/src/main/scala/com/wavesenterprise/database/RocksDBDeque.scala @@ -0,0 +1,368 @@ +package com.wavesenterprise.database + +import com.google.common.primitives.Ints +import com.wavesenterprise.database.rocksdb._ +import cats.implicits._ + +import java.nio.ByteBuffer + +class RocksDBDeque[T]( + name: String, + columnFamily: ColumnFamily, + prefix: Array[Byte], + storage: RocksDBStorage, + itemEncoder: T => Array[Byte], + itemDecoder: Array[Byte] => T +) extends InternalRocksDBDeque[T](name, columnFamily, prefix, itemEncoder, itemDecoder) { + def this( + name: String, + prefix: Array[Byte], + storage: RocksDBStorage, + itemEncoder: T => Array[Byte], + itemDecoder: Array[Byte] => T + ) { + this(name, ColumnFamily.DefaultCF, prefix, storage, itemEncoder, itemDecoder) + } + @inline def addFirst(value: T): Unit = + storage.readWrite(rw => addFirst(rw, value)) + + @inline def addFirstN(values: Iterable[T]): Unit = + storage.readWrite(rw => values.foreach(addFirst(rw, _))) + + @inline def addLast(value: T): Unit = + storage.readWrite(rw => addLast(rw, value)) + + @inline def addLastN(values: Iterable[T]): Unit = + storage.readWrite(rw => values.foreach(addLast(rw, _))) + + @inline def pollFirst: Option[T] = { + storage.readWrite(pollFirst) + } + + @inline def pollFirstN(n: Int): Seq[T] = { + storage.readWrite(rw => (0 until n).flatMap(_ => pollFirst(rw))) + } + + @inline def pollLast: Option[T] = { + storage.readWrite(pollLast) + } + + @inline def pollLastN(n: Int): Seq[T] = { + storage.readWrite(rw => pollLastN(rw, n)) + } + + @inline def head: Option[T] = { + storage.readOnly(peekFirst) + } + + @inline def take(n: Int): Seq[T] = { + storage.readOnly(ro => (0 until n).flatMap(_ => peekLast(ro))) + } + + @inline def last: Option[T] = { + storage.readOnly(peekLast) + } + + def takeRight(n: Int): Seq[T] = { + slice(size - n, size) + } + + @inline def contains(value: T): Boolean = + storage.readOnly(ro => contains(ro, value)) + + @inline def isEmpty: Boolean = + storage.readOnly(isEmpty) + + @inline def nonEmpty: Boolean = + storage.readOnly(nonEmpty) + + @inline def size: Int = + storage.readOnly(size) + + @inline def clear(): Unit = + storage.readWrite(clear) + + @inline def toList: List[T] = { + storage.readOnly(toList) + } + + def slice(from: Int, until: Int): List[T] = { + storage.readOnly(ro => slice(from, until, ro)) + } +} + +class InternalRocksDBDeque[T]( + name: String, + columnFamily: ColumnFamily, + prefix: Array[Byte], + itemEncoder: T => Array[Byte], + itemDecoder: Array[Byte] => T +) { + case class DequeMetaKey( + head: Int, + tail: Int, + size: Int + ) + + object DequeMetaKey { + def decodeMetaKey(encodedMetaKey: Array[Byte]): DequeMetaKey = { + val buffer = ByteBuffer.wrap(encodedMetaKey) + val head = buffer.getInt + val tail = buffer.getInt + val size = buffer.getInt + + DequeMetaKey(head, tail, size) + } + + def encodeMetaKey(metaKey: DequeMetaKey): Array[Byte] = + ByteBuffer.allocate(Ints.BYTES * 12) + .putInt(metaKey.head) + .putInt(metaKey.tail) + .putInt(metaKey.size) + .array() + + lazy val initState: DequeMetaKey = { + val minHead = 1000 + val maxHead = Int.MaxValue - 1000 + val initHeadTail = minHead + (maxHead - minHead) / 2 + + DequeMetaKey( + head = initHeadTail, + tail = initHeadTail, + size = 0 + ) + } + } + + private[database] val metaKey: Key[Option[DequeMetaKey]] = { + val meta = "meta-key" + + val keyBytes = ByteBuffer + .allocate(prefix.length + meta.length) + .put(prefix) + .put(meta.getBytes) + .array() + + Key.opt(s"$name-deque-meta-key", columnFamily, keyBytes, DequeMetaKey.decodeMetaKey, DequeMetaKey.encodeMetaKey) + } + + private def putMeta(head: Int, tail: Int, size: Int, rw: RW): Unit = + rw.put(metaKey, DequeMetaKey(head, tail, size).some) + + private def encodeDequeKey(seq: Int): Key[T] = { + val keyBytes = ByteBuffer + .allocate(prefix.length + 1 + Ints.BYTES) + .put(prefix) + .putInt(seq) + .array() + + Key(s"$name-deque-item", columnFamily, keyBytes, itemDecoder, itemEncoder) + } + + private def headUntilSize(ro: ReadOnlyDB): Range = { + val meta = ro.get(metaKey).getOrElse(DequeMetaKey.initState) + val head = meta.head + val size = meta.size + + head until head + size + } + + protected[database] def addFirst(rw: RW, value: T): Unit = { + val meta = rw.get(metaKey).getOrElse(DequeMetaKey.initState) + val size = meta.size + var head = meta.head + + if (size == 0) { + val itemKey = encodeDequeKey(head) + rw.put(itemKey, value) + } else { + head -= 1 + val itemKey = encodeDequeKey(head) + rw.put(itemKey, value) + } + + putMeta(head, meta.tail, size + 1, rw) + } + + protected[database] def addLast(rw: RW, value: T): Unit = { + val meta = rw.get(metaKey).getOrElse(DequeMetaKey.initState) + val size = meta.size + var tail = meta.tail + + if (size == 0) { + val itemKey = encodeDequeKey(tail) + rw.put(itemKey, value) + } else { + tail += 1 + val itemKey = encodeDequeKey(tail) + rw.put(itemKey, value) + } + + putMeta(meta.head, tail, size + 1, rw) + } + + protected[database] def addLastN(rw: RW, values: Iterable[T]): Unit = { + values.size match { + case 0 => () + case 1 => addLast(rw, values.head) + case _ => + val meta = rw.get(metaKey).getOrElse(DequeMetaKey.initState) + val size = meta.size + var newTail = if (size == 0) meta.tail else meta.tail + 1 + + var itemKey = encodeDequeKey(newTail) + rw.put(itemKey, values.head) + + for ((value, idx) <- values.zipWithIndex.tail) { + itemKey = encodeDequeKey(newTail + idx) + rw.put(itemKey, value) + } + + newTail += values.size - 1 + + putMeta(meta.head, newTail, size + values.size, rw) + } + + } + + protected[database] def pollFirst(rw: RW): Option[T] = + if (nonEmpty(rw)) { + val meta = rw.get(metaKey).getOrElse(DequeMetaKey.initState) + val itemKey = encodeDequeKey(meta.head) + val value = rw.get(itemKey) + + if (meta.size == 1) { + clear(rw) + } else { + putMeta(meta.head + 1, meta.tail, meta.size - 1, rw) + rw.delete(itemKey) + } + + value.some + } else None + + protected[database] def pollLast(rw: RW): Option[T] = + if (nonEmpty(rw)) { + val meta = rw.get(metaKey).getOrElse(DequeMetaKey.initState) + val itemKey = encodeDequeKey(meta.tail) + val value = rw.get(itemKey) + + if (meta.size == 1) { + clear(rw) + } else { + putMeta(meta.head, meta.tail - 1, meta.size - 1, rw) + rw.delete(itemKey) + } + + value.some + } else None + + protected[database] def pollLastN(rw: RW, n: Int): Seq[T] = { + val meta = rw.get(metaKey).getOrElse(DequeMetaKey.initState) + require(meta.size >= n, "There is no as many elements as you try to remove") + + meta.size match { + case 0 => Seq() + case 1 => + val itemKey = encodeDequeKey(meta.tail) + val value = rw.get(itemKey) + + clear(rw) + + Seq(value) + case _ => + val values = (0 until n).map { i => + val itemKey = encodeDequeKey(meta.tail - i) + val value = rw.get(itemKey) + + rw.delete(itemKey) + + value + } + + val dequeResultSize = meta.size - n + + if (dequeResultSize == 0) { + rw.put(metaKey, DequeMetaKey.initState.some) + } else { + putMeta(meta.head, meta.tail - n, dequeResultSize, rw) + } + + values + } + } + + protected[database] def peekFirst(ro: ReadOnlyDB): Option[T] = { + if (nonEmpty(ro)) { + val head = ro.get(metaKey).getOrElse(DequeMetaKey.initState).head + val itemKey = encodeDequeKey(head) + + ro.get(itemKey).some + } else None + } + + protected[database] def peekLast(ro: ReadOnlyDB): Option[T] = + if (isEmpty(ro)) { + None + } else { + val head = ro.get(metaKey).getOrElse(DequeMetaKey.initState).tail + val itemKey = encodeDequeKey(head) + + ro.get(itemKey).some + } + + protected[database] def contains(ro: ReadOnlyDB, value: T): Boolean = { + headUntilSize(ro).exists { i => + val itemKey = encodeDequeKey(i) + val item = ro.get(itemKey) + + item == value + } + } + + protected[database] def toList(ro: ReadOnlyDB): List[T] = { + headUntilSize(ro).view.map { i => + val itemKey = encodeDequeKey(i) + val item = ro.get(itemKey) + item + }.toList + } + + protected[database] def clear(rw: RW): Unit = { + // delete all keys + headUntilSize(rw).foreach { i => + val itemKey = encodeDequeKey(i) + rw.delete(itemKey) + } + + // reset meta key + rw.put(metaKey, DequeMetaKey.initState.some) + } + + protected[database] def size(ro: ReadOnlyDB): Int = + ro.get(metaKey).getOrElse(DequeMetaKey.initState).size + + protected[database] def isEmpty(ro: ReadOnlyDB): Boolean = { + size(ro) == 0 + } + + protected[database] def nonEmpty(ro: ReadOnlyDB): Boolean = + !isEmpty(ro) + + protected[database] def slice(from: Int, until: Int, ro: ReadOnlyDB): List[T] = { + val head = ro.get(metaKey).getOrElse(DequeMetaKey.initState).head + + val l = math.max(from, 0) + val r = math.min(until, size(ro)) + + if (r <= 0) { + List.empty + } else { + (l until r).map { i => + val itemKey = encodeDequeKey(i + head) + val item = ro.get(itemKey) + item + }.toList + } + } +} diff --git a/node/src/main/scala/com/wavesenterprise/database/keys/ContractCFKeys.scala b/node/src/main/scala/com/wavesenterprise/database/keys/ContractCFKeys.scala index 1010a60..9374609 100644 --- a/node/src/main/scala/com/wavesenterprise/database/keys/ContractCFKeys.scala +++ b/node/src/main/scala/com/wavesenterprise/database/keys/ContractCFKeys.scala @@ -6,7 +6,7 @@ import com.wavesenterprise.database._ import com.wavesenterprise.database.rocksdb.ColumnFamily.ContractCF import com.wavesenterprise.database.rocksdb.RocksDBStorage import com.wavesenterprise.docker.ContractInfo -import com.wavesenterprise.state.{ByteStr, DataEntry} +import com.wavesenterprise.state.{ByteStr, DataEntry, LeaseBalance} import com.wavesenterprise.transaction.docker.ContractTransactionEntryOps import java.nio.charset.StandardCharsets.UTF_8 @@ -31,6 +31,8 @@ object ContractCFKeys { val ContractAssetInfoHistoryPrefix: Short = 18 val LastContractStateIdPrefix: Short = 19 val ChangedContractsPrefix: Short = 20 + val ContractLeaseBalanceHistoryPrefix: Short = 21 + val ContractLeaseBalancePrefix: Short = 22 def contractIdsSet(storage: RocksDBStorage): RocksDBSet[ByteStr] = new RocksDBSet[ByteStr]( @@ -133,6 +135,12 @@ object ContractCFKeys { encoder = Longs.toByteArray ) + def contractLeaseBalanceHistory(stateId: BigInt): Key[Seq[Int]] = + historyKey("contract-lease-balance-history", ContractLeaseBalanceHistoryPrefix, stateId.toByteArray) + + def contractLeaseBalance(stateId: BigInt)(height: Int): Key[LeaseBalance] = + Key("lease-balance", hAddr(ContractLeaseBalancePrefix, height, stateId), readLeaseBalance, writeLeaseBalance) + val LastContractStateId: Key[Option[BigInt]] = Key.opt("last-contract-state-id", ContractCF, bytes(LastContractStateIdPrefix, Array.emptyByteArray), BigInt(_), _.toByteArray) diff --git a/node/src/main/scala/com/wavesenterprise/database/keys/LeaseCFKeys.scala b/node/src/main/scala/com/wavesenterprise/database/keys/LeaseCFKeys.scala new file mode 100644 index 0000000..0e23423 --- /dev/null +++ b/node/src/main/scala/com/wavesenterprise/database/keys/LeaseCFKeys.scala @@ -0,0 +1,47 @@ +package com.wavesenterprise.database.keys + +import com.wavesenterprise.database.KeyHelpers.bytes +import com.wavesenterprise.database.rocksdb.ColumnFamily.LeaseCF +import com.wavesenterprise.database.rocksdb.RocksDBStorage +import com.wavesenterprise.database.{Key, RocksDBDeque} +import com.wavesenterprise.state.reader.LeaseDetails +import com.wavesenterprise.state.{ByteStr, LeaseId} + +object LeaseCFKeys { + + val LeaseDetailsPrefix: Short = 1 + val LeasesForAddressPrefix: Short = 2 + + def leaseDetails(leaseId: LeaseId): Key[Option[LeaseDetails]] = { + Key.opt( + "lease-details", + LeaseCF, + bytes(LeaseDetailsPrefix, leaseId.arr), + LeaseDetails.fromBytes, + (leaseDetails: LeaseDetails) => leaseDetails.toBytes + ) + } + + def leasesForAddress(addressId: BigInt, storage: RocksDBStorage): RocksDBDeque[LeaseId] = { + new RocksDBDeque[LeaseId]( + name = "leases-for-address", + columnFamily = LeaseCF, + prefix = bytes(LeasesForAddressPrefix, addressId.toByteArray), + storage = storage, + itemEncoder = _.arr, + itemDecoder = bytes => LeaseId(ByteStr(bytes)) + ) + } + + def leasesForContract(contractStateId: BigInt, storage: RocksDBStorage): RocksDBDeque[LeaseId] = { + new RocksDBDeque[LeaseId]( + name = "leases-for-contract", + columnFamily = LeaseCF, + prefix = bytes(LeasesForAddressPrefix, contractStateId.toByteArray), + storage = storage, + itemEncoder = _.arr, + itemDecoder = bytes => LeaseId(ByteStr(bytes)) + ) + } + +} diff --git a/node/src/main/scala/com/wavesenterprise/database/migration/MigrationType.scala b/node/src/main/scala/com/wavesenterprise/database/migration/MigrationType.scala index f0ba73e..1207363 100644 --- a/node/src/main/scala/com/wavesenterprise/database/migration/MigrationType.scala +++ b/node/src/main/scala/com/wavesenterprise/database/migration/MigrationType.scala @@ -10,15 +10,16 @@ object MigrationType extends IntEnum[MigrationEntry] { type Version = Int - case object `1` extends MigrationEntry(1, _ => ()) - case object `2` extends MigrationEntry(2, MigrationV2.apply) - case object `3` extends MigrationEntry(3, MigrationV3.apply) - case object `4` extends MigrationEntry(4, MigrationV4.apply) - case object `5` extends MigrationEntry(5, MigrationV5.apply) - case object `6` extends MigrationEntry(6, MigrationV6.apply) - case object `7` extends MigrationEntry(7, MigrationV7.apply) - case object `8` extends MigrationEntry(8, MigrationV8.apply) - case object `9` extends MigrationEntry(9, MigrationV9.apply) + case object `1` extends MigrationEntry(1, _ => ()) + case object `2` extends MigrationEntry(2, MigrationV2.apply) + case object `3` extends MigrationEntry(3, MigrationV3.apply) + case object `4` extends MigrationEntry(4, MigrationV4.apply) + case object `5` extends MigrationEntry(5, MigrationV5.apply) + case object `6` extends MigrationEntry(6, MigrationV6.apply) + case object `7` extends MigrationEntry(7, MigrationV7.apply) + case object `8` extends MigrationEntry(8, MigrationV8.apply) + case object `9` extends MigrationEntry(9, MigrationV9.apply) + case object `10` extends MigrationEntry(10, MigrationV10.apply) override val values: immutable.IndexedSeq[MigrationEntry] = findValues.sortBy(_.version) val all: List[Migration] = values.toList diff --git a/node/src/main/scala/com/wavesenterprise/database/migration/MigrationV10.scala b/node/src/main/scala/com/wavesenterprise/database/migration/MigrationV10.scala new file mode 100644 index 0000000..70db60e --- /dev/null +++ b/node/src/main/scala/com/wavesenterprise/database/migration/MigrationV10.scala @@ -0,0 +1,74 @@ +package com.wavesenterprise.database.migration + +import com.wavesenterprise.account.Address +import com.wavesenterprise.database.KeyHelpers.{bytes, hBytes, historyKey} +import com.wavesenterprise.database.Keys.{LeaseStatusHistoryPrefix, LeaseStatusPrefix} +import com.wavesenterprise.database.address.AddressTransactions +import com.wavesenterprise.database.keys.LeaseCFKeys +import com.wavesenterprise.database.keys.LeaseCFKeys.LeasesForAddressPrefix +import com.wavesenterprise.database.rocksdb.ColumnFamily.LeaseCF +import com.wavesenterprise.database.rocksdb.RW +import com.wavesenterprise.database.{InternalRocksDBDeque, Key, Keys} +import com.wavesenterprise.state.reader.LeaseDetails +import com.wavesenterprise.state.{ByteStr, LeaseId} +import com.wavesenterprise.transaction.lease.LeaseTransaction + +object MigrationV10 { + + def apply(rw: RW): Unit = { + val lastAddressId = rw.get(Keys.lastAddressId).getOrElse(BigInt(0)) + + for { + addressId <- BigInt(1) to lastAddressId + address = rw.get(Keys.idToAddress(addressId)) + rocksDBLeasesForAddress = RocksDBKeys.leasesForAddress(addressId) + } { + + val addressLeaseIds = for { + (h, leaseTx) <- leasesForAddress(rw, address).fold(msg => throw new RuntimeException(msg), identity) + leaseId = LeaseId(leaseTx.id()) + isLeaseActive = getLeaseStatus(rw, leaseId) + } yield { + val leaseDetails = LeaseDetails.fromLeaseTx(leaseTx, h).copy(isActive = isLeaseActive) + rw.put(LeaseCFKeys.leaseDetails(leaseId), Some(leaseDetails)) + deleteLeaseStatusData(rw, leaseId) + leaseId + } + + rocksDBLeasesForAddress.addLastN(rw, addressLeaseIds) + } + } + + def leasesForAddress(rw: RW, address: Address): Either[String, Seq[(Int, LeaseTransaction)]] = { + AddressTransactions(rw, address, Set(LeaseTransaction.typeId), Int.MaxValue, None) + .map(_.collect { case (h, tx: LeaseTransaction) => (h, tx) }) + } + + private def getLeaseStatus(rw: RW, leaseId: LeaseId): Boolean = { + rw.get(RocksDBKeys.leaseStatusHistory(leaseId)).headOption.fold(false)(h => rw.get(RocksDBKeys.leaseStatus(leaseId)(h))) + } + + private def deleteLeaseStatusData(rw: RW, leaseId: LeaseId): Unit = { + val leaseStatusHistoryHeights = rw.get(RocksDBKeys.leaseStatusHistory(leaseId)) + rw.delete(RocksDBKeys.leaseStatusHistory(leaseId)) + leaseStatusHistoryHeights.foreach(height => rw.delete(RocksDBKeys.leaseStatus(leaseId)(height))) + } + + object RocksDBKeys { + def leaseStatusHistory(leaseId: LeaseId): Key[Seq[Int]] = historyKey("lease-status-history", LeaseStatusHistoryPrefix, leaseId.arr) + + def leaseStatus(leaseId: LeaseId)(height: Int): Key[Boolean] = + Key("lease-status", hBytes(LeaseStatusPrefix, height, leaseId.arr), _(0) == 1, active => Array[Byte](if (active) 1 else 0)) + + def leasesForAddress(addressId: BigInt): InternalRocksDBDeque[LeaseId] = { + new InternalRocksDBDeque[LeaseId]( + name = "leases-for-address", + columnFamily = LeaseCF, + prefix = bytes(LeasesForAddressPrefix, addressId.toByteArray), + itemEncoder = _.arr, + itemDecoder = bytes => LeaseId(ByteStr(bytes)) + ) + } + } + +} diff --git a/node/src/main/scala/com/wavesenterprise/database/package.scala b/node/src/main/scala/com/wavesenterprise/database/package.scala index 8a2c7b4..b0c11c5 100644 --- a/node/src/main/scala/com/wavesenterprise/database/package.scala +++ b/node/src/main/scala/com/wavesenterprise/database/package.scala @@ -1,6 +1,6 @@ package com.wavesenterprise -import com.google.common.io.{ByteArrayDataInput, ByteArrayDataOutput} +import com.google.common.io.ByteArrayDataInput import com.google.common.io.ByteStreams.{newDataInput, newDataOutput} import com.google.common.primitives.{Ints, Shorts} import com.wavesenterprise.account.Address @@ -322,28 +322,17 @@ package object database { AssetInfo(issuer, height, timestamp, name, description, decimals, reissuable, volume) } - def writeAssetInfo(ai: AssetInfo): Array[Byte] = { + def writeAssetInfo(assetInfo: AssetInfo): Array[Byte] = { val ndo = newDataOutput() - def writeAssetHolder(output: ByteArrayDataOutput, assetHolder: AssetHolder): Unit = { - assetHolder match { - case Account(address) => - output.write(Account.binaryHeader) - output.write(address.bytes.arr) - case Contract(contractId) => - output.write(Contract.binaryHeader) - output.write(contractId.byteStr.arr) - } - } - - writeAssetHolder(ndo, ai.issuer) - ndo.writeInt(ai.height) - ndo.writeLong(ai.timestamp) - ndo.writeString(ai.name) - ndo.writeString(ai.description) - ndo.writeByte(ai.decimals) - ndo.writeBoolean(ai.reissuable) - ndo.writeBigInt(ai.volume) + ndo.write(assetInfo.issuer.toBytes) + ndo.writeInt(assetInfo.height) + ndo.writeLong(assetInfo.timestamp) + ndo.writeString(assetInfo.name) + ndo.writeString(assetInfo.description) + ndo.writeByte(assetInfo.decimals) + ndo.writeBoolean(assetInfo.reissuable) + ndo.writeBigInt(assetInfo.volume) ndo.toByteArray } diff --git a/node/src/main/scala/com/wavesenterprise/database/rocksdb/ColumnFamily.scala b/node/src/main/scala/com/wavesenterprise/database/rocksdb/ColumnFamily.scala index 94d026f..c45295e 100644 --- a/node/src/main/scala/com/wavesenterprise/database/rocksdb/ColumnFamily.scala +++ b/node/src/main/scala/com/wavesenterprise/database/rocksdb/ColumnFamily.scala @@ -16,6 +16,7 @@ object ColumnFamily extends Enum[ColumnFamily] { case object ContractCF extends ColumnFamily("contract") case object PrivacyCF extends ColumnFamily("privacy") case object CertsCF extends ColumnFamily("certs") + case object LeaseCF extends ColumnFamily("lease") override def values: immutable.IndexedSeq[ColumnFamily] = findValues } diff --git a/node/src/main/scala/com/wavesenterprise/database/rocksdb/RocksDBStorage.scala b/node/src/main/scala/com/wavesenterprise/database/rocksdb/RocksDBStorage.scala index a116de3..9843294 100644 --- a/node/src/main/scala/com/wavesenterprise/database/rocksdb/RocksDBStorage.scala +++ b/node/src/main/scala/com/wavesenterprise/database/rocksdb/RocksDBStorage.scala @@ -145,24 +145,6 @@ object DefaultReadOnlyParams extends RocksDBParams { override val readOnly: Boolean = true } -object MigrationSourceParams extends RocksDBParams { - override def cacheSize: Long = 256 * FileUtils.ONE_MB - override val maxWriteBufferNumber: Int = 1 - override val disableWal: Boolean = true - override val atomicFlush: Boolean = false - override val unorderedWrite: Boolean = false - override val readOnly: Boolean = true - override val onlyDefaultColumnFamily: Boolean = true -} - -object MigrationDestinationParams extends RocksDBParams { - override def cacheSize: Long = 256 * FileUtils.ONE_MB - override val maxWriteBufferNumber: Int = 4 - override val disableWal: Boolean = true - override val atomicFlush: Boolean = true - override val unorderedWrite: Boolean = true -} - object SnapshotParams extends RocksDBParams { override def cacheSize: Long = 8 * FileUtils.ONE_MB override val maxWriteBufferNumber: Int = 1 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 c26076c..7314384 100644 --- a/node/src/main/scala/com/wavesenterprise/database/rocksdb/RocksDBWriter.scala +++ b/node/src/main/scala/com/wavesenterprise/database/rocksdb/RocksDBWriter.scala @@ -4,7 +4,7 @@ import cats.implicits._ import com.google.common.cache.CacheBuilder import com.google.common.io.ByteStreams.newDataOutput import com.google.common.primitives.{Longs, Shorts} -import com.wavesenterprise.account.{Address, Alias, PublicKeyAccount} +import com.wavesenterprise.account.{Address, AddressOrAlias, Alias, PublicKeyAccount} import com.wavesenterprise.acl.{OpType, PermissionOp, Permissions, Role} import com.wavesenterprise.block.Block.BlockId import com.wavesenterprise.block._ @@ -14,10 +14,10 @@ import com.wavesenterprise.crypto.PublicKey import com.wavesenterprise.database._ import com.wavesenterprise.database.address.AddressTransactions import com.wavesenterprise.database.certs.CertificatesWriter +import com.wavesenterprise.database.docker.{KeysPagination, KeysRequest} import com.wavesenterprise.database.keys.CertificatesCFKeys.CrlByKeyPrefix -import com.wavesenterprise.database.keys.CrlKey +import com.wavesenterprise.database.keys.{ContractCFKeys, CrlKey, LeaseCFKeys} import com.wavesenterprise.database.rocksdb.ColumnFamily.CertsCF -import com.wavesenterprise.database.docker.{KeysPagination, KeysRequest} import com.wavesenterprise.docker.ContractInfo import com.wavesenterprise.features.BlockchainFeature import com.wavesenterprise.privacy._ @@ -34,7 +34,9 @@ import com.wavesenterprise.transaction.assets.exchange.ExchangeTransaction import com.wavesenterprise.transaction.docker._ import com.wavesenterprise.transaction.docker.assets.ContractAssetOperation.{ ContractBurnV1, + ContractCancelLeaseV1, ContractIssueV1, + ContractLeaseV1, ContractReissueV1, ContractTransferOutV1 } @@ -67,9 +69,6 @@ object RocksDBWriter { ) } - private def loadLeaseStatus(db: ReadOnlyDB, leaseId: ByteStr): Boolean = - db.get(Keys.leaseStatusHistory(leaseId)).headOption.fold(false)(h => db.get(Keys.leaseStatus(leaseId)(h))) - /** {{{ * ([10, 7, 4], 5, 11) => [10, 7, 4] * ([10, 7], 5, 11) => [10, 7, 1] @@ -98,6 +97,9 @@ object RocksDBWriter { lastChange <- db.get(historyKey).headOption } yield db.get(valueKey(lastChange)) } + + case class LeaseParticipantsInfo(leaseId: LeaseId, sender: AssetHolder, recipient: Address) + } trait ReadWriteDB { @@ -255,18 +257,28 @@ class RocksDBWriter(val storage: RocksDBStorage, } yield db.get(Keys.idToAddress(addressId)) -> balance).toMap.seq } - private def loadLeaseBalance(db: ReadOnlyDB, addressId: BigInt): LeaseBalance = { + private def loadAddressLeaseBalanceInternal(db: ReadOnlyDB, addressId: BigInt): LeaseBalance = { val lease = db.fromHistory(Keys.leaseBalanceHistory(addressId), Keys.leaseBalance(addressId)).getOrElse(LeaseBalance.empty) lease } override protected def loadAddressLeaseBalance(address: Address): LeaseBalance = readOnly { db => - addressId(address).fold(LeaseBalance.empty)(loadLeaseBalance(db, _)) + addressId(address).fold(LeaseBalance.empty)(loadAddressLeaseBalanceInternal(db, _)) + } + + private def loadContractLeaseBalanceInternal(db: ReadOnlyDB, contractId: BigInt): LeaseBalance = { + val lease = db.fromHistory(ContractCFKeys.contractLeaseBalanceHistory(contractId), ContractCFKeys.contractLeaseBalance(contractId)).getOrElse( + LeaseBalance.empty) + lease + } + + override protected def loadContractLeaseBalance(contractId: ContractId): LeaseBalance = readOnly { db => + stateIdByContractId(contractId).fold(LeaseBalance.empty)(loadContractLeaseBalanceInternal(db, _)) } private def loadLposAddressPortfolio(db: ReadOnlyDB, addressId: BigInt) = Portfolio( db.fromHistory(Keys.westBalanceHistory(addressId), Keys.westBalance(addressId)).getOrElse(0L), - loadLeaseBalance(db, addressId), + loadAddressLeaseBalanceInternal(db, addressId), Map.empty ) @@ -343,10 +355,12 @@ class RocksDBWriter(val storage: RocksDBStorage, newNonEmptyRoleAddresses: Map[Address, BigInt], westAddressesBalances: Map[BigInt, Long], assetAddressesBalances: Map[BigInt, Map[ByteStr, Long]], - leaseBalances: Map[BigInt, LeaseBalance], + addressLeaseBalances: Map[BigInt, LeaseBalance], + contractLeaseBalances: Map[BigInt, LeaseBalance], westContractsBalances: Map[BigInt, Long], assetContractsBalances: Map[BigInt, Map[ByteStr, Long]], - leaseStates: Map[ByteStr, Boolean], + leaseMap: Map[LeaseId, LeaseDetails], + leaseCancelMap: Map[LeaseId, LeaseDetails], transactions: Seq[Transaction], addressTransactions: Map[BigInt, List[(Int, ByteStr)]], contractTransactions: Map[BigInt, List[(Int, ByteStr)]], @@ -444,11 +458,19 @@ class RocksDBWriter(val storage: RocksDBStorage, val changedContracts = contractTransactions.keys ++ updatedBalanceContracts - for ((addressId, leaseBalance) <- leaseBalances) { + for ((addressId, leaseBalance) <- addressLeaseBalances) { rw.put(Keys.leaseBalance(addressId)(height), leaseBalance) expiredKeys += updateHistory(rw, Keys.leaseBalanceHistory(addressId), balanceThreshold, Keys.leaseBalance(addressId)) } + for ((contractStateId, leaseBalance) <- contractLeaseBalances) { + rw.put(ContractCFKeys.contractLeaseBalance(contractStateId)(height), leaseBalance) + expiredKeys += updateHistory(rw, + ContractCFKeys.contractLeaseBalanceHistory(contractStateId), + balanceThreshold, + ContractCFKeys.contractLeaseBalance(contractStateId)) + } + val newAddressesForAsset = mutable.AnyRefMap.empty[ByteStr, Set[BigInt]] for ((addressId, assets) <- assetAddressesBalances) { val prevAssets = rw.get(Keys.assetList(addressId)) @@ -506,11 +528,6 @@ class RocksDBWriter(val storage: RocksDBStorage, expiredKeys += updateHistory(rw, Keys.assetInfoHistory(assetId), threshold, Keys.assetInfo(assetId)) } - for ((leaseId, state) <- leaseStates) { - rw.put(Keys.leaseStatus(leaseId)(height), state) - expiredKeys += updateHistory(rw, Keys.leaseStatusHistory(leaseId), threshold, Keys.leaseStatus(leaseId)) - } - for ((addressId, script) <- scripts) { expiredKeys += updateHistory(rw, Keys.addressScriptHistory(addressId), threshold, Keys.addressScript(addressId)) script.foreach(s => rw.put(Keys.addressScript(addressId)(height), Some(s))) @@ -668,6 +685,59 @@ class RocksDBWriter(val storage: RocksDBStorage, minersBanHistory.foreach { case (address, history) => updateMinerBanHistory(rw, address, history, height) } minersCancelledWarnings.foreach((updateMinerCancelledWarnings(rw) _).tupled) + + for ((leaseId, leaseDetails) <- leaseMap) { + rw.put(LeaseCFKeys.leaseDetails(leaseId), Some(leaseDetails)) + } + + for ((leaseId, leaseDetails) <- leaseCancelMap) { + rw.put(LeaseCFKeys.leaseDetails(leaseId), Some(leaseDetails)) + } + + val orderedLeasePairs = transactions + .collect { + case tx: LeaseTransaction => + val leaseId = LeaseId(tx.id()) + Seq(leaseId -> leaseMap(leaseId)) + + case tx: ExecutedContractTransactionV3 => + val leaseOps = tx.assetOperations.collect { + case leaseOp: ContractLeaseV1 => + val leaseId = LeaseId(leaseOp.leaseId) + leaseId -> leaseMap(leaseId) + } + leaseOps + + }.flatten + + val leasesByAssetHolder = groupOrderedLeasesByAssetHolder(orderedLeasePairs) + + for ((assetHolder, leases) <- leasesByAssetHolder) { + val leasesForAssetHolderDB = assetHolder match { + case account @ Account(address) => + val addressId = newBalancedAccountsMap.getOrElse(account, addressIdUnsafe(address)) + LeaseCFKeys.leasesForAddress(addressId, storage) + case contract @ Contract(contractId) => + val contractStateId = newBalancedContractsMap.getOrElse(contract, contractStateIdUnsafe(contractId)) + LeaseCFKeys.leasesForContract(contractStateId, storage) + } + + leasesForAssetHolderDB.addLastN(rw, leases) + } + + } + + private def addressIdUnsafe(address: Address): BigInt = { + addressId(address).getOrElse(throw new RuntimeException(s"Unknown address: $address, can't find id for it")) + } + + private def contractStateIdUnsafe(contract: ContractId): BigInt = { + stateIdByContractId(contract).getOrElse(throw new RuntimeException(s"Unknown contract id: $contract, can't find id for it")) + } + + private def extractAddressUnsafe(addressOrAlias: AddressOrAlias): Address = addressOrAlias match { + case a: Address => a + case a: Alias => resolveAlias(a).getOrElse(throw new RuntimeException(s"Can't resolve alias ${a.stringRepr}")) } private def appendNonEmptyRoleAddresses(newNonEmptyRoleAddresses: Map[Address, BigInt], rw: RW): Unit = { @@ -689,13 +759,21 @@ class RocksDBWriter(val storage: RocksDBStorage, */ override protected def doRollback(targetBlockId: ByteStr): Seq[Block] = { - case class AssetInfoInvalidationData(assetIdsForInfoInvalidation: Seq[AssetId], assetIdsForDiscard: Seq[AssetId]) + case class AssetInfoInvalidationData( + assetIdsForInfoInvalidation: Seq[AssetId], + assetIdsForDiscard: Seq[AssetId], + leaseInfoForDiscard: Seq[LeaseParticipantsInfo], + leaseCancelIdsForDiscard: Seq[LeaseId] + ) - def assetOpsToDataForRollback(assetOps: Seq[com.wavesenterprise.transaction.docker.assets.ContractAssetOperation]) = { + def assetOpsToDataForRollback(contractId: ContractId, + assetOpsRollbackOrder: Seq[com.wavesenterprise.transaction.docker.assets.ContractAssetOperation]) = { val assetIdsForInfoInvalidationBuilder = Seq.newBuilder[AssetId] val assetIdsForDiscardBuilder = Seq.newBuilder[AssetId] + val leaseInfoForDiscardBuilder = Seq.newBuilder[LeaseParticipantsInfo] + val leaseCancelIdsForDiscardBuilder = Seq.newBuilder[LeaseId] - assetOps.foreach { + assetOpsRollbackOrder.foreach { case issueOp: ContractIssueV1 => assetIdsForInfoInvalidationBuilder += issueOp.assetId assetIdsForDiscardBuilder += issueOp.assetId @@ -703,12 +781,20 @@ class RocksDBWriter(val storage: RocksDBStorage, assetIdsForInfoInvalidationBuilder += reissueOp.assetId case burnOp: ContractBurnV1 if burnOp.assetId.isDefined => assetIdsForInfoInvalidationBuilder += burnOp.assetId.get + case leaseOp: ContractLeaseV1 => + val recipientAddress = extractAddressUnsafe(leaseOp.recipient) + val leaseInfo = LeaseParticipantsInfo(LeaseId(leaseOp.leaseId), Contract(contractId), recipientAddress) + leaseInfoForDiscardBuilder += leaseInfo + case leaseCancelOp: ContractCancelLeaseV1 => + leaseCancelIdsForDiscardBuilder += LeaseId(leaseCancelOp.leaseId) case _: ContractTransferOutV1 => () } AssetInfoInvalidationData( assetIdsForInfoInvalidationBuilder.result(), - assetIdsForDiscardBuilder.result() + assetIdsForDiscardBuilder.result(), + leaseInfoForDiscardBuilder.result(), + leaseCancelIdsForDiscardBuilder.result() ) } @@ -739,6 +825,9 @@ class RocksDBWriter(val storage: RocksDBStorage, val contractsKeysToDiscard = mutable.Map[ByteStr, Set[String]]() val dataKeysToDiscard = mutable.Map[(BigInt, Address), Set[String]]() + val leasesToDiscard = Seq.newBuilder[LeaseParticipantsInfo] + val leaseCancelIdsToDiscard = Seq.newBuilder[LeaseId] + val (discardedHeader, _) = rw .get(Keys.blockHeaderAndSizeAt(currentHeight)) .getOrElse(throw new IllegalArgumentException(s"No block at height $currentHeight")) @@ -760,9 +849,9 @@ class RocksDBWriter(val storage: RocksDBStorage, log.trace(s"Discarding portfolio for $address") portfoliosToInvalidate += address.toAssetHolder - balanceAtHeightCache.invalidate((currentHeight, addressId)) // todo: separate address and contract cache + addressBalanceAtHeightCache.invalidate((currentHeight, addressId)) leaseBalanceAtHeightCache.invalidate((currentHeight, addressId)) - discardLeaseBalance(address) + discardAddressLeaseBalance(address) val kTxSeqNr = Keys.addressTransactionSeqNr(addressId) val txSeqNr = rw.get(kTxSeqNr) @@ -784,8 +873,11 @@ class RocksDBWriter(val storage: RocksDBStorage, rw.delete(WEKeys.contractWestBalance(contractStateId)(currentHeight)) rw.filterHistory(Keys.westBalanceHistory(contractStateId), currentHeight) + rw.filterHistory(ContractCFKeys.contractLeaseBalanceHistory(contractStateId), currentHeight) + portfoliosToInvalidate += ContractId(contractId).toAssetHolder - balanceAtHeightCache.invalidate((currentHeight, contractStateId)) + contractBalanceAtHeightCache.invalidate((currentHeight, contractStateId)) + leaseBalanceAtHeightCache.invalidate((currentHeight, contractStateId)) } val txIdsAtHeight = Keys.transactionIdsAtHeight(currentHeight) @@ -811,9 +903,16 @@ class RocksDBWriter(val storage: RocksDBStorage, case tx: SponsorFeeTransaction => assetInfoToInvalidate += rollbackSponsorship(rw, tx.assetId, currentHeight) case tx: LeaseTransaction => - rollbackLeaseStatus(rw, tx.id(), currentHeight) + val recipientAddress = tx.recipient match { + case a: Address => a + case a: Alias => resolveAlias(a).getOrElse(throw new RuntimeException(s"Can't resolve alias ${a.stringRepr}")) + } + + val leaseDiscardInfo: LeaseParticipantsInfo = + LeaseParticipantsInfo(LeaseId(tx.id()), Account(tx.sender.toAddress), recipientAddress) + leasesToDiscard += leaseDiscardInfo case tx: LeaseCancelTransaction => - rollbackLeaseStatus(rw, tx.leaseId, currentHeight) + leaseCancelIdsToDiscard += LeaseId(tx.leaseId) case tx: SetScriptTransaction => val address = tx.sender.toAddress @@ -876,11 +975,16 @@ class RocksDBWriter(val storage: RocksDBStorage, tx match { case executedTxV3: ExecutedContractTransactionV3 => - val assetOps = executedTxV3.assetOperations - val AssetInfoInvalidationData(assetIdsForInfoInvalidation, assetIdsForDiscard) = assetOpsToDataForRollback(assetOps) + val contractId = ContractId(executedTxV3.tx.contractId) + val AssetInfoInvalidationData(assetIdsForInfoInvalidation, assetIdsForDiscard, leaseInfoForDiscard, leaseCancelIdsForDiscard) = + assetOpsToDataForRollback(contractId, executedTxV3.assetOperations.reverse) + assetInfoToInvalidate ++= assetIdsForInfoInvalidation.map(rollbackAssetInfo(rw, _, currentHeight)) assetIdsToDiscard ++= assetIdsForDiscard collectKeysToDiscard(tx, contractsKeysToDiscard) + + leasesToDiscard ++= leaseInfoForDiscard + leaseCancelIdsToDiscard ++= leaseCancelIdsForDiscard case _ => () } @@ -936,6 +1040,8 @@ class RocksDBWriter(val storage: RocksDBStorage, updatePolicy(rw, policyId, policyDiff) } + rollbackLeaseActions(rw, leasesToDiscard.result(), leaseCancelIdsToDiscard.result()) + rw.delete(txIdsAtHeight) rw.delete(Keys.blockHeaderAndSizeAt(currentHeight)) rw.delete(Keys.blockTransactionsAtHeight(currentHeight)) @@ -1035,11 +1141,6 @@ class RocksDBWriter(val storage: RocksDBStorage, orderId } - private def rollbackLeaseStatus(rw: RW, leaseId: ByteStr, currentHeight: Int): Unit = { - rw.delete(Keys.leaseStatus(leaseId)(currentHeight)) - rw.filterHistory(Keys.leaseStatusHistory(leaseId), currentHeight) - } - private def rollbackSponsorship(rw: RW, assetId: ByteStr, currentHeight: Int): ByteStr = { rw.delete(Keys.sponsorship(assetId)(currentHeight)) rw.filterHistory(Keys.sponsorshipHistory(assetId), currentHeight) @@ -1208,6 +1309,60 @@ class RocksDBWriter(val storage: RocksDBStorage, } } + private def groupOrderedLeasesByAssetHolder(leasePairs: Seq[(LeaseId, LeaseDetails)]): Map[AssetHolder, Seq[LeaseId]] = { + val leasesByAssetHolder = scala.collection.mutable.Map[AssetHolder, Seq[LeaseId]]().withDefaultValue(Seq()) + + leasePairs.foreach { case (leaseId, leaseDetails) => + val sender = leaseDetails.sender + val recipient = { + val recipientAddress = leaseDetails.recipient match { + case a: Address => a + case a: Alias => resolveAlias(a).getOrElse(throw new RuntimeException(s"Can't resolve alias ${a.stringRepr}")) + } + Account(recipientAddress) + } + + leasesByAssetHolder(sender) = leasesByAssetHolder(sender) :+ leaseId + leasesByAssetHolder(recipient) = leasesByAssetHolder(recipient) :+ leaseId + } + + leasesByAssetHolder.toMap + } + + private def rollbackLeaseActions(rw: RW, leasesRollbackOrdered: Seq[LeaseParticipantsInfo], leaseCancelIds: Seq[LeaseId]) = { + val leasesByAssetHolder = scala.collection.mutable.Map[AssetHolder, Seq[LeaseId]]().withDefaultValue(Seq()) + + leasesRollbackOrdered.foreach { lease => + rw.delete(LeaseCFKeys.leaseDetails(lease.leaseId)) + leasesByAssetHolder(lease.sender) = lease.leaseId +: leasesByAssetHolder(lease.sender) + leasesByAssetHolder(Account(lease.recipient)) = lease.leaseId +: leasesByAssetHolder(Account(lease.recipient)) + } + + leasesByAssetHolder.foreach { case (assetHolder, leaseIds) => + val storedLeaseIds = assetHolder match { + case Account(address) => + val addressId = loadAddressId(address).get + LeaseCFKeys.leasesForAddress(addressId, storage) + case Contract(contractId) => + val contractStateId = stateIdByContractId(contractId).get + LeaseCFKeys.leasesForContract(contractStateId, storage) + } + + val lastStored = storedLeaseIds.takeRight(leaseIds.size) + + if (lastStored != leaseIds) { + throw new RuntimeException(s"Wrong unexpected leases' ids order for ${assetHolder.description}") + + } + + storedLeaseIds.pollLastN(leaseIds.size) + } + + leaseCancelIds.map(leaseId => + rw.update(LeaseCFKeys.leaseDetails(leaseId))(leaseDetails => leaseDetails.map(_.copy(isActive = true)))) + + } + override def transactionInfo(id: ByteStr): Option[(Int, Transaction)] = readOnly(transactionInfo(id, _)) protected def transactionInfo(id: ByteStr, db: ReadOnlyDB): Option[(Int, Transaction)] = { @@ -1231,22 +1386,26 @@ class RocksDBWriter(val storage: RocksDBStorage, .toRight(AliasDoesNotExist(alias)) } - override def leaseDetails(leaseId: ByteStr): Option[LeaseDetails] = readOnly { db => - transactionInfo(leaseId, db) match { - case Some((h, lt: LeaseTransaction)) => - Some(LeaseDetails(lt.sender, lt.recipient, h, lt.amount, loadLeaseStatus(db, leaseId))) - case _ => None - } + override def leaseDetails(leaseId: LeaseId): Option[LeaseDetails] = readOnly { db => + db.get(LeaseCFKeys.leaseDetails(leaseId)) } // These two caches are used exclusively for balance snapshots. They are not used for portfolios, because there aren't // as many miners, so snapshots will rarely be evicted due to overflows. - private val balanceAtHeightCache = CacheBuilder + import java.lang.{Long => JLong} + + private val addressBalanceAtHeightCache = CacheBuilder .newBuilder() .maximumSize(100000) .recordStats() - .build[(Int, BigInt), java.lang.Long]() + .build[(Int, BigInt), JLong]() + + private val contractBalanceAtHeightCache = CacheBuilder + .newBuilder() + .maximumSize(10000) + .recordStats() + .build[(Int, BigInt), JLong] private val leaseBalanceAtHeightCache = CacheBuilder .newBuilder() @@ -1254,13 +1413,19 @@ class RocksDBWriter(val storage: RocksDBStorage, .recordStats() .build[(Int, BigInt), LeaseBalance]() + private val contractLeaseBalanceAtHeightCache = CacheBuilder + .newBuilder() + .maximumSize(10000) + .recordStats() + .build[(Int, BigInt), LeaseBalance]() + override def addressBalanceSnapshots(address: Address, from: Int, to: Int): Seq[BalanceSnapshot] = readOnly { db => db.get(Keys.addressId(address)).fold(Seq(BalanceSnapshot(1, 0, 0, 0))) { addressId => val wbh = slice(db.get(Keys.westBalanceHistory(addressId)), from, to) val lbh = slice(db.get(Keys.leaseBalanceHistory(addressId)), from, to) for { (wh, lh) <- merge(wbh, lbh) - wb = balanceAtHeightCache.get((wh, addressId), () => db.get(Keys.westBalance(addressId)(wh))) + wb = addressBalanceAtHeightCache.get((wh, addressId), () => db.get(Keys.westBalance(addressId)(wh))) lb = leaseBalanceAtHeightCache.get((lh, addressId), () => db.get(Keys.leaseBalance(addressId)(lh))) } yield BalanceSnapshot(wh.max(lh), wb, lb.in, lb.out) } @@ -1269,10 +1434,12 @@ class RocksDBWriter(val storage: RocksDBStorage, override def contractBalanceSnapshots(contractId: ContractId, from: Int, to: Int): Seq[BalanceSnapshot] = readOnly { db => db.get(WEKeys.contractIdToStateId(contractId.byteStr)).fold(Seq(BalanceSnapshot(1, 0, 0, 0))) { contractStateId => val wbh = slice(db.get(WEKeys.contractWestBalanceHistory(contractStateId)), from, to) + val lbh = slice(db.get(ContractCFKeys.contractLeaseBalanceHistory(contractStateId)), from, to) for { - (wh, lh) <- merge(wbh, Seq.empty) - wb = balanceAtHeightCache.get((wh, contractStateId), () => db.get(WEKeys.contractWestBalance(contractStateId)(wh))) - } yield BalanceSnapshot(wh.max(lh), wb, 0L, 0L) + (wh, lh) <- merge(wbh, lbh) + wb = contractBalanceAtHeightCache.get((wh, contractStateId), () => db.get(WEKeys.contractWestBalance(contractStateId)(wh))) + lb = contractLeaseBalanceAtHeightCache.get((lh, contractStateId), () => db.get(ContractCFKeys.contractLeaseBalance(contractStateId)(lh))) + } yield BalanceSnapshot(wh.max(lh), wb, lb.in, lb.out) } } @@ -1337,17 +1504,6 @@ class RocksDBWriter(val storage: RocksDBStorage, recMerge(wbh.head, wbh.tail, lbh.head, lbh.tail, ArrayBuffer.empty) } - override def allActiveLeases: Set[LeaseTransaction] = readOnly { db => - val txs = for { - h <- 1 to db.get(Keys.height) - id <- db.get(Keys.transactionIdsAtHeight(h)) - if loadLeaseStatus(db, id) - (_, tx) <- db.get(Keys.transactionInfo(id)) - } yield tx - - txs.collect { case lt: LeaseTransaction => lt }.toSet - } - override def collectAddressLposPortfolios[A](pf: PartialFunction[(Address, Portfolio), A]): Map[Address, A] = readOnly { db => val b = Map.newBuilder[Address, A] for (id <- BigInt(1) to db.get(Keys.lastAddressId).getOrElse(BigInt(0))) { diff --git a/node/src/main/scala/com/wavesenterprise/docker/ContractExecutionComponents.scala b/node/src/main/scala/com/wavesenterprise/docker/ContractExecutionComponents.scala index 5b1f22c..b903cd0 100644 --- a/node/src/main/scala/com/wavesenterprise/docker/ContractExecutionComponents.scala +++ b/node/src/main/scala/com/wavesenterprise/docker/ContractExecutionComponents.scala @@ -2,27 +2,23 @@ package com.wavesenterprise.docker import akka.actor.ActorSystem import akka.http.scaladsl.model.{HttpRequest, HttpResponse} -import com.wavesenterprise.account.{Address, PrivateKeyAccount} -import com.wavesenterprise.api.http.ApiRoute -import com.wavesenterprise.api.http.docker.InternalContractsApiRoute +import com.wavesenterprise.account.PrivateKeyAccount import com.wavesenterprise.api.http.service.{AddressApiService, ContractsApiService, PermissionApiService} import com.wavesenterprise.block.Block.BlockId -import com.wavesenterprise.block.KeyBlockIdsCache import com.wavesenterprise.docker.grpc.GrpcContractExecutor import com.wavesenterprise.docker.grpc.service._ import com.wavesenterprise.docker.validator.{ContractValidatorResultsStore, ExecutableTransactionsValidator} import com.wavesenterprise.mining.{TransactionsAccumulator, TransactionsAccumulatorProvider} import com.wavesenterprise.network.peers.ActivePeerConnections import com.wavesenterprise.protobuf.service.contract._ -import com.wavesenterprise.settings.{ApiSettings, WESettings} +import com.wavesenterprise.settings.WESettings import com.wavesenterprise.state.reader.DelegatingBlockchain import com.wavesenterprise.state.{Blockchain, ByteStr, MiningConstraintsHolder, NG} import com.wavesenterprise.transaction.BlockchainUpdater -import com.wavesenterprise.utils.{NTP, ScorexLogging, Time} +import com.wavesenterprise.utils.{NTP, ScorexLogging} import com.wavesenterprise.utx.UtxPool import com.wavesenterprise.wallet.Wallet import monix.execution.Scheduler -import monix.execution.schedulers.SchedulerService import scala.concurrent.Future @@ -35,13 +31,11 @@ case class ContractExecutionComponents( blockchain: Blockchain with NG with MiningConstraintsHolder, activePeerConnections: ActivePeerConnections, contractReusedContainers: ContractReusedContainers, - legacyContractExecutor: LegacyContractExecutor, grpcContractExecutor: GrpcContractExecutor, contractExecutionMessagesCache: ContractExecutionMessagesCache, contractValidatorResultsStore: ContractValidatorResultsStore, contractAuthTokenService: ContractAuthTokenService, dockerExecutorScheduler: Scheduler, - contractsRoutes: Seq[ApiRoute], partialHandlers: Seq[PartialFunction[HttpRequest, Future[HttpResponse]]], private val delegatingState: DelegatingBlockchain ) extends AutoCloseable @@ -58,7 +52,6 @@ case class ContractExecutionComponents( utx = utx, blockchain = blockchain, time = time, - legacyContractExecutor = legacyContractExecutor, grpcContractExecutor = grpcContractExecutor, contractValidatorResultsStore = contractValidatorResultsStore, keyBlockId = keyBlockId, @@ -88,7 +81,6 @@ case class ContractExecutionComponents( blockchain = blockchain, time = time, activePeerConnections = activePeerConnections, - legacyContractExecutor = legacyContractExecutor, grpcContractExecutor = grpcContractExecutor, keyBlockId = keyBlockId, parallelism = settings.dockerEngine.contractsParallelism.value @@ -106,9 +98,6 @@ case class ContractExecutionComponents( object ContractExecutionComponents extends ScorexLogging { - type BuildInternalContractsApiRoute = - (ContractsApiService, ApiSettings, Time, ContractAuthTokenService, Address, SchedulerService) => InternalContractsApiRoute - def apply( settings: WESettings, dockerExecutorScheduler: Scheduler, @@ -120,30 +109,15 @@ object ContractExecutionComponents extends ScorexLogging { wallet: Wallet, privacyServiceImpl: PrivacyServiceImpl, activePeerConnections: ActivePeerConnections, - schedulerService: SchedulerService, - legacyContractExecutor: LegacyContractExecutor, grpcContractExecutor: GrpcContractExecutor, dockerEngine: DockerEngine, contractAuthTokenService: ContractAuthTokenService, contractReusedContainers: ContractReusedContainers, - buildInternalContractsApiRoute: BuildInternalContractsApiRoute, - keyBlockIdsCache: KeyBlockIdsCache )(implicit grpcSystem: ActorSystem): ContractExecutionComponents = { - val nodeOwner = nodeOwnerAccount.toAddress val contractValidatorResultsStore = new ContractValidatorResultsStore val delegatingState = new DelegatingBlockchain(blockchainUpdater) val contractsApiService = new ContractsApiService(delegatingState, contractExecutionMessagesCache) - val internalContractsApiRoute = - buildInternalContractsApiRoute( - contractsApiService, - settings.api, - time, - contractAuthTokenService, - nodeOwner, - schedulerService - ) - val partialHandlers = Seq( AddressServicePowerApiHandler.partial( @@ -167,13 +141,11 @@ object ContractExecutionComponents extends ScorexLogging { blockchainUpdater, activePeerConnections, contractReusedContainers, - legacyContractExecutor, grpcContractExecutor, contractExecutionMessagesCache, contractValidatorResultsStore, contractAuthTokenService, dockerExecutorScheduler, - Seq(internalContractsApiRoute), partialHandlers, delegatingState ) diff --git a/node/src/main/scala/com/wavesenterprise/docker/LegacyContractExecutor.scala b/node/src/main/scala/com/wavesenterprise/docker/LegacyContractExecutor.scala deleted file mode 100644 index 36a04a2..0000000 --- a/node/src/main/scala/com/wavesenterprise/docker/LegacyContractExecutor.scala +++ /dev/null @@ -1,114 +0,0 @@ -package com.wavesenterprise.docker - -import cats.implicits._ -import com.wavesenterprise.docker.ContractExecutor.{ContractSuccessCode, ContractTxClaimContent} -import com.wavesenterprise.metrics.Metrics.CircuitBreakerCacheSettings -import com.wavesenterprise.metrics.docker.{ContractExecutionMetrics, ExecContractTx, ParseContractResults} -import com.wavesenterprise.settings.dockerengine.DockerEngineSettings -import com.wavesenterprise.state.DataEntry -import com.wavesenterprise.transaction.docker.{CallContractTransaction, CreateContractTransaction, ExecutableTransaction} -import monix.eval.Task -import monix.execution.Scheduler -import play.api.libs.json.Json - -class LegacyContractExecutor( - val dockerEngine: DockerEngine, - val dockerEngineSettings: DockerEngineSettings, - val nodeApiSettings: NodeRestApiSettings, - val contractAuthTokenService: ContractAuthTokenService, - val contractReusedContainers: ContractReusedContainers, - val circuitBreakerCacheSettings: CircuitBreakerCacheSettings, - val scheduler: Scheduler -) extends ContractExecutor { - - def createContainerEnvParams: List[String] = - List( - s"NODE=${nodeApiSettings.node}", - s"NODE_PORT=${nodeApiSettings.restApiPort}", - s"NODE_API=${nodeApiSettings.nodeRestAPI}" - ) - - protected override def startContainer( - contract: ContractInfo, - metrics: ContractExecutionMetrics - ): Task[String] = deferEither { - dockerEngine.createAndStartContainer(contract, metrics, createContainerEnvParams) - } - - override protected def executeCreate(containerId: String, - contract: ContractInfo, - tx: CreateContractTransaction, - metrics: ContractExecutionMetrics): Task[ContractExecution] = Task.defer { - val task = deferEither(dockerEngine.executeRunScript(containerId, createEnvParams("CREATE", tx), metrics)) - metrics.measureTask(ExecContractTx, task).flatMap(toContractExecution(_, metrics)) - } - - override protected def executeCall(containerId: String, - contract: ContractInfo, - tx: CallContractTransaction, - metrics: ContractExecutionMetrics): Task[ContractExecution] = Task.defer { - val task = deferEither(dockerEngine.executeRunScript(containerId, createEnvParams("CALL", tx), metrics)) - metrics.measureTask(ExecContractTx, task).flatMap(toContractExecution(_, metrics)) - } - - private def createEnvParams(command: String, tx: ExecutableTransaction): Map[String, String] = { - Map( - "TX" -> tx.json().toString(), - "COMMAND" -> command, - "API_TOKEN" -> contractAuthTokenService.create(ContractTxClaimContent(tx.id(), tx.contractId), dockerEngineSettings.contractAuthExpiresIn) - ) - } - - private def toContractExecution(result: (Int, String), metrics: ContractExecutionMetrics): Task[ContractExecution] = Task.defer { - result match { - case (ContractSuccessCode, string: String) => - if (string.isEmpty) { - Task.now(ContractExecutionSuccess(List.empty)) - } else { - metrics.measureTask(ParseContractResults, deferEither(parseContractResult(string))) - } - case (code: Int, message: String) => - Task.now(ContractExecutionError(code, message)) - } - } - - private def parseContractResult(string: String): Either[ContractExecutionException, ContractExecution] = { - (for { - parsed <- Either.catchNonFatal(Json.parse(string)).leftMap(_ -> s"Can't parse and validate contract execution result '$string' as JSON") - success <- Either - .catchNonFatal(ContractExecutionSuccess(parsed.as[List[DataEntry[_]]])) - .leftMap(_ -> s"Can't parse contract execution result '$string' as results array") - } yield success).leftMap(ContractExecutionException.apply) - } -} - -object LegacyContractExecutor { - def apply( - dockerEngine: DockerEngine, - dockerEngineSettings: DockerEngineSettings, - circuitBreakerCacheSettings: CircuitBreakerCacheSettings, - contractAuthTokenService: ContractAuthTokenService, - contractReusedContainers: ContractReusedContainers, - scheduler: Scheduler, - restApiPort: Int, - localDockerHostResolver: LocalDockerHostResolver, - ): LegacyContractExecutor = { - val nodeRestApiSettings = NodeRestApiSettings - .createApiSettings( - localDockerHostResolver, - dockerEngineSettings, - restApiPort - ) - .fold(ex => throw ex, identity) - - new LegacyContractExecutor( - dockerEngine, - dockerEngineSettings, - nodeRestApiSettings, - contractAuthTokenService, - contractReusedContainers, - circuitBreakerCacheSettings, - scheduler - ) - } -} diff --git a/node/src/main/scala/com/wavesenterprise/docker/MinerTransactionsExecutor.scala b/node/src/main/scala/com/wavesenterprise/docker/MinerTransactionsExecutor.scala index 1bd3c46..a01e7f2 100644 --- a/node/src/main/scala/com/wavesenterprise/docker/MinerTransactionsExecutor.scala +++ b/node/src/main/scala/com/wavesenterprise/docker/MinerTransactionsExecutor.scala @@ -30,7 +30,6 @@ class MinerTransactionsExecutor( val utx: UtxPool, val blockchain: Blockchain with NG, val time: Time, - val legacyContractExecutor: LegacyContractExecutor, val grpcContractExecutor: GrpcContractExecutor, val contractValidatorResultsStore: ContractValidatorResultsStore, val keyBlockId: ByteStr, @@ -45,8 +44,12 @@ class MinerTransactionsExecutor( private[this] val txMetrics = new ConcurrentHashMap[ByteStr, ContractExecutionMetrics]() private[this] val validationFeatureActivated: Boolean = blockchain.isFeatureActivated(BlockchainFeature.ContractValidationsSupport, blockchain.height) - private[this] val contractNativeTokenFeatureActivated: Boolean = + private[this] val contractNativeTokenFeatureActivated: Boolean = { blockchain.isFeatureActivated(BlockchainFeature.ContractNativeTokenSupportAndPkiV1Support, blockchain.height) + } + private[this] val leaseOpsForContractsFeatureActivated: Boolean = { + blockchain.isFeatureActivated(BlockchainFeature.LeaseOpsForContractsSupport, blockchain.height) + } contractValidatorResultsStore.removeExceptFor(keyBlockId) @@ -143,7 +146,8 @@ class MinerTransactionsExecutor( resultsHash = ContractTransactionValidation.resultsHash(results, assetOperations) validators = blockchain.lastBlockContractValidators - minerAddress validationProofs <- selectValidationProofs(tx.id(), validators, validationPolicy, resultsHash) - _ <- checkAssetOperationsAreSupported(contractNativeTokenFeatureActivated, assetOperations) + _ <- checkAssetOperationsSupported(contractNativeTokenFeatureActivated, assetOperations) + _ <- checkLeaseOpsForContractSupported(leaseOpsForContractsFeatureActivated, assetOperations) executedTx <- if (contractNativeTokenFeatureActivated) { ExecutedContractTransactionV3.selfSigned( nodeOwnerAccount, diff --git a/node/src/main/scala/com/wavesenterprise/docker/TransactionsExecutor.scala b/node/src/main/scala/com/wavesenterprise/docker/TransactionsExecutor.scala index 216b9fa..242a083 100644 --- a/node/src/main/scala/com/wavesenterprise/docker/TransactionsExecutor.scala +++ b/node/src/main/scala/com/wavesenterprise/docker/TransactionsExecutor.scala @@ -10,14 +10,16 @@ import com.wavesenterprise.docker.exceptions.FatalExceptionsMatchers._ import com.wavesenterprise.docker.grpc.GrpcContractExecutor import com.wavesenterprise.metrics.docker.ContractExecutionMetrics import com.wavesenterprise.mining.{ExecutableTxSetup, TransactionWithDiff, TransactionsAccumulator} -import com.wavesenterprise.state.{Blockchain, ByteStr, ContractId, DataEntry, NG} import com.wavesenterprise.state.diffs.AssetTransactionsDiff.checkAssetIdLength +import com.wavesenterprise.state.{Blockchain, ByteStr, ContractId, DataEntry, NG} import com.wavesenterprise.transaction.ValidationError.ContractNotFound import com.wavesenterprise.transaction.docker._ import com.wavesenterprise.transaction.docker.assets.ContractAssetOperation import com.wavesenterprise.transaction.docker.assets.ContractAssetOperation.{ ContractBurnV1, + ContractCancelLeaseV1, ContractIssueV1, + ContractLeaseV1, ContractReissueV1, ContractTransferOutV1 } @@ -44,7 +46,6 @@ trait TransactionsExecutor extends ScorexLogging { def messagesCache: ContractExecutionMessagesCache def nodeOwnerAccount: PrivateKeyAccount def time: Time - def legacyContractExecutor: LegacyContractExecutor def grpcContractExecutor: GrpcContractExecutor def keyBlockId: ByteStr @@ -171,8 +172,10 @@ trait TransactionsExecutor extends ScorexLogging { private def selectExecutor(executableTransaction: ExecutableTransaction): Either[ContractExecutionException, ContractExecutor] = { executableTransaction match { - case _: CreateContractTransactionV1 => Right(legacyContractExecutor) - // all versions except for V1 (matched above) would use a gRPC executor + case _: CreateContractTransactionV1 => + Left(ContractExecutionException(ValidationError.ContractExecutionError( + executableTransaction.contractId, + "CreateContractTransactionV1 support was deleted as deprecated"))) case _: CreateContractTransaction => Right(grpcContractExecutor) case _ => for { @@ -293,18 +296,31 @@ trait TransactionsExecutor extends ScorexLogging { atomically: Boolean ): Either[ValidationError, TransactionWithDiff] - def checkAssetOperationsAreSupported( + def checkAssetOperationsSupported( contractNativeTokenFeatureActivated: Boolean, assetOperations: List[ContractAssetOperation] ): Either[ValidationError, Unit] = - Either.cond(contractNativeTokenFeatureActivated || assetOperations.isEmpty, (), ValidationError.UnsupportedAssetOperations) + Either.cond(contractNativeTokenFeatureActivated || assetOperations.isEmpty, (), ValidationError.BaseAssetOpsNotSupported) + + def checkLeaseOpsForContractSupported( + leaseOpsForContractsFeatureActivated: Boolean, + assetOperation: List[ContractAssetOperation] + ): Either[ValidationError, Unit] = { + val containsLeaseOps = assetOperation.exists { + case _: ContractLeaseV1 | _: ContractCancelLeaseV1 => true + case _ => false + } + + Either.cond(leaseOpsForContractsFeatureActivated || !containsLeaseOps, (), ValidationError.LeaseAssetOpsNotSupported) + } def validateAssetIdLength(assetOperations: List[ContractAssetOperation]): Either[ValidationError, Unit] = assetOperations.traverse { - case op: ContractIssueV1 => checkAssetIdLength(op.assetId) - case op: ContractReissueV1 => checkAssetIdLength(op.assetId) - case op: ContractTransferOutV1 => op.assetId.fold[Either[ValidationError, Unit]](Right(()))(checkAssetIdLength) - case op: ContractBurnV1 => op.assetId.fold[Either[ValidationError, Unit]](Right(()))(checkAssetIdLength) + case op: ContractIssueV1 => checkAssetIdLength(op.assetId) + case op: ContractReissueV1 => checkAssetIdLength(op.assetId) + case op: ContractTransferOutV1 => op.assetId.fold[Either[ValidationError, Unit]](Right(()))(checkAssetIdLength) + case op: ContractBurnV1 => op.assetId.fold[Either[ValidationError, Unit]](Right(()))(checkAssetIdLength) + case _: ContractLeaseV1 | _: ContractCancelLeaseV1 => Right(()) }.void } diff --git a/node/src/main/scala/com/wavesenterprise/docker/ValidatorTransactionsExecutor.scala b/node/src/main/scala/com/wavesenterprise/docker/ValidatorTransactionsExecutor.scala index e3505b7..bfdbb13 100644 --- a/node/src/main/scala/com/wavesenterprise/docker/ValidatorTransactionsExecutor.scala +++ b/node/src/main/scala/com/wavesenterprise/docker/ValidatorTransactionsExecutor.scala @@ -38,7 +38,6 @@ class ValidatorTransactionsExecutor( val blockchain: Blockchain with NG, val time: Time, val activePeerConnections: ActivePeerConnections, - val legacyContractExecutor: LegacyContractExecutor, val grpcContractExecutor: GrpcContractExecutor, val keyBlockId: ByteStr, val parallelism: Int @@ -49,6 +48,9 @@ class ValidatorTransactionsExecutor( private[this] val contractNativeTokenFeatureActivated: Boolean = blockchain.isFeatureActivated(BlockchainFeature.ContractNativeTokenSupportAndPkiV1Support, blockchain.height) + private[this] val leaseOpsForContractsFeatureActivated: Boolean = { + blockchain.isFeatureActivated(BlockchainFeature.LeaseOpsForContractsSupport, blockchain.height) + } override protected def handleUpdateSuccess(metrics: ContractExecutionMetrics, tx: ExecutableTransaction, @@ -91,7 +93,9 @@ class ValidatorTransactionsExecutor( atomically: Boolean ): Either[ValidationError, TransactionWithDiff] = { (for { - _ <- checkAssetOperationsAreSupported(contractNativeTokenFeatureActivated, assetOperations) + _ <- checkAssetOperationsSupported(contractNativeTokenFeatureActivated, assetOperations) + _ <- checkLeaseOpsForContractSupported(leaseOpsForContractsFeatureActivated, assetOperations) + _ <- validateAssetIdLength(assetOperations) executedTx <- if (contractNativeTokenFeatureActivated) { diff --git a/node/src/main/scala/com/wavesenterprise/mining/TxEstimators.scala b/node/src/main/scala/com/wavesenterprise/mining/TxEstimators.scala index 72d7288..aca26d8 100644 --- a/node/src/main/scala/com/wavesenterprise/mining/TxEstimators.scala +++ b/node/src/main/scala/com/wavesenterprise/mining/TxEstimators.scala @@ -2,7 +2,7 @@ package com.wavesenterprise.mining import com.wavesenterprise.state.Blockchain import com.wavesenterprise.transaction.assets.exchange.ExchangeTransaction -import com.wavesenterprise.transaction.assets.{BurnTransaction, ReissueTransaction, SponsorFeeTransaction, SponsorFeeTransactionV1} +import com.wavesenterprise.transaction.assets.{BurnTransaction, ReissueTransaction, SponsorFeeTransaction} import com.wavesenterprise.transaction.transfer.{MassTransferTransaction, TransferTransaction} import com.wavesenterprise.transaction.{Authorized, Transaction} diff --git a/node/src/main/scala/com/wavesenterprise/privacy/s3/multipartUpload/MultiPartUploader.scala b/node/src/main/scala/com/wavesenterprise/privacy/s3/multipartUpload/MultiPartUploader.scala index 09f8040..bc3f35f 100644 --- a/node/src/main/scala/com/wavesenterprise/privacy/s3/multipartUpload/MultiPartUploader.scala +++ b/node/src/main/scala/com/wavesenterprise/privacy/s3/multipartUpload/MultiPartUploader.scala @@ -99,7 +99,7 @@ final case class MultiPartUploader( private def uploadParts(content: Observable[Array[Byte]], key: String, uploadId: String): Task[(Array[Byte], List[(String, Int)])] = { val consumer = Consumer.foldLeftTask[(Sha256Hash, List[(String, Int)]), (Array[Byte], Long)]((Sha256Hash(), List.empty)) { case ((hash, acc), (chunk, index)) => - uploadPart(chunk, key, uploadId, index.toInt).map { + uploadPart(chunk, key, uploadId, { index + 1 }.toInt).map { case (str, i) => hash.update(chunk) -> (acc :+ (str -> i)) } } diff --git a/node/src/main/scala/com/wavesenterprise/state/Blockchain.scala b/node/src/main/scala/com/wavesenterprise/state/Blockchain.scala index afc3717..6a9e751 100644 --- a/node/src/main/scala/com/wavesenterprise/state/Blockchain.scala +++ b/node/src/main/scala/com/wavesenterprise/state/Blockchain.scala @@ -8,7 +8,6 @@ import com.wavesenterprise.consensus._ import com.wavesenterprise.database.certs.CertificatesState import com.wavesenterprise.state.ContractBlockchain.ContractReadingContext import com.wavesenterprise.state.reader.LeaseDetails -import com.wavesenterprise.transaction.lease.LeaseTransaction import com.wavesenterprise.transaction.smart.script.Script import com.wavesenterprise.transaction.{AssetId, Transaction, ValidationError} @@ -69,7 +68,7 @@ trait Blockchain extends ContractBlockchain with PrivacyBlockchain with Certific def resolveAlias(a: Alias): Either[ValidationError, Address] - def leaseDetails(leaseId: ByteStr): Option[LeaseDetails] + def leaseDetails(leaseId: LeaseId): Option[LeaseDetails] def filledVolumeAndFee(orderId: ByteStr): VolumeAndFee @@ -103,9 +102,6 @@ trait Blockchain extends ContractBlockchain with PrivacyBlockchain with Certific def addressWestDistribution(height: Int): Map[Address, Long] - // the following methods are used exclusively by patches - def allActiveLeases: Set[LeaseTransaction] - /** Builds a new portfolio map by applying a partial function to all portfolios on which the function is defined. * * @note Portfolios passed to `pf` only contain WEST and Leasing balances to improve performance */ diff --git a/node/src/main/scala/com/wavesenterprise/state/BlockchainUpdaterImpl.scala b/node/src/main/scala/com/wavesenterprise/state/BlockchainUpdaterImpl.scala index c8b6c7b..da9312e 100644 --- a/node/src/main/scala/com/wavesenterprise/state/BlockchainUpdaterImpl.scala +++ b/node/src/main/scala/com/wavesenterprise/state/BlockchainUpdaterImpl.scala @@ -29,7 +29,6 @@ import com.wavesenterprise.transaction.BlockchainEventError.{BlockAppendError, M import com.wavesenterprise.transaction.ValidationError.{GenericError => ValidationGenericError} import com.wavesenterprise.transaction._ import com.wavesenterprise.transaction.docker.{ExecutedContractData, ExecutedContractTransaction} -import com.wavesenterprise.transaction.lease._ import com.wavesenterprise.transaction.smart.script.Script import com.wavesenterprise.utils.pki.CrlData import com.wavesenterprise.utils.{ScorexLogging, Time, UnsupportedFeature, forceStopApplication} @@ -94,7 +93,7 @@ class BlockchainUpdaterImpl( private val internalLastBlockInfo = ConcurrentSubject.publish[LastBlockInfo](schedulers.blockchainUpdatesScheduler) - override def isLastBlockId(id: ByteStr): Boolean = readLock { + override def isLastLiquidBlockId(id: ByteStr): Boolean = readLock { innerNgState.exists(_.contains(id)) || lastBlock.exists(_.uniqueId == id) } @@ -848,14 +847,21 @@ class BlockchainUpdaterImpl( state.addressLeaseBalance(address) }) - override def leaseDetails(leaseId: AssetId): Option[LeaseDetails] = + override def contractLeaseBalance(contractId: ContractId): LeaseBalance = readLock(innerNgState match { case Some(ng) => - state.leaseDetails(leaseId).map(ld => ld.copy(isActive = ng.bestLiquidDiff.leaseState.getOrElse(leaseId, ld.isActive))) orElse - ng.bestLiquidDiff.transactionsMap.get(leaseId).collect { - case (h, lt: LeaseTransaction, _) => - LeaseDetails(lt.sender, lt.recipient, h, lt.amount, ng.bestLiquidDiff.leaseState(lt.id())) - } + cats.Monoid.combine(state.contractLeaseBalance(contractId), + ng.bestLiquidDiff.portfolios.getOrElse(contractId.toAssetHolder, Portfolio.empty).lease) + case None => + state.contractLeaseBalance(contractId) + }) + + override def leaseDetails(leaseId: LeaseId): Option[LeaseDetails] = + readLock(innerNgState match { + case Some(ng) => + state.leaseDetails(leaseId).map(ld => + ld.copy(isActive = ng.bestLiquidDiff.leaseMap.get(leaseId).map(_.isActive).getOrElse(ld.isActive))) orElse + ng.bestLiquidDiff.leaseMap.get(leaseId) case None => state.leaseDetails(leaseId) }) @@ -971,19 +977,6 @@ class BlockchainUpdaterImpl( } }) - override def allActiveLeases: Set[LeaseTransaction] = - readLock(innerNgState.fold(state.allActiveLeases) { ng => - val (active, canceled) = ng.bestLiquidDiff.leaseState.partition(_._2) - val fromDiff = active.keys - .map { id => - ng.bestLiquidDiff.transactionsMap(id)._2 - } - .collect { case lt: LeaseTransaction => lt } - .toSet - val fromInner = state.allActiveLeases.filterNot(ltx => canceled.keySet.contains(ltx.id())) - fromDiff ++ fromInner - }) - /** Builds a new portfolio map by applying a partial function to all portfolios on which the function is defined. * * @note Portfolios passed to `pf` only contain WEST and Leasing balances to improve performance */ @@ -991,7 +984,7 @@ class BlockchainUpdaterImpl( innerNgState.fold(state.collectAddressLposPortfolios(pf)) { ng => val b = Map.newBuilder[Address, A] for ((a, p) <- ng.bestLiquidDiff.portfolios.collectAddresses if p.lease != LeaseBalance.empty || p.balance != 0) { - pf.runWith(b += a -> _)(a -> this.westPortfolio(a)) + pf.runWith(b += a -> _)(a -> this.addressWestPortfolio(a)) } state.collectAddressLposPortfolios(pf) ++ b.result() diff --git a/node/src/main/scala/com/wavesenterprise/state/ContractBlockchain.scala b/node/src/main/scala/com/wavesenterprise/state/ContractBlockchain.scala index 0bffc1c..d64ec3c 100644 --- a/node/src/main/scala/com/wavesenterprise/state/ContractBlockchain.scala +++ b/node/src/main/scala/com/wavesenterprise/state/ContractBlockchain.scala @@ -35,6 +35,8 @@ trait ContractBlockchain { def contractBalance(contractId: ContractId, maybeAssetId: Option[AssetId], readingContext: ContractReadingContext): Long + def contractLeaseBalance(contractId: ContractId): LeaseBalance + def contractPortfolio(contractId: ContractId): Portfolio def contractValidators: ContractValidatorPool diff --git a/node/src/main/scala/com/wavesenterprise/state/Diff.scala b/node/src/main/scala/com/wavesenterprise/state/Diff.scala index 9fac308..082dffe 100644 --- a/node/src/main/scala/com/wavesenterprise/state/Diff.scala +++ b/node/src/main/scala/com/wavesenterprise/state/Diff.scala @@ -8,6 +8,8 @@ import com.wavesenterprise.acl.{OpType, Permissions} import com.wavesenterprise.certs.CertChain import com.wavesenterprise.docker.ContractInfo import com.wavesenterprise.privacy.PolicyDataHash +import com.wavesenterprise.serialization.BinarySerializer.Offset +import com.wavesenterprise.state.reader.LeaseDetails import com.wavesenterprise.transaction._ import com.wavesenterprise.transaction.docker.ExecutedContractData import com.wavesenterprise.transaction.smart.script.Script @@ -184,12 +186,24 @@ object PolicyDiff { case class ParticipantRegistration(address: Address, pubKey: PublicKeyAccount, opType: OpType) -sealed trait AssetHolder -case class Account(address: Address) extends AssetHolder +sealed trait AssetHolder { + def toBytes: Array[Byte] +} +case class Account(address: Address) extends AssetHolder { + def toBytes: Array[Byte] = { + Account.binaryHeader +: address.bytes.arr + } +} object Account { val binaryHeader: Byte = 0x00.toByte } -case class Contract(contractId: ContractId) extends AssetHolder +case class Contract(contractId: ContractId) extends AssetHolder { + + override def toBytes: Array[Byte] = { + Contract.binaryHeader +: contractId.byteStr.arr + } +} + object Contract { val binaryHeader: Byte = 0x01.toByte } @@ -200,8 +214,41 @@ case class ContractId(byteStr: ByteStr) { override def toString: String = byteStr.toString() } +case class LeaseId(byteStr: ByteStr) { + def arr: Array[Byte] = byteStr.arr + override def toString: String = byteStr.toString() +} + object AssetHolder { + /** + * Adds description of AssetHolder for the means of current scope + */ + implicit class AssetHolderDescription(assetHolder: AssetHolder) { + val description: String = assetHolder match { + case Account(address) => s"address '$address'" + case Contract(contractId) => s"contract '$contractId'" + } + } + + /** + * Use with caution — this method is unsafe + * @param bytes + * @param offset + * @return + */ + def fromBytes(bytes: Array[Byte], offset: Offset = 0): (AssetHolder, Offset) = { + bytes(offset) match { + case Account.binaryHeader => + val addressBytes = bytes.slice(offset + 1, offset + Address.AddressLength + 1) + val address = Address.fromBytes(addressBytes).fold(err => throw new RuntimeException(err.message), identity) + (Account(address), offset + Address.AddressLength + 1) + case Contract.binaryHeader => + val contractBytes = bytes.slice(offset + 1, offset + com.wavesenterprise.crypto.DigestSize + 1) + (Contract(ContractId(ByteStr(contractBytes))), com.wavesenterprise.crypto.DigestSize + 1) + } + } + implicit class AssetHolderExt(val assetHolder: AssetHolder) extends AnyVal { def toJson: JsValue = { @@ -273,7 +320,8 @@ case class Diff(transactions: List[Transaction], assets: Map[AssetId, AssetInfo], aliases: Map[Alias, Address], orderFills: Map[ByteStr, VolumeAndFee], - leaseState: Map[ByteStr, Boolean], + leaseMap: Map[LeaseId, LeaseDetails], + leaseCancelMap: Map[LeaseId, LeaseDetails], scripts: Map[Address, Option[Script]], assetScripts: Map[AssetId, Option[Script]], accountData: Map[Address, AccountDataInfo], @@ -319,7 +367,8 @@ object Diff { assets: Map[AssetId, AssetInfo] = Map.empty, aliases: Map[Alias, Address] = Map.empty, orderFills: Map[ByteStr, VolumeAndFee] = Map.empty, - leaseState: Map[ByteStr, Boolean] = Map.empty, + leaseMap: Map[LeaseId, LeaseDetails] = Map.empty, + leaseCancelMap: Map[LeaseId, LeaseDetails] = Map.empty, scripts: Map[Address, Option[Script]] = Map.empty, assetScripts: Map[AssetId, Option[Script]] = Map.empty, accountData: Map[Address, AccountDataInfo] = Map.empty, @@ -346,7 +395,8 @@ object Diff { assets = assets, aliases = aliases, orderFills = orderFills, - leaseState = leaseState, + leaseMap = leaseMap, + leaseCancelMap = leaseCancelMap, scripts = scripts, assetScripts = assetScripts, accountData = accountData, @@ -376,7 +426,8 @@ object Diff { assets = Map.empty, aliases = Map.empty, orderFills = Map.empty, - leaseState = Map.empty, + leaseMap = Map.empty, + leaseCancelMap = Map.empty, scripts = Map.empty, assetScripts = Map.empty, accountData = Map.empty, @@ -471,7 +522,8 @@ object Diff { assets = older.assets ++ newer.assets, aliases = older.aliases ++ newer.aliases, orderFills = older.orderFills.combine(newer.orderFills), - leaseState = older.leaseState ++ newer.leaseState, + leaseMap = older.leaseMap ++ newer.leaseMap, + leaseCancelMap = older.leaseCancelMap ++ newer.leaseCancelMap, scripts = older.scripts ++ newer.scripts, assetScripts = older.assetScripts ++ newer.assetScripts, accountData = older.accountData.combine(newer.accountData), diff --git a/node/src/main/scala/com/wavesenterprise/state/appender/BaseAppender.scala b/node/src/main/scala/com/wavesenterprise/state/appender/BaseAppender.scala index 6d3ee81..6514041 100644 --- a/node/src/main/scala/com/wavesenterprise/state/appender/BaseAppender.scala +++ b/node/src/main/scala/com/wavesenterprise/state/appender/BaseAppender.scala @@ -56,29 +56,31 @@ class BaseAppender( maxAttempts: Int = keyBlockAppendingSettings.maxAttempts.value ): Task[Either[ValidationError, Option[BigInt]]] = { def measuredAction: Either[ValidationError, Option[BigInt]] = { - blockchainUpdater.lastBlock - .map { lastBlock => - if (lastBlock.uniqueId == keyBlock.reference) { - appendBlock(keyBlock, blockType = Liquid, alreadyVerifiedTxIds = alreadyVerifiedTxIds, certChainStore = CertChainStore.empty) - .map(_ => Some(blockchainUpdater.score)) - } else if (blockchainUpdater.contains(keyBlock.uniqueId)) { - Right(None) - } else if (consensus.blockCanBeReplaced(time.correctedTime(), keyBlock, lastBlock)) { - replaceNotFinalizedBlock(keyBlock, lastBlock) - } else { - val lastBlockTime = formatBlockTime(lastBlock.timestamp) - val newBlockTime = formatBlockTime(keyBlock.timestamp) - - Left( - BlockAppendError( - s"Broadcast block is not a child of the last block. Last block '$lastBlock', lastBlockTime: '$lastBlockTime', newBlockTime: '$newBlockTime'", - keyBlock - )) + if (blockchainUpdater.isLastLiquidBlockId(keyBlock.reference)) { + appendBlock(keyBlock, blockType = Liquid, alreadyVerifiedTxIds = alreadyVerifiedTxIds, certChainStore = CertChainStore.empty) + .map(_ => Some(blockchainUpdater.score)) + } else if (blockchainUpdater.contains(keyBlock.uniqueId)) { + Right(None) + } else { + blockchainUpdater.lastBlock + .map { lastBlock => + if (consensus.blockCanBeReplaced(time.correctedTime(), keyBlock, lastBlock)) { + replaceNotFinalizedBlock(keyBlock, lastBlock) + } else { + val lastBlockTime = formatBlockTime(lastBlock.timestamp) + val newBlockTime = formatBlockTime(keyBlock.timestamp) + + Left { + BlockAppendError( + s"Broadcast block is not a child of the last block. Last block '$lastBlock', lastBlockTime: '$lastBlockTime', newBlockTime: '$newBlockTime'", + keyBlock + ) + } + } + }.getOrElse { + Left(BlockAppendError(s"Last block not found", keyBlock)) } - } - .getOrElse { - Left(BlockAppendError(s"Last block not found", keyBlock)) - } + } } Task { diff --git a/node/src/main/scala/com/wavesenterprise/state/diffs/AssetOpsSupport.scala b/node/src/main/scala/com/wavesenterprise/state/diffs/AssetOpsSupport.scala index df16b46..653cc3b 100644 --- a/node/src/main/scala/com/wavesenterprise/state/diffs/AssetOpsSupport.scala +++ b/node/src/main/scala/com/wavesenterprise/state/diffs/AssetOpsSupport.scala @@ -1,7 +1,20 @@ package com.wavesenterprise.state.diffs import cats.implicits._ -import com.wavesenterprise.state.{Account, AssetInfo, Blockchain, ByteStr, Contract, ContractId, Diff, LeaseBalance, Portfolio, SponsorshipValue} +import com.wavesenterprise.state.reader.LeaseDetails +import com.wavesenterprise.state.{ + Account, + AssetInfo, + Blockchain, + ByteStr, + Contract, + ContractId, + Diff, + LeaseBalance, + LeaseId, + Portfolio, + SponsorshipValue +} import com.wavesenterprise.transaction.ValidationError.{GenericError, InvalidAssetId} import com.wavesenterprise.transaction.assets._ import com.wavesenterprise.transaction.docker.ExecutedContractTransactionV3 @@ -59,6 +72,18 @@ trait AssetOpsSupport { case None => ().asRight } + protected def checkLeaseIdNotExist(blockchain: Blockchain, leaseId: ByteStr): Either[ValidationError, Unit] = + blockchain.leaseDetails(LeaseId(leaseId)) match { + case Some(_) => GenericError(s"Lease '$leaseId' already exists").asLeft + case None => ().asRight + } + + protected def checkLeaseActive(lease: LeaseDetails): Either[GenericError, Unit] = { + if (!lease.isActive) { + Left(GenericError(s"Cannot cancel already cancelled lease")) + } else Right(()) + } + protected def checkOverflowAfterReissue( asset: AssetInfo, additionalQuantity: Long, @@ -77,6 +102,16 @@ trait AssetOpsSupport { ) } + def checkLeaseIdLength(leaseId: ByteStr): Either[ValidationError, Unit] = { + val requiredLength = com.wavesenterprise.crypto.DigestSize + + Either.cond( + test = leaseId.arr.length == requiredLength, + right = (), + left = InvalidAssetId(s"Invalid assetId length. Current - '${leaseId.arr.length}', expected – '$requiredLength'") + ) + } + protected def assetInfoFromIssueTransaction(tx: IssueTransaction, height: Int): AssetInfo = AssetInfo( issuer = tx.sender.toAddress.toAssetHolder, diff --git a/node/src/main/scala/com/wavesenterprise/state/diffs/BalanceDiffValidation.scala b/node/src/main/scala/com/wavesenterprise/state/diffs/BalanceDiffValidation.scala index 1b5d39d..de6fbcb 100644 --- a/node/src/main/scala/com/wavesenterprise/state/diffs/BalanceDiffValidation.scala +++ b/node/src/main/scala/com/wavesenterprise/state/diffs/BalanceDiffValidation.scala @@ -4,7 +4,7 @@ import cats.data.{NonEmptyList, Validated, ValidatedNel} import cats.implicits._ import cats.kernel.Monoid import com.wavesenterprise.metrics.Instrumented -import com.wavesenterprise.state.{Account, AssetHolder, Blockchain, Contract, Diff} +import com.wavesenterprise.state.{Account, Blockchain, Contract, Diff} import com.wavesenterprise.transaction.ValidationError import com.wavesenterprise.transaction.ValidationError.BalanceErrors import com.wavesenterprise.utils.ScorexLogging @@ -25,8 +25,22 @@ object BalanceDiffValidation extends ScorexLogging with Instrumented { // checking leasing and WEST balance changes regarding to leases val leasingValidation: ValidatedNel[String, Unit] = assetHolder match { - case Contract(_) => - Validated.condNel(portfolioDiff.lease.isEmpty, (), s"contracts don't support leasing") + case Contract(contractId) if westDelta < 0 => + val leaseBalance = blockchain.contractLeaseBalance(contractId) + val newLeaseBalance = leaseBalance |+| portfolioDiff.lease + + val validations = List( + Validated.condNel(newLeaseBalance.out >= 0, (), s"cannot lease-out negative amount"), + Validated.condNel(newLeaseBalance.in == 0, (), s"contract lease-in not supported"), + Validated.condNel(newWestBalance - newLeaseBalance.out >= 0, (), s"cannot spend leased balance") + ) + + validations.combineAll + .leftMap { errors => + val errorConditions = + s"old(west balance, lease balance): ${(oldWestBalance, leaseBalance)}, new: ${(newWestBalance, newLeaseBalance)}" + NonEmptyList.one(errors.toList.mkString("[", ", ", "]") + ": " + errorConditions) + } case Account(sender) if westDelta < 0 => val leaseBalance = blockchain.addressLeaseBalance(sender) @@ -45,7 +59,7 @@ object BalanceDiffValidation extends ScorexLogging with Instrumented { NonEmptyList.one(errors.toList.mkString("[", ", ", "]") + ": " + errorConditions) } - case Account(_) => + case _ => Validated.Valid(()) } @@ -78,14 +92,4 @@ object BalanceDiffValidation extends ScorexLogging with Instrumented { portfoliosValidationResults.toEither.map(_ => diff) } - - /** - * Adds description of AssetHolder for the means of current scope - */ - implicit class AssetHolderDescription(assetHolder: AssetHolder) { - val description: String = assetHolder match { - case Account(address) => s"address '$address'" - case Contract(contractId) => s"contract '$contractId'" - } - } } diff --git a/node/src/main/scala/com/wavesenterprise/state/diffs/LeaseTransactionsDiff.scala b/node/src/main/scala/com/wavesenterprise/state/diffs/LeaseTransactionsDiff.scala index fbf1502..d410bae 100644 --- a/node/src/main/scala/com/wavesenterprise/state/diffs/LeaseTransactionsDiff.scala +++ b/node/src/main/scala/com/wavesenterprise/state/diffs/LeaseTransactionsDiff.scala @@ -9,6 +9,7 @@ import com.wavesenterprise.transaction.ValidationError import com.wavesenterprise.transaction.ValidationError.GenericError import com.wavesenterprise.transaction.lease._ import AssetHolder._ +import com.wavesenterprise.state.reader.LeaseDetails import scala.util.{Left, Right} @@ -28,7 +29,9 @@ object LeaseTransactionsDiff { sender -> Portfolio(-tx.fee, LeaseBalance(0, tx.amount), Map.empty), recipient -> Portfolio(0, LeaseBalance(tx.amount, 0), Map.empty) ) - Right(Diff(height = height, tx = tx, portfolios = portfolioDiff.toAssetHolderMap, leaseState = Map(tx.id() -> true))) + val leaseDetails = LeaseDetails.fromLeaseTx(tx, height) + + Right(Diff(height = height, tx = tx, portfolios = portfolioDiff.toAssetHolderMap, leaseMap = Map(LeaseId(tx.id()) -> leaseDetails))) } } } @@ -36,7 +39,7 @@ object LeaseTransactionsDiff { def leaseCancel(blockchain: Blockchain, settings: FunctionalitySettings, time: Long, height: Int)( tx: LeaseCancelTransaction): Either[ValidationError, Diff] = { - val leaseEi = blockchain.leaseDetails(tx.leaseId) match { + val leaseEi = blockchain.leaseDetails(LeaseId(tx.leaseId)) match { case None => Left(GenericError(s"Related LeaseTransaction not found")) case Some(l) => Right(l) } @@ -49,14 +52,14 @@ object LeaseTransactionsDiff { else Right(()) canceller = Address.fromPublicKey(tx.sender.publicKey) - portfolioDiff <- if (tx.sender == lease.sender) { + portfolioDiff <- if (Account(tx.sender.toAddress) == lease.sender) { Right( Monoid.combine(Map(canceller -> Portfolio(-tx.fee, LeaseBalance(0, -lease.amount), Map.empty)), Map(recipient -> Portfolio(0, LeaseBalance(-lease.amount, 0), Map.empty)))) } else { Left(GenericError(s"LeaseTransaction was leased by other sender")) } - - } yield Diff(height = height, tx = tx, portfolios = portfolioDiff.toAssetHolderMap, leaseState = Map(tx.leaseId -> false)) + leaseDetails = lease.copy(isActive = false) + } yield Diff(height = height, tx = tx, portfolios = portfolioDiff.toAssetHolderMap, leaseCancelMap = Map(LeaseId(tx.leaseId) -> leaseDetails)) } } diff --git a/node/src/main/scala/com/wavesenterprise/state/diffs/RegisterNodeTransactionDiff.scala b/node/src/main/scala/com/wavesenterprise/state/diffs/RegisterNodeTransactionDiff.scala index 3d2b92d..1ed40e3 100644 --- a/node/src/main/scala/com/wavesenterprise/state/diffs/RegisterNodeTransactionDiff.scala +++ b/node/src/main/scala/com/wavesenterprise/state/diffs/RegisterNodeTransactionDiff.scala @@ -3,7 +3,7 @@ package com.wavesenterprise.state.diffs import com.wavesenterprise.acl.OpType import com.wavesenterprise.state.{Blockchain, Diff, LeaseBalance, ParticipantRegistration, Portfolio} import com.wavesenterprise.transaction.ValidationError.GenericError -import com.wavesenterprise.transaction.{RegisterNodeTransaction, RegisterNodeTransactionV1, ValidationError} +import com.wavesenterprise.transaction.{RegisterNodeTransaction, ValidationError} import com.wavesenterprise.state.AssetHolder._ case class RegisterNodeTransactionDiff(blockchain: Blockchain, height: Int) { diff --git a/node/src/main/scala/com/wavesenterprise/state/diffs/TransactionDiffer.scala b/node/src/main/scala/com/wavesenterprise/state/diffs/TransactionDiffer.scala index c8431a6..ddd37b5 100644 --- a/node/src/main/scala/com/wavesenterprise/state/diffs/TransactionDiffer.scala +++ b/node/src/main/scala/com/wavesenterprise/state/diffs/TransactionDiffer.scala @@ -5,6 +5,7 @@ import com.wavesenterprise.account.PublicKeyAccount import com.wavesenterprise.acl.PermissionValidator import com.wavesenterprise.metrics._ import com.wavesenterprise.certs.CertChain +import com.wavesenterprise.features.BlockchainFeature import com.wavesenterprise.settings.BlockchainSettings import com.wavesenterprise.state.diffs.TransactionDiffer.{TransactionValidationError, stats} import com.wavesenterprise.state.diffs.docker.ExecutedContractTransactionDiff.{ContractTxExecutorType, MiningExecutor} @@ -51,6 +52,8 @@ class TransactionDiffer( } yield positiveDiff).leftMap(TransactionValidationError(_, tx)) private def verify(blockchain: Blockchain, tx: Transaction): Either[ValidationError, Unit] = { + import com.wavesenterprise.features.FeatureProvider._ + if (alreadyVerified) { Right(()) } else { @@ -59,6 +62,9 @@ class TransactionDiffer( _ <- stats.commonValidation .measureForType(tx.builder.typeId) { for { + _ <- if (blockchain.isFeatureActivated(BlockchainFeature.ContractNativeTokenSupportAndPkiV1Support, blockchain.height)) { + containsLegacyRestContractsCalls(tx, blockchain) + } else Right(()) _ <- CommonValidation.disallowTxFromFuture(currentBlockTimestamp, tx) _ <- CommonValidation.disallowTxFromPast(prevBlockTimestamp, tx, txExpireTimeout) _ <- CommonValidation.disallowBeforeActivationTime(blockchain, currentBlockHeight, tx) @@ -72,6 +78,38 @@ class TransactionDiffer( } } + private def containsLegacyRestContractsCalls(tx: Transaction, blockchain: Blockchain): Either[ValidationError, Unit] = { + def isLegacy(executableTransaction: ExecutableTransaction): Boolean = { + executableTransaction match { + case _: CreateContractTransactionV1 => true + // all versions except for V1 (matched above) + case _: CreateContractTransaction => false + case _ => + blockchain + .executedTxFor(executableTransaction.contractId) + .exists(executed => isLegacy(executed.tx)) + } + } + + tx match { + case atomic: AtomicTransaction => + atomic.transactions + .collectFirst { + case tx: ExecutableTransaction if isLegacy(tx) => + Left { + GenericError("REST contracts is not supported anymore, " + + s"rejecting atomic ${atomic.id()} with REST contract tx ${tx.id()}") + } + } + .getOrElse { Right(()) } + case executableTransaction: ExecutableTransaction if isLegacy(executableTransaction) => + Left(GenericError("REST contracts is not supported anymore, " + + s"rejecting REST contract tx ${executableTransaction.id()}")) + case _ => Right(()) + } + + } + private def validateBalance(blockchain: Blockchain, tx: Transaction, diff: Diff): Either[ValidationError, Diff] = { if (alreadyVerified) { Right(diff) diff --git a/node/src/main/scala/com/wavesenterprise/state/diffs/docker/ExecutedContractTransactionDiff.scala b/node/src/main/scala/com/wavesenterprise/state/diffs/docker/ExecutedContractTransactionDiff.scala index c34e427..0d4408a 100644 --- a/node/src/main/scala/com/wavesenterprise/state/diffs/docker/ExecutedContractTransactionDiff.scala +++ b/node/src/main/scala/com/wavesenterprise/state/diffs/docker/ExecutedContractTransactionDiff.scala @@ -6,30 +6,24 @@ import com.wavesenterprise.account.{Address, PublicKeyAccount} import com.wavesenterprise.crypto import com.wavesenterprise.docker.ContractInfo import com.wavesenterprise.docker.validator.ValidationPolicy -import com.wavesenterprise.state.ContractId import com.wavesenterprise.state.AssetHolder._ -import com.wavesenterprise.state.diffs.docker.ExecutedContractTransactionDiff.{ - ContractTxExecutorType, - MiningExecutor, - NonceDuplicatesError, - ValidatingExecutor, - ZeroNonceError -} +import com.wavesenterprise.state.diffs.docker.ExecutedContractTransactionDiff._ import com.wavesenterprise.state.diffs.{AssetOpsSupport, TransferOpsSupport} -import com.wavesenterprise.state.{AssetInfo, Blockchain, ByteStr, Contract, Diff} +import com.wavesenterprise.state.reader.LeaseDetails +import com.wavesenterprise.state.{Account, AssetHolder, AssetInfo, Blockchain, ByteStr, Contract, ContractId, Diff, LeaseBalance, LeaseId, Portfolio} import com.wavesenterprise.transaction.ValidationError.{ContractNotFound, GenericError, InvalidSender} import com.wavesenterprise.transaction.docker._ import com.wavesenterprise.transaction.docker.assets.ContractAssetOperation import com.wavesenterprise.transaction.docker.assets.ContractAssetOperation.{ ContractBurnV1, + ContractCancelLeaseV1, ContractIssueV1, + ContractLeaseV1, ContractReissueV1, ContractTransferOutV1 } import com.wavesenterprise.transaction.{AssetId, Signed, ValidationError} -import scala.annotation.tailrec - /** * Creates [[Diff]] for [[ExecutedContractTransaction]] */ @@ -110,7 +104,8 @@ case class ExecutedContractTransactionDiff( private def calcAssetOperationsDiff(initDiff: Diff)(executedTx: ExecutedContractTransactionV3): Either[ValidationError, Diff] = for { - _ <- checkContractIssuesNonces(executedTx.assetOperations) + _ <- checkContractIssueNonces(executedTx.assetOperations) + _ <- checkContractLeaseNonces(executedTx.assetOperations) totalDiff <- applyContractOpsToDiff(initDiff, executedTx) } yield totalDiff @@ -131,7 +126,7 @@ case class ExecutedContractTransactionDiff( fixedDiff } - val contractId = executedTx.tx.contractId + val contractId = ContractId(executedTx.tx.contractId) val appliedAssetOpsDiff = executedTx.assetOperations.foldLeft(initDiff.asRight[ValidationError]) { case (Right(diff), issueOp: ContractIssueV1) => @@ -143,7 +138,7 @@ case class ExecutedContractTransactionDiff( case (Right(diff), reissueOp: ContractReissueV1) => val assetId = reissueOp.assetId - val contract = Contract(ContractId(contractId)) + val contract = Contract(contractId) for { asset <- findAssetForContract(blockchain, diff, assetId) _ <- checkAssetIdLength(assetId) @@ -184,28 +179,107 @@ case class ExecutedContractTransactionDiff( for { _ <- validateAssetExistence recipient <- blockchain.resolveAlias(transferOp.recipient).map(_.toAssetHolder) - transferDiff = Diff(height, executedTx, getPortfoliosMap(transferOp, ContractId(contractId).toAssetHolder, recipient)) + transferDiff = Diff(height, executedTx, getPortfoliosMap(transferOp, contractId.toAssetHolder, recipient)) } yield diff |+| transferDiff + + case (Right(diff), leaseOp: ContractLeaseV1) => + for { + _ <- checkLeaseIdLength(leaseOp.leaseId) + _ <- checkLeaseIdNotExist(blockchain, leaseOp.leaseId) + + recipientAddress <- blockchain.resolveAlias(leaseOp.recipient) + contractPortfolio = diff.portfolios.getOrElse(Contract(contractId), Portfolio.empty) |+| blockchain.contractPortfolio(contractId) + _ <- Either.cond( + contractPortfolio.balance - contractPortfolio.lease.out >= leaseOp.amount, + (), + GenericError(s"Cannot lease more than own: balance:${contractPortfolio.balance}, already leased: ${contractPortfolio.lease.out}") + ) + } yield { + val sender = Contract(contractId) + val recipient = Account(recipientAddress) + + val portfolioDiff: Map[AssetHolder, Portfolio] = Map( + sender -> Portfolio(0, LeaseBalance(0, leaseOp.amount), Map.empty), + recipient -> Portfolio(0, LeaseBalance(leaseOp.amount, 0), Map.empty) + ) + + val leaseDetails = LeaseDetails( + sender, + leaseOp.recipient, + height, + leaseOp.amount, + isActive = true, + leaseTxId = Some(executedTx.id()) + ) + + val leaseDiff = Diff( + height = height, + tx = executedTx, + portfolios = portfolioDiff, + leaseMap = Map(LeaseId(leaseOp.leaseId) -> leaseDetails) + ) + + diff |+| leaseDiff + } + + case (Right(diff), leaseCancelOp: ContractCancelLeaseV1) => + val maybeLease = diff.leaseMap.get(LeaseId(leaseCancelOp.leaseId)).orElse(blockchain.leaseDetails(LeaseId(leaseCancelOp.leaseId))) + + for { + lease <- maybeLease.toRight(GenericError("Related LeaseTransaction not found")) + recipientAddress <- blockchain.resolveAlias(lease.recipient) + _ <- checkLeaseActive(lease) + _ <- Either.cond(Contract(contractId) == lease.sender, (), GenericError(s"Lease '${leaseCancelOp.leaseId}' was leased by other sender")) + + } yield { + val sender = Contract(contractId) + val recipient = Account(recipientAddress) + + val portfolioDiff: Map[AssetHolder, Portfolio] = Map( + sender -> Portfolio(0, LeaseBalance(0, -lease.amount), Map.empty), + recipient -> Portfolio(0, LeaseBalance(-lease.amount, 0), Map.empty) + ) + + val leaseDetails = lease.copy(isActive = false) + + val leaseCancelDiff = Diff( + height = height, + tx = executedTx, + portfolios = portfolioDiff, + leaseCancelMap = Map(LeaseId(leaseCancelOp.leaseId) -> leaseDetails) + ) + + diff |+| leaseCancelDiff + } + case (diffError @ Left(_), _) => diffError } appliedAssetOpsDiff } - private def checkContractIssuesNonces(assetOperations: List[ContractAssetOperation]): Either[ValidationError, Unit] = { + private def checkContractIssueNonces(assetOperations: List[ContractAssetOperation]): Either[ValidationError, Unit] = { val issueNoncesList = assetOperations.collect { case i: ContractIssueV1 => i.nonce } - @tailrec - def checkZeroNonces(issueNonces: List[Byte], result: Either[ValidationError, Unit] = ().asRight): Either[ValidationError, Unit] = result match { - case Left(_) => result - case _ => - issueNonces match { - case Nil => result - case nonce :: lastNonces => checkZeroNonces(lastNonces, Either.cond(nonce != 0, (), ZeroNonceError)) - } + def checkZeroNonces(issueNonces: List[Byte]): Either[ValidationError, Unit] = { + Either.cond(!issueNonces.contains(0.toByte), (), AssetIdZeroNonceError) + } + + Either.cond(issueNoncesList.distinct.size == issueNoncesList.size, (), AssetIdNonceDuplicatesError) -> checkZeroNonces(issueNoncesList) match { + case success @ (Right(()), Right(())) => success._1 + case duplicateNonceError @ (Left(_), _) => duplicateNonceError._1 + case zeroNonceError @ (_, Left(_)) => zeroNonceError._2 + } + } + + private def checkContractLeaseNonces(assetOperations: List[ContractAssetOperation]): Either[ValidationError, Unit] = { + val leaseNoncesList = assetOperations.collect { case i: ContractLeaseV1 => i.nonce } + + def checkZeroNonces(leaseNonces: List[Byte]): Either[ValidationError, Unit] = { + Either.cond(!leaseNonces.contains(0.toByte), (), LeaseIdZeroNonceError) } - Either.cond(issueNoncesList.distinct.size == issueNoncesList.size, (), NonceDuplicatesError) -> checkZeroNonces(issueNoncesList) match { + Either.cond(leaseNoncesList.distinct.size == leaseNoncesList.size, (), LeaseIdNonceDuplicatesError) -> checkZeroNonces(leaseNoncesList) match { case success @ (Right(()), Right(())) => success._1 case duplicateNonceError @ (Left(_), _) => duplicateNonceError._1 case zeroNonceError @ (_, Left(_)) => zeroNonceError._2 @@ -310,23 +384,29 @@ case class ExecutedContractTransactionDiff( } object ExecutedContractTransactionDiff { - private val NonceDuplicatesError = GenericError("Attempt to issue multiple assets with the same nonce") - private val ZeroNonceError = GenericError("Attempt to issue asset with nonce = 0") + private val AssetIdNonceDuplicatesError = GenericError("Attempt to issue multiple assets with the same nonce") + private val AssetIdZeroNonceError = GenericError("Attempt to issue asset with nonce = 0") + private val LeaseIdNonceDuplicatesError = GenericError("Attempt to create multiple contract leases with the same nonce") + private val LeaseIdZeroNonceError = GenericError("Attempt to create contract lease with nonce = 0") case class TxContractOpsData(issueOps: Seq[ContractIssueV1], reissueOps: Seq[ContractReissueV1], burnOps: Seq[ContractBurnV1], - transferOps: Seq[ContractTransferOutV1]) { + transferOps: Seq[ContractTransferOutV1], + leaseOps: Seq[ContractLeaseV1], + leaseCancelOps: Seq[ContractCancelLeaseV1]) { lazy val issuedAssetsIds: Set[ByteStr] = issueOps.map(_.assetId).toSet } def extractTxContractOpsData(executedTx: ExecutedContractTransactionV3): TxContractOpsData = { - val issueOpsBuilder = Seq.newBuilder[ContractIssueV1] - val reissueOpsBuilder = Seq.newBuilder[ContractReissueV1] - val burnOpsBuilder = Seq.newBuilder[ContractBurnV1] - val transferOpsBuilder = Seq.newBuilder[ContractTransferOutV1] + val issueOpsBuilder = Seq.newBuilder[ContractIssueV1] + val reissueOpsBuilder = Seq.newBuilder[ContractReissueV1] + val burnOpsBuilder = Seq.newBuilder[ContractBurnV1] + val transferOpsBuilder = Seq.newBuilder[ContractTransferOutV1] + val leaseOpsBuilder = Seq.newBuilder[ContractLeaseV1] + val leaseCancelOpsBuilder = Seq.newBuilder[ContractCancelLeaseV1] executedTx.assetOperations.foreach { case issue: ContractIssueV1 => @@ -337,13 +417,19 @@ object ExecutedContractTransactionDiff { burnOpsBuilder += burn case transfer: ContractTransferOutV1 => transferOpsBuilder += transfer + case lease: ContractLeaseV1 => + leaseOpsBuilder += lease + case leaseCancel: ContractCancelLeaseV1 => + leaseCancelOpsBuilder += leaseCancel } TxContractOpsData( issueOpsBuilder.result(), reissueOpsBuilder.result(), burnOpsBuilder.result, - transferOpsBuilder.result() + transferOpsBuilder.result(), + leaseOpsBuilder.result(), + leaseCancelOpsBuilder.result() ) } diff --git a/node/src/main/scala/com/wavesenterprise/state/package.scala b/node/src/main/scala/com/wavesenterprise/state/package.scala index 57a3710..86bf40a 100644 --- a/node/src/main/scala/com/wavesenterprise/state/package.scala +++ b/node/src/main/scala/com/wavesenterprise/state/package.scala @@ -127,19 +127,25 @@ package object state { blockchain .addressTransactions(address, Set(LeaseTransaction.typeId), Int.MaxValue, None) .explicitGet() - .collect { case (h, l: LeaseTransaction) if blockchain.leaseDetails(l.id()).exists(_.isActive) => h -> l } + .collect { case (h, l: LeaseTransaction) if blockchain.leaseDetails(LeaseId(l.id())).exists(_.isActive) => h -> l } def unsafeHeightOf(id: ByteStr): Int = blockchain .heightOf(id) .getOrElse(throw new IllegalStateException(s"Can't find a block: $id")) - def westPortfolio(address: Address): Portfolio = Portfolio( + def addressWestPortfolio(address: Address): Portfolio = Portfolio( balance = blockchain.addressBalance(address), lease = blockchain.addressLeaseBalance(address), assets = Map.empty ) + def contractWestPortfolio(contractId: ContractId): Portfolio = Portfolio( + balance = blockchain.contractBalance(contractId, None, ContractReadingContext.Default), + lease = blockchain.contractLeaseBalance(contractId), + assets = Map.empty + ) + def assetHolderSpendableBalance(assetHolder: AssetHolder): Long = assetHolder match { case Account(address) => val westBalance = blockchain.addressBalance(address) diff --git a/node/src/main/scala/com/wavesenterprise/state/reader/CompositeBlockchain.scala b/node/src/main/scala/com/wavesenterprise/state/reader/CompositeBlockchain.scala index d4b1004..c3d3e36 100644 --- a/node/src/main/scala/com/wavesenterprise/state/reader/CompositeBlockchain.scala +++ b/node/src/main/scala/com/wavesenterprise/state/reader/CompositeBlockchain.scala @@ -14,7 +14,6 @@ import com.wavesenterprise.state.ContractBlockchain.ContractReadingContext import com.wavesenterprise.state._ import com.wavesenterprise.transaction.ValidationError.AliasDoesNotExist import com.wavesenterprise.transaction.docker.{ExecutedContractData, ExecutedContractTransaction} -import com.wavesenterprise.transaction.lease.LeaseTransaction import com.wavesenterprise.transaction.smart.script.Script import com.wavesenterprise.transaction.{AssetId, Transaction, ValidationError} import com.wavesenterprise.utils.pki.CrlData @@ -45,6 +44,10 @@ class CompositeBlockchain(inner: Blockchain, maybeDiff: Option[Diff], carry: Lon inner.addressLeaseBalance(address) |+| diff.portfolios.getOrElse(address.toAssetHolder, Portfolio.empty).lease } + override def contractLeaseBalance(contractId: ContractId): LeaseBalance = { + inner.contractLeaseBalance(contractId) |+| diff.portfolios.getOrElse(contractId.toAssetHolder, Portfolio.empty).lease + } + override def assetScript(id: ByteStr): Option[Script] = maybeDiff.flatMap(_.assetScripts.get(id)).getOrElse(inner.assetScript(id)) override def hasAssetScript(id: ByteStr): Boolean = maybeDiff.flatMap(_.assetScripts.get(id)) match { @@ -71,12 +74,10 @@ class CompositeBlockchain(inner: Blockchain, maybeDiff: Option[Diff], carry: Lon } } - override def leaseDetails(leaseId: ByteStr): Option[LeaseDetails] = { - inner.leaseDetails(leaseId).map(ld => ld.copy(isActive = diff.leaseState.getOrElse(leaseId, ld.isActive))) orElse - diff.transactionsMap.get(leaseId).collect { - case (h, lt: LeaseTransaction, _) => - LeaseDetails(lt.sender, lt.recipient, h, lt.amount, diff.leaseState(lt.id())) - } + override def leaseDetails(leaseId: LeaseId): Option[LeaseDetails] = { + inner.leaseDetails(leaseId).map { ld => + ld.copy(isActive = diff.leaseMap.get(leaseId).map(_.isActive).getOrElse(ld.isActive)) + } orElse diff.leaseMap.get(leaseId) } override def transactionInfo(id: ByteStr): Option[(Int, Transaction)] = @@ -104,22 +105,10 @@ class CompositeBlockchain(inner: Blockchain, maybeDiff: Option[Diff], carry: Lon case Left(_) => diff.aliases.get(alias).toRight(AliasDoesNotExist(alias)) } - override def allActiveLeases: Set[LeaseTransaction] = { - val (active, canceled) = diff.leaseState.partition(_._2) - val fromDiff = active.keys - .map { id => - diff.transactionsMap(id)._2 - } - .collect { case lt: LeaseTransaction => lt } - .toSet - val fromInner = inner.allActiveLeases.filterNot(ltx => canceled.keySet.contains(ltx.id())) - fromDiff ++ fromInner - } - override def collectAddressLposPortfolios[A](pf: PartialFunction[(Address, Portfolio), A]): Map[Address, A] = { val b = Map.newBuilder[Address, A] for ((a, p) <- diff.portfolios.collectAddresses if p.lease != LeaseBalance.empty || p.balance != 0) { - pf.runWith(b += a -> _)(a -> this.westPortfolio(a)) + pf.runWith(b += a -> _)(a -> this.addressWestPortfolio(a)) } inner.collectAddressLposPortfolios(pf) ++ b.result() diff --git a/node/src/main/scala/com/wavesenterprise/state/reader/DelegatingBlockchain.scala b/node/src/main/scala/com/wavesenterprise/state/reader/DelegatingBlockchain.scala index f381278..9bc0cde 100644 --- a/node/src/main/scala/com/wavesenterprise/state/reader/DelegatingBlockchain.scala +++ b/node/src/main/scala/com/wavesenterprise/state/reader/DelegatingBlockchain.scala @@ -11,7 +11,6 @@ import com.wavesenterprise.privacy.{PolicyDataHash, PolicyDataId} import com.wavesenterprise.state.ContractBlockchain.ContractReadingContext import com.wavesenterprise.state._ import com.wavesenterprise.transaction.docker.{ExecutedContractData, ExecutedContractTransaction} -import com.wavesenterprise.transaction.lease.LeaseTransaction import com.wavesenterprise.transaction.smart.script.Script import com.wavesenterprise.transaction.{AssetId, Transaction, ValidationError} import com.wavesenterprise.utils.pki.CrlData @@ -98,7 +97,9 @@ class DelegatingBlockchain(blockchain: Blockchain) extends Blockchain { override def addressLeaseBalance(address: Address): LeaseBalance = state.addressLeaseBalance(address) - override def leaseDetails(leaseId: ByteStr): Option[LeaseDetails] = state.leaseDetails(leaseId) + override def contractLeaseBalance(contractId: ContractId): LeaseBalance = state.contractLeaseBalance(contractId) + + override def leaseDetails(leaseId: LeaseId): Option[LeaseDetails] = state.leaseDetails(leaseId) override def filledVolumeAndFee(orderId: ByteStr): VolumeAndFee = state.filledVolumeAndFee(orderId) @@ -133,8 +134,6 @@ class DelegatingBlockchain(blockchain: Blockchain) extends Blockchain { override def addressWestDistribution(height: Int): Map[Address, Long] = state.addressWestDistribution(height) - override def allActiveLeases: Set[LeaseTransaction] = state.allActiveLeases - override def collectAddressLposPortfolios[A](pf: PartialFunction[(Address, Portfolio), A]): Map[Address, A] = state.collectAddressLposPortfolios(pf) diff --git a/node/src/main/scala/com/wavesenterprise/state/reader/LeaseDetails.scala b/node/src/main/scala/com/wavesenterprise/state/reader/LeaseDetails.scala index 2ef2568..66f02ab 100644 --- a/node/src/main/scala/com/wavesenterprise/state/reader/LeaseDetails.scala +++ b/node/src/main/scala/com/wavesenterprise/state/reader/LeaseDetails.scala @@ -1,5 +1,68 @@ package com.wavesenterprise.state.reader -import com.wavesenterprise.account.{AddressOrAlias, PublicKeyAccount} +import com.google.common.io.ByteStreams.newDataOutput +import com.wavesenterprise.account.AddressOrAlias +import com.wavesenterprise.crypto.DigestSize +import com.wavesenterprise.serialization.BinarySerializer +import com.wavesenterprise.state.{Account, AssetHolder, ByteStr} +import com.wavesenterprise.transaction.lease.LeaseTransaction -case class LeaseDetails(sender: PublicKeyAccount, recipient: AddressOrAlias, height: Int, amount: Long, isActive: Boolean) +case class LeaseDetails( + sender: AssetHolder, + recipient: AddressOrAlias, + height: Int, + amount: Long, + isActive: Boolean, + leaseTxId: Option[ByteStr] // non-empty if lease was created by native token operation (in other words when leaseId is not equal to leaseTxId) +) { + def toBytes: Array[Byte] = { + val output = newDataOutput() + output.write(sender.toBytes) + output.write(recipient.bytes.arr) + output.writeInt(height) + output.writeLong(amount) + output.writeBoolean(isActive) + leaseTxId.foreach(txId => output.write(txId.arr)) + + output.toByteArray + } +} + +object LeaseDetails { + + def fromBytes(bytes: Array[Byte]): LeaseDetails = { + + val (sender, senderEnd) = AssetHolder.fromBytes(bytes) + val (recipient, addressOrAliasEnd) = AddressOrAlias.fromBytesUnsafe(bytes, senderEnd) + val (height, heightEnd) = BinarySerializer.parseInt(bytes, addressOrAliasEnd) + val (amount, amountEnd) = BinarySerializer.parseLong(bytes, heightEnd) + val (isActive, isActiveEnd) = (bytes(amountEnd) == 1) -> (amountEnd + 1) + + val leaseTxId = if (bytes.length > isActiveEnd) { + Some(ByteStr(bytes.slice(isActiveEnd, DigestSize))) + } else { + None + } + + LeaseDetails( + sender, + recipient, + height, + amount, + isActive, + leaseTxId + ) + + } + + def fromLeaseTx(tx: LeaseTransaction, height: Int): LeaseDetails = { + LeaseDetails( + Account(tx.sender.toAddress), + tx.recipient, + height, + tx.amount, + true, + None + ) + } +} diff --git a/node/src/main/scala/com/wavesenterprise/state/reader/ReadWriteLockingBlockchain.scala b/node/src/main/scala/com/wavesenterprise/state/reader/ReadWriteLockingBlockchain.scala index 772b187..f3a028e 100644 --- a/node/src/main/scala/com/wavesenterprise/state/reader/ReadWriteLockingBlockchain.scala +++ b/node/src/main/scala/com/wavesenterprise/state/reader/ReadWriteLockingBlockchain.scala @@ -11,7 +11,6 @@ import com.wavesenterprise.privacy.{PolicyDataHash, PolicyDataId} import com.wavesenterprise.state.ContractBlockchain.ContractReadingContext import com.wavesenterprise.state._ import com.wavesenterprise.transaction.docker.{ExecutedContractData, ExecutedContractTransaction} -import com.wavesenterprise.transaction.lease.LeaseTransaction import com.wavesenterprise.transaction.smart.script.Script import com.wavesenterprise.transaction.{AssetId, Transaction, ValidationError} import com.wavesenterprise.utils.ReadWriteLocking @@ -92,7 +91,7 @@ trait ReadWriteLockingBlockchain extends Blockchain with ReadWriteLocking { override def resolveAlias(a: Alias): Either[ValidationError, Address] = readLock { state.resolveAlias(a) } - override def leaseDetails(leaseId: ByteStr): Option[LeaseDetails] = readLock { state.leaseDetails(leaseId) } + override def leaseDetails(leaseId: LeaseId): Option[LeaseDetails] = readLock { state.leaseDetails(leaseId) } override def filledVolumeAndFee(orderId: ByteStr): VolumeAndFee = readLock { state.filledVolumeAndFee(orderId) } @@ -132,6 +131,10 @@ trait ReadWriteLockingBlockchain extends Blockchain with ReadWriteLocking { state.contractBalance(contractId, mayBeAssetId, readingContext) } + override def contractLeaseBalance(contractId: ContractId): LeaseBalance = { + state.contractLeaseBalance(contractId) + } + override def addressAssetDistribution(assetId: ByteStr): AssetDistribution = readLock { state.addressAssetDistribution(assetId) } @@ -147,10 +150,6 @@ trait ReadWriteLockingBlockchain extends Blockchain with ReadWriteLocking { state.addressWestDistribution(height) } - override def allActiveLeases: Set[LeaseTransaction] = readLock { - state.allActiveLeases - } - override def collectAddressLposPortfolios[A](pf: PartialFunction[(Address, Portfolio), A]): Map[Address, A] = readLock { state.collectAddressLposPortfolios(pf) } diff --git a/node/src/main/scala/com/wavesenterprise/transaction/BlockchainUpdater.scala b/node/src/main/scala/com/wavesenterprise/transaction/BlockchainUpdater.scala index 4f6fda2..7137b76 100644 --- a/node/src/main/scala/com/wavesenterprise/transaction/BlockchainUpdater.scala +++ b/node/src/main/scala/com/wavesenterprise/transaction/BlockchainUpdater.scala @@ -44,7 +44,7 @@ trait BlockchainUpdater extends PrivacyLostItemUpdater with CertificatesState { def policyRollbacks: Observable[PolicyDataId] - def isLastBlockId(id: ByteStr): Boolean + def isLastLiquidBlockId(id: ByteStr): Boolean def isRecentlyApplied(id: ByteStr): Boolean diff --git a/node/src/main/scala/com/wavesenterprise/utils/EmptyBlockchain.scala b/node/src/main/scala/com/wavesenterprise/utils/EmptyBlockchain.scala index 56dbbb9..54c3651 100644 --- a/node/src/main/scala/com/wavesenterprise/utils/EmptyBlockchain.scala +++ b/node/src/main/scala/com/wavesenterprise/utils/EmptyBlockchain.scala @@ -14,7 +14,6 @@ import com.wavesenterprise.state._ import com.wavesenterprise.state.reader.LeaseDetails import com.wavesenterprise.transaction.ValidationError.GenericError import com.wavesenterprise.transaction.docker.{ExecutedContractData, ExecutedContractTransaction} -import com.wavesenterprise.transaction.lease.LeaseTransaction import com.wavesenterprise.transaction.smart.script.Script import com.wavesenterprise.transaction.{AssetId, Transaction, ValidationError} import com.wavesenterprise.utils.pki.CrlData @@ -77,6 +76,8 @@ object EmptyBlockchain extends Blockchain { override def contractBalance(contractId: ContractId, mayBeAssetId: Option[AssetId], readingContext: ContractReadingContext): Long = 0 + override def contractLeaseBalance(contractId: ContractId): LeaseBalance = LeaseBalance.empty + override def addressWestDistribution(height: Int): Map[Address, Long] = Map.empty override def addressTransactions(address: Address, @@ -100,7 +101,7 @@ object EmptyBlockchain extends Blockchain { override def resolveAlias(a: Alias): Either[ValidationError, Address] = Left(GenericError("Empty blockchain")) - override def leaseDetails(leaseId: ByteStr): Option[LeaseDetails] = None + override def leaseDetails(leaseId: LeaseId): Option[LeaseDetails] = None override def filledVolumeAndFee(orderId: ByteStr): VolumeAndFee = VolumeAndFee(0, 0) @@ -122,8 +123,6 @@ object EmptyBlockchain extends Blockchain { override def addressAssetDistribution(assetId: ByteStr): AssetDistribution = Monoid.empty[AssetDistribution] - override def allActiveLeases: Set[LeaseTransaction] = Set.empty - override def addressAssetDistributionAtHeight(assetId: AssetId, height: Int, count: Int, diff --git a/node/src/test/scala/com/wavesenterprise/api/http/AssetsApiRouteSpec.scala b/node/src/test/scala/com/wavesenterprise/api/http/AssetsApiRouteSpec.scala index df59c91..47e9480 100644 --- a/node/src/test/scala/com/wavesenterprise/api/http/AssetsApiRouteSpec.scala +++ b/node/src/test/scala/com/wavesenterprise/api/http/AssetsApiRouteSpec.scala @@ -1,9 +1,7 @@ package com.wavesenterprise.api.http -import java.nio.charset.StandardCharsets - -import com.wavesenterprise.TestSchedulers.apiComputationsScheduler import akka.http.scaladsl.model.StatusCodes +import com.wavesenterprise.TestSchedulers.apiComputationsScheduler import com.wavesenterprise.account.Address import com.wavesenterprise.api.http.assets.AssetsApiRoute import com.wavesenterprise.http.{ApiSettingsHelper, RouteSpec} @@ -15,6 +13,8 @@ import org.scalamock.scalatest.PathMockFactory import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import play.api.libs.json._ +import java.nio.charset.StandardCharsets + class AssetsApiRouteSpec extends RouteSpec("/assets") with PathMockFactory @@ -123,6 +123,19 @@ class AssetsApiRouteSpec Post(routePath("/balance"), invalidBody) ~> route ~> check { status shouldBe StatusCodes.BadRequest } + + Get(routePath(s"/balance/${allAddresses.head}/fake")) ~> route ~> check { + val response = responseAs[JsObject] + (response \ "address").as[String] shouldBe allAddresses.head + (response \ "assetId").as[String] shouldBe "fake" + (response \ "balance").as[Long] shouldBe 0 + } + + Get(routePath(s"/balance/${allAddresses.head}/fake.")) ~> route ~> check { + status shouldBe StatusCodes.BadRequest + val response = responseAs[JsObject] + (response \ "message").as[String] should include("Asset id 'fake.' is invalid") + } } } diff --git a/node/src/test/scala/com/wavesenterprise/api/http/service/AddressApiServiceSpec.scala b/node/src/test/scala/com/wavesenterprise/api/http/service/AddressApiServiceSpec.scala index f65476d..c3cbc7a 100644 --- a/node/src/test/scala/com/wavesenterprise/api/http/service/AddressApiServiceSpec.scala +++ b/node/src/test/scala/com/wavesenterprise/api/http/service/AddressApiServiceSpec.scala @@ -1,5 +1,7 @@ package com.wavesenterprise.api.http.service +import com.wavesenterprise.api.http.ApiError.InvalidPublicKey +import com.wavesenterprise.api.http.SignedMessage import com.wavesenterprise.state.{AccountDataInfo, Blockchain} import com.wavesenterprise.wallet.Wallet import com.wavesenterprise.{TransactionGen, crypto} @@ -68,4 +70,11 @@ class AddressApiServiceSpec extends AnyFunSpecLike with Matchers with MockFactor accountData.right.get should contain theSameElementsAs dataEntries } + it("invalid public key") { + val signedMessage = SignedMessage("ping_pong", "GmU5d7pjZmQrs5EKgR9CSz5L6Np", "IO123") + val verificationResult = addressApiService.verifySignedMessage(signedMessage, addressStr, isMessageEncoded = false) + verificationResult shouldBe 'left + val validationError = verificationResult.left.get.asInstanceOf[InvalidPublicKey] + validationError.message should equal("invalid public key: IO123") + } } diff --git a/node/src/test/scala/com/wavesenterprise/database/RocksDBDequeSpec.scala b/node/src/test/scala/com/wavesenterprise/database/RocksDBDequeSpec.scala new file mode 100644 index 0000000..45ae658 --- /dev/null +++ b/node/src/test/scala/com/wavesenterprise/database/RocksDBDequeSpec.scala @@ -0,0 +1,220 @@ +package com.wavesenterprise.database + +import com.google.common.primitives.Shorts +import com.wavesenterprise.WithDB +import monix.execution.atomic.AtomicShort +import org.scalacheck.Gen +import org.scalatest.Assertion +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks +import org.testcontainers.shaded.com.google.common.primitives.Ints +import org.scalatest.matchers.should.Matchers +import org.scalatest.propspec.AnyPropSpec + +import java.util +import scala.collection.convert.ImplicitConversions._ + +class RocksDBDequeSpec extends AnyPropSpec + with ScalaCheckPropertyChecks + with WithDB + with Matchers { + + private sealed trait Operation[T] { + def apply(referenceDeque: util.ArrayDeque[T], rocksDBDeque: RocksDBDeque[T]): Assertion + } + + private object Operation { + + case class AddFirst[T](value: T) extends Operation[T] { + def apply(referenceDeque: util.ArrayDeque[T], rocksDBDeque: RocksDBDeque[T]): Assertion = { + referenceDeque.addFirst(value) + rocksDBDeque.addFirst(value) + + referenceDeque.toList shouldBe rocksDBDeque.toList + } + } + + case class AddLast[T](value: T) extends Operation[T] { + def apply(referenceDeque: util.ArrayDeque[T], rocksDBDeque: RocksDBDeque[T]): Assertion = { + referenceDeque.addLast(value) + rocksDBDeque.addLast(value) + + referenceDeque.toList shouldBe rocksDBDeque.toList + } + } + + case class PeekFirst[T]() extends Operation[T] { + def apply(referenceDeque: util.ArrayDeque[T], rocksDBDeque: RocksDBDeque[T]): Assertion = { + val refHead = Option(referenceDeque.peekFirst()) + val rocksDBHead = rocksDBDeque.head + + refHead shouldBe rocksDBHead + } + } + + case class PeekLast[T]() extends Operation[T] { + def apply(referenceDeque: util.ArrayDeque[T], rocksDBDeque: RocksDBDeque[T]): Assertion = { + val refTail = Option(referenceDeque.peekLast()) + val rocksDBTail = rocksDBDeque.last + + refTail shouldBe rocksDBTail + } + } + + case class PollFirst[T]() extends Operation[T] { + def apply(referenceDeque: util.ArrayDeque[T], rocksDBDeque: RocksDBDeque[T]): Assertion = { + referenceDeque.pollFirst() + rocksDBDeque.pollFirst + + referenceDeque.toList shouldBe rocksDBDeque.toList + } + } + + case class PollLast[T]() extends Operation[T] { + def apply(referenceDeque: util.ArrayDeque[T], rocksDBDeque: RocksDBDeque[T]): Assertion = { + referenceDeque.pollLast() + rocksDBDeque.pollLast + + referenceDeque.toList shouldBe rocksDBDeque.toList + } + } + + case class Contains[T](value: T) extends Operation[T] { + def apply(referenceDeque: util.ArrayDeque[T], rocksDBDeque: RocksDBDeque[T]): Assertion = { + referenceDeque.contains(value) shouldBe rocksDBDeque.contains(value) + } + } + + case class Size[T]() extends Operation[T] { + def apply(referenceDeque: util.ArrayDeque[T], rocksDBDeque: RocksDBDeque[T]): Assertion = { + referenceDeque.size shouldBe rocksDBDeque.size + } + } + + case class IsEmpty[T]() extends Operation[T] { + def apply(referenceDeque: util.ArrayDeque[T], rocksDBDeque: RocksDBDeque[T]): Assertion = { + referenceDeque.isEmpty shouldBe rocksDBDeque.isEmpty + } + } + + case class NonEmpty[T]() extends Operation[T] { + def apply(referenceDeque: util.ArrayDeque[T], rocksDBDeque: RocksDBDeque[T]): Assertion = { + referenceDeque.nonEmpty shouldBe rocksDBDeque.nonEmpty + } + } + + case class ToList[T]() extends Operation[T] { + def apply(referenceDeque: util.ArrayDeque[T], rocksDBDeque: RocksDBDeque[T]): Assertion = { + referenceDeque.toList shouldBe rocksDBDeque.toList + } + } + + case class Clear[T]() extends Operation[T] { + def apply(referenceDeque: util.ArrayDeque[T], rocksDBDeque: RocksDBDeque[T]): Assertion = { + referenceDeque.clear() + rocksDBDeque.clear() + + referenceDeque.toList shouldBe rocksDBDeque.toList + referenceDeque.size shouldBe rocksDBDeque.size + } + } + + case class Slice[T](from: Int, until: Int) extends Operation[T] { + def apply(referenceDeque: util.ArrayDeque[T], rocksDBDeque: RocksDBDeque[T]): Assertion = { + referenceDeque.slice(from, until) shouldBe rocksDBDeque.slice(from, until) + } + } + + def gen[T](valueGen: Gen[T], indexGen: Gen[Int]): Gen[Operation[T]] = + Gen.oneOf( + addFirstGen(valueGen), + addLastGen(valueGen), + peekFirstGen[T], + peekLastGen[T], + pollFirstGen[T], + pollLastGen[T], + containsGen(valueGen), + sizeGen[T], + isEmptyGen[T], + nonEmptyGen[T], + toListGen[T], + clearGen[T], + sliceGen[T](indexGen) + ) + + def addFirstGen[T](valueGen: Gen[T]): Gen[AddFirst[T]] = + valueGen.flatMap(value => Gen.const(AddFirst(value))) + + def addLastGen[T](valueGen: Gen[T]): Gen[AddLast[T]] = + valueGen.flatMap(value => Gen.const(AddLast(value))) + + def peekFirstGen[T]: Gen[PeekFirst[T]] = + Gen.const(PeekFirst[T]()) + + def peekLastGen[T]: Gen[PeekLast[T]] = + Gen.const(PeekLast[T]()) + + def pollFirstGen[T]: Gen[PollFirst[T]] = + Gen.const(PollFirst[T]()) + + def pollLastGen[T]: Gen[PollLast[T]] = + Gen.const(PollLast[T]()) + + def containsGen[T](valueGen: Gen[T]): Gen[Contains[T]] = + valueGen.flatMap(value => Gen.const(Contains(value))) + + def sizeGen[T]: Gen[Size[T]] = Gen.const(Size[T]()) + + def isEmptyGen[T]: Gen[IsEmpty[T]] = Gen.const(IsEmpty[T]()) + + def nonEmptyGen[T]: Gen[NonEmpty[T]] = Gen.const(NonEmpty[T]()) + + def toListGen[T]: Gen[ToList[T]] = Gen.const(ToList[T]()) + + def clearGen[T]: Gen[Clear[T]] = Gen.const(Clear[T]()) + + def sliceGen[T](indexGen: Gen[Int]): Gen[Slice[T]] = { + for { + from <- indexGen + until <- indexGen + } yield Slice[T](from, until) + } + } + + private val prefixCounter = AtomicShort(0) + + property("rocksDBDeque state equivalent to the ArrayDeque after several operations") { + forAll(operationsGen) { operations => + withClue(s"Operations seq [${operations.mkString(", ")}]") { + val uniquePrefix = prefixCounter.incrementAndGet() + + val rocksDBDeque = new RocksDBDeque( + s"$uniquePrefix-test-deque", + Shorts.toByteArray(uniquePrefix), + storage, + Ints.toByteArray, + Ints.fromByteArray + ) + + val referenceDeque = new util.ArrayDeque[Int]() + + operations.foreach { operation => + withClue(s"Incorrect operation – $operation:") { + operation.apply(referenceDeque, rocksDBDeque) + } + } + + referenceDeque.toList shouldBe rocksDBDeque.toList + } + } + } + + private def valueGen: Gen[Int] = Gen.chooseNum(0, 10) + private def indexGen: Gen[Int] = Gen.chooseNum(-10, 1000) + + private def operationsGen: Gen[List[Operation[Int]]] = + for { + count <- Gen.chooseNum(150, 1000) + operationGen = Operation.gen(valueGen, indexGen) + result <- Gen.listOfN(count, operationGen) + } yield result +} diff --git a/node/src/test/scala/com/wavesenterprise/database/migration/MigrationV10Test.scala b/node/src/test/scala/com/wavesenterprise/database/migration/MigrationV10Test.scala new file mode 100644 index 0000000..5844ca2 --- /dev/null +++ b/node/src/test/scala/com/wavesenterprise/database/migration/MigrationV10Test.scala @@ -0,0 +1,127 @@ +package com.wavesenterprise.database.migration + +import com.wavesenterprise.account.Address +import com.wavesenterprise.database.Keys +import com.wavesenterprise.database.keys.LeaseCFKeys +import com.wavesenterprise.database.migration.MigrationV10Test.LeaseStatusInfo +import com.wavesenterprise.state.{Account, LeaseId} +import com.wavesenterprise.state.reader.LeaseDetails +import com.wavesenterprise.transaction.docker.ContractTransactionGen +import com.wavesenterprise.transaction.lease.{LeaseCancelTransaction, LeaseTransaction} +import com.wavesenterprise.{TransactionGen, WithDB} +import org.scalacheck.Gen +import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.matchers.should.Matchers + +class MigrationV10Test extends AnyFreeSpec with Matchers with WithDB with TransactionGen with ContractTransactionGen { + + private val height = 2 + private val leaseTxsCount = 20 + private val leaseCancelTxsCount = leaseTxsCount / 2 + + private val stateGen: Gen[List[(LeaseTransaction, LeaseCancelTransaction)]] = { + Gen.listOfN(leaseTxsCount, + leaseAndCancelGen.map { + case (lease: LeaseTransaction, _, leaseCancel: LeaseCancelTransaction, _) => (lease, leaseCancel) + }) + } + override protected def migrateScheme: Boolean = false + + def combineIterables[K, V](a: Map[K, Iterable[V]], b: Map[K, Iterable[V]]): Map[K, Iterable[V]] = { + a ++ b.map { case (k, v) => k -> (v ++ a.getOrElse(k, Iterable.empty)) } + } + + "MigrationV10 should work correctly" in { + val txs = stateGen.sample.get + val leaseTxs = txs.map(_._1) + val leaseCancelTxs = txs.take(leaseCancelTxsCount).map(_._2) + + leaseTxs.foreach { leaseTx => + val leaseId = LeaseId(leaseTx.id()) + storage.put(Keys.transactionInfo(leaseTx.id()), Some((height, leaseTx))) + storage.put(MigrationV10.RocksDBKeys.leaseStatusHistory(leaseId), Seq(2)) + storage.put(MigrationV10.RocksDBKeys.leaseStatus(leaseId)(height), true) + + val senderAddress = leaseTx.sender.toAddress + val recipientAddress = leaseTx.recipient match { case address: Address => address } + val lastAddressIdKey = Keys.lastAddressId + + if (storage.get(Keys.addressId(senderAddress)).isEmpty) { + val addressId = storage.get(lastAddressIdKey).getOrElse(BigInt(0)) + BigInt(1) + storage.put(Keys.addressId(senderAddress), Some(addressId)) + storage.put(Keys.idToAddress(addressId), senderAddress) + storage.put(lastAddressIdKey, Some(addressId)) + val addressSeqNr = storage.get(Keys.addressTransactionSeqNr(addressId)) + 1 + storage.put(Keys.addressTransactionSeqNr(addressId), addressSeqNr) + storage.put(Keys.addressTransactionIds(addressId, addressSeqNr), Seq(leaseTx.builder.typeId.toInt -> leaseTx.id())) + } + + if (storage.get(Keys.addressId(recipientAddress)).isEmpty) { + val addressId = storage.get(lastAddressIdKey).getOrElse(BigInt(0)) + BigInt(1) + storage.put(Keys.addressId(recipientAddress), Some(addressId)) + storage.put(Keys.idToAddress(addressId), recipientAddress) + storage.put(lastAddressIdKey, Some(addressId)) + val addressSeqNr = storage.get(Keys.addressTransactionSeqNr(addressId)) + 1 + storage.put(Keys.addressTransactionSeqNr(addressId), addressSeqNr) + storage.put(Keys.addressTransactionIds(addressId, addressSeqNr), Seq(leaseTx.builder.typeId.toInt -> leaseTx.id())) + } + + } + + leaseCancelTxs.foreach { leaseCancelTx => + val leaseId = LeaseId(leaseCancelTx.leaseId) + storage.put(Keys.transactionInfo(leaseCancelTx.id()), Some((height + 1, leaseCancelTx))) + storage.put(MigrationV10.RocksDBKeys.leaseStatusHistory(leaseId), Seq(2, 3)) + storage.put(MigrationV10.RocksDBKeys.leaseStatus(leaseId)(height), false) + } + + val schemaManager = new SchemaManager(storage) + schemaManager.applyMigrations(List(MigrationType.`10`)) shouldBe 'right + + val leasesBySender = leaseTxs.groupBy(_.sender.toAddress) + val leasesByRecipient = leaseTxs.groupBy { leaseTx => + leaseTx.recipient match { + case address: Address => address + } + } + + val leaseFinalStatusInfos = for ((leaseTx, idx) <- leaseTxs.zipWithIndex) yield { + if (idx < leaseCancelTxsCount) { + LeaseId(leaseTx.id()) -> LeaseStatusInfo(leaseTx, isActive = false) + } else { + LeaseId(leaseTx.id()) -> LeaseStatusInfo(leaseTx, isActive = true) + } + } + + leaseFinalStatusInfos.foreach { case (leaseId, leaseStatusInfo) => + val actualLeaseDetails = storage.get(LeaseCFKeys.leaseDetails(leaseId)).get + val expectedLeaseDetails = leaseStatusInfo.toLeaseDetails(height) + + actualLeaseDetails shouldBe expectedLeaseDetails + } + + val leasesByAddress = combineIterables(leasesBySender, leasesByRecipient) + + leasesByAddress.foreach { case (address, leases) => + val addressId = storage.get(Keys.addressId(address)).get + val actualLeasesForAddress = LeaseCFKeys.leasesForAddress(addressId, storage).toList + val expectedLeasesForAddress = leases.map(leaseTx => LeaseId(leaseTx.id())) + + actualLeasesForAddress should contain theSameElementsAs expectedLeasesForAddress + } + + } +} + +object MigrationV10Test { + private case class LeaseStatusInfo(leaseTx: LeaseTransaction, isActive: Boolean) { + def toLeaseDetails(height: Int): LeaseDetails = LeaseDetails( + Account(leaseTx.sender.toAddress), + leaseTx.recipient, + height, + leaseTx.amount, + isActive = isActive, + leaseTxId = None + ) + } +} diff --git a/node/src/test/scala/com/wavesenterprise/state/RollbackSpec.scala b/node/src/test/scala/com/wavesenterprise/state/RollbackSpec.scala index 52340aa..c34c595 100644 --- a/node/src/test/scala/com/wavesenterprise/state/RollbackSpec.scala +++ b/node/src/test/scala/com/wavesenterprise/state/RollbackSpec.scala @@ -29,7 +29,9 @@ import com.wavesenterprise.transaction.assets._ import com.wavesenterprise.transaction.docker._ import com.wavesenterprise.transaction.docker.assets.ContractAssetOperation.{ ContractBurnV1, + ContractCancelLeaseV1, ContractIssueV1, + ContractLeaseV1, ContractReissueV1, ContractTransferOutV1 } @@ -291,7 +293,12 @@ class RollbackSpec extends AnyFreeSpec with Matchers with WithDomain with Transa val lt = LeaseTransactionV2.selfSigned(None, sender, recipient, leaseAmount, leaseFee, nextTs).explicitGet() d.appendBlock(TestBlock.create(nextTs, genesisBlockId, Seq(lt))) val blockWithLeaseId = d.lastBlockId - d.blockchainUpdater.leaseDetails(lt.id()) should contain(LeaseDetails(sender, recipient, 2, leaseAmount, true)) + d.blockchainUpdater.leaseDetails(LeaseId(lt.id())) should contain(LeaseDetails(Account(sender.toAddress), + recipient, + 2, + leaseAmount, + true, + None)) d.portfolio(sender.toAddress).lease.out shouldEqual leaseAmount d.portfolio(recipient).lease.in shouldEqual leaseAmount @@ -307,17 +314,27 @@ class RollbackSpec extends AnyFreeSpec with Matchers with WithDomain with Transa d.appendBlock(leaseCancelTransactionBlock) d.appendBlock(TestBlock.create(nextTs, leaseCancelTransactionBlock.uniqueId, Seq.empty)) - d.blockchainUpdater.leaseDetails(lt.id()) should contain(LeaseDetails(sender, recipient, 2, leaseAmount, false)) + d.blockchainUpdater.leaseDetails(LeaseId(lt.id())) should contain(LeaseDetails(Account(sender.toAddress), + recipient, + 2, + leaseAmount, + false, + None)) d.portfolio(sender.toAddress).lease.out shouldEqual 0 d.portfolio(recipient).lease.in shouldEqual 0 d.removeAfter(blockWithLeaseId) - d.blockchainUpdater.leaseDetails(lt.id()) should contain(LeaseDetails(sender, recipient, 2, leaseAmount, true)) + d.blockchainUpdater.leaseDetails(LeaseId(lt.id())) should contain(LeaseDetails(Account(sender.toAddress), + recipient, + 2, + leaseAmount, + true, + None)) d.portfolio(sender.toAddress).lease.out shouldEqual leaseAmount d.portfolio(recipient).lease.in shouldEqual leaseAmount d.removeAfter(genesisBlockId) - d.blockchainUpdater.leaseDetails(lt.id()) shouldBe 'empty + d.blockchainUpdater.leaseDetails(LeaseId(lt.id())) shouldBe 'empty d.portfolio(sender.toAddress).lease.out shouldEqual 0 d.portfolio(recipient).lease.in shouldEqual 0 } @@ -1353,9 +1370,10 @@ class RollbackSpec extends AnyFreeSpec with Matchers with WithDomain with Transa } "executed contract create with native token operations" in forAll(accountGen, + accountGen, Gen.listOfN(10, dataEntryGen(10)), Gen.listOfN(10, dataEntryGen(10))) { - case (sender, createResults, callResults) => + case (sender, recipient, createResults, callResults) => withDomain(createSettings()) { d => val initialSenderBalance = com.wavesenterprise.state.diffs.ENOUGH_AMT d.appendBlock(genesisBlock(nextTs, sender.toAddress, initialSenderBalance)) @@ -1389,7 +1407,7 @@ class RollbackSpec extends AnyFreeSpec with Matchers with WithDomain with Transa val callContractTx = CallContractTransactionV5 .selfSigned(sender, contractId, List.empty, callContractFee, nextTs, 1, None, atomicBadge, List.empty) .explicitGet() - val callContractWestOutputAmount = createContractInputAmount + val callContractWestOutputAmount = createContractInputAmount / 2 val contractIssue = { val nonce = 1.toByte val assetId = ByteStr(crypto.fastHash(callContractTx.id().arr :+ nonce)) @@ -1406,7 +1424,21 @@ class RollbackSpec extends AnyFreeSpec with Matchers with WithDomain with Transa val assetTransferOut = ContractTransferOutV1(Some(contractIssue.assetId), sender.toAddress, contractAssetTransferAmount) val contractAssetTransferOuts = List(westTransferOut, assetTransferOut) - val assetOperations = List(contractIssue, contractReissue, contractBurn) ++ contractAssetTransferOuts + val (contractLeaseOps, contractCancelLeaseOps) = { + val leaseOps = (1 to 10).map { nonce => + val leaseId = ByteStr(crypto.fastHash(callContractTx.id().arr :+ nonce.toByte)) + ContractLeaseV1(leaseId, nonce.toByte, recipient.toAddress, 10000) + } + + val leaseCancelOps = leaseOps.take(leaseOps.size / 2).map { leaseOp => + ContractCancelLeaseV1(leaseOp.leaseId) + } + + (leaseOps, leaseCancelOps) + } + + val assetOperations = + List(contractIssue, contractReissue, contractBurn) ++ contractAssetTransferOuts ++ contractLeaseOps ++ contractCancelLeaseOps val callResultsHash = ContractTransactionValidation.resultsHash(callResults, assetOperations) val executedCallTx = ExecutedContractTransactionV3 @@ -1424,8 +1456,9 @@ class RollbackSpec extends AnyFreeSpec with Matchers with WithDomain with Transa val nextBlock = TestBlock.create(nextTs, blockWithAtomicTx.uniqueId, Seq.empty) d.appendBlock(nextBlock) - val expectedAssetVolume = contractIssue.quantity + contractReissue.quantity - contractBurn.amount - val expectedSenderWestBalance = initialSenderBalance - createContractFee - callContractFee + val expectedAssetVolume = contractIssue.quantity + contractReissue.quantity - contractBurn.amount + val expectedSenderWestBalance = + initialSenderBalance - createContractFee - callContractFee - createContractInputAmount + callContractWestOutputAmount val expectedContractAssetBalance = (contractIssue.quantity + contractReissue.quantity) - contractAssetTransferAmount - contractBurn.amount d.blockchainUpdater.containsTransaction(atomicTx) shouldBe true @@ -1436,6 +1469,7 @@ class RollbackSpec extends AnyFreeSpec with Matchers with WithDomain with Transa d.blockchainUpdater.contract(ContractId(contractId)).isDefined shouldBe true d.blockchainUpdater.contract(ContractId(contractId)).map(_.version) shouldBe Some(1) d.blockchainUpdater.addressBalance(sender.toAddress) shouldBe expectedSenderWestBalance + d.blockchainUpdater.addressBalance(sender.toAddress, Some(contractIssue.assetId)) shouldBe contractAssetTransferAmount d.blockchainUpdater.contractBalance(ContractId(contractId), Some(contractIssue.assetId), diff --git a/node/src/test/scala/com/wavesenterprise/state/diffs/docker/ContractAssetOperationsDiffTest.scala b/node/src/test/scala/com/wavesenterprise/state/diffs/docker/ContractAssetOperationsDiffTest.scala index 12fe6ae..0fceca8 100644 --- a/node/src/test/scala/com/wavesenterprise/state/diffs/docker/ContractAssetOperationsDiffTest.scala +++ b/node/src/test/scala/com/wavesenterprise/state/diffs/docker/ContractAssetOperationsDiffTest.scala @@ -4,13 +4,16 @@ import cats.Monoid import cats.implicits._ import com.wavesenterprise.block.Block import com.wavesenterprise.db.WithDomain +import com.wavesenterprise.docker.ContractApiVersion +import com.wavesenterprise.docker.validator.ValidationPolicy import com.wavesenterprise.lagonaki.mocks.TestBlock.{create => block} import com.wavesenterprise.settings.TestFunctionalitySettings.EnabledForNativeTokens import com.wavesenterprise.state.ContractBlockchain.ContractReadingContext import com.wavesenterprise.state._ import com.wavesenterprise.state.diffs.{ENOUGH_AMT, assertDiffAndState, assertDiffEither, produce} -import com.wavesenterprise.transaction.GenesisTransaction +import com.wavesenterprise.transaction.{AssetId, GenesisTransaction} import com.wavesenterprise.transaction.docker.assets.ContractAssetOperation +import com.wavesenterprise.transaction.docker.assets.ContractAssetOperation.{ContractCancelLeaseV1, ContractLeaseV1} import com.wavesenterprise.transaction.docker.{ContractTransactionGen, ExecutedContractTransactionV3} import com.wavesenterprise.{NoShrink, TransactionGen, crypto} import org.scalacheck.Gen @@ -29,6 +32,7 @@ class ContractAssetOperationsDiffTest with ExecutableTransactionGen with NoShrink with WithDomain { + import ExecutableTransactionGen._ property("Cannot reissue/burn non-existing alias") { @@ -227,7 +231,7 @@ class ContractAssetOperationsDiffTest } val errorMessage = - s"contract '${contractId}' balance validation errors: [negative asset balance: [asset: '${assetId.base58}' -> balance: '-${burnOp.amount}']]" + s"contract '$contractId' balance validation errors: [negative asset balance: [asset: '${assetId.base58}' -> balance: '-${burnOp.amount}']]" error.getMessage should include(errorMessage) @@ -236,4 +240,166 @@ class ContractAssetOperationsDiffTest } + def total(l: LeaseBalance): Long = l.in - l.out + + val leaseNonceGen: Gen[Byte] = Gen.choose(-128: Byte, 127: Byte).suchThat(_ != 0) + + import com.wavesenterprise.account.PublicKeyAccount._ + + def contractLeaseAndCancelGen(amountGen: Gen[Long]): Gen[(ContractLeaseV1, ContractCancelLeaseV1)] = + for { + recipient <- accountGen + amount <- amountGen + nonce <- leaseNonceGen + leaseId <- bytes32gen.map(ByteStr(_)) + lease = ContractLeaseV1(leaseId, nonce, recipient.toAddress, amount) + leaseCancel = ContractCancelLeaseV1(leaseId) + } yield (lease, leaseCancel) + + property("can lease/cancel lease preserving WEST invariant") { + + val setup = for { + executedSigner <- accountGen + ts <- ntpTimestampGen + (creatorAccount, creatorGenesisTrx) <- accountGenesisGen(ts) + create <- createContractV5Gen( + None, + (None, CreateFee), + creatorAccount, + ValidationPolicy.Any, + ContractApiVersion.Initial, + List.empty[(Option[AssetId], Long)] + ) + refillContract <- contractTransferInV1Gen(None, ENOUGH_AMT / 1000) + (lease, leaseCancel) <- contractLeaseAndCancelGen(positiveLongGen.suchThat(_ <= refillContract.amount)) + executedCreate <- executedTxV3ParamGen(executedSigner, create) + callWithLease <- callContractV5ParamGen(None, callTxFeeGen, creatorAccount, create.contractId, 1, List(refillContract)) + executedCallWithLease <- executedTxV3ParamWithOperationsGen( + executedSigner, + callWithLease, + assetOperations = lease :: Nil + ) + callWithLeaseCancel <- callContractV5ParamGen(None, callTxFeeGen, creatorAccount, create.contractId, 1, List(refillContract)) + executedCallWithLeaseCancel <- executedTxV3ParamWithOperationsGen( + executedSigner, + callWithLeaseCancel, + assetOperations = leaseCancel :: Nil + ) + block1 = block(Seq(creatorGenesisTrx)) + block2 = block(executedSigner, Seq(executedCreate)) + } yield (Seq(block1, block2), Seq(lease, leaseCancel), executedSigner, executedCallWithLease, executedCallWithLeaseCancel) + + forAll(setup) { + case (blocks, _, txSigner, executedTxWithLease, executedTxWithLeaseCancel) => + val blockWithLease = block(txSigner, Seq(executedTxWithLease)) + + assertDiffAndState(blocks, blockWithLease, EnabledForNativeTokens) { + case (totalDiff, _) => + val totalPortfolioDiff = Monoid.combineAll(totalDiff.portfolios.values) + totalPortfolioDiff.balance shouldBe 0 + total(totalPortfolioDiff.lease) shouldBe 0 + totalPortfolioDiff.effectiveBalance shouldBe 0 + totalPortfolioDiff.assets.values.foreach(_ shouldBe 0) + } + + val blockWithLeaseCancel = block(txSigner, Seq(executedTxWithLeaseCancel)) + assertDiffAndState(blocks :+ blockWithLease, blockWithLeaseCancel, EnabledForNativeTokens) { + case (totalDiff, _) => + val totalPortfolioDiff = Monoid.combineAll(totalDiff.portfolios.values) + totalPortfolioDiff.balance shouldBe 0 + total(totalPortfolioDiff.lease) shouldBe 0 + totalPortfolioDiff.effectiveBalance shouldBe 0 + totalPortfolioDiff.assets.values.foreach(_ shouldBe 0) + } + } + } + + property("cannot cancel lease twice") { + + val setup = for { + executedSigner <- accountGen + ts <- ntpTimestampGen + (creatorAccount, creatorGenesisTrx) <- accountGenesisGen(ts) + create <- createContractV5Gen( + None, + (None, CreateFee), + creatorAccount, + ValidationPolicy.Any, + ContractApiVersion.Initial, + List.empty[(Option[AssetId], Long)] + ) + refillContract <- contractTransferInV1Gen(None, ENOUGH_AMT / 1000) + (lease, leaseCancel) <- contractLeaseAndCancelGen(positiveLongGen.suchThat(_ <= refillContract.amount)) + executedCreate <- executedTxV3ParamGen(executedSigner, create) + callWithLease <- callContractV5ParamGen(None, callTxFeeGen, creatorAccount, create.contractId, 1, List(refillContract)) + executedCallWithLease <- executedTxV3ParamWithOperationsGen( + executedSigner, + callWithLease, + assetOperations = lease :: Nil + ) + callWithLeaseCancel <- callContractV5ParamGen(None, callTxFeeGen, creatorAccount, create.contractId, 1, List(refillContract)) + executedCallWithLeaseCancel <- executedTxV3ParamWithOperationsGen( + executedSigner, + callWithLeaseCancel, + assetOperations = leaseCancel :: Nil + ) + callWithSecondLeaseCancel <- callContractV5ParamGen(None, callTxFeeGen, creatorAccount, create.contractId, 1, List(refillContract)) + executedCallWithSecondLeaseCancel <- executedTxV3ParamWithOperationsGen( + executedSigner, + callWithSecondLeaseCancel, + assetOperations = leaseCancel :: Nil + ) + block1 = block(Seq(creatorGenesisTrx)) + block2 = block(executedSigner, Seq(executedCreate)) + block3 = block(executedSigner, Seq(executedCallWithLease, executedCallWithLeaseCancel)) + } yield (Seq(block1, block2, block3), leaseCancel, executedSigner, executedCallWithSecondLeaseCancel) + + forAll(setup) { + case (blocks, _, txSigner, executedCallWithSecondLeaseCancel) => + val blockWithSecondLeaseCancel = block(txSigner, Seq(executedCallWithSecondLeaseCancel)) + + assertDiffEither(blocks, blockWithSecondLeaseCancel, EnabledForNativeTokens) { + totalDiffEi => + totalDiffEi should produce("Cannot cancel already cancelled lease") + } + } + } + + property("cannot lease more than own") { + + val setup = for { + executedSigner <- accountGen + ts <- ntpTimestampGen + (creatorAccount, creatorGenesisTrx) <- accountGenesisGen(ts) + create <- createContractV5Gen( + None, + (None, CreateFee), + creatorAccount, + ValidationPolicy.Any, + ContractApiVersion.Initial, + List.empty[(Option[AssetId], Long)] + ) + (lease, _) <- contractLeaseAndCancelGen(Gen.const(Long.MaxValue)) + executedCreate <- executedTxV3ParamGen(executedSigner, create) + refillContract <- contractTransferInV1Gen(None, ENOUGH_AMT / 1000) + callWithLeaseForward <- callContractV5ParamGen(None, callTxFeeGen, creatorAccount, create.contractId, 1, List(refillContract)) + executedCallLeaseForward <- executedTxV3ParamWithOperationsGen( + executedSigner, + callWithLeaseForward, + assetOperations = lease :: Nil + ) + block1 = block(Seq(creatorGenesisTrx)) + block2 = block(executedSigner, Seq(executedCreate)) + } yield (Seq(block1, block2), executedSigner, executedCallLeaseForward) + + forAll(setup) { + case (blocks, txSigner, executedCallLeaseForward) => + val blockWithLeaseForward = block(txSigner, Seq(executedCallLeaseForward)) + + assertDiffEither(blocks, blockWithLeaseForward, EnabledForNativeTokens) { + totalDiffEi => + totalDiffEi should produce("Cannot lease more than own") + } + } + } } diff --git a/node/src/test/scala/com/wavesenterprise/state/reader/StateReaderEffectiveBalancePropertyTest.scala b/node/src/test/scala/com/wavesenterprise/state/reader/StateReaderEffectiveBalancePropertyTest.scala index e70a567..b3a4f83 100644 --- a/node/src/test/scala/com/wavesenterprise/state/reader/StateReaderEffectiveBalancePropertyTest.scala +++ b/node/src/test/scala/com/wavesenterprise/state/reader/StateReaderEffectiveBalancePropertyTest.scala @@ -53,7 +53,7 @@ class StateReaderEffectiveBalancePropertyTest extends AnyPropSpec with ScalaChec forAll(setup) { case (leaser, genesis, xfer1, lease1, xfer2, lease2) => assertDiffAndState(Seq(block(Seq(genesis)), block(Seq(xfer1, lease1))), block(Seq(xfer2, lease2)), fs) { (_, state) => - val portfolio = state.westPortfolio(lease1.sender.toAddress) + val portfolio = state.addressWestPortfolio(lease1.sender.toAddress) val expectedBalance = xfer1.amount + xfer2.amount - 2 * Fee portfolio.balance shouldBe expectedBalance GeneratingBalanceProvider.balance(state, fs, state.height, leaser.toAddress) shouldBe 0 diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 87e16f9..190bb10 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -69,7 +69,7 @@ object Dependencies { "org.julienrf" %% "play-json-derived-codecs" % "6.0.0" ) - lazy val db = Seq("org.rocksdb" % "rocksdbjni" % "6.22.1.1") + lazy val db = Seq("org.rocksdb" % "rocksdbjni" % "8.0.0") lazy val logging = Seq( "ch.qos.logback" % "logback-classic" % "1.2.3", diff --git a/project/build.properties b/project/build.properties index a5ac093..9b4dfd2 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1,2 +1,2 @@ sbt.version=1.6.2 -wecore.version=1.12.2-RC2 \ No newline at end of file +wecore.version=1.12.3 \ No newline at end of file