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 b0ba456b2f1f..8ee26e7e83e9 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 @@ -146,21 +146,16 @@ final class Engine { * If let undefined, no discriminator will be generated. */ def reinterpret( - submissionSeedAndTime: Option[(crypto.Hash, Time.Timestamp)], - participantId: Ref.ParticipantId, + rootSeedAndSubmissionTime: Option[(crypto.Hash, Time.Timestamp)], submitters: Set[Party], nodes: Seq[GenNode.WithTxValue[Value.NodeId, Value.ContractId]], ledgerEffectiveTime: Time.Timestamp, ): Result[(Transaction.Transaction, Boolean)] = { - val transactionSeedAndSubmissionTime = submissionSeedAndTime.map { - case (seed, time) => - crypto.Hash.deriveTransactionSeed(seed, participantId, time) -> time - } - val commandTranslation = new CommandPreprocessor(_compiledPackages) + val values = ImmArray(nodes).map(translateNode(commandTranslation)) for { - commands <- Result.sequence(ImmArray(nodes).map(translateNode(commandTranslation))) + commands <- Result.sequence(values) checkSubmitterInMaintainers <- ShouldCheckSubmitterInMaintainers( _compiledPackages, commands.map(_.templateId)) @@ -171,7 +166,7 @@ final class Engine { submitters = submitters, commands = commands, ledgerTime = ledgerEffectiveTime, - transactionSeedAndSubmissionTime, + rootSeedAndSubmissionTime, ) } yield result } 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 6c02ac51b375..8bcef5b0e86a 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 @@ -452,16 +452,14 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf } "reinterpret to the same result" in { - val Right((tx, metaData)) = interpretResult + val Right((tx, txMeta)) = interpretResult val txRoots = tx.roots.map(id => tx.nodes(id)).toSeq + val txSeed = + crypto.Hash.deriveTransactionSeed(submissionSeed, participant, txMeta.submissionTime) + val Right((rtx, _)) = engine - .reinterpret( - Some(submissionSeed -> metaData.submissionTime), - participant, - Set(party), - txRoots, - let) + .reinterpret(Some(txSeed -> txMeta.submissionTime), Set(party), txRoots, let) .consume(lookupContract, lookupPackage, lookupKey) (tx isReplayedBy rtx) shouldBe true } @@ -490,7 +488,7 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf val templateId = Identifier(basicTestsPkgId, "BasicTests:Simple") val hello = Identifier(basicTestsPkgId, "BasicTests:Hello") val let = Time.Timestamp.now() - val transactionSeed = crypto.Hash.deriveTransactionSeed(submissionSeed, participant, let) + val txSeed = crypto.Hash.deriveTransactionSeed(submissionSeed, participant, let) val command = ExerciseCommand( templateId, @@ -513,7 +511,7 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf submitters = Set(party), commands = r, ledgerTime = let, - transactionSeedAndSubmissionTime = Some(transactionSeed -> let) + transactionSeedAndSubmissionTime = Some(txSeed -> let) ) .consume(lookupContract, lookupPackage, lookupKey)) val Right((tx, _)) = interpretResult @@ -527,8 +525,9 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf "reinterpret to the same result" in { val txRoots = tx.roots.map(id => tx.nodes(id)).toSeq + val Right((rtx, _)) = engine - .reinterpret(Some(submissionSeed -> let), participant, Set(party), txRoots, let) + .reinterpret(Some(txSeed -> let), Set(party), txRoots, let) .consume(lookupContract, lookupPackage, lookupKey) (tx isReplayedBy rtx) shouldBe true } @@ -585,7 +584,7 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf val submissionSeed = hash("exercise-by-key command with existing key") val templateId = Identifier(basicTestsPkgId, "BasicTests:WithKey") val let = Time.Timestamp.now() - val transactionSeed = crypto.Hash.deriveTransactionSeed(submissionSeed, participant, let) + val txSeed = crypto.Hash.deriveTransactionSeed(submissionSeed, participant, let) val command = ExerciseByKeyCommand( templateId, ValueRecord(None, ImmArray((None, ValueParty(alice)), (None, ValueInt64(42)))), @@ -608,7 +607,7 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf submitters = Set(alice), commands = r, ledgerTime = let, - transactionSeedAndSubmissionTime = Some(transactionSeed -> let) + transactionSeedAndSubmissionTime = Some(txSeed -> let) ) .consume(lookupContractWithKey, lookupPackage, lookupKey)) .map(_._1) @@ -626,7 +625,7 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf val txRoots = tx.roots.map(id => tx.nodes(id)).toSeq val reinterpretResult = engine - .reinterpret(Some(submissionSeed -> let), participant, Set(alice), txRoots, let) + .reinterpret(Some(txSeed -> let), Set(alice), txRoots, let) .consume(lookupContractWithKey, lookupPackage, lookupKey) .map(_._1) (result |@| reinterpretResult)(_ isReplayedBy _) shouldBe Right(true) @@ -661,7 +660,7 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf val templateId = Identifier(basicTestsPkgId, "BasicTests:Simple") val hello = Identifier(basicTestsPkgId, "BasicTests:Hello") val let = Time.Timestamp.now() - val transactionSeed = crypto.Hash.deriveTransactionSeed(submissionSeed, participant, let) + val txSeed = crypto.Hash.deriveTransactionSeed(submissionSeed, participant, let) val command = CreateAndExerciseCommand( templateId, @@ -685,7 +684,7 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf submitters = Set(party), commands = r, ledgerTime = let, - transactionSeedAndSubmissionTime = Some(transactionSeed -> let) + transactionSeedAndSubmissionTime = Some(txSeed -> let) ) .consume(lookupContract, lookupPackage, lookupKey)) .map(_._1) @@ -703,7 +702,7 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf val txRoots = tx.roots.map(id => tx.nodes(id)).toSeq val reinterpretResult = engine - .reinterpret(Some(submissionSeed -> let), participant, Set(party), txRoots, let) + .reinterpret(Some(txSeed -> let), Set(party), txRoots, let) .consume(lookupContract, lookupPackage, lookupKey) .map(_._1) (interpretResult |@| reinterpretResult)(_ isReplayedBy _) shouldBe Right(true) @@ -901,7 +900,7 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf val submissionTime = meta.submissionTime - val transactionSeed = + val txSeed = crypto.Hash.deriveTransactionSeed(submissionSeed, participant, submissionTime) val Right(cmds) = commandTranslator .preprocessCommands(Commands(bob, ImmArray(command), let, "test")) @@ -913,7 +912,7 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf submitters = Set(bob), commands = cmds, ledgerTime = let, - transactionSeedAndSubmissionTime = Some(transactionSeed -> submissionTime) + transactionSeedAndSubmissionTime = Some(txSeed -> submissionTime) ) .consume(lookupContractForPayout, lookupPackage, lookupKey) @@ -928,7 +927,7 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf val txRoots = tx.roots.map(id => tx.nodes(id)).toSeq val Right((rtx, _)) = engine - .reinterpret(Some(submissionSeed -> submissionTime), participant, Set(bob), txRoots, let) + .reinterpret(Some(txSeed -> submissionTime), Set(bob), txRoots, let) .consume(lookupContractForPayout, lookupPackage, lookupKey) (rtx isReplayedBy tx) shouldBe true } @@ -1161,7 +1160,7 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf case (nid, n) => val fetchTx = GenTx(HashMap(nid -> n), ImmArray(nid)) val Right((reinterpreted, _)) = engine - .reinterpret(None, participant, n.requiredAuthorizers, Seq(n), let) + .reinterpret(None, n.requiredAuthorizers, Seq(n), let) .consume(lookupContract, lookupPackage, lookupKey) (fetchTx isReplayedBy reinterpreted) shouldBe true } @@ -1212,9 +1211,10 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf ) val let = Time.Timestamp.now() + val txSeed = crypto.Hash.deriveTransactionSeed(submissionSeed, participant, let) val reinterpreted = engine - .reinterpret(Some(submissionSeed -> let), participant, Set.empty, Seq(fetchNode), let) + .reinterpret(Some(txSeed -> let), Set.empty, Seq(fetchNode), let) .consume(lookupContract, lookupPackage, lookupKey) reinterpreted shouldBe 'right @@ -1268,19 +1268,27 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf lookerUpCid, "Lookup", ValueRecord(None, ImmArray((Some[Name]("n"), ValueInt64(42))))) - val Right((tx, txMeta @ _)) = engine + val Right((tx, txMeta)) = engine .submit( Commands(alice, ImmArray(exerciseCmd), now, "test"), participant, Some(submissionSeed)) .consume(lookupContractMap.get, lookupPackage, lookupKey) + val txSeed = + crypto.Hash.deriveTransactionSeed(submissionSeed, participant, txMeta.submissionTime) + val Some(lookupNode) = firstLookupNode(tx) lookupNode.result shouldBe Some(lookedUpCid) val freshEngine = Engine() val Right((reinterpreted, dependsOnTime @ _)) = freshEngine - .reinterpret(Some(submissionSeed -> now), participant, Set.empty, Seq(lookupNode), now) + .reinterpret( + Some(txSeed -> txMeta.submissionTime), + Set.empty, + Seq(lookupNode), + now, + ) .consume(lookupContract, lookupPackage, lookupKey) firstLookupNode(reinterpreted) shouldEqual Some(lookupNode) @@ -1298,13 +1306,15 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf participant, Some(submissionSeed)) .consume(lookupContractMap.get, lookupPackage, lookupKey) + val txSeed = + crypto.Hash.deriveTransactionSeed(submissionSeed, participant, txMeta.submissionTime) val Some(lookupNode) = firstLookupNode(tx) lookupNode.result shouldBe None val freshEngine = Engine() val Right((reinterpreted, dependsOnTime @ _)) = freshEngine - .reinterpret(Some(submissionSeed -> now), participant, Set.empty, Seq(lookupNode), now) + .reinterpret(Some(txSeed -> now), Set.empty, Seq(lookupNode), now) .consume(lookupContract, lookupPackage, lookupKey) firstLookupNode(reinterpreted) shouldEqual Some(lookupNode) @@ -1417,6 +1427,69 @@ class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunf } } + "nested transactions" should { + + val forkableTemplate = "BasicTests:Forkable" + val forkableTemplateId = Identifier(basicTestsPkgId, forkableTemplate) + val forkableInst = + ValueRecord( + Some(forkableTemplateId), + ImmArray( + (Some[Name]("party"), ValueParty(alice)), + (Some[Name]("parent"), ValueOptional(None))) + ) + + val submissionSeed = hash("nested transaction test") + val let = Time.Timestamp.now() + + def run(n: Int) = { + val command = CreateAndExerciseCommand( + templateId = forkableTemplateId, + createArgument = forkableInst, + choiceId = "Fork", + choiceArgument = ValueRecord(None, ImmArray((None, ValueInt64(n.toLong)))), + ) + engine + .submit(Commands(party, ImmArray(command), let, "test"), participant, Some(submissionSeed)) + .consume(_ => None, lookupPackage, _ => None) + } + + "produce a quadratic number of nodes" in { + run(0).map(_._1.nodes.size) shouldBe Right(2) + run(1).map(_._1.nodes.size) shouldBe Right(6) + run(2).map(_._1.nodes.size) shouldBe Right(14) + run(3).map(_._1.nodes.size) shouldBe Right(30) + } + + "be validable in whole" in { + def validate(tx: Tx.Transaction, metaData: Tx.Metadata) = + engine + .validate(tx, let, participant, Some(submissionSeed -> metaData.submissionTime)) + .consume(_ => None, lookupPackage, _ => None) + + run(0).flatMap { case (tx, metaData) => validate(tx, metaData) } shouldBe Right(()) + run(3).flatMap { case (tx, metaData) => validate(tx, metaData) } shouldBe Right(()) + } + + "be partially reinterpretable" in { + val Right((tx, metaData)) = run(3) + val ImmArray(_, exeNode1) = tx.roots + val NodeExercises(Some(exeNode1Seed), _, _, _, _, _, _, _, _, _, _, children, _, _) = + tx.nodes(exeNode1) + val ImmArray(createNode2, exeNode2, _, _) = children + + engine + .reinterpret( + Some(exeNode1Seed -> metaData.submissionTime), + Set(party), + List(createNode2, exeNode2).map(tx.nodes), + let + ) + .consume(_ => None, lookupPackage, _ => None) shouldBe 'right + + } + } + } object EngineTest { 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 36464f32f608..53392e06500a 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 @@ -453,6 +453,6 @@ case class PartialTransaction( } def deriveChildSeed: Option[crypto.Hash] = - context.contextSeed.map(crypto.Hash.deriveNodeSeed(_, nodes.size)) + context.contextSeed.map(crypto.Hash.deriveNodeSeed(_, context.children.length)) } diff --git a/daml-lf/tests/BasicTests.daml b/daml-lf/tests/BasicTests.daml index 3038a9ec7029..95eab3f136ed 100644 --- a/daml-lf/tests/BasicTests.daml +++ b/daml-lf/tests/BasicTests.daml @@ -384,6 +384,29 @@ template Fetcher with cid : ContractId Fetched do fetch cid + +template Forkable + with + party: Party + parent: Optional (ContractId Forkable) + where + signatory party + + controller party can + Fork: [ContractId Forkable] + with n: Int + do + if (n == 0) then + pure [self] + else + do + c1 <- create Forkable with party = party, parent = Some self + l1 <- exercise c1 Fork with n = n-1 + c2 <- create Forkable with party = party, parent = Some self + l2 <- exercise c2 Fork with n = n-1 + pure (l1 ++ l2) + + data Nesting0 = Nesting0 {x: Nesting1} data Nesting1 = Nesting1 {x: Nesting2} data Nesting2 = Nesting2 {x: Nesting3}