-
Notifications
You must be signed in to change notification settings - Fork 5
Offline evaluator to use in NewmChainService
#701
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
Open
sae3023
wants to merge
4
commits into
projectNEWM:master
Choose a base branch
from
sae3023:offline-evaluator
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
186 changes: 186 additions & 0 deletions
186
newm-tx-builder/src/main/kotlin/io/newm/txbuilder/ScalusEvaluator.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| package io.newm.txbuilder | ||
|
|
||
| import io.newm.chain.grpc.Utxo | ||
| import io.newm.chain.util.config.Config | ||
| import io.newm.kogmios.protocols.model.ExecutionUnits | ||
| import io.newm.kogmios.protocols.model.Validator | ||
| import io.newm.kogmios.protocols.model.result.EvaluateTx | ||
| import io.newm.kogmios.protocols.model.result.EvaluateTxResult | ||
| import io.newm.kogmios.protocols.model.result.ProtocolParametersResult | ||
| import scalus.cardano.ledger.Redeemer | ||
| import scalus.cardano.ledger.RedeemerTag | ||
| import scalus.cardano.ledger.SlotConfig | ||
| import java.math.BigInteger | ||
| import scala.Option | ||
| import scala.Tuple2 | ||
| import scalus.builtin.ByteString | ||
| import scalus.cardano.address.Address | ||
| import scalus.cardano.ledger.CardanoInfo | ||
| import scalus.cardano.ledger.Coin | ||
| import scalus.cardano.ledger.EvaluatorMode | ||
| import scalus.cardano.ledger.ExUnits | ||
| import scalus.cardano.ledger.MajorProtocolVersion | ||
| import scalus.builtin.Data | ||
| import scalus.cardano.ledger.DatumOption | ||
| import scalus.cardano.ledger.MultiAsset | ||
| import scalus.cardano.ledger.PlutusScriptEvaluator | ||
| import scalus.cardano.ledger.ProtocolVersion | ||
| import scalus.cardano.ledger.Transaction | ||
| import scalus.cardano.ledger.TransactionInput | ||
| import scalus.cardano.ledger.TransactionOutput | ||
| import scalus.cardano.ledger.Value | ||
| import io.newm.chain.util.hexToByteArray | ||
|
|
||
| import scalus.cardano.ledger.`Hashes$package$` | ||
|
|
||
| /** | ||
| * A script evaluator that uses Scalus' local CEK machine implementation, allowing to get tx redeemers without | ||
| * using the network. | ||
| */ | ||
| object ScalusEvaluator { | ||
| /** | ||
| * Evaluates a transaction's Plutus scripts and returns execution units. | ||
| * | ||
| * @param cborBytes The transaction CBOR bytes | ||
| * @param utxos All UTxOs needed for evaluation (source + reference inputs) | ||
| * @param protocolParameters Protocol parameters containing cost models and major protocol version | ||
| * @param config Configuration object to determine network (mainnet vs testnet) | ||
| */ | ||
| fun evaluateTx( | ||
| cborBytes: ByteArray, | ||
| utxos: Set<Utxo>, | ||
| protocolParameters: ProtocolParametersResult, | ||
| config: Config, | ||
| ): EvaluateTxResult { | ||
| val slotConfig = if (config.isMainnet) SlotConfig.Mainnet() else SlotConfig.Preprod() | ||
| val costModels = convertCostModels(protocolParameters) | ||
|
|
||
| val initialBudget = ExUnits( | ||
| protocolParameters.maxExecutionUnitsPerTransaction.memory.toLong(), | ||
| protocolParameters.maxExecutionUnitsPerTransaction.cpu.toLong() | ||
| ) | ||
|
|
||
| val evaluator = PlutusScriptEvaluator.apply( | ||
| slotConfig, | ||
| initialBudget, | ||
| MajorProtocolVersion(protocolParameters.version.major), | ||
| costModels, | ||
| EvaluatorMode.EvaluateAndComputeCost, | ||
| false, // debugDumpFilesForTesting | ||
| false // logBudgetDifferences | ||
| ) | ||
|
|
||
| val tx = Transaction.fromCbor(cborBytes, ProtocolVersion.conwayPV()) | ||
| val scalusUtxos = convertToScalusUtxos(utxos) | ||
|
|
||
| val evaluatedRedeemers = evaluator.evalPlutusScripts(tx, scalusUtxos) | ||
| return convertToEvaluateTxResult(evaluatedRedeemers) | ||
| } | ||
|
|
||
| private fun convertCostModels(protocolParameters: ProtocolParametersResult): scalus.cardano.ledger.CostModels { | ||
| val kogmiosCostModels = protocolParameters.plutusCostModels | ||
| val tuples = mutableListOf<scala.Tuple2<Int, scala.collection.immutable.IndexedSeq<Long>>>() | ||
|
|
||
| // PlutusV1 = language 0 | ||
| kogmiosCostModels.plutusV1?.let { v1 -> | ||
| tuples.add(scala.Tuple2(0, convertCostModelArray(v1))) | ||
| } | ||
|
|
||
| // PlutusV2 = language 1 | ||
| kogmiosCostModels.plutusV2?.let { v2 -> | ||
| tuples.add(scala.Tuple2(1, convertCostModelArray(v2))) | ||
| } | ||
|
|
||
| // PlutusV3 = language 2 | ||
| kogmiosCostModels.plutusV3?.let { v3 -> | ||
| tuples.add(scala.Tuple2(2, convertCostModelArray(v3))) | ||
| } | ||
|
|
||
| // Convert to Scala immutable.Map | ||
| val scalaSeq = scala.jdk.javaapi.CollectionConverters.asScala(tuples).toSeq() | ||
| @Suppress("UNCHECKED_CAST") | ||
| val scalaMap = scala.collection.immutable.Map.from(scalaSeq) as scala.collection.immutable.Map<Any, scala.collection.immutable.IndexedSeq<Any>> | ||
|
|
||
| return scalus.cardano.ledger.CostModels(scalaMap) | ||
| } | ||
|
|
||
| private fun convertCostModelArray(costModel: List<BigInteger>): scala.collection.immutable.IndexedSeq<Long> { | ||
| val longArray = costModel.map { it.toLong() } | ||
| return scala.jdk.javaapi.CollectionConverters.asScala(longArray).toIndexedSeq() | ||
| } | ||
|
|
||
| private fun convertToEvaluateTxResult(evaluatedRedeemers: scala.collection.immutable.Seq<Redeemer>): EvaluateTxResult { | ||
| val result = EvaluateTxResult() | ||
| val iterator = evaluatedRedeemers.iterator() | ||
| while (iterator.hasNext()) { | ||
| val redeemer = iterator.next() | ||
|
|
||
| val tag = redeemer.tag() | ||
| val purpose = | ||
| when (tag) { | ||
| RedeemerTag.valueOf("Spend") -> "spend" | ||
| RedeemerTag.valueOf("Mint") -> "mint" | ||
| RedeemerTag.valueOf("Cert") -> "certificate" | ||
| RedeemerTag.valueOf("Reward") -> "withdrawal" | ||
| RedeemerTag.valueOf("Voting") -> "vote" | ||
| RedeemerTag.valueOf("Proposing") -> "propose" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is |
||
| else -> throw IllegalStateException("Unknown redeemer tag: $tag") | ||
| } | ||
|
|
||
| val validator = Validator(redeemer.index().toInt(), purpose) | ||
| val executionUnits = | ||
| ExecutionUnits( | ||
| memory = BigInteger.valueOf(redeemer.exUnits().memory()), | ||
| cpu = BigInteger.valueOf(redeemer.exUnits().steps()) | ||
| ) | ||
|
|
||
| result.add(EvaluateTx(validator, executionUnits)) | ||
| } | ||
|
|
||
| return result | ||
| } | ||
|
|
||
| private fun convertToScalusUtxos(utxos: Set<Utxo>): scala.collection.immutable.Map<TransactionInput, TransactionOutput> { | ||
| val entries = mutableListOf<Tuple2<TransactionInput, TransactionOutput>>() | ||
|
|
||
| // Map the utxo to Scalus ledger types. | ||
| utxos.forEach { utxo -> | ||
| val txHash = `Hashes$package$`.TransactionHash.fromHex(utxo.hash) | ||
| val txInput = TransactionInput.apply(txHash, utxo.ix.toInt()) | ||
|
|
||
| val address = Address.fromBech32(utxo.address) | ||
|
|
||
| val lovelace = Coin.apply(utxo.lovelace.toLong()) | ||
| val value = Value.apply(lovelace, MultiAsset.empty()) | ||
|
|
||
| val datumOption: Option<DatumOption> = | ||
| when { | ||
| utxo.hasDatum() && utxo.isInlineDatum -> { | ||
| // Inline datum - parse from CBOR hex | ||
| val datumBytes = utxo.datum.cborHex.hexToByteArray() | ||
| val datumData = Data.fromCbor(datumBytes) | ||
| Option.apply(DatumOption.Inline.apply(datumData)) | ||
| } | ||
|
|
||
| !utxo.datumHash.isNullOrEmpty() -> { | ||
| val datumHash = `Hashes$package$`.DataHash.fromHex(utxo.datumHash) | ||
| Option.apply(DatumOption.Hash.apply(datumHash)) | ||
| } | ||
|
|
||
| else -> { | ||
| Option.empty<DatumOption>() | ||
| } | ||
| } | ||
|
|
||
| val txOutput = TransactionOutput.apply(address, value, datumOption, Option.empty()) | ||
|
|
||
| entries.add(Tuple2(txInput, txOutput)) | ||
| } | ||
|
|
||
| val scalaBuffer = scala.jdk.javaapi.CollectionConverters | ||
| .asScala(entries.toList()) | ||
| val scalaSeq = scalaBuffer.toSeq() | ||
| return scala.collection.immutable.Map | ||
| .from(scalaSeq) | ||
| } | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where do the plutus cost models come into play here? Are they hardcoded somewhere in CardanoInfo? If so, this seems brittle and we should get the plutus cost model from the blockchain instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, the
CostModelsare inProtocolParameters, which are inCardanoInfo.The evaluator itself needs only the slot config, the major protocol version and the cost models, so if the following signature is fine:
Both of these types seem to be very straightforward to map to Scalus ledger types
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if this looks fine, I'd appreciate a hint of where these types are described in newm, and I'll ping you when I fix the function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sae3023 In TransactionBuilder.kt, it is constructed with a
protocolParametersResult. Inside this structure is theplutusCostModels. All of this data currently comes from Ogmios through Kogmios which is our Ogmios gRPC wrapper in Kotlin. We're not trying to entirely eliminate Ogmios with this fix, just utilize Scalus where it makes sense for efficiency.