Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NODE-2600 Optionally override a blockchain state in /utils/script/evaluate #3858

Merged
merged 17 commits into from
Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 129 additions & 42 deletions node/src/main/resources/swagger-ui/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
openapi: 3.0.1
openapi: 3.1.0
info:
version: '{{version}}'
title: Waves Full Node ({{chainId}})
Expand Down Expand Up @@ -287,7 +287,7 @@ paths:
application/json;large-significand-format=string:
schema:
allOf:
- $ref: '#/components/schemas/BalanceLSF'
- $ref: '#/components/schemas/BalanceLSF'
example:
address: 2eEUvypDSivnzPiLrbYEW39SM8yMZ1aq4eJuiKfs4sEY
confirmations: 3
Expand Down Expand Up @@ -598,7 +598,7 @@ paths:
application/json;large-significand-format=string:
schema:
allOf:
- $ref: '#/components/schemas/BalanceLSF'
- $ref: '#/components/schemas/BalanceLSF'
example:
address: 2eEUvypDSivnzPiLrbYEW39SM8yMZ1aq4eJuiKfs4sEY
confirmations: 3
Expand Down Expand Up @@ -631,7 +631,7 @@ paths:
application/json;large-significand-format=string:
schema:
allOf:
- $ref: '#/components/schemas/BalanceLSF'
- $ref: '#/components/schemas/BalanceLSF'
example:
address: 2eEUvypDSivnzPiLrbYEW39SM8yMZ1aq4eJuiKfs4sEY
confirmations: 3
Expand Down Expand Up @@ -1991,41 +1991,9 @@ paths:
content:
application/json:
schema:
type: object
properties:
expr:
type: string
id:
type: string
example: "C8AgSFP8y91XUTpGtEQAQyjsSemxoY61ocGM852DFKF6"
nullable: true
fee:
type: number
example: 500000
nullable: true
feeAssetId:
type: string
example: null
nullable: true
sender:
type: string
example: "3Mds6m8XZf4biC72NRkFx2kyoBdC9UvYRUR"
nullable: true
senderPublicKey:
type: string
example: "3T9fL3XpeaHYbumohePPUmeuUqhyEeyzifi9vKouV8QoNtAT6kYV1oPAe9e2KCVquPcyXJpr2QwUiQKEUQPGZnc6"
nullable: true
payment:
type: array
items:
type: object
properties:
amount:
$ref: '#/components/schemas/Amount'
assetId:
allOf:
- $ref: '#/components/schemas/AssetId'
nullable: true
oneOf:
- $ref: '#/components/schemas/RideExprRequest'
- $ref: '#/components/schemas/RideInvocationRequest'
required: true
responses:
'200':
Expand Down Expand Up @@ -3358,6 +3326,13 @@ paths:
type: string
components:
schemas:
NonNegativeAmount:
oneOf:
- type: integer
format: int64
minimum: 0
- type: string
pattern: '^(0|[1-9]\d*)$'
Timestamp:
type: integer
format: int64
Expand Down Expand Up @@ -5364,6 +5339,118 @@ components:
type: array
items:
$ref: '#/components/schemas/InvokeActionLSF'
RideFunctionCallArg:
type: object
oneOf:
- properties:
type:
const: 'integer'
value:
type: integer
format: int64
- properties:
type:
const: 'boolean'
value:
type: boolean
- properties:
type:
const: 'string'
value:
type: string
- properties:
type:
const: 'binary'
value:
type: string
description: 'Evaluated Ride expression either as a Ride code, or as compiled in base64 representation'
- properties:
type:
const: 'list'
value:
type: array
items:
$ref: '#/components/schemas/RideFunctionCallArg'
BlockchainOverrides:
type: object
properties:
accounts:
type: object
description: 'Key is Address'
additionalProperties:
type: object
properties:
assetBalances:
type: object
description: 'Key is AssetId'
additionalProperties:
$ref: '#/components/schemas/NonNegativeAmount'
regularBalance:
$ref: '#/components/schemas/NonNegativeAmount'
example:
accounts:
'3P274YB5qseSE9DTTL3bpSjosZrYBPDpJ8k':
assetBalances:
'DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p': '141592653'
'34N9YcEETLWn93qYQ64EsP1x89tSruJU44RrEMSXXEPJ': 5897932
regularBalance: '3846264338327'
RideExprRequest:
type: object
properties:
expr:
type: string
example: "default()"
description: "Ride expression either as a Ride code, or as compiled in base64 representation"
state:
$ref: '#/components/schemas/BlockchainOverrides'
RideInvocationRequest:
type: object
properties:
call:
type: object
properties:
function:
type: string
default: "default"
nullable: true
args:
type: array
items:
$ref: '#/components/schemas/RideFunctionCallArg'
nullable: true
id:
type: string
example: "C8AgSFP8y91XUTpGtEQAQyjsSemxoY61ocGM852DFKF6"
nullable: true
fee:
type: number
example: 500000
nullable: true
feeAssetId:
type: string
example: null
nullable: true
sender:
type: string
example: "3Mds6m8XZf4biC72NRkFx2kyoBdC9UvYRUR"
nullable: true
senderPublicKey:
type: string
example: "3T9fL3XpeaHYbumohePPUmeuUqhyEeyzifi9vKouV8QoNtAT6kYV1oPAe9e2KCVquPcyXJpr2QwUiQKEUQPGZnc6"
nullable: true
payment:
type: array
items:
type: object
properties:
amount:
$ref: '#/components/schemas/Amount'
assetId:
allOf:
- $ref: '#/components/schemas/AssetId'
nullable: true
state:
$ref: '#/components/schemas/BlockchainOverrides'
responses:
Height:
description: Block height
Expand Down Expand Up @@ -5436,7 +5523,7 @@ components:
generation-signature: 'qwertyuikmn56khjkKHKJHK787'
blocksize: 11234
transactionCount: 10
features: [0]
features: [ 0 ]
reward: '30000'
rewardShares:
3MtmVzUQQj1keBsr3Pq5DYkutZzu5H8wdgA: '200000000'
Expand All @@ -5453,7 +5540,7 @@ components:
version: 2
sender: '3PGS9W5aZ4kdBFJJ9jxmgjLW8qfqu1b3Y66'
senderPublicKey: 'EAtFj2ycPFH6hzAadJ9UDbo6UGTbo4fenFbGiSDVYPU6'
proof: ['3n8tCEh9dxqvGrc3wuaKpoYX6mpXHfEeY7GFeAKpxZP7yJpqRiiSsyHoD9uCELnNoYBamnnrvY46WzKMVfSRVWGE']
proof: [ '3n8tCEh9dxqvGrc3wuaKpoYX6mpXHfEeY7GFeAKpxZP7yJpqRiiSsyHoD9uCELnNoYBamnnrvY46WzKMVfSRVWGE' ]
recipient: '3PDXpDCRkGz3jNMozjpbbrJeJZhKJHkoqjj'
assetId: null
feeAsset: null
Expand Down Expand Up @@ -5486,7 +5573,7 @@ components:
generation-signature: 'qwertyuikmn56khjkKHKJHK787'
blocksize: 11234
transactionCount: 10
features: [0]
features: [ 0 ]
reward: '30000'
rewardShares:
3MtmVzUQQj1keBsr3Pq5DYkutZzu5H8wdgA: '200000000'
Expand Down
2 changes: 1 addition & 1 deletion node/src/main/resources/swagger-ui/swagger-ui-bundle.js

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion node/src/main/resources/swagger-ui/swagger-ui.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion node/src/main/resources/swagger-ui/swagger-ui.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion node/src/main/resources/swagger-ui/swagger-ui.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion node/src/main/resources/swagger-ui/swagger-ui.js.map

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions node/src/main/scala/com/wavesplatform/api/http/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import akka.http.scaladsl.marshalling.ToResponseMarshallable
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.*
import akka.http.scaladsl.server.Directives.*
import cats.syntax.either.*
import com.typesafe.scalalogging.Logger
import com.wavesplatform.account.{Address, PublicKey}
import com.wavesplatform.api.http.ApiError.{InvalidAssetId, InvalidBlockId, InvalidPublicKey, InvalidSignature, InvalidTransactionId, WrongJson}
Expand All @@ -22,12 +23,24 @@ import play.api.libs.json.*

import java.util.concurrent.ExecutionException
import scala.concurrent.Future
import scala.reflect.ClassTag
import scala.util.control.NonFatal
import scala.util.{Failure, Success, Try}

package object http {
import ApiMarshallers.*

implicit def eitherReads[L, R](implicit leftReads: Reads[L], rightReads: Reads[R], leftCT: ClassTag[L], rightCT: ClassTag[R]): Reads[Either[L, R]] =
Reads { js =>
leftReads
.reads(js)
.map(_.asLeft[R])
.orElse {
rightReads.reads(js).map(_.asRight[L])
}
.orElse(JsError(s"Can't read JSON neither as ${leftCT.runtimeClass.getSimpleName}, nor ${rightCT.runtimeClass.getSimpleName}"))
}

val versionReads: Reads[Byte] = {
val defaultByteReads = implicitly[Reads[Byte]]
val intToByteReads = implicitly[Reads[Int]].map(_.toByte)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import cats.syntax.either.*
import com.wavesplatform.account.{Address, PublicKey}
import com.wavesplatform.api.http.*
import com.wavesplatform.api.http.ApiError.{CustomValidationError, ScriptCompilerError, ScriptExecutionError, TooBigArrayAllocation}
import com.wavesplatform.api.http.requests.{ScriptWithImportsRequest, byteStrFormat}
import com.wavesplatform.api.http.requests.ScriptWithImportsRequest
import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.common.utils.*
import com.wavesplatform.crypto
Expand All @@ -17,12 +17,11 @@ import com.wavesplatform.lang.script.Script.ComplexityInfo
import com.wavesplatform.lang.v1.ContractLimits
import com.wavesplatform.lang.v1.estimator.ScriptEstimator
import com.wavesplatform.lang.v1.evaluator.ContractEvaluator
import com.wavesplatform.lang.v1.serialization.SerdeV1
import com.wavesplatform.lang.{API, CompileResult, ValidationError}
import com.wavesplatform.serialization.ScriptValuesJson
import com.wavesplatform.settings.RestAPISettings
import com.wavesplatform.state.Blockchain
import com.wavesplatform.state.diffs.FeeValidation
import com.wavesplatform.state.{Blockchain, OverriddenBlockchain}
import com.wavesplatform.transaction.TxValidationError.{GenericError, InvokeRejectError}
import com.wavesplatform.transaction.smart.script.ScriptCompiler
import com.wavesplatform.transaction.smart.script.trace.TraceStep
Expand Down Expand Up @@ -267,23 +266,26 @@ case class UtilsApiRoute(
val script = scriptInfo.script
val limit = settings.evaluateScriptComplexityLimit

val evaluated = (request.value.get("expr"), request.asOpt[UtilsInvocationRequest]) match {
case (Some(_), Some(_)) if request.fields.size > 1 => Left(ConflictingRequestStructure)
case (None, None) => Left(WrongJson)
def withoutCommonFields = request - "state"
val evaluated = (request.asOpt[UtilsExprRequest], request.asOpt[UtilsInvocationRequest]) match {
case (Some(_), Some(_)) if withoutCommonFields.fields.size > 1 => Left(ConflictingRequestStructure)
case (None, None) => Left(WrongJson)
case (Some(exprRequest), _) =>
parseCall(exprRequest, script.stdLibVersion).flatMap(expr =>
UtilsEvaluator.executeExpression(blockchain, script, address, pk, limit)(
exprRequest.parseCall(script.stdLibVersion).flatMap { expr =>
val overridden = new OverriddenBlockchain(blockchain, exprRequest.state)
UtilsEvaluator.executeExpression(overridden, script, address, pk, limit)(
UtilsEvaluator.emptyInvokeScriptLike(address),
dApp => Right(ContractEvaluator.buildSyntheticCall(dApp, expr, ByteStr(DefaultAddress.bytes), DefaultPublicKey))
)
)
}
case (None, Some(invocationRequest)) =>
invocationRequest.toInvocation.flatMap(invocation =>
UtilsEvaluator.executeExpression(blockchain, script, address, pk, limit)(
invocationRequest.toInvocation.flatMap { invocation =>
val overridden = new OverriddenBlockchain(blockchain, invocationRequest.state)
UtilsEvaluator.executeExpression(overridden, script, address, pk, limit)(
UtilsEvaluator.toInvokeScriptLike(invocation, address),
ContractEvaluator.buildExprFromInvocation(_, invocation, script.stdLibVersion).bimap(e => GenericError(e.message), _.expr)
)
)
}
}

val apiResult = evaluated
Expand All @@ -296,7 +298,7 @@ case class UtilsApiRoute(
},
{ case (result, complexity, log, scriptResult) =>
val intAsString = accept.exists(_.mediaRanges.exists(CustomJson.acceptsNumbersAsStrings))
val traceObj = if (trace) Json.obj(TraceStep.logJson(log)) else Json.obj()
val traceObj = if (trace) Json.obj(TraceStep.logJson(log)) else Json.obj()
traceObj ++ Json.obj(
"result" -> ScriptValuesJson.serializeValue(result, intAsString),
"complexity" -> complexity,
Expand All @@ -308,20 +310,6 @@ case class UtilsApiRoute(
complete(apiResult ++ request ++ Json.obj("address" -> address.toString))
}

private def parseCall(js: JsReadable, version: StdLibVersion) = {
val binaryCall = js
.asOpt[ByteStr]
.toRight(GenericError("Unable to parse expr bytes"))
.flatMap(bytes => SerdeV1.deserialize(bytes.arr).bimap(GenericError(_), _._1))

val textCall = js
.asOpt[String]
.toRight(GenericError("Unable to read expr string"))
.flatMap(UtilsEvaluator.compile(version))

binaryCall.orElse(textCall)
}

private[this] val ScriptedAddress: PathMatcher1[Address] = AddrSegment.map {
case address: Address if blockchain.hasAccountScript(address) => address
case other => throw ApiException(CustomValidationError(s"Address $other is not dApp"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.wavesplatform.api.http.utils

import cats.syntax.either.*
import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.lang.directives.values.StdLibVersion
import com.wavesplatform.lang.v1.compiler.Terms
import com.wavesplatform.lang.v1.serialization.SerdeV1
import com.wavesplatform.state.BlockchainOverrides
import com.wavesplatform.transaction.TxValidationError.GenericError
import play.api.libs.json.*

case class UtilsExprRequest(
expr: Either[ByteStr, String],
state: BlockchainOverrides = BlockchainOverrides()
) {
def parseCall(version: StdLibVersion): Either[GenericError, Terms.EXPR] = expr match {
case Left(binaryCall) => SerdeV1.deserialize(binaryCall.arr).bimap(GenericError(_), _._1)
case Right(textCall) => UtilsEvaluator.compile(version)(textCall)
}
}

object UtilsExprRequest {
implicit val byteStrReads: Reads[ByteStr] = com.wavesplatform.utils.byteStrFormat
implicit val exprReads: Reads[Either[ByteStr, String]] = com.wavesplatform.api.http.eitherReads[ByteStr, String]
implicit val utilsExprRequestReads: Reads[UtilsExprRequest] = Json.using[Json.WithDefaultValues].reads[UtilsExprRequest]
}
Loading