LF: Contract ID suffix check in Preprocessor (#10642)

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
This commit is contained in:
Remy 2021-08-24 18:45:33 +02:00 committed by GitHub
parent 7b94b0674e
commit b22de6893b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 405 additions and 168 deletions

View File

@ -29,6 +29,7 @@ da_scala_library(
"//daml-lf/transaction",
"//daml-lf/validation",
"//libs-scala/nameof",
"@maven//:com_google_protobuf_protobuf_java",
],
)

View File

@ -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.
@ -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)
@ -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,
@ -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 {
@ -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()
def StableEngine(): Engine = new Engine(StableConfig)
def DevEngine(): Engine = new Engine(
StableConfig.copy(allowedLanguageVersions = LanguageVersion.DevVersions)
)
}

View File

@ -22,6 +22,10 @@ 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],
@ -29,6 +33,9 @@ final case class EngineConfig(
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 =

View File

@ -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
}

View File

@ -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]]

View File

@ -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,
) {
val valueTranslator = new ValueTranslator(interface, requiredCidSuffix)
import Preprocessor._
import compiledPackages.interface
val valueTranslator = new ValueTranslator(interface)
@throws[Error.Preprocessing.Error]
def unsafePreprocessCreate(
@ -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]
@ -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)

View File

@ -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],
@ -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.
@ -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)
}
}

View File

@ -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)
@ -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(

View File

@ -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._
@ -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]
@ -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) =>

View File

@ -39,8 +39,6 @@ import org.scalatest.EitherValues
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.Inside._
import scalaz.std.either._
import scalaz.syntax.apply._
import scala.collection.immutable.HashMap
import scala.language.implicitConversions
@ -158,10 +156,12 @@ class EngineTest
None
}
// TODO make these two per-test, so that we make sure not to pollute the package cache and other possibly mutable stuff
val engine = Engine.DevEngine()
val suffixLenientEngine = newEngine()
val suffixStrictEngine = newEngine(requireCidSuffixes = true)
val preprocessor =
new preprocessing.Preprocessor(ConcurrentCompiledPackages(engine.config.getCompilerConfig))
new preprocessing.Preprocessor(
ConcurrentCompiledPackages(suffixLenientEngine.config.getCompilerConfig)
)
"valid data variant identifier" should {
"found and return the argument types" in {
@ -437,7 +437,9 @@ class EngineTest
loadPackage("daml-lf/tests/Optional.dar")
val translator =
new preprocessing.Preprocessor(ConcurrentCompiledPackages(engine.config.getCompilerConfig))
new preprocessing.Preprocessor(
ConcurrentCompiledPackages(suffixLenientEngine.config.getCompilerConfig)
)
val id = Identifier(optionalPkgId, "Optional:Rec")
val someValue =
@ -463,7 +465,9 @@ class EngineTest
"returns correct error when resuming" in {
val translator =
new preprocessing.Preprocessor(ConcurrentCompiledPackages(engine.config.getCompilerConfig))
new preprocessing.Preprocessor(
ConcurrentCompiledPackages(suffixLenientEngine.config.getCompilerConfig)
)
val id = Identifier(basicTestsPkgId, "BasicTests:MyRec")
val wrongRecord =
ValueRecord(Some(id), ImmArray(Some[Name]("wrongLbl") -> ValueText("foo")))
@ -491,7 +495,7 @@ class EngineTest
.preprocessCommands(ImmArray(command))
.consume(lookupContract, lookupPackage, lookupKey)
res shouldBe a[Right[_, _]]
val interpretResult = engine
val interpretResult = suffixLenientEngine
.submit(
submitters,
readAs,
@ -507,18 +511,19 @@ class EngineTest
"reinterpret to the same result" in {
val Right((tx, txMeta)) = interpretResult
val stx = suffix(tx)
val Right((rtx, newMeta)) =
reinterpret(
engine,
suffixStrictEngine,
Set(party),
tx.roots,
tx,
stx.roots,
stx,
txMeta,
let,
lookupPackage,
)
isReplayedBy(tx, rtx) shouldBe Right(())
isReplayedBy(stx, rtx) shouldBe Right(())
txMeta.nodeSeeds shouldBe newMeta.nodeSeeds
}
@ -527,7 +532,7 @@ class EngineTest
val Right(submitter) = tx.guessSubmitter
val submitters = Set(submitter)
val ntx = SubmittedTransaction(Normalization.normalizeTx(tx))
val validated = engine
val validated = suffixLenientEngine
.validate(submitters, ntx, let, participant, meta.submissionTime, submissionSeed)
.consume(lookupContract, lookupPackage, lookupKey)
validated match {
@ -581,7 +586,7 @@ class EngineTest
.consume(lookupContract, lookupPackage, lookupKey)
withClue("Preprocessing result: ")(res shouldBe a[Right[_, _]])
engine
suffixLenientEngine
.submit(actAs, readAs, Commands(ImmArray(cmd), let, "test"), participant, submissionSeed)
.consume(lookupContract, lookupPackage, lookupKey)
}
@ -595,18 +600,19 @@ class EngineTest
"reinterpret to the same result" in {
forAll(cases) { case (templateId, signatories, submitters) =>
val Right((tx, txMeta)) = interpretResult(templateId, signatories, submitters)
val stx = suffix(tx)
val Right((rtx, _)) =
reinterpret(
engine,
suffixStrictEngine,
signatories.map(_._2),
tx.roots,
tx,
stx.roots,
stx,
txMeta,
let,
lookupPackage,
)
isReplayedBy(tx, rtx) shouldBe Right(())
isReplayedBy(stx, rtx) shouldBe Right(())
}
}
@ -614,7 +620,7 @@ class EngineTest
forAll(cases) { case (templateId, signatories, submitters) =>
val Right((tx, meta)) = interpretResult(templateId, signatories, submitters)
val ntx = SubmittedTransaction(Normalization.normalizeTx(tx))
val validated = engine
val validated = suffixLenientEngine
.validate(submitters, ntx, let, participant, meta.submissionTime, submissionSeed)
.consume(
lookupContract,
@ -634,7 +640,7 @@ class EngineTest
val Right((tx, _)) = interpretResult(templateId, signatories, submitters)
val replaySubmitters = submitters + party
val replayResult = engine.replay(
val replayResult = suffixLenientEngine.replay(
submitters = replaySubmitters,
tx = tx,
ledgerEffectiveTime = let,
@ -652,7 +658,7 @@ class EngineTest
val Right((tx, _)) = interpretResult(templateId, signatories, submitters)
val replaySubmitters = submitters.drop(1)
val replayResult = engine.replay(
val replayResult = suffixLenientEngine.replay(
submitters = replaySubmitters,
tx = tx,
ledgerEffectiveTime = let,
@ -685,7 +691,7 @@ class EngineTest
val interpretResult =
res
.flatMap { cmds =>
engine
suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -705,7 +711,7 @@ class EngineTest
val Right(submitter) = tx.guessSubmitter
"be translated" in {
val Right((rtx, _)) = engine
val Right((rtx, _)) = suffixLenientEngine
.submit(
Set(party),
readAs,
@ -722,23 +728,25 @@ class EngineTest
}
"reinterpret to the same result" in {
val stx = suffix(tx)
val Right((rtx, _)) =
reinterpret(
engine,
suffixStrictEngine,
Set(party),
tx.roots,
tx,
stx.roots,
stx,
txMeta,
let,
lookupPackage,
defaultContracts,
)
isReplayedBy(tx, rtx) shouldBe Right(())
isReplayedBy(stx, rtx) shouldBe Right(())
}
"be validated" in {
val ntx = SubmittedTransaction(Normalization.normalizeTx(tx))
val validated = engine
val validated = suffixLenientEngine
.validate(Set(submitter), ntx, let, participant, let, submissionSeed)
.consume(
lookupContract,
@ -772,7 +780,7 @@ class EngineTest
res shouldBe a[Right[_, _]]
"fail at submission" in {
val submitResult = engine
val submitResult = suffixLenientEngine
.submit(
submitters,
readAs,
@ -821,7 +829,7 @@ class EngineTest
val result =
res
.flatMap { cmds =>
engine
suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -840,7 +848,7 @@ class EngineTest
val Right((tx, txMeta)) = result
"be translated" in {
val submitResult = engine
val Right((rtx, _)) = suffixLenientEngine
.submit(
submitters,
readAs,
@ -848,40 +856,33 @@ class EngineTest
participant,
submissionSeed,
)
.consume(
lookupContract,
lookupPackage,
lookupKey,
)
.map(_._1)
(result.map(_._1) |@| submitResult)((tx, rtx) => isReplayedBy(tx, rtx)) shouldBe Right(
Right(())
)
.consume(lookupContract, lookupPackage, lookupKey)
isReplayedBy(tx, rtx) shouldBe Right(())
}
"reinterpret to the same result" in {
val stx = suffix(tx)
val reinterpretResult =
val Right((rtx, _)) =
reinterpret(
engine,
suffixStrictEngine,
Set(alice),
tx.roots,
tx,
stx.roots,
stx,
txMeta,
let,
lookupPackage,
defaultContracts,
defaultKey,
)
.map(_._1)
(result.map(_._1) |@| reinterpretResult)((tx, rtx) => isReplayedBy(tx, rtx)) shouldBe Right(
Right(())
)
isReplayedBy(stx, rtx) shouldBe Right(())
}
"be validated" in {
val ntx = SubmittedTransaction(Normalization.normalizeTx(tx))
val validated = engine
val validated = suffixLenientEngine
.validate(submitters, ntx, let, participant, let, submissionSeed)
.consume(
lookupContract,
@ -924,7 +925,7 @@ class EngineTest
)
val submitters = Set(alice)
val result = engine
val result = suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -962,7 +963,7 @@ class EngineTest
val submitters = Set(alice)
val result = engine
val result = suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -997,7 +998,7 @@ class EngineTest
val submitters = Set(alice)
val result = engine
val result = suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -1043,7 +1044,7 @@ class EngineTest
val submitters = Set(alice)
val result = engine
val result = suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -1098,7 +1099,7 @@ class EngineTest
val interpretResult =
res
.flatMap { cmds =>
engine
suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -1127,18 +1128,17 @@ class EngineTest
}
"reinterpret to the same result" in {
val stx = suffix(tx)
val reinterpretResult =
reinterpret(engine, Set(party), tx.roots, tx, txMeta, let, lookupPackage)
.map(_._1)
(interpretResult.map(_._1) |@| reinterpretResult)((tx, rtx) =>
isReplayedBy(tx, rtx)
) shouldBe Right(Right(()))
val Right((rtx, _)) =
reinterpret(suffixStrictEngine, Set(party), stx.roots, stx, txMeta, let, lookupPackage)
isReplayedBy(stx, rtx) shouldBe Right(())
}
"be validated" in {
val ntx = SubmittedTransaction(Normalization.normalizeTx(tx))
val validated = engine
val validated = suffixLenientEngine
.validate(Set(submitter), ntx, let, participant, let, submissionSeed)
.consume(
lookupContract,
@ -1363,7 +1363,7 @@ class EngineTest
val submitters = Set(bob)
val readAs = (Set.empty: Set[Party])
val Right((tx, txMeta)) = engine
val Right((tx, txMeta)) = suffixLenientEngine
.submit(
submitters,
readAs,
@ -1380,7 +1380,7 @@ class EngineTest
val Right(cmds) = preprocessor
.preprocessCommands(ImmArray(command))
.consume(lookupContract, lookupPackage, lookupKey)
val Right((rtx, _)) = engine
val Right((rtx, _)) = suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -1399,19 +1399,21 @@ class EngineTest
val blindingInfo = Blinding.blind(tx)
"reinterpret to the same result" in {
val stx = suffix(tx)
val Right((rtx, _)) =
reinterpret(
engine,
suffixStrictEngine,
Set(bob),
tx.transaction.roots,
tx,
stx.transaction.roots,
stx,
txMeta,
let,
lookupPackage,
defaultContracts,
)
isReplayedBy(rtx, tx) shouldBe Right(())
isReplayedBy(rtx, stx) shouldBe Right(())
}
"blinded correctly" in {
@ -1545,7 +1547,7 @@ class EngineTest
res
.flatMap { cmds =>
engine
suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -1586,7 +1588,7 @@ class EngineTest
val nid = NodeId(0) //we must use node-0 so the constructed tx is normalized
val fetchTx = VersionedTransaction(n.version, Map(nid -> n), ImmArray(nid))
val Right((reinterpreted, _)) =
engine
suffixLenientEngine
.reinterpret(
n.requiredAuthorizers,
FetchCommand(n.templateId, n.coid),
@ -1637,7 +1639,7 @@ class EngineTest
}
"succeed with a fresh engine, correctly compiling packages" in {
val engine = Engine.DevEngine()
val engine = newEngine()
val fetchNode = FetchCommand(
templateId = fetchedTid,
@ -1713,7 +1715,7 @@ class EngineTest
)
val submitters = Set(alice)
val readAs = (Set.empty: Set[Party])
val Right((tx, _)) = engine
val Right((tx, _)) = newEngine()
.submit(submitters, readAs, Commands(ImmArray(exerciseCmd), now, "test"), participant, seed)
.consume(
lookupContractMap.get,
@ -1739,7 +1741,7 @@ class EngineTest
val submitters = Set(alice)
val readAs = (Set.empty: Set[Party])
val Right((tx, txMeta)) = engine
val Right((tx, txMeta)) = suffixLenientEngine
.submit(submitters, readAs, Commands(ImmArray(exerciseCmd), now, "test"), participant, seed)
.consume(
lookupContractMap.get,
@ -1752,8 +1754,7 @@ class EngineTest
lookupNode.result shouldBe Some(lookedUpCid)
val Right((reinterpreted, _)) =
Engine
.DevEngine()
newEngine()
.reinterpret(
submitters,
LookupByKeyCommand(lookupNode.templateId, lookupNode.key.key),
@ -1780,7 +1781,7 @@ class EngineTest
val submitters = Set(alice)
val readAs = (Set.empty: Set[Party])
val Right((tx, txMeta)) = engine
val Right((tx, txMeta)) = suffixLenientEngine
.submit(submitters, readAs, Commands(ImmArray(exerciseCmd), now, "test"), participant, seed)
.consume(
lookupContractMap.get,
@ -1794,8 +1795,7 @@ class EngineTest
lookupNode.result shouldBe None
val Right((reinterpreted, _)) =
Engine
.DevEngine()
newEngine()
.reinterpret(
submitters,
LookupByKeyCommand(lookupNode.templateId, lookupNode.key.key),
@ -1822,7 +1822,7 @@ class EngineTest
val submitters = Set(alice)
val result = engine
val result = suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -1855,7 +1855,7 @@ class EngineTest
)
val submitters = Set(party)
val readAs = (Set.empty: Set[Party])
engine
suffixLenientEngine
.submit(
submitters,
readAs,
@ -1888,7 +1888,7 @@ class EngineTest
val submitters = Set(alice)
val Right((tx, _)) = engine
val Right((tx, _)) = suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -1961,7 +1961,7 @@ class EngineTest
lookupKey,
)
val Right((tx, _)) = engine
val Right((tx, _)) = suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -2026,7 +2026,7 @@ class EngineTest
val submitters = Set(alice)
val readAs = (Set.empty: Set[Party])
def run(cmds: ImmArray[ApiCommand]) =
engine
suffixLenientEngine
.submit(submitters, readAs, Commands(cmds, now, ""), participant, submissionSeed)
.consume(lookupContract, lookupPackage, lookupKey)
@ -2075,7 +2075,7 @@ class EngineTest
)
val submitters = Set(party)
val readAs = (Set.empty: Set[Party])
engine
suffixLenientEngine
.submit(
submitters,
readAs,
@ -2098,7 +2098,7 @@ class EngineTest
for {
submitter <- tx.guessSubmitter
ntx = SubmittedTransaction(Normalization.normalizeTx(tx))
res <- engine
res <- suffixLenientEngine
.validate(
Set(submitter),
ntx,
@ -2122,16 +2122,18 @@ class EngineTest
"be partially reinterpretable" in {
val Right((tx, txMeta)) = run(3)
val stx = suffix(tx)
val ImmArray(_, exeNode1) = tx.transaction.roots
val Node.NodeExercises(_, _, _, _, _, _, _, _, _, children, _, _, _, _) =
tx.transaction.nodes(exeNode1)
val nids = children.toSeq.take(2).toImmArray
reinterpret(
engine,
suffixStrictEngine,
Set(party),
nids,
tx,
stx,
txMeta,
let,
lookupPackage,
@ -2169,7 +2171,7 @@ class EngineTest
)
.consume(_ => None, lookupPackage, lookupKey)
val result = engine
val result = suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -2198,7 +2200,7 @@ class EngineTest
.preprocessCommands(ImmArray(CreateCommand(templateId, createArg)))
.consume(_ => None, lookupPackage, lookupKey)
val result = engine
val result = suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -2229,7 +2231,7 @@ class EngineTest
val Right(cmds) = preprocessor
.preprocessCommands(ImmArray(CreateCommand(templateId, createArg)))
.consume(_ => None, lookupPackage, lookupKey)
val result = engine
val result = suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -2257,12 +2259,14 @@ class EngineTest
EngineConfig(
allowedLanguageVersions = LV.DevVersions,
contractKeyUniqueness = ContractKeyUniquenessMode.Off,
requireSuffixedGlobalCids = true,
)
)
val uckEngine = new Engine(
EngineConfig(
allowedLanguageVersions = LV.DevVersions,
contractKeyUniqueness = ContractKeyUniquenessMode.On,
requireSuffixedGlobalCids = true,
)
)
val (multiKeysPkgId, _, allMultiKeysPkgs) = loadPackage("daml-lf/tests/MultiKeys.dar")
@ -2427,7 +2431,7 @@ class EngineTest
lookupPackage,
lookupKey,
)
engine
suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -2537,7 +2541,7 @@ class EngineTest
lookupPackage,
lookupKey,
)
engine
suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -2619,7 +2623,7 @@ class EngineTest
lookupPackage,
mockedKeyLookup,
)
val result = engine
val result = suffixLenientEngine
.interpretCommands(
validating = false,
submitters = submitters,
@ -2672,7 +2676,8 @@ class EngineTest
def engine(min: LV, max: LV) =
new Engine(
EngineConfig(
allowedLanguageVersions = VersionRange(min, max)
allowedLanguageVersions = VersionRange(min, max),
requireSuffixedGlobalCids = true,
)
)
@ -2718,6 +2723,14 @@ class EngineTest
object EngineTest {
private def engineConfig(requireCidSuffixes: Boolean) = EngineConfig(
allowedLanguageVersions = language.LanguageVersion.DevVersions,
requireSuffixedGlobalCids = requireCidSuffixes,
)
private def newEngine(requireCidSuffixes: Boolean = false) =
new Engine(engineConfig(requireCidSuffixes))
private implicit def qualifiedNameStr(s: String): QualifiedName =
QualifiedName.assertFromString(s)
@ -2751,6 +2764,13 @@ object EngineTest {
Validation.isReplayedBy(Normalization.normalizeTx(recorded), replayed)
}
private val dummySuffix = Bytes.assertFromString("00")
private def suffix(tx: Tx.Transaction) =
data.assertRight(tx.suffixCid(_ => dummySuffix))
// Mimics Canton reinterpreation
// requires a suffixed transaction.
private def reinterpret(
engine: Engine,
submitters: Set[Party],
@ -2811,7 +2831,8 @@ object EngineTest {
lookupPackages,
k => keys0.get(k.globalKey),
)
(tr1, meta1) = currentStep
(tr0, meta1) = currentStep
tr1 = suffix(tr0)
(contracts1, keys1) = tr1.transaction.fold((contracts0, keys0)) {
case (
(contracts, keys),

View File

@ -32,35 +32,71 @@ class PreprocessorSpec
private implicit def toName(s: String): Ref.Name = Ref.Name.assertFromString(s)
private[this] val recordCon =
Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Module:Record"))
Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Mod:Record"))
private[this] val recordRefCon =
Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Mod:RecordRef"))
private[this] val variantCon =
Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Module:Variant"))
Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Mod:Either"))
private[this] val enumCon =
Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Module:Enum"))
Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Mod:Enum"))
private[this] val tricky =
Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Module:Tricky"))
Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Mod:Tricky"))
private[this] val myListTyCons =
Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Module:MyList"))
Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Mod:MyList"))
private[this] val myNilCons = Ref.Name.assertFromString("MyNil")
private[this] val myConsCons = Ref.Name.assertFromString("MyCons")
private[this] val alice = Ref.Party.assertFromString("Alice")
private[this] val dummySuffix = Bytes.assertFromString("00")
private[this] val cidWithSuffix =
ContractId.V1(crypto.Hash.hashPrivateKey("cidWithSuffix"), dummySuffix)
private[this] val cidWithoutSuffix =
ContractId.V1(crypto.Hash.hashPrivateKey("cidWithoutSuffix"), Bytes.Empty)
private[this] val typ = t"ContractId Mod:Record"
val pkg =
private[this] def suffixValue(v: Value[ContractId]) =
data.assertRight(v.suffixCid(_ => dummySuffix))
lazy val pkg =
p"""
module Module {
module Mod {
record Record = { field : Int64 };
variant Variant = variant1 : Text | variant2 : Int64;
record @serializable Record = { field : Int64 };
variant @serializable Either (a: *) (b: *) = Left : a | Right : b;
enum Enum = value1 | value2;
record Tricky (b: * -> *) = { x : b Unit };
record MyCons = { head : Int64, tail: Module:MyList };
variant MyList = MyNil : Unit | MyCons: Module:MyCons ;
record MyCons = { head : Int64, tail: Mod:MyList };
variant MyList = MyNil : Unit | MyCons: Mod:MyCons ;
record @serializable RecordRef = { owner: Party, cid: (ContractId Mod:Record) };
val @noPartyLiterals toParties: Mod:RecordRef -> List Party =
\ (ref: Mod:RecordRef) -> Cons @Party [Mod:RecordRef {owner} ref] (Nil @Party);
template (this : RecordRef) = {
precondition True,
signatories Mod:toParties this,
observers Mod:toParties this,
agreement "Agreement",
choices {
choice Change (self) (newCid: ContractId Mod:Record) : ContractId Mod:RecordRef,
controllers Mod:toParties this,
observers Nil @Party
to create @Mod:RecordRef Mod:RecordRef { owner = Mod:RecordRef {owner} this, cid = newCid }
},
key @Party (Mod:RecordRef {owner} this) (\ (p: Party) -> Cons @Party [p] (Nil @Party))
};
}
"""
private[this] val compiledPackage = ConcurrentCompiledPackages()
assert(compiledPackage.addPackage(pkgId, pkg) == ResultDone.Unit)
private[this] val preprocessor = new Preprocessor(compiledPackage, requiredCidSuffix = true)
import preprocessor.{translateValue, preprocessCommand}
"translateValue" should {
val testCases = Table[Ast.Type, Value[ContractId], speedy.SValue](
@ -86,11 +122,7 @@ class PreprocessorSpec
),
// TNumeric(TNat(9)) ,
// ValueNumeric(Numeric.assertFromString("9.000000000")),
(
TParty,
ValueParty(Ref.Party.assertFromString("Alice")),
SParty(Ref.Party.assertFromString("Alice")),
),
(TParty, ValueParty(alice), SParty(alice)),
(
TContractId(Ast.TTyCon(recordCon)),
ValueContractId(ContractId.assertFromString("#contractId")),
@ -118,9 +150,9 @@ class PreprocessorSpec
SRecord(recordCon, ImmArray("field"), ArrayList(SInt64(33))),
),
(
Ast.TTyCon(variantCon),
ValueVariant(None, "variant1", ValueText("some test")),
SVariant(variantCon, "variant1", 0, SText("some test")),
TTyConApp(variantCon, ImmArray(TText, TInt64)),
ValueVariant(None, "Left", ValueText("some test")),
SVariant(variantCon, "Left", 0, SText("some test")),
),
(Ast.TTyCon(enumCon), ValueEnum(None, "value1"), SEnum(enumCon, "value1", 0)),
(
@ -130,11 +162,6 @@ class PreprocessorSpec
),
)
val compiledPackage = ConcurrentCompiledPackages()
assert(compiledPackage.addPackage(pkgId, pkg) == ResultDone.Unit)
val preprocessor = new Preprocessor(compiledPackage)
import preprocessor.translateValue
"succeeds on well type values" in {
forAll(testCases) { (typ, value, svalue) =>
translateValue(typ, value) shouldBe ResultDone(svalue)
@ -171,6 +198,123 @@ class PreprocessorSpec
err shouldBe Error.Preprocessing.ValueNesting(tooBig)
}
}
"reject non suffix Contract IDs" in {
val cidValueWithoutSuffix = ValueContractId(cidWithoutSuffix)
val testCases = Table[Ast.Type, Value[ContractId]](
("type" -> "value"),
t"ContractId Mod:Record" -> cidValueWithoutSuffix,
TList(typ) -> ValueList(FrontStack(cidValueWithoutSuffix)),
TTextMap(typ) -> ValueTextMap(SortedLookupList(Map("0" -> cidValueWithoutSuffix))),
TGenMap(TInt64, typ) -> ValueGenMap(ImmArray(ValueInt64(1) -> cidValueWithoutSuffix)),
TGenMap(typ, TInt64) -> ValueGenMap(ImmArray(cidValueWithoutSuffix -> ValueInt64(0))),
TOptional(typ) -> ValueOptional(Some(cidValueWithoutSuffix)),
Ast.TTyCon(recordRefCon) -> ValueRecord(
None,
ImmArray(None -> ValueParty(alice), None -> cidValueWithoutSuffix),
),
TTyConApp(variantCon, ImmArray(typ, TInt64)) -> ValueVariant(
None,
"Left",
cidValueWithoutSuffix,
),
)
forAll(testCases) { (typ, value) =>
translateValue(typ, suffixValue(value)) shouldBe a[ResultDone[_]]
translateValue(typ, value) shouldBe a[ResultError]
}
}
}
"preprocessCommand" should {
import command._
def suffixCid(cid: ContractId) =
cid match {
case ContractId.V1(discriminator, suffix) if suffix.isEmpty =>
ContractId.V1(discriminator, dummySuffix)
case otherwise =>
otherwise
}
def suffixCmd(cmd: ApiCommand) =
cmd match {
case CreateCommand(templateId, argument) =>
CreateCommand(templateId, suffixValue(argument))
case ExerciseCommand(templateId, contractId, choiceId, argument) =>
ExerciseCommand(templateId, suffixCid(contractId), choiceId, suffixValue(argument))
case ExerciseByKeyCommand(templateId, contractKey, choiceId, argument) =>
ExerciseByKeyCommand(templateId, contractKey, choiceId, suffixValue(argument))
case CreateAndExerciseCommand(templateId, createArgument, choiceId, choiceArgument) =>
CreateAndExerciseCommand(
templateId,
suffixValue(createArgument),
choiceId,
suffixValue(choiceArgument),
)
}
"reject non suffix Contract IDs" in {
val key = ValueParty(alice)
val payloadWithoutSuffix = ValueRecord(
None,
ImmArray(None -> ValueParty(alice), None -> ValueContractId(cidWithoutSuffix)),
)
val payloadWithSuffix = ValueRecord(
None,
ImmArray(None -> ValueParty(alice), None -> ValueContractId(cidWithSuffix)),
)
val testCases = Table[ApiCommand](
"command",
CreateCommand(
recordRefCon,
payloadWithoutSuffix,
),
ExerciseCommand(
recordRefCon,
cidWithSuffix,
"Change",
ValueContractId(cidWithoutSuffix),
),
ExerciseCommand(
recordRefCon,
cidWithoutSuffix,
"Change",
ValueContractId(cidWithSuffix),
),
CreateAndExerciseCommand(
recordRefCon,
payloadWithoutSuffix,
"Change",
ValueContractId(cidWithSuffix),
),
CreateAndExerciseCommand(
recordRefCon,
payloadWithSuffix,
"Change",
ValueContractId(cidWithoutSuffix),
),
ExerciseByKeyCommand(
recordRefCon,
key,
"Change",
ValueContractId(cidWithoutSuffix),
),
)
forAll(testCases) { cmd =>
preprocessCommand(suffixCmd(cmd)) shouldBe a[ResultDone[_]]
preprocessCommand(cmd) shouldBe a[ResultError]
}
}
}
}

View File

@ -69,7 +69,12 @@ class ReinterpretTest
val lookupPackage = allPackages.get(_)
val lookupKey = { _: GlobalKeyWithMaintainers => None }
private val engine = Engine.DevEngine()
private val engine = new Engine(
EngineConfig(
allowedLanguageVersions = language.LanguageVersion.DevVersions,
requireSuffixedGlobalCids = true,
)
)
def Top(xs: Shape*) = Shape.Top(xs.toList)
def Exercise(xs: Shape*) = Shape.Exercise(xs.toList)

View File

@ -675,7 +675,8 @@ object Converter {
} catch {
case e: Exception => Left(s"LF conversion failed: ${e.toString}")
}
valueTranslator = new preprocessing.ValueTranslator(compiledPackages.interface)
valueTranslator =
new preprocessing.ValueTranslator(compiledPackages.interface, requiredCidSuffix = false)
sValue <- valueTranslator
.translateValue(ty, lfValue)
.left

View File

@ -64,7 +64,8 @@ object ScriptF {
) {
def clients = _clients
def compiledPackages = machine.compiledPackages
val valueTranslator = new ValueTranslator(compiledPackages.interface)
val valueTranslator =
new ValueTranslator(interface = compiledPackages.interface, requiredCidSuffix = false)
val utcClock = Clock.systemUTC()
def addPartyParticipantMapping(party: Party, participant: Participant) = {
_clients =

View File

@ -46,7 +46,8 @@ class IdeLedgerClient(
def currentSubmission: Option[ScenarioRunner.CurrentSubmission] = _currentSubmission
private[this] val preprocessor = new engine.preprocessing.CommandPreprocessor(compiledPackages)
private[this] val preprocessor =
new preprocessing.CommandPreprocessor(compiledPackages.interface, requiredCidSuffix = false)
private var _ledger: ScenarioLedger = ScenarioLedger.initialLedger(Time.Timestamp.Epoch)
def ledger: ScenarioLedger = _ledger

View File

@ -18,7 +18,11 @@ class SpeedyToValueBenchmark extends BenchmarkWithLedgerExport {
override def setup(): Unit = {
super.setup()
val decodedValues = submissions.values.map(_.mapValue(assertDecode)).toVector
val translator = new ValueTranslator(submissions.compiledPackages.interface)
val translator =
new ValueTranslator(
interface = submissions.compiledPackages.interface,
requiredCidSuffix = false,
)
speedyValues = decodedValues.map(x => assertTranslate(translator)(x.mapValue(_.value)))
}

View File

@ -19,7 +19,8 @@ class ValueTranslatorBenchmark extends BenchmarkWithLedgerExport {
override def setup(): Unit = {
super.setup()
decodedValues = submissions.values.map(_.mapValue(assertDecode).mapValue(_.value)).toVector
translator = new ValueTranslator(submissions.compiledPackages.interface)
translator =
new ValueTranslator(submissions.compiledPackages.interface, requiredCidSuffix = false)
}
@Benchmark

View File

@ -498,7 +498,8 @@ object Converter {
}
def apply(compiledPackages: CompiledPackages, triggerIds: TriggerIds): Converter = {
val valueTranslator = new preprocessing.ValueTranslator(compiledPackages.interface)
val valueTranslator =
new preprocessing.ValueTranslator(compiledPackages.interface, requiredCidSuffix = false)
Converter(
fromTransaction(valueTranslator, triggerIds, _),
fromCompletion(triggerIds, _),