diff --git a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala index 0b031ed33d8b..2bf54bf3f4d2 100644 --- a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala +++ b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala @@ -6,16 +6,20 @@ package speedy import com.daml.lf.data.Ref import com.daml.lf.data.Ref.{Location, Party} +import com.daml.lf.interpretation.Error._ import com.daml.lf.language.Ast._ import com.daml.lf.language.{PackageInterface} import com.daml.lf.speedy.Compiler.FullStackTrace +import com.daml.lf.speedy.SError._ import com.daml.lf.speedy.SExpr._ -import com.daml.lf.speedy.SValue._ import com.daml.lf.speedy.SResult._ +import com.daml.lf.speedy.SValue._ import com.daml.lf.testing.parser.Implicits._ import com.daml.lf.validation.Validation +import com.daml.lf.value.Value.ContractId +import org.scalatest.Inside import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec +import org.scalatest.freespec.AnyFreeSpec import scala.collection.mutable.ArrayBuffer class TestTraceLog extends TraceLog { @@ -30,25 +34,49 @@ class TestTraceLog extends TraceLog { def getMessages: Seq[String] = messages.view.map(_._1).toSeq } -class EvaluationOrderTest extends AnyWordSpec with Matchers { +class EvaluationOrderTest extends AnyFreeSpec with Matchers with Inside { - val pkgs: PureCompiledPackages = typeAndCompile(p""" + private val pkgs: PureCompiledPackages = typeAndCompile(p""" module M { - record @serializable T = { signatory : Party, observer : Party } ; + record @serializable TKey = { maintainers : List Party, optCid : Option (ContractId Unit), nested: M:Nested }; + + record @serializable Nested = { f : Option M:Nested }; + + val buildNested : Int64 -> M:Nested = \(i: Int64) -> + case (EQUAL @Int64 i 0) of + True -> M:Nested { f = None @M:Nested } + | _ -> M:Nested { f = Some @M:Nested (M:buildNested (SUB_INT64 i 1)) }; + + val toKey : Party -> M:TKey = \(p : Party) -> + M:TKey { maintainers = Cons @Party [p] (Nil @Party), optCid = None @(ContractId Unit), nested = M:buildNested 0 }; + val keyNoMaintainers : M:TKey = M:TKey { maintainers = Nil @Party, optCid = None @(ContractId Unit), nested = M:buildNested 0 }; + val toKeyWithCid : Party -> ContractId Unit -> M:TKey = \(p : Party) (cid : ContractId Unit) -> M:TKey { maintainers = Cons @Party [p] (Nil @Party), optCid = Some @(ContractId Unit) cid, nested = M:buildNested 0 }; + + + record @serializable T = { signatory : Party, observer : Party, precondition : Bool, key: M:TKey, nested: M:Nested }; template (this : T) = { - precondition TRACE @Bool "precondition" True; + precondition TRACE @Bool "precondition" (M:T {precondition} this); signatories TRACE @(List Party) "signatories" (Cons @Party [M:T {signatory} this] (Nil @Party)); observers TRACE @(List Party) "observers" (Cons @Party [M:T {observer} this] (Nil @Party)); agreement TRACE @Text "agreement" ""; - key @Party - (TRACE @Party "key" (M:T {signatory} this)) - (\(key : Party) -> TRACE @(List Party) "maintainers" (Cons @Party [key] (Nil @Party))); + key @M:TKey + (TRACE @M:TKey "key" (M:T {key} this)) + (\(key : M:TKey) -> TRACE @(List Party) "maintainers" (M:TKey {maintainers} key)); }; } """) private val seed = crypto.Hash.hashPrivateKey("seed") + private val allTraces = Seq( + "precondition", + "agreement", + "signatories", + "observers", + "key", + "maintainers", + ) + private def evalUpdateApp( pkgs: CompiledPackages, e: Expr, @@ -67,24 +95,155 @@ class EvaluationOrderTest extends AnyWordSpec with Matchers { private val alice = Ref.Party.assertFromString("alice") private val bob = Ref.Party.assertFromString("bob") - "evaluation order" should { - "evaluate in correct order for successful create" in { - val (res, msgs) = evalUpdateApp( - pkgs, - e"\(sig : Party) (obs : Party) -> create @M:T M:T { signatory = sig, observer = obs }", - Array(SParty(alice), SParty(bob)), - alice, - ) - res shouldBe a[SResultFinalValue] - msgs shouldBe Seq( - "precondition", - "agreement", - "signatories", - "observers", - "key", - "maintainers", - ) - succeed + // We cover all errors for each node in the order they are defined + // in com.daml.lf.interpretation.Error. + // We don’t check for exceptions/aborts during evaluation of an expression instead + // assume that those always stop at the point of the corresponding + // trace statement. + // The important cases to test are ones that result in either a different transaction + // or a transaction that is rejected vs one that is accepted. Cases where the transaction + // is rejected in both cases “only” change the error message which is relatively harmless. + // Specifically this means that we need to test ordering of catchable errors + // relative to other catchable errors and other non-catchable errors but we don’t + // need to check ordering of non-catchable errors relative to other non-cachable errors. + + "evaluation order" - { + "create node" - { + // TEST_EVIDENCE: Semantics: Evaluation order of successful create + "successful create" in { + val (res, msgs) = evalUpdateApp( + pkgs, + e"""\(sig : Party) (obs : Party) -> create @M:T + M:T { signatory = sig, observer = obs, precondition = True, key = M:toKey sig, nested = M:buildNested 0 } + """, + Array(SParty(alice), SParty(bob)), + alice, + ) + res shouldBe a[SResultFinalValue] + msgs shouldBe allTraces + } + // TEST_EVIDENCE: Semantics: Evaluation order of create with failed precondition + "failed precondition" in { + // Note that for LF >= 1.14 we don’t hit this as the compiler + // generates code that throws an exception instead of returning False. + val (res, msgs) = evalUpdateApp( + pkgs, + e"""\(sig : Party) (obs : Party) -> create @M:T + M:T { signatory = sig, observer = obs, precondition = False, key = M:toKey sig, nested = M:buildNested 0 } + """, + Array(SParty(alice), SParty(bob)), + alice, + ) + inside(res) { case SResultError(SErrorDamlException(err)) => + err shouldBe a[TemplatePreconditionViolated] + + } + msgs shouldBe Seq( + "precondition" + ) + } + // TEST_EVIDENCE: Semantics: Evaluation order of create with duplicate contract key + "duplicate contract key" in { + val (res, msgs) = evalUpdateApp( + pkgs, + e"""\(sig : Party) (obs : Party) -> + let c: M:T = M:T { signatory = sig, observer = obs, precondition = True, key = M:toKey sig, nested = M:buildNested 0 } + in sbind x : ContractId M:T <- create @M:T c + in create @M:T c + """, + Array(SParty(alice), SParty(bob)), + alice, + ) + inside(res) { case SResultError(SErrorDamlException(err)) => + err shouldBe a[DuplicateContractKey] + } + msgs shouldBe allTraces ++ allTraces + } + // TEST_EVIDENCE: Semantics: Evaluation order of create with empty contract key maintainers + "empty contract key maintainers" in { + val (res, msgs) = evalUpdateApp( + pkgs, + e"""\(sig : Party) (obs : Party) -> create @M:T + M:T { signatory = sig, observer = obs, precondition = True, key = M:keyNoMaintainers, nested = M:buildNested 0 } + """, + Array(SParty(alice), SParty(bob)), + alice, + ) + inside(res) { case SResultError(SErrorDamlException(err)) => + err shouldBe a[CreateEmptyContractKeyMaintainers] + + } + msgs shouldBe allTraces + } + // TEST_EVIDENCE: Semantics: Evaluation order of create with authorization failure + "authorization failure" in { + val (res, msgs) = evalUpdateApp( + pkgs, + e"""\(sig : Party) (obs : Party) -> create @M:T + M:T { signatory = sig, observer = obs, precondition = True, key = M:toKey sig, nested = M:buildNested 0 } + """, + Array(SParty(alice), SParty(bob)), + bob, + ) + inside(res) { case SResultError(SErrorDamlException(err)) => + err shouldBe a[FailedAuthorization] + + } + msgs shouldBe allTraces + } + // TEST_EVIDENCE: Semantics: Evaluation order of create with contract id in contract key + "contract id in contract key" in { + val (res, msgs) = evalUpdateApp( + pkgs, + e"""\(sig : Party) (obs : Party) (cid : ContractId Unit) -> create @M:T + M:T { signatory = sig, observer = obs, precondition = True, key = M:toKeyWithCid sig cid, nested = M:buildNested 0 } + """, + Array( + SParty(alice), + SParty(bob), + SContractId(ContractId.V1.assertFromString("00" * 32 + "0000")), + ), + alice, + ) + inside(res) { case SResultError(SErrorDamlException(err)) => + err shouldBe a[ContractIdInContractKey] + + } + msgs shouldBe allTraces + } + // TEST_EVIDENCE: Semantics: Evaluation order of create with create argument exceeding max nesting + "create argument exceeds max nesting" in { + val (res, msgs) = evalUpdateApp( + pkgs, + e"""\(sig : Party) (obs : Party) -> create @M:T + M:T { signatory = sig, observer = obs, precondition = True, key = M:toKey sig, nested = M:buildNested 100 } + """, + Array(SParty(alice), SParty(bob)), + alice, + ) + inside(res) { case SResultError(SErrorDamlException(err)) => + err shouldBe ValueExceedsMaxNesting + + } + msgs shouldBe allTraces + } + // TEST_EVIDENCE: Semantics: Evaluation order of create with contract key exceeding max nesting + "contract key exceeds max nesting" in { + val (res, msgs) = evalUpdateApp( + pkgs, + e"""\(sig : Party) (obs : Party) -> create @M:T + let key: M:TKey = M:TKey { maintainers = Cons @Party [sig] (Nil @Party), optCid = None @(ContractId Unit), nested = M:buildNested 100 } + in M:T { signatory = sig, observer = obs, precondition = True, key = key, nested = M:buildNested 0 } + """, + Array(SParty(alice), SParty(bob)), + alice, + ) + inside(res) { case SResultError(SErrorDamlException(err)) => + err shouldBe ValueExceedsMaxNesting + + } + msgs shouldBe allTraces + } } } diff --git a/security-evidence.md b/security-evidence.md index 328cbc28e086..865728bb5d8d 100644 --- a/security-evidence.md +++ b/security-evidence.md @@ -29,6 +29,14 @@ - ensure correct privacy for rollback subtree: [BlindingSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/BlindingSpec.scala#L201) ## Semantics: +- Evaluation order of create with authorization failure: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L178) +- Evaluation order of create with contract id in contract key: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L194) +- Evaluation order of create with contract key exceeding max nesting: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L230) +- Evaluation order of create with create argument exceeding max nesting: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L214) +- Evaluation order of create with duplicate contract key: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L145) +- Evaluation order of create with empty contract key maintainers: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L162) +- Evaluation order of create with failed precondition: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L125) +- Evaluation order of successful create: [EvaluationOrderTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/EvaluationOrderTest.scala#L112) - Exceptions, throw/catch.: [ExceptionTest.scala](daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ExceptionTest.scala#L25) - contract key behaviour (non-unique mode): [ContractKeySpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ContractKeySpec.scala#L383) - contract key behaviour (unique mode): [ContractKeySpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ContractKeySpec.scala#L389)