From 26b48133b4796306b2f24e8b9e253f6bea86fc04 Mon Sep 17 00:00:00 2001 From: Carl Pulley <106966370+carlpulley-da@users.noreply.github.com> Date: Tue, 16 Aug 2022 14:15:47 +0100 Subject: [PATCH] Test expected behaviour when superfluous (i.e. unused) contracts are disclosed in commands (#14617) * Resolves #14350 CHANGELOG_BEGIN * Engine/speedy-level tests for explicit disclosure (#14227): Test expected behaviour when superfluous (i.e. unused) contracts are disclosed in commands. CHANGELOG_END --- .../digitalasset/daml/lf/engine/Engine.scala | 7 +- .../daml/lf/engine/EngineTest.scala | 585 ++++++++++++------ .../daml/lf/speedy/SBuiltin.scala | 2 +- .../digitalasset/daml/lf/speedy/Speedy.scala | 10 +- .../lf/transaction/PartialTransaction.scala | 10 +- .../lf/speedy/BuildDisclosureTableTest.scala | 1 - .../lf/speedy/ExplicitDisclosureLib.scala | 46 +- .../daml/lf/transaction/Transaction.scala | 2 +- security-evidence.md | 2 +- 9 files changed, 464 insertions(+), 201 deletions(-) diff --git a/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Engine.scala b/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Engine.scala index b45254e7bd..7022c14f89 100644 --- a/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Engine.scala +++ b/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Engine.scala @@ -111,7 +111,11 @@ class Engine(val config: EngineConfig = Engine.StableConfig) { // TODO (drsk) remove this assertion once disclosed contracts feature becomes stable. // https://github.com/digital-asset/daml/issues/13952. - assert(disclosures.isEmpty || config.allowedLanguageVersions.contains(LanguageVersion.v1_dev)) + assert( + disclosures.isEmpty || config.allowedLanguageVersions.contains( + LanguageVersion.Features.explicitDisclosure + ) + ) val submissionTime = cmds.ledgerEffectiveTime for { @@ -462,6 +466,7 @@ class Engine(val config: EngineConfig = Engine.StableConfig) { } ResultDone((tx, meta)) } + case SResultFinal(_, None) => ResultError( Error.Interpretation.Internal( diff --git a/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala b/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala index dcd6836b83..83a3c04485 100644 --- a/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala +++ b/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala @@ -6,9 +6,8 @@ package engine import java.io.File import com.daml.lf.archive.UniversalArchiveDecoder -import com.daml.bazeltools.BazelRunfiles import com.daml.lf.data.Ref._ -import com.daml.lf.data._ +import com.daml.lf.data.{Ref, _} import com.daml.lf.language.Ast._ import com.daml.lf.language.Util._ import com.daml.lf.transaction.{ @@ -16,27 +15,34 @@ import com.daml.lf.transaction.{ GlobalKeyWithMaintainers, Node, NodeId, + Normalization, + ReplayMismatch, SubmittedTransaction, + Validation, VersionedTransaction, Transaction => Tx, TransactionVersion => TxVersions, } -import com.daml.lf.transaction.{Normalization, Validation, ReplayMismatch} import com.daml.lf.value.Value import Value._ -import com.daml.lf.speedy.{ArrayList, InitialSeeding, SValue, svalue} +import com.daml.bazeltools.BazelRunfiles.rlocation +import com.daml.lf +import com.daml.lf.speedy.{ArrayList, DisclosedContract, InitialSeeding, SValue, svalue} import com.daml.lf.speedy.SValue._ import com.daml.lf.command._ +import com.daml.lf.crypto.Hash import com.daml.lf.engine.Error.Interpretation import com.daml.lf.engine.Error.Interpretation.DamlException +import com.daml.lf.language.PackageInterface import com.daml.lf.transaction.test.TransactionBuilder.assertAsVersionedContract import com.daml.logging.LoggingContext import org.scalactic.Equality import org.scalatest.prop.TableDrivenPropertyChecks -import org.scalatest.EitherValues +import org.scalatest.{Assertion, EitherValues} import org.scalatest.wordspec.AnyWordSpec import org.scalatest.matchers.should.Matchers import org.scalatest.Inside._ +import org.scalatest.matchers.{MatchResult, Matcher} import scala.collection.immutable.HashMap import scala.language.implicitConversions @@ -52,104 +58,10 @@ class EngineTest extends AnyWordSpec with Matchers with TableDrivenPropertyChecks - with EitherValues - with BazelRunfiles { + with EitherValues { import EngineTest._ - private def loadPackage(resource: String): (PackageId, Package, Map[PackageId, Package]) = { - val packages = UniversalArchiveDecoder.assertReadFile(new File(rlocation(resource))) - val (mainPkgId, mainPkg) = packages.main - (mainPkgId, mainPkg, packages.all.toMap) - } - - private val (basicTestsPkgId, basicTestsPkg, allPackages) = loadPackage( - "daml-lf/tests/BasicTests.dar" - ) - - val basicTestsSignatures = language.PackageInterface(Map(basicTestsPkgId -> basicTestsPkg)) - - val withKeyTemplate = "BasicTests:WithKey" - val BasicTests_WithKey = Identifier(basicTestsPkgId, withKeyTemplate) - val withKeyContractInst: VersionedContractInstance = - assertAsVersionedContract( - ContractInstance( - TypeConName(basicTestsPkgId, withKeyTemplate), - ValueRecord( - Some(BasicTests_WithKey), - ImmArray( - (Some[Ref.Name]("p"), ValueParty(alice)), - (Some[Ref.Name]("k"), ValueInt64(42)), - ), - ), - "", - ) - ) - - val defaultContracts: Map[ContractId, VersionedContractInstance] = - Map( - toContractId("BasicTests:Simple:1") -> - assertAsVersionedContract( - ContractInstance( - TypeConName(basicTestsPkgId, "BasicTests:Simple"), - ValueRecord( - Some(Identifier(basicTestsPkgId, "BasicTests:Simple")), - ImmArray((Some[Name]("p"), ValueParty(party))), - ), - "", - ) - ), - toContractId("BasicTests:CallablePayout:1") -> - assertAsVersionedContract( - ContractInstance( - TypeConName(basicTestsPkgId, "BasicTests:CallablePayout"), - ValueRecord( - Some(Identifier(basicTestsPkgId, "BasicTests:CallablePayout")), - ImmArray( - (Some[Ref.Name]("giver"), ValueParty(alice)), - (Some[Ref.Name]("receiver"), ValueParty(bob)), - ), - ), - "", - ) - ), - toContractId("BasicTests:WithKey:1") -> - withKeyContractInst, - ) - - val defaultKey = Map( - GlobalKey.assertBuild( - TypeConName(basicTestsPkgId, withKeyTemplate), - ValueRecord(None, ImmArray((None, ValueParty(alice)), (None, ValueInt64(42)))), - ) - -> - toContractId("BasicTests:WithKey:1") - ) - - private[this] val lookupContract = defaultContracts.get(_) - - private[this] def lookupPackage(pkgId: PackageId): Option[Package] = { - allPackages.get(pkgId) - } - - private[this] def lookupKey(key: GlobalKeyWithMaintainers): Option[ContractId] = - (key.globalKey.templateId, key.globalKey.key) match { - case ( - BasicTests_WithKey, - ValueRecord(_, ImmArray((_, ValueParty(`alice`)), (_, ValueInt64(42)))), - ) => - Some(toContractId("BasicTests:WithKey:1")) - case _ => - None - } - - private[this] val suffixLenientEngine = newEngine() - private[this] val suffixStrictEngine = newEngine(requireCidSuffixes = true) - private[this] val preprocessor = - new preprocessing.Preprocessor( - ConcurrentCompiledPackages(suffixLenientEngine.config.getCompilerConfig) - ) - "minimal create command" should { val id = Identifier(basicTestsPkgId, "BasicTests:Simple") val let = Time.Timestamp.now() @@ -666,7 +578,7 @@ class EngineTest } } - "fecth-by-key" should { + "fetch-by-key" should { val seed = hash("fetch-by-key") val now = Time.Timestamp.now() @@ -749,6 +661,7 @@ class EngineTest ) } } + "error if Speedy fails to find the key" in { val templateId = Identifier(basicTestsPkgId, "BasicTests:FailedFetchByKey") @@ -795,6 +708,61 @@ class EngineTest ) } } + + "unused disclosed contracts not saved to ledger" in { + val templateId = Identifier(basicTestsPkgId, "BasicTests:WithKey") + val usedContractSKey = SValue.SRecord( + templateId, + ImmArray("_1", "_2").map(Ref.Name.assertFromString), + values = ArrayList(SValue.SParty(alice), SValue.SInt64(42)), + ) + val usedContractKey = usedContractSKey.toNormalizedValue(TxVersions.minExplicitDisclosure) + val unusedContractKey = Value.ValueRecord( + None, + ImmArray( + None -> Value.ValueParty(alice), + None -> Value.ValueInt64(69), + ), + ) + val usedDisclosedContract = DisclosedContract( + templateId, + SValue.SContractId(toContractId("BasicTests:WithKey:1")), + SValue.SRecord( + templateId, + ImmArray(Ref.Name.assertFromString("p"), Ref.Name.assertFromString("k")), + ArrayList(SValue.SParty(alice), SValue.SInt64(42)), + ), + ContractMetadata( + now, + Some(crypto.Hash.assertHashContractKey(templateId, usedContractKey)), + ImmArray.empty, + ), + ) + val unusedDisclosedContract = DisclosedContract( + templateId, + SValue.SContractId(toContractId("BasicTests:WithKey:2")), + SValue.SRecord( + templateId, + ImmArray(Ref.Name.assertFromString("p"), Ref.Name.assertFromString("k")), + ArrayList(SValue.SParty(alice), SValue.SInt64(42)), + ), + ContractMetadata( + now, + Some(crypto.Hash.assertHashContractKey(templateId, unusedContractKey)), + ImmArray.empty, + ), + ) + val fetchByKeyCommand = speedy.Command.FetchByKey( + templateId = templateId, + key = usedContractSKey, + ) + + ExplicitDisclosureTesting.unusedDisclosedContractsNotSavedToLedger( + fetchByKeyCommand, + unusedDisclosedContract, + usedDisclosedContract, + ) + } } "create-and-exercise command" should { @@ -1379,13 +1347,10 @@ class EngineTest reinterpreted shouldBe a[Right[_, _]] } - } "lookup by key" should { - val seed = hash("interpreting lookup by key nodes") - val lookedUpCid = toContractId("1") val lookerUpTemplate = "BasicTests:LookerUpByKey" val lookerUpTemplateId = Identifier(basicTestsPkgId, lookerUpTemplate) @@ -1570,6 +1535,104 @@ class EngineTest ) } } + + "unused disclosed contracts not saved to ledger" in { + val templateId = Identifier(basicTestsPkgId, "BasicTests:WithKey") + val usedContractSKey = SValue.SRecord( + templateId, + ImmArray("_1", "_2").map(Ref.Name.assertFromString), + values = ArrayList(SValue.SParty(alice), SValue.SInt64(42)), + ) + val usedContractKey = Value.ValueRecord( + None, + ImmArray( + None -> Value.ValueParty(alice), + None -> Value.ValueInt64(42), + ), + ) + val unusedContractKey = Value.ValueRecord( + None, + ImmArray( + None -> Value.ValueParty(alice), + None -> Value.ValueInt64(69), + ), + ) + val usedDisclosedContract = DisclosedContract( + templateId, + SValue.SContractId(toContractId("BasicTests:WithKey:1")), + SValue.SRecord( + templateId, + ImmArray(Ref.Name.assertFromString("p"), Ref.Name.assertFromString("k")), + ArrayList(SValue.SParty(alice), SValue.SInt64(42)), + ), + ContractMetadata( + now, + Some(crypto.Hash.assertHashContractKey(templateId, usedContractKey)), + ImmArray.empty, + ), + ) + val unusedDisclosedContract = DisclosedContract( + templateId, + SValue.SContractId(toContractId("BasicTests:WithKey:2")), + SValue.SRecord( + templateId, + ImmArray(Ref.Name.assertFromString("p"), Ref.Name.assertFromString("k")), + ArrayList(SValue.SParty(alice), SValue.SInt64(42)), + ), + ContractMetadata( + now, + Some(crypto.Hash.assertHashContractKey(templateId, unusedContractKey)), + ImmArray.empty, + ), + ) + val lookupByKeyCommand = speedy.Command.LookupByKey( + templateId = templateId, + contractKey = usedContractSKey, + ) + + ExplicitDisclosureTesting.unusedDisclosedContractsNotSavedToLedger( + lookupByKeyCommand, + unusedDisclosedContract, + usedDisclosedContract, + ) + } + } + + "fetch template" should { + val templateId = Identifier(basicTestsPkgId, "BasicTests:Simple") + val usedDisclosedContract = DisclosedContract( + templateId, + SValue.SContractId(toContractId("BasicTests:Simple:1")), + SValue.SRecord( + templateId, + ImmArray(Ref.Name.assertFromString("p")), + ArrayList(SValue.SParty(alice)), + ), + ContractMetadata(Time.Timestamp.now(), None, ImmArray.empty), + ) + val unusedDisclosedContract = DisclosedContract( + templateId, + SValue.SContractId(toContractId("BasicTests:Simple:2")), + SValue.SRecord( + templateId, + ImmArray(Ref.Name.assertFromString("p")), + ArrayList(SValue.SParty(alice)), + ), + ContractMetadata(Time.Timestamp.now(), None, ImmArray.empty), + ) + + "unused disclosed contracts not saved to ledger" in { + val fetchTemplateCommand = speedy.Command.FetchTemplate( + templateId = templateId, + coid = usedDisclosedContract.contractId, + ) + + ExplicitDisclosureTesting.unusedDisclosedContractsNotSavedToLedger( + fetchTemplateCommand, + unusedDisclosedContract, + usedDisclosedContract, + ) + } } "getTime set dependsOnTime flag" in { @@ -1743,7 +1806,7 @@ class EngineTest ) ) val contracts = defaultContracts + (fetcherCid -> fetcherInst) - val lookupContract = contracts.get(_) + val lookupContract = contracts.get _ val correctCommand = ApiCommand.Exercise( withKeyId, @@ -1892,7 +1955,7 @@ class EngineTest "exceptions" should { val (exceptionsPkgId, _, allExceptionsPkgs) = loadPackage("daml-lf/tests/Exceptions.dar") - val lookupPackage = allExceptionsPkgs.get(_) + val lookupPackage = allExceptionsPkgs.get _ val kId = Identifier(exceptionsPkgId, "Exceptions:K") val tId = Identifier(exceptionsPkgId, "Exceptions:T") val let = Time.Timestamp.now() @@ -1908,7 +1971,7 @@ class EngineTest ) ) ) - val lookupContract = contracts.get(_) + val lookupContract = contracts.get _ def lookupKey(key: GlobalKeyWithMaintainers): Option[ContractId] = (key.globalKey.templateId, key.globalKey.key) match { case ( @@ -2041,7 +2104,7 @@ class EngineTest "action node seeds" should { val (exceptionsPkgId, _, allExceptionsPkgs) = loadPackage("daml-lf/tests/Exceptions.dar") - val lookupPackage = allExceptionsPkgs.get(_) + val lookupPackage = allExceptionsPkgs.get _ val kId = Identifier(exceptionsPkgId, "Exceptions:K") val seedId = Identifier(exceptionsPkgId, "Exceptions:NodeSeeds") val let = Time.Timestamp.now() @@ -2057,7 +2120,7 @@ class EngineTest ) ) ) - val lookupContract = contracts.get(_) + val lookupContract = contracts.get _ def lookupKey(key: GlobalKeyWithMaintainers): Option[ContractId] = (key.globalKey.templateId, key.globalKey.key) match { case ( @@ -2093,6 +2156,7 @@ class EngineTest lookupKey, ) } + "Only create and exercise nodes end up in actionNodeSeeds" in { val command = ApiCommand.CreateAndExercise( seedId, @@ -2118,7 +2182,7 @@ class EngineTest "global key lookups" should { val (exceptionsPkgId, _, allExceptionsPkgs) = loadPackage("daml-lf/tests/Exceptions.dar") - val lookupPackage = allExceptionsPkgs.get(_) + val lookupPackage = allExceptionsPkgs.get _ val kId = Identifier(exceptionsPkgId, "Exceptions:K") val tId = Identifier(exceptionsPkgId, "Exceptions:GlobalLookups") val let = Time.Timestamp.now() @@ -2134,7 +2198,7 @@ class EngineTest ) ) ) - val lookupContract = contracts.get(_) + val lookupContract = contracts.get _ def lookupKey(key: GlobalKeyWithMaintainers): Option[ContractId] = (key.globalKey.templateId, key.globalKey.key) match { case ( @@ -2249,24 +2313,134 @@ class EngineTest } } - } object EngineTest { - private implicit def logContext: LoggingContext = LoggingContext.ForTesting + import Matchers._ - private def hash(s: String) = crypto.Hash.hashPrivateKey(s) - private def participant = Ref.ParticipantId.assertFromString("participant") - private def byKeyNodes(tx: VersionedTransaction) = + implicit val logContext: LoggingContext = LoggingContext.ForTesting + + @SuppressWarnings(Array("org.wartremover.warts.Any")) + implicit val resultEq: Equality[Either[Error, SValue]] = { + case (Right(v1: SValue), Right(v2: SValue)) => svalue.Equality.areEqual(v1, v2) + case (Left(e1), Left(e2)) => e1 == e2 + case _ => false + } + + implicit def qualifiedNameStr(s: String): QualifiedName = + QualifiedName.assertFromString(s) + + implicit def toName(s: String): Name = + Name.assertFromString(s) + + val (basicTestsPkgId, basicTestsPkg, allPackages) = loadPackage( + "daml-lf/tests/BasicTests.dar" + ) + + val basicTestsSignatures: PackageInterface = + language.PackageInterface(Map(basicTestsPkgId -> basicTestsPkg)) + + val party: Ref.IdString.Party = Party.assertFromString("Party") + val alice: Ref.IdString.Party = Party.assertFromString("Alice") + val bob: Ref.IdString.Party = Party.assertFromString("Bob") + val clara: Ref.IdString.Party = Party.assertFromString("Clara") + + val dummySuffix: Bytes = Bytes.assertFromString("00") + + val withKeyTemplate = "BasicTests:WithKey" + val BasicTests_WithKey: lf.data.Ref.ValueRef = Identifier(basicTestsPkgId, withKeyTemplate) + val withKeyContractInst: VersionedContractInstance = + assertAsVersionedContract( + ContractInstance( + TypeConName(basicTestsPkgId, withKeyTemplate), + ValueRecord( + Some(BasicTests_WithKey), + ImmArray( + (Some[Ref.Name]("p"), ValueParty(alice)), + (Some[Ref.Name]("k"), ValueInt64(42)), + ), + ), + "", + ) + ) + + val defaultContracts: Map[ContractId, VersionedContractInstance] = + Map( + toContractId("BasicTests:Simple:1") -> + assertAsVersionedContract( + ContractInstance( + TypeConName(basicTestsPkgId, "BasicTests:Simple"), + ValueRecord( + Some(Identifier(basicTestsPkgId, "BasicTests:Simple")), + ImmArray((Some[Name]("p"), ValueParty(party))), + ), + "", + ) + ), + toContractId("BasicTests:CallablePayout:1") -> + assertAsVersionedContract( + ContractInstance( + TypeConName(basicTestsPkgId, "BasicTests:CallablePayout"), + ValueRecord( + Some(Identifier(basicTestsPkgId, "BasicTests:CallablePayout")), + ImmArray( + (Some[Ref.Name]("giver"), ValueParty(alice)), + (Some[Ref.Name]("receiver"), ValueParty(bob)), + ), + ), + "", + ) + ), + toContractId("BasicTests:WithKey:1") -> + withKeyContractInst, + ) + + val defaultKey = Map( + GlobalKey.assertBuild( + TypeConName(basicTestsPkgId, withKeyTemplate), + ValueRecord(None, ImmArray((None, ValueParty(alice)), (None, ValueInt64(42)))), + ) + -> + toContractId("BasicTests:WithKey:1") + ) + + val lookupContract: ContractId => Option[VersionedContractInstance] = defaultContracts.get + + val suffixLenientEngine: Engine = newEngine() + val suffixStrictEngine: Engine = newEngine(requireCidSuffixes = true) + val preprocessor = + new preprocessing.Preprocessor( + ConcurrentCompiledPackages(suffixLenientEngine.config.getCompilerConfig) + ) + + def loadPackage(resource: String): (PackageId, Package, Map[PackageId, Package]) = { + val packages = UniversalArchiveDecoder.assertReadFile(new File(rlocation(resource))) + val (mainPkgId, mainPkg) = packages.main + (mainPkgId, mainPkg, packages.all.toMap) + } + + def lookupPackage(pkgId: PackageId): Option[Package] = { + allPackages.get(pkgId) + } + + def lookupKey(key: GlobalKeyWithMaintainers): Option[ContractId] = + (key.globalKey.templateId, key.globalKey.key) match { + case ( + BasicTests_WithKey, + ValueRecord(_, ImmArray((_, ValueParty(`alice`)), (_, ValueInt64(42)))), + ) => + Some(toContractId("BasicTests:WithKey:1")) + case _ => + None + } + + def hash(s: String): Hash = crypto.Hash.hashPrivateKey(s) + def participant: Ref.IdString.ParticipantId = Ref.ParticipantId.assertFromString("participant") + def byKeyNodes(tx: VersionedTransaction): Set[NodeId] = tx.nodes.collect { case (nodeId, node: Node.Action) if node.byKey => nodeId }.toSet - private val party = Party.assertFromString("Party") - private val alice = Party.assertFromString("Alice") - private val bob = Party.assertFromString("Bob") - private val clara = Party.assertFromString("Clara") - - private def newEngine(requireCidSuffixes: Boolean = false) = + def newEngine(requireCidSuffixes: Boolean = false) = new Engine( EngineConfig( allowedLanguageVersions = language.LanguageVersion.DevVersions, @@ -2275,28 +2449,13 @@ object EngineTest { ) ) - private implicit def qualifiedNameStr(s: String): QualifiedName = - QualifiedName.assertFromString(s) - - private implicit def toName(s: String): Name = - Name.assertFromString(s) - - private val dummySuffix = Bytes.assertFromString("00") - - private def toContractId(s: String): ContractId = + def toContractId(s: String): ContractId = ContractId.V1.assertBuild(crypto.Hash.hashPrivateKey(s), dummySuffix) - private def findNodeByIdx[Cid](nodes: Map[NodeId, Node], idx: Int) = + def findNodeByIdx[Cid](nodes: Map[NodeId, Node], idx: Int): Option[Node] = nodes.collectFirst { case (nodeId, node) if nodeId.index == idx => node } - @SuppressWarnings(Array("org.wartremover.warts.Any")) - private implicit def resultEq: Equality[Either[Error, SValue]] = { - case (Right(v1: SValue), Right(v2: SValue)) => svalue.Equality.areEqual(v1, v2) - case (Left(e1), Left(e2)) => e1 == e2 - case _ => false - } - - private def isReplayedBy( + def isReplayedBy( recorded: VersionedTransaction, replayed: VersionedTransaction, ): Either[ReplayMismatch, Unit] = { @@ -2304,47 +2463,12 @@ object EngineTest { Validation.isReplayedBy(Normalization.normalizeTx(recorded), replayed) } - private def suffix(tx: VersionedTransaction) = + def suffix(tx: VersionedTransaction): VersionedTransaction = data.assertRight(tx.suffixCid(_ => dummySuffix)) - private[this] case class ReinterpretState( - contracts: Map[ContractId, VersionedContractInstance], - keys: Map[GlobalKey, ContractId], - nodes: HashMap[NodeId, Node] = HashMap.empty, - roots: BackStack[NodeId] = BackStack.empty, - dependsOnTime: Boolean = false, - nodeSeeds: BackStack[(NodeId, crypto.Hash)] = BackStack.empty, - ) { - def commit(tr: Tx, meta: Tx.Metadata) = { - val (newContracts, newKeys) = tr.fold((contracts, keys)) { - case ((contracts, keys), (_, exe: Node.Exercise)) => - (contracts - exe.targetCoid, keys) - case ((contracts, keys), (_, create: Node.Create)) => - ( - contracts.updated( - create.coid, - create.versionedCoinst, - ), - create.key.fold(keys)(k => - keys.updated(GlobalKey.assertBuild(create.templateId, k.key), create.coid) - ), - ) - case (acc, _) => acc - } - ReinterpretState( - newContracts, - newKeys, - nodes ++ tr.nodes, - roots :++ tr.roots, - dependsOnTime || meta.dependsOnTime, - nodeSeeds :++ meta.nodeSeeds, - ) - } - } - - // Mimics Canton reinterpreation + // Mimics Canton reinterpretation // requires a suffixed transaction. - private def reinterpret( + def reinterpret( engine: Engine, submitters: Set[Party], nodes: ImmArray[NodeId], @@ -2434,4 +2558,109 @@ object EngineTest { ) } + object ExplicitDisclosureTesting { + def unusedDisclosedContractsNotSavedToLedger( + cmd: speedy.Command, + unusedDisclosedContract: DisclosedContract, + usedDisclosedContract: DisclosedContract, + ): Assertion = { + val result = suffixLenientEngine + .interpretCommands( + validating = false, + submitters = Set(alice), + readAs = Set.empty, + commands = ImmArray(cmd), + ledgerTime = Time.Timestamp.now(), + submissionTime = Time.Timestamp.now(), + seeding = InitialSeeding.TransactionSeed(hash(s"$cmd")), + disclosures = ImmArray(unusedDisclosedContract, usedDisclosedContract), + ) + .consume(_ => None, lookupPackage, lookupKey) + + inside(result) { case Right((transaction, metadata)) => + transaction should haveDisclosedInputContracts(usedDisclosedContract) + metadata should haveDisclosedContracts(usedDisclosedContract)(preprocessor) + } + } + + @SuppressWarnings( + Array( + "org.wartremover.warts.JavaSerializable", + "org.wartremover.warts.Product", + "org.wartremover.warts.Serializable", + ) + ) + def haveDisclosedContracts( + disclosedContracts: DisclosedContract* + )(preprocessor: preprocessing.Preprocessor): Matcher[Tx.Metadata] = + Matcher { metadata => + val expectedResult = ImmArray(disclosedContracts: _*) + val actualResult = metadata.disclosures + .map(_.unversioned) + .map(preprocessor.commandPreprocessor.unsafePreprocessDisclosedContract) + val debugMessage = Seq( + s"expected but missing contract IDs: ${expectedResult.filter(!actualResult.toSeq.contains(_)).map(_.contractId.value)}", + s"unexpected but found contract IDs: ${actualResult.filter(!expectedResult.toSeq.contains(_)).map(_.contractId)}", + ).mkString("\n ", "\n ", "") + + MatchResult( + expectedResult == actualResult, + s"Failed with unexpected disclosed contracts: $expectedResult != $actualResult $debugMessage", + s"Failed with unexpected disclosed contracts: $expectedResult == $actualResult", + ) + } + + def haveDisclosedInputContracts( + disclosedContracts: DisclosedContract* + ): Matcher[VersionedTransaction] = + Matcher { transaction => + val expectedResult = Set(disclosedContracts: _*).map(_.contractId.value) + val actualResult = transaction.inputContracts + val debugMessage = Seq( + s"expected but missing contract IDs: ${expectedResult.filter(!actualResult.toSeq.contains(_))}", + s"unexpected but found contract IDs: ${actualResult.filter(!expectedResult.toSeq.contains(_))}", + ).mkString("\n ", "\n ", "") + + MatchResult( + expectedResult == actualResult, + s"Failed with unexpected disclosed contracts: $expectedResult != $actualResult $debugMessage", + s"Failed with unexpected disclosed contracts: $expectedResult == $actualResult", + ) + } + } + + case class ReinterpretState( + contracts: Map[ContractId, VersionedContractInstance], + keys: Map[GlobalKey, ContractId], + nodes: HashMap[NodeId, Node] = HashMap.empty, + roots: BackStack[NodeId] = BackStack.empty, + dependsOnTime: Boolean = false, + nodeSeeds: BackStack[(NodeId, crypto.Hash)] = BackStack.empty, + ) { + def commit(tr: Tx, meta: Tx.Metadata): ReinterpretState = { + val (newContracts, newKeys) = tr.fold((contracts, keys)) { + case ((contracts, keys), (_, exe: Node.Exercise)) => + (contracts - exe.targetCoid, keys) + case ((contracts, keys), (_, create: Node.Create)) => + ( + contracts.updated( + create.coid, + create.versionedCoinst, + ), + create.key.fold(keys)(k => + keys.updated(GlobalKey.assertBuild(create.templateId, k.key), create.coid) + ), + ) + case (acc, _) => acc + } + ReinterpretState( + newContracts, + newKeys, + nodes ++ tr.nodes, + roots :++ tr.roots, + dependsOnTime || meta.dependsOnTime, + nodeSeeds :++ meta.nodeSeeds, + ) + } + } } diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SBuiltin.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SBuiltin.scala index 1f7a915634..e807fdb648 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SBuiltin.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SBuiltin.scala @@ -1570,12 +1570,12 @@ private[lf] object SBuiltin { NameOf.qualifiedNameOfCurrentFunc, skey, ) + if (keyWithMaintainers.maintainers.isEmpty) { Control.Error( IE.FetchEmptyContractKeyMaintainers(operation.templateId, keyWithMaintainers.key) ) } else { - val gkey = GlobalKey(operation.templateId, keyWithMaintainers.key) onLedger.ptx.contractState.resolveKey(gkey) match { diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Speedy.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Speedy.scala index 01f2fba0e8..fd12cae458 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Speedy.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Speedy.scala @@ -241,7 +241,7 @@ private[lf] object Speedy { ) case None => - val m1_prime = table.contractById + (coid -> (d.templateId, arg)) + val contractByIdUpdates = table.contractById + (coid -> (d.templateId, arg)) d.metadata.keyHash match { case Some(hash) => // check for duplicate contract key hashes @@ -256,14 +256,18 @@ private[lf] object Speedy { ) ) - case None => DisclosureTable(table.contractIdByKey + (hash -> coid), m1_prime) + case None => + DisclosureTable( + table.contractIdByKey + (hash -> coid), + contractByIdUpdates, + ) } case None => packageInterface.lookupTemplate(d.templateId) match { case Right(template) if template.key.isEmpty => // Success - template exists, but has no key defined - table.copy(contractById = m1_prime) + table.copy(contractById = contractByIdUpdates) case Right(_) => // Error - disclosed contract lacks a key hash, but the template requires a key diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/transaction/PartialTransaction.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/transaction/PartialTransaction.scala index 272a768c0a..ae2540c20e 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/transaction/PartialTransaction.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/transaction/PartialTransaction.scala @@ -216,7 +216,7 @@ private[lf] object PartialTransaction { * @param contractState summarizes the changes to the contract states caused by nodes up to now * @param actionNodeLocations The optional locations of create/exercise/fetch/lookup nodes in pre-order. * Used by 'locationInfo()', called by 'finish()' and 'finishIncomplete()' - * @param disclosedContracts contracts that have been explicitly disclosed + * @param disclosedContracts contracts that have been explicitly disclosed to Speedy (usage will be determined by 'finish()') */ private[speedy] case class PartialTransaction( nextNodeIdx: Int, @@ -308,13 +308,17 @@ private[speedy] case class PartialTransaction( val roots = context.children.toImmArray val tx0 = Tx(nodes, roots) val (tx, seeds) = NormalizeRollbacks.normalizeTx(tx0) + val txResult = SubmittedTransaction(TxVersion.asVersionedTransaction(tx)) Result( - SubmittedTransaction(TxVersion.asVersionedTransaction(tx)), + txResult, locationInfo(), seeds.zip(actionNodeSeeds.toImmArray), contractState.globalKeyInputs.transform((_, v) => v.toKeyMapping), - disclosedContracts, + disclosedContracts.filter(disclosedContract => + txResult.inputContracts.contains(disclosedContract.contractId.value) + ), ) + case _ => InternalError.runtimeException( NameOf.qualifiedNameOfCurrentFunc, diff --git a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/BuildDisclosureTableTest.scala b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/BuildDisclosureTableTest.scala index 55b3d91b56..4711a9895e 100644 --- a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/BuildDisclosureTableTest.scala +++ b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/BuildDisclosureTableTest.scala @@ -75,7 +75,6 @@ class BuildDisclosureTableTest extends AnyFreeSpec with Inside with Matchers { DisclosurePreprocessing.DuplicateContractKeys(houseTemplateId, collidingKeyHash) ) ) - } } } diff --git a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ExplicitDisclosureLib.scala b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ExplicitDisclosureLib.scala index 08035979d7..748ef904e3 100644 --- a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ExplicitDisclosureLib.scala +++ b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ExplicitDisclosureLib.scala @@ -298,28 +298,50 @@ object ExplicitDisclosureLib { def haveInactiveContractIds(contractIds: ContractId*): Matcher[Speedy.OnLedger] = Matcher { ledger => + val expectedResult = contractIds.toSet + val actualResult = ledger.ptx.contractState.activeState.consumedBy.keySet + val debugMessage = Seq( + s"expected but missing contract IDs: ${expectedResult.filter(!actualResult.toSeq.contains(_))}", + s"unexpected but found contract IDs: ${actualResult.filter(!expectedResult.toSeq.contains(_))}", + ).mkString("\n ", "\n ", "") + MatchResult( - ledger.ptx.contractState.activeState.consumedBy.keySet == contractIds.toSet, - s"Failed with unexpected inactive contracts: ${ledger.ptx.contractState.activeState.consumedBy.keySet} != $contractIds", - s"Failed with unexpected inactive contracts: ${ledger.ptx.contractState.activeState.consumedBy.keySet} == $contractIds", + expectedResult == actualResult, + s"Failed with unexpected inactive contracts: $expectedResult != $actualResult $debugMessage", + s"Failed with unexpected inactive contracts: $expectedResult == $actualResult", ) } def haveCachedContractIds(contractIds: ContractId*): Matcher[Speedy.OnLedger] = Matcher { ledger => + val expectedResult = contractIds.toSet + val actualResult = ledger.cachedContracts.keySet + val debugMessage = Seq( + s"expected but missing contract IDs: ${expectedResult.filter(!actualResult.toSeq.contains(_))}", + s"unexpected but found contract IDs: ${actualResult.filter(!expectedResult.toSeq.contains(_))}", + ).mkString("\n ", "\n ", "") + MatchResult( - ledger.cachedContracts.keySet == contractIds.toSet, - s"Failed with unexpected cached contracts: ${ledger.cachedContracts.keySet} != $contractIds", - s"Failed with unexpected cached contracts: ${ledger.cachedContracts.keySet} == $contractIds", + expectedResult == actualResult, + s"Failed with unexpected cached contracts: $expectedResult != $actualResult $debugMessage", + s"Failed with unexpected cached contracts: $expectedResult == $actualResult", ) } - def haveDisclosedContracts(contractIds: DisclosedContract*): Matcher[Speedy.OnLedger] = Matcher { - ledger => + def haveDisclosedContracts(disclosedContracts: DisclosedContract*): Matcher[Speedy.OnLedger] = + Matcher { ledger => + val expectedResult = ImmArray(disclosedContracts: _*) + val actualResult = ledger.ptx.disclosedContracts + val debugMessage = Seq( + s"expected but missing contract IDs: ${expectedResult.filter(!actualResult.toSeq.contains(_)).map(_.contractId)}", + s"unexpected but found contract IDs: ${actualResult.filter(!expectedResult.toSeq.contains(_)).map(_.contractId)}", + ).mkString("\n ", "\n ", "") + MatchResult( - ledger.ptx.disclosedContracts == ImmArray(contractIds: _*), - s"Failed with unexpected disclosed contracts: ${ledger.ptx.disclosedContracts} != $contractIds", - s"Failed with unexpected disclosed contracts: ${ledger.ptx.disclosedContracts} == $contractIds", + expectedResult == actualResult, + s"Failed with unexpected disclosed contracts: $expectedResult != $actualResult $debugMessage", + s"Failed with unexpected disclosed contracts: $expectedResult == $actualResult", ) - } + + } } diff --git a/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/transaction/Transaction.scala b/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/transaction/Transaction.scala index f2428aca7b..5895b7a358 100644 --- a/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/transaction/Transaction.scala +++ b/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/transaction/Transaction.scala @@ -701,7 +701,7 @@ object Transaction { * @param nodeSeeds : An association list that maps to each ID of create and exercise * nodes its seeds. * @param globalKeyMapping : input key mapping inferred by interpretation - * @param disclosures : contracts explicitly disclosed to this transaction + * @param disclosures : contracts passed via explicit disclosure that have been used in this transaction */ final case class Metadata( submissionSeed: Option[crypto.Hash], diff --git a/security-evidence.md b/security-evidence.md index 2a203e86b5..e8c3f83568 100644 --- a/security-evidence.md +++ b/security-evidence.md @@ -175,7 +175,7 @@ - Evaluation order of successful lookup_by_key of a local contract: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L2913) - Evaluation order of successful lookup_by_key of a non-cached global contract: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L2761) - Exceptions, throw/catch.: [ExceptionTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ExceptionTest.scala#L25) -- Rollback creates cannot be exercise: [EngineTest.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala#L2001) +- Rollback creates cannot be exercise: [EngineTest.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala#L2064) - This checks that type checking in exercise_interface is done after checking activeness.: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L1851) - This checks that type checking is done after checking activeness.: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L1743) - This checks that type checking is done after checking activeness.: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L2705)