Skip to content

Commit

Permalink
LF: Contract ID suffix check in Preprocessor (#10642)
Browse files Browse the repository at this point in the history
This PR makes possible to check for contract IDs suffix during
preprocessing.

This is the first part of the task 3 described in #10504.

CHANGELOG_BEGIN
CHANGELOG_END
  • Loading branch information
remyhaemmerle-da authored Aug 24, 2021
1 parent 7b94b06 commit b22de68
Show file tree
Hide file tree
Showing 18 changed files with 405 additions and 168 deletions.
1 change: 1 addition & 0 deletions daml-lf/engine/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ da_scala_library(
"//daml-lf/transaction",
"//daml-lf/validation",
"//libs-scala/nameof",
"@maven//:com_google_protobuf_protobuf_java",
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,10 @@ import com.daml.lf.speedy.Speedy.Machine
import com.daml.lf.speedy.SResult._
import com.daml.lf.transaction.{SubmittedTransaction, Transaction => Tx}
import com.daml.lf.transaction.Node._
import com.daml.lf.value.Value
import java.nio.file.Files

import com.daml.lf.language.{Interface, LanguageVersion, LookupError, StablePackages}
import com.daml.lf.validation.Validation
import com.daml.lf.value.Value.ContractId
import com.daml.nameof.NameOf

/** Allows for evaluating [[Commands]] and validating [[Transaction]]s.
Expand Down Expand Up @@ -50,13 +48,17 @@ import com.daml.nameof.NameOf
*
* This class is thread safe as long `nextRandomInt` is.
*/
class Engine(val config: EngineConfig = new EngineConfig(LanguageVersion.StableVersions)) {
class Engine(val config: EngineConfig = Engine.StableConfig) {

config.profileDir.foreach(Files.createDirectories(_))

private[this] val compiledPackages = ConcurrentCompiledPackages(config.getCompilerConfig)

private[this] val preprocessor = new preprocessing.Preprocessor(compiledPackages)
private[engine] val preprocessor =
new preprocessing.Preprocessor(
compiledPackages = compiledPackages,
requiredCidSuffix = config.requireSuffixedGlobalCids,
)

def info = new EngineInfo(config)

Expand Down Expand Up @@ -169,7 +171,7 @@ class Engine(val config: EngineConfig = new EngineConfig(LanguageVersion.StableV
submissionSeed: crypto.Hash,
): Result[(SubmittedTransaction, Tx.Metadata)] =
for {
commands <- preprocessor.translateTransactionRoots(tx.transaction)
commands <- preprocessor.translateTransactionRoots(tx)
result <- interpretCommands(
validating = true,
submitters = submitters,
Expand Down Expand Up @@ -466,9 +468,6 @@ class Engine(val config: EngineConfig = new EngineConfig(LanguageVersion.StableV
} yield ()
}

private[engine] def enrich(typ: Type, value: Value[ContractId]): Result[Value[ContractId]] =
preprocessor.translateValue(typ, value).map(_.toValue)

}

object Engine {
Expand Down Expand Up @@ -501,8 +500,13 @@ object Engine {
}
}

def DevEngine(): Engine = new Engine(new EngineConfig(LanguageVersion.DevVersions))
private def StableConfig =
EngineConfig(allowedLanguageVersions = LanguageVersion.StableVersions)

def StableEngine(): Engine = new Engine(StableConfig)

def StableEngine(): Engine = new Engine()
def DevEngine(): Engine = new Engine(
StableConfig.copy(allowedLanguageVersions = LanguageVersion.DevVersions)
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,20 @@ import com.daml.lf.transaction.ContractKeyUniquenessMode
* @param profileDir The optional specifies the directory where to
* save the output of the Daml scenario profiler. The profiler is
* disabled if the option is empty.
* @param requireSuffixedGlobalCids Since August 2018 we expect new
* ledgers to suffix CIDs before committing a transaction.
* This option should be disable for backward compatibility in ledger
* that do not (i.e. Sandboxes, KV, Corda).
*/
final case class EngineConfig(
allowedLanguageVersions: VersionRange[language.LanguageVersion],
packageValidation: Boolean = true,
stackTraceMode: Boolean = false,
profileDir: Option[Path] = None,
contractKeyUniqueness: ContractKeyUniquenessMode = ContractKeyUniquenessMode.On,
// TODO: https://github.com/digital-asset/daml/issues/10504
// switch the default to true
requireSuffixedGlobalCids: Boolean = false,
) {

private[lf] def getCompilerConfig: speedy.Compiler.Config =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ object Error {
s"Provided value exceeds maximum nesting level of ${Value.MAXIMUM_NESTING}"
}

final case class NonSuffixedCid(cid: Value.ContractId.V1) extends Error {
override def message: String =
s"Provided Contract ID $cid is not suffixed"
}

final case class RootNode(nodeId: NodeId, override val message: String) extends Error

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import com.daml.lf.value.Value.ContractId
final class ValueEnricher(engine: Engine) {

def enrichValue(typ: Ast.Type, value: Value[ContractId]): Result[Value[ContractId]] =
engine.enrich(typ, value)
engine.preprocessor.translateValue(typ, value).map(_.toValue)

def enrichContract(
contract: Value.ContractInst[Value[ContractId]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ package preprocessing

import com.daml.lf.data._
import com.daml.lf.language.Ast
import com.daml.lf.speedy.SValue
import com.daml.lf.value.Value

import scala.annotation.tailrec

private[lf] final class CommandPreprocessor(compiledPackages: CompiledPackages) {
private[lf] final class CommandPreprocessor(
interface: language.Interface,
requiredCidSuffix: Boolean,
) {

import Preprocessor._
import compiledPackages.interface
val valueTranslator = new ValueTranslator(interface, requiredCidSuffix)

val valueTranslator = new ValueTranslator(interface)
import Preprocessor._

@throws[Error.Preprocessing.Error]
def unsafePreprocessCreate(
Expand All @@ -35,9 +36,10 @@ private[lf] final class CommandPreprocessor(compiledPackages: CompiledPackages)
choiceId: Ref.ChoiceName,
argument: Value[Value.ContractId],
): speedy.Command.Exercise = {
val cid = valueTranslator.unsafeTranslateCid(contractId)
val choice = handleLookup(interface.lookupChoice(templateId, choiceId)).argBinder._2
val arg = valueTranslator.unsafeTranslateValue(choice, argument)
speedy.Command.Exercise(templateId, SValue.SContractId(contractId), choiceId, arg)
speedy.Command.Exercise(templateId, cid, choiceId, arg)
}

@throws[Error.Preprocessing.Error]
Expand Down Expand Up @@ -109,7 +111,8 @@ private[lf] final class CommandPreprocessor(compiledPackages: CompiledPackages)
choiceArgument,
)
case command.FetchCommand(templateId, coid) =>
speedy.Command.Fetch(templateId, SValue.SContractId(coid))
val cid = valueTranslator.unsafeTranslateCid(coid)
speedy.Command.Fetch(templateId, cid)
case command.FetchByKeyCommand(templateId, key) =>
val ckTtype = handleLookup(interface.lookupTemplateKey(templateId)).typ
val sKey = valueTranslator.unsafeTranslateValue(ckTtype, key)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,47 @@ package engine
package preprocessing

import java.util

import com.daml.lf.data.{ImmArray, Ref}
import com.daml.lf.language.{Ast, LookupError}
import com.daml.lf.speedy.SValue
import com.daml.lf.transaction.{GenTransaction, NodeId}
import com.daml.lf.transaction.SubmittedTransaction
import com.daml.lf.value.Value
import com.daml.nameof.NameOf

import scala.annotation.tailrec

private[engine] final class Preprocessor(compiledPackages: MutableCompiledPackages) {
/** The Command Preprocessor is responsible of the following tasks:
* - normalizes value representation (e.g. resolves missing type
* reference in record/variant/enumeration, infers missing labeled
* record fields, orders labeled record fields, ...);
* - checks value nesting does not overpass 100;
* - checks a LF command/value is properly typed according the
* Daml-LF package definitions;
* - checks for Contract ID suffix (see [[requiredCidSuffix]]);
* - translates a LF command/value into speedy command/value; and
* - translates a complete transaction into a list of speedy
* commands.
*
* @param compiledPackages a [[MutableCompiledPackages]] contains the
* Daml-LF package definitions against the command should
* resolved/typechecked. It is updated dynamically each time the
* [[ResultNeedPackage]] continuation is called.
* @param requiredCidSuffix when `true` the preprocessor will reject
* any value/command/transaction that contains V1 Contract IDs
* without suffixed.
*/
private[engine] final class Preprocessor(
compiledPackages: MutableCompiledPackages,
requiredCidSuffix: Boolean = true,
) {

import Preprocessor._
val transactionPreprocessor = new TransactionPreprocessor(compiledPackages)
import transactionPreprocessor._
import commandPreprocessor._
import valueTranslator.unsafeTranslateValue

import compiledPackages.interface

val commandPreprocessor = new CommandPreprocessor(interface, requiredCidSuffix)
val transactionPreprocessor = new TransactionPreprocessor(commandPreprocessor)

// This pulls all the dependencies of in `typesToProcess0` and `tyConAlreadySeen0`
private def getDependencies(
typesToProcess0: List[Ast.Type],
Expand Down Expand Up @@ -127,14 +149,14 @@ private[engine] final class Preprocessor(compiledPackages: MutableCompiledPackag
*/
def translateValue(ty0: Ast.Type, v0: Value[Value.ContractId]): Result[SValue] =
safelyRun(getDependencies(List(ty0), List.empty)) {
unsafeTranslateValue(ty0, v0)
commandPreprocessor.valueTranslator.unsafeTranslateValue(ty0, v0)
}

private[engine] def preprocessCommand(
cmd: command.Command
): Result[speedy.Command] =
safelyRun(getDependencies(List.empty, List(cmd.templateId))) {
unsafePreprocessCommand(cmd)
commandPreprocessor.unsafePreprocessCommand(cmd)
}

/** Translates LF commands to a speedy commands.
Expand All @@ -143,16 +165,17 @@ private[engine] final class Preprocessor(compiledPackages: MutableCompiledPackag
cmds: data.ImmArray[command.ApiCommand]
): Result[ImmArray[speedy.Command]] =
safelyRun(getDependencies(List.empty, cmds.map(_.templateId).toList)) {
unsafePreprocessCommands(cmds)
commandPreprocessor.unsafePreprocessCommands(cmds)
}

def translateTransactionRoots[Cid <: Value.ContractId](
tx: GenTransaction[NodeId, Cid]
/** Translates a complete transaction. Assumes no contract ID suffixes are used */
def translateTransactionRoots(
tx: SubmittedTransaction
): Result[ImmArray[speedy.Command]] =
safelyRun(
getDependencies(List.empty, tx.rootNodes.toList.map(_.templateId))
) {
unsafeTranslateTransactionRoots(tx)
transactionPreprocessor.unsafeTranslateTransactionRoots(tx)
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@ package engine
package preprocessing

import com.daml.lf.data.{BackStack, ImmArray}
import com.daml.lf.transaction.{GenTransaction, Node, NodeId}
import com.daml.lf.value.Value
import com.daml.lf.transaction.{Node, NodeId, SubmittedTransaction}
import com.daml.lf.value.Value.ContractId

private[preprocessing] final class TransactionPreprocessor(
compiledPackages: MutableCompiledPackages
commandPreprocessor: CommandPreprocessor
) {

val commandPreprocessor = new CommandPreprocessor(compiledPackages)

private[this] def invalidRootNode(nodeId: NodeId, message: String) =
throw Error.Preprocessing.RootNode(nodeId, message)

Expand Down Expand Up @@ -65,18 +63,18 @@ private[preprocessing] final class TransactionPreprocessor(
* for more details.
*/
@throws[Error.Preprocessing.Error]
def unsafeTranslateTransactionRoots[Cid <: Value.ContractId](
tx: GenTransaction[NodeId, Cid]
def unsafeTranslateTransactionRoots(
tx: SubmittedTransaction
): ImmArray[speedy.Command] = {

val result = tx.roots.foldLeft(BackStack.empty[speedy.Command]) { (acc, id) =>
tx.nodes.get(id) match {
case Some(node: Node.GenActionNode[_, Cid]) =>
case Some(node: Node.GenActionNode[_, ContractId]) =>
node match {
case create: Node.NodeCreate[Cid] =>
case create: Node.NodeCreate[ContractId] =>
val cmd = commandPreprocessor.unsafePreprocessCreate(create.templateId, create.arg)
acc :+ cmd
case exe: Node.NodeExercises[_, Cid] =>
case exe: Node.NodeExercises[_, ContractId] =>
val cmd = exe.key match {
case Some(key) if exe.byKey =>
commandPreprocessor.unsafePreprocessExerciseByKey(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import com.daml.lf.value.Value._

import scala.annotation.tailrec

private[engine] final class ValueTranslator(interface: language.Interface) {
private[engine] final class ValueTranslator(
interface: language.Interface,
// See Preprocessor.requiredCidSuffix for more details about the following flag.
requiredCidSuffix: Boolean,
) {

import Preprocessor._

Expand All @@ -38,6 +42,19 @@ private[engine] final class ValueTranslator(interface: language.Interface) {
go(fields, Map.empty)
}

private[preprocessing] val unsafeTranslateCid: ContractId => SValue.SContractId =
if (requiredCidSuffix) {
case cid1: ContractId.V1 =>
if (cid1.suffix.isEmpty)
throw Error.Preprocessing.NonSuffixedCid(cid1)
else
SValue.SContractId(cid1)
case cid0: ContractId.V0 =>
SValue.SContractId(cid0)
}
else
SValue.SContractId

// For efficient reason we do not produce here the monad Result[SValue] but rather throw
// exception in case of error or package missing.
@throws[Error.Preprocessing.Error]
Expand Down Expand Up @@ -89,7 +106,7 @@ private[engine] final class ValueTranslator(interface: language.Interface) {
typeError()
}
case (BTContractId, ValueContractId(c)) =>
SValue.SContractId(c)
unsafeTranslateCid(c)
case (BTOptional, ValueOptional(mbValue)) =>
mbValue match {
case Some(v) =>
Expand Down
Loading

0 comments on commit b22de68

Please sign in to comment.