LF: provide methods to enrich LF values missing type and field names (#8493)

CHANGELOG_BEGIN
CHANGELOG_END
This commit is contained in:
Remy 2021-01-13 19:02:51 +01:00 committed by GitHub
parent 89bc670445
commit 77bec01db3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 413 additions and 10 deletions

View File

@ -19,6 +19,7 @@ import java.nio.file.Files
import com.daml.lf.language.LanguageVersion
import com.daml.lf.validation.Validation
import com.daml.lf.value.Value.ContractId
/** Allows for evaluating [[Commands]] and validating [[Transaction]]s.
* <p>
@ -217,7 +218,7 @@ class Engine(val config: EngineConfig = new EngineConfig(LanguageVersion.StableV
} yield validationResult
}
private def loadPackages(pkgIds: List[PackageId]): Result[Unit] =
private[engine] def loadPackages(pkgIds: List[PackageId]): Result[Unit] =
pkgIds.dropWhile(compiledPackages.signatures.isDefinedAt) match {
case pkgId :: rest =>
ResultNeedPackage(
@ -447,6 +448,9 @@ 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 {

View File

@ -0,0 +1,152 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf
package engine
import com.daml.lf.data.Ref.{Identifier, Name, PackageId}
import com.daml.lf.language.Ast
import com.daml.lf.language.Ast.PackageSignature
import com.daml.lf.transaction.Node.{GenNode, KeyWithMaintainers}
import com.daml.lf.transaction.{CommittedTransaction, Node, NodeId, VersionedTransaction}
import com.daml.lf.value.Value
import com.daml.lf.value.Value.ContractId
// Provide methods to add missing information in values (and value containers):
// - type constructor in records, variants, and enums
// - Records' field names
final class ValueEnricher(engine: Engine) {
private[this] def getPackage(pkgId: PackageId): Result[PackageSignature] = {
val signature = engine.compiledPackages().signatures
if (signature.isDefinedAt(pkgId)) {
ResultDone(signature(pkgId))
} else {
engine.loadPackages(List(pkgId)).map(_ => signature(pkgId))
}
}
def enrichValue(typ: Ast.Type, value: Value[ContractId]): Result[Value[ContractId]] =
engine.enrich(typ, value)
def enrichContract(
contract: Value.ContractInst[Value[ContractId]]
): Result[Value.ContractInst[Value[ContractId]]] =
for {
arg <- enrichContract(contract.template, contract.arg)
} yield contract.copy(arg = arg)
def enrichContract(tyCon: Identifier, value: Value[ContractId]): Result[Value[ContractId]] =
enrichValue(Ast.TTyCon(tyCon), value)
def enrichChoiceArgument(
tyCon: Identifier,
choiceName: Name,
value: Value[ContractId],
): Result[Value[ContractId]] =
for {
pkg <- getPackage(tyCon.packageId)
tmpl <- Result.fromEither(SignatureLookup.lookupTemplate(pkg, tyCon.qualifiedName))
enrichedValue <- tmpl.choices.get(choiceName) match {
case Some(choice) =>
enrichValue(choice.argBinder._2, value)
case None =>
ResultError(Error(s"choice $tyCon:$choiceName not found"))
}
} yield enrichedValue
def enrichChoiceResult(
tyCon: Identifier,
choiceName: Name,
value: Value[ContractId],
): Result[Value[ContractId]] =
for {
pkg <- getPackage(tyCon.packageId)
tmpl <- Result.fromEither(SignatureLookup.lookupTemplate(pkg, tyCon.qualifiedName))
enrichedValue <- tmpl.choices.get(choiceName) match {
case Some(choice) =>
enrichValue(choice.returnType, value)
case None =>
ResultError(Error(s"choice $tyCon:$choiceName not found"))
}
} yield enrichedValue
def enrichContractKey(tyCon: Identifier, value: Value[ContractId]): Result[Value[ContractId]] =
for {
pkg <- getPackage(tyCon.packageId)
tmpl <- Result.fromEither(SignatureLookup.lookupTemplate(pkg, tyCon.qualifiedName))
enrichedValue <- tmpl.key match {
case Some(contractKey) =>
enrichValue(contractKey.typ, value)
case None =>
ResultError(Error(s"template $tyCon does not have contract Key"))
}
} yield enrichedValue
private val ResultNone = ResultDone(None)
def enrichContractKey(
tyCon: Identifier,
key: KeyWithMaintainers[Value[ContractId]],
): Result[KeyWithMaintainers[Value[ContractId]]] =
enrichContractKey(tyCon, key.key).map(normalizedKey => key.copy(key = normalizedKey))
def enrichContractKey(
tyCon: Identifier,
key: Option[KeyWithMaintainers[Value[ContractId]]],
): Result[Option[KeyWithMaintainers[Value[ContractId]]]] =
key match {
case Some(k) =>
enrichContractKey(tyCon, k).map(Some(_))
case None =>
ResultNone
}
def enrichNode[Nid](node: GenNode[Nid, ContractId]): Result[GenNode[Nid, ContractId]] =
node match {
case create: Node.NodeCreate[ContractId] =>
for {
contractInstance <- enrichContract(create.coinst)
key <- enrichContractKey(create.templateId, create.key)
} yield create.copy(coinst = contractInstance, key = key)
case fetch: Node.NodeFetch[ContractId] =>
for {
key <- enrichContractKey(fetch.templateId, fetch.key)
} yield fetch.copy(key = key)
case lookup: Node.NodeLookupByKey[ContractId] =>
for {
key <- enrichContractKey(lookup.templateId, lookup.key)
} yield lookup.copy(key = key)
case exe: Node.NodeExercises[Nid, ContractId] =>
for {
choiceArg <- enrichChoiceArgument(exe.templateId, exe.choiceId, exe.chosenValue)
result <- exe.exerciseResult match {
case Some(exeResult) =>
enrichChoiceResult(exe.templateId, exe.choiceId, exeResult).map(Some(_))
case None =>
ResultNone
}
key <- enrichContractKey(exe.templateId, exe.key)
} yield exe.copy(chosenValue = choiceArg, exerciseResult = result, key = key)
}
def enrichTransaction(tx: CommittedTransaction): Result[CommittedTransaction] = {
for {
normalizedNodes <-
tx.nodes.foldLeft[Result[Map[NodeId, GenNode[NodeId, ContractId]]]](ResultDone(Map.empty)) {
case (acc, (nid, node)) =>
for {
nodes <- acc
normalizedNode <- enrichNode(node)
} yield nodes.updated(nid, normalizedNode)
}
} yield CommittedTransaction(
VersionedTransaction(
version = tx.version,
nodes = normalizedNodes,
roots = tx.roots,
)
)
}
}

View File

@ -0,0 +1,244 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf
package engine
import com.daml.lf.data._
import com.daml.lf.language.Ast.{TNat, TTyCon}
import com.daml.lf.language.Util._
import com.daml.lf.testing.parser.Implicits._
import com.daml.lf.transaction.TransactionVersion
import com.daml.lf.transaction.test.TransactionBuilder
import com.daml.lf.value.Value
import com.daml.lf.value.Value._
import org.scalatest.prop.TableDrivenPropertyChecks
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import scala.language.implicitConversions
class ValueEnricherSpec extends AnyWordSpec with Matchers with TableDrivenPropertyChecks {
import defaultParserParameters.{defaultPackageId => pkgId}
private implicit def toName(s: String): Ref.Name = Ref.Name.assertFromString(s)
val pkg =
p"""
module Mod {
record @serializable Record = { field : Int64 };
variant @serializable Variant = variant1 : Text | variant2 : Int64 ;
enum Enum = value1 | value2;
record @serializable Key = {
party: Party,
idx: Int64
};
record @serializable Contract = {
key: Mod:Key,
cids: List (ContractId Mod:Contract)
};
val @noPartyLiterals keyParties: (Mod:Key -> List Party) =
\(key: Mod:Key) ->
Cons @Party [Mod:Key {party} key] (Nil @Party);
val @noPartyLiterals contractParties : (Mod:Contract -> List Party) =
\(contract: Mod:Contract) ->
Mod:keyParties (Mod:Contract {key} contract);
template (this : Contract) = {
precondition True,
signatories Mod:contractParties this,
observers Mod:contractParties this,
agreement "Agreement",
choices {
choice @nonConsuming Noop (self) (r: Mod:Record) : Mod:Record,
controllers
Mod:contractParties this
to
upure @Mod:Record r
},
key @Mod:Key (Mod:Contract {key} this) Mod:keyParties
};
}
"""
val recordCon = Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Mod:Record"))
val variantCon = Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Mod:Variant"))
val enumCon = Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Mod:Enum"))
val contractCon = Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Mod:Contract"))
val keyCon = Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Mod:Key"))
private[this] val engine = Engine.DevEngine()
engine.preloadPackage(pkgId, pkg).consume(_ => None, _ => None, _ => None)
private[this] val enricher = new ValueEnricher(engine)
"enrichValue" should {
val testCases = Table(
("type", "input", "expected output"),
(TUnit, ValueUnit, ValueUnit),
(TBool, ValueTrue, ValueTrue),
(TInt64, ValueInt64(42), ValueInt64(42)),
(
TTimestamp,
ValueTimestamp(Time.Timestamp.assertFromString("1969-07-20T20:17:00Z")),
ValueTimestamp(Time.Timestamp.assertFromString("1969-07-20T20:17:00Z")),
),
(
TDate,
ValueDate(Time.Date.assertFromString("1879-03-14")),
ValueDate(Time.Date.assertFromString("1879-03-14")),
),
(TText, ValueText("daml"), ValueText("daml")),
(
TNumeric(TNat(Decimal.scale)),
ValueNumeric(Numeric.assertFromString("10.")),
ValueNumeric(Numeric.assertFromString("10.0000000000")),
),
(
TParty,
ValueParty(Ref.Party.assertFromString("Alice")),
ValueParty(Ref.Party.assertFromString("Alice")),
),
(
TContractId(TTyCon(recordCon)),
ValueContractId(ContractId.assertFromString("#contractId")),
ValueContractId(ContractId.assertFromString("#contractId")),
),
(
TList(TText),
ValueList(FrontStack(ValueText("a"), ValueText("b"))),
ValueList(FrontStack(ValueText("a"), ValueText("b"))),
),
(
TTextMap(TBool),
ValueTextMap(SortedLookupList(Map("0" -> ValueTrue, "1" -> ValueFalse))),
ValueTextMap(SortedLookupList(Map("0" -> ValueTrue, "1" -> ValueFalse))),
),
(
TOptional(TText),
ValueOptional(Some(ValueText("text"))),
ValueOptional(Some(ValueText("text"))),
),
(
TTyCon(recordCon),
ValueRecord(None, ImmArray(None -> ValueInt64(33))),
ValueRecord(Some(recordCon), ImmArray(Some[Ref.Name]("field") -> ValueInt64(33))),
),
(
TTyCon(variantCon),
ValueVariant(None, "variant1", ValueText("some test")),
ValueVariant(Some(variantCon), "variant1", ValueText("some test")),
),
(TTyCon(enumCon), ValueEnum(None, "value1"), ValueEnum(Some(enumCon), "value1")),
)
"enrich values as expected" in {
forAll(testCases) { (typ, input, output) =>
enricher.enrichValue(typ, input) shouldBe ResultDone(output)
}
}
}
"enrichTransaction" should {
val alice = Ref.Party.assertFromString("Alice")
def buildTransaction(
contract: Value[ContractId],
key: Value[ContractId],
record: Value[ContractId],
) = {
val builder = TransactionBuilder(TransactionVersion.minTypeErasure)
val create =
builder.create(
id = "#01",
template = s"$pkgId:Mod:Contract",
argument = contract,
signatories = Seq(alice),
observers = Seq(alice),
key = Some(key),
)
builder.add(create)
builder.add(builder.fetch(create))
builder.lookupByKey(create, true)
builder.exercise(
create,
"Noop",
false,
Set(alice),
record,
Some(record),
)
builder.buildCommitted()
}
"enrich transaction as expected" in {
val inputKey = ValueRecord(
None,
ImmArray(
None -> ValueParty(alice),
None -> Value.ValueInt64(0),
),
)
val inputContract =
ValueRecord(
None,
ImmArray(
None -> inputKey,
None -> Value.ValueNil,
),
)
val inputRecord =
ValueRecord(None, ImmArray(None -> ValueInt64(33)))
val inputTransaction = buildTransaction(
inputContract,
inputKey,
inputRecord,
)
val outputKey = ValueRecord(
Some(keyCon),
ImmArray(
Some[Ref.Name]("party") -> ValueParty(alice),
Some[Ref.Name]("idx") -> Value.ValueInt64(0),
),
)
val outputContract =
ValueRecord(
Some(contractCon),
ImmArray(
Some[Ref.Name]("key") -> outputKey,
Some[Ref.Name]("cids") -> Value.ValueNil,
),
)
val outputRecord =
ValueRecord(Some(recordCon), ImmArray(Some[Ref.Name]("field") -> ValueInt64(33)))
val outputTransaction = buildTransaction(
outputContract,
outputKey,
outputRecord,
)
enricher.enrichTransaction(inputTransaction) shouldNot be(ResultDone(inputTransaction))
enricher.enrichTransaction(inputTransaction) shouldBe ResultDone(outputTransaction)
}
}
}

View File

@ -90,7 +90,7 @@ final class TransactionBuilder(
argument: Value,
signatories: Seq[String],
observers: Seq[String],
key: Option[String],
key: Option[Value],
): Create = {
val templateId = Ref.Identifier.assertFromString(template)
Create(
@ -114,6 +114,7 @@ final class TransactionBuilder(
consuming: Boolean,
actingParties: Set[String],
argument: Value,
result: Option[Value] = None,
choiceObservers: Set[String] = Set.empty,
byKey: Boolean = true,
): Exercise =
@ -129,7 +130,7 @@ final class TransactionBuilder(
stakeholders = contract.stakeholders,
signatories = contract.signatories,
children = ImmArray.empty,
exerciseResult = None,
exerciseResult = result,
key = contract.key,
byKey = byKey,
version = pkgTxVersion(contract.coinst.template.packageId),
@ -213,12 +214,9 @@ object TransactionBuilder {
),
)
def tuple(values: String*): Value =
record(values.zipWithIndex.map { case (v, i) => s"_$i" -> v }: _*)
def keyWithMaintainers(maintainers: Seq[String], value: String): KeyWithMaintainers =
def keyWithMaintainers(maintainers: Seq[String], key: Value): KeyWithMaintainers =
KeyWithMaintainers(
key = tuple(maintainers :+ value: _*),
key = key,
maintainers = maintainers.map(Ref.Party.assertFromString).toSet,
)

View File

@ -44,6 +44,7 @@ object TransactionVersion {
private[lf] val minGenMap = V11
private[lf] val minChoiceObservers = V11
private[lf] val minNodeVersion = V11
private[lf] val minTypeErasure = VDev
private[lf] val assignNodeVersion: LanguageVersion => TransactionVersion = {
import LanguageVersion._

View File

@ -11,6 +11,7 @@ import com.daml.ledger.api.domain.PartyDetails
import com.daml.ledger.participant.state.v1.RejectionReason
import com.daml.lf.transaction.GlobalKey
import com.daml.lf.transaction.test.{TransactionBuilder => TxBuilder}
import com.daml.lf.value.Value.ValueText
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
@ -412,7 +413,7 @@ object PostCommitValidationSpec {
argument = TxBuilder.record("field" -> "value"),
signatories = Seq("Alice"),
observers = Seq.empty,
key = Some("key"),
key = Some(ValueText("key")),
)
private def genTestExercise(create: TxBuilder.Create): TxBuilder.Exercise =

View File

@ -365,6 +365,9 @@ class TransactionCommitterSpec extends AnyWordSpec with Matchers with MockitoSug
val maintainer = "maintainer"
val dummyValue = TransactionBuilder.record("field" -> "value")
def tuple(values: String*) =
TransactionBuilder.record(values.zipWithIndex.map { case (v, i) => s"_$i" -> v }: _*)
def create(contractId: String, key: String = "key"): TransactionBuilder.Create =
txBuilder.create(
id = contractId,
@ -372,7 +375,7 @@ class TransactionCommitterSpec extends AnyWordSpec with Matchers with MockitoSug
argument = dummyValue,
signatories = Seq(maintainer),
observers = Seq.empty,
key = Some(key),
key = Some(tuple(maintainer, key)),
)
def mkMismatch(