Skip to content

Commit

Permalink
Add a guard when exercising by interface. (#11836)
Browse files Browse the repository at this point in the history
* Add a guard when exercising by interface.

This fixes part of #11703, when exercising an inherited choice by
interface and you know the template id, via the command preprocessor.

It does this by inserting a "guard" in between the interface fetch and
the exercise body. The guard is a function Interface -> Bool, which
is general enough to check the template id, without complicating too
much in speedy. And can be generalized in the future to check more,
like signatories, etc.

I added the guard as an optional argument to UExerciseByInterface.
This isn't hooked up to the protobuf AST yet (or Haskell side for
that matter) -- but I'll do it in the next PR! For now you can invoke
the guarded exercise via the command preprocessor, so I can enable the
approprate engine tests. (There's still some failing fetch tests left,
but I decided to leave this for later. Fetch can be a lot simpler than
guarded choices, since you always add a fetch node. No need for fancy
continuations.)

changelog_begin
changelog_end

* scalafmt

* Feedback and fix matches

* Update comments, we are always going to abort the transaction

* Raise WronglyTypedContract in SBGuardTemplateId.

* rebase and fix parser

* restore ANF

* scalafmt
  • Loading branch information
sofiafaro-da authored Nov 24, 2021
1 parent 7c3a2a7 commit 5c12d75
Show file tree
Hide file tree
Showing 19 changed files with 265 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ final class Conversions(
builder.setComparableValueError(proto.Empty.newBuilder)
case ValueExceedsMaxNesting =>
builder.setValueExceedsMaxNesting(proto.Empty.newBuilder)
case _: ChoiceGuardFailed =>
// TODO https://github.com/digital-asset/daml/issues/11703
// Implement this.
builder.setCrash(s"ChoiceGuardFailed unhandled in scenario service")
}
}
case Error.ContractNotEffective(coid, tid, effectiveAt) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1304,6 +1304,7 @@ private[archive] class DecodeV1(minor: LV.Minor) {
choice = handleInternedName(exercise.getChoiceInternedStr),
cidE = decodeExpr(exercise.getCid, definition),
argE = decodeExpr(exercise.getArg, definition),
guardE = None, // TODO https://github.com/digital-asset/daml/issues/11703
)

case PLF.Update.SumCase.EXERCISE_BY_KEY =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,9 @@ private[daml] class EncodeV1(minor: LV.Minor) {
b.setCid(cid)
b.setArg(arg)
builder.setExercise(b)
case UpdateExerciseInterface(interface, choice, cid, arg) =>
case UpdateExerciseInterface(interface, choice, cid, arg, guard @ _) =>
// TODO https://github.com/digital-asset/daml/issues/11703
// Encode guard.
val b = PLF.Update.ExerciseInterface.newBuilder()
b.setInterface(interface)
setInternedString(choice, b.setChoiceInternedStr)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,18 +166,14 @@ class InterfacesTest
preprocess(command) shouldBe a[Left[_, _]]
}

// TODO https://github.com/digital-asset/daml/issues/11703
// Enable these tests.
/*
"be unable to exercise T1 (disguised as T2) by interface I1" in {
val command = ExerciseCommand(idT2, cid1, "C1", ValueRecord(None, ImmArray.empty))
run(command) shouldBe a[Left[_, _]]
}
"be unable to exercise T2 (disguised as T1) by interface I1" in {
val command = ExerciseCommand(idT1, cid2, "C1", ValueRecord(None, ImmArray.empty))
run(command) shouldBe a[Left[_, _]]
}
*/
"be unable to exercise T1 (disguised as T2) by interface I1" in {
val command = ExerciseCommand(idT2, cid1, "C1", ValueRecord(None, ImmArray.empty))
run(command) shouldBe a[Left[_, _]]
}
"be unable to exercise T2 (disguised as T1) by interface I1" in {
val command = ExerciseCommand(idT1, cid2, "C1", ValueRecord(None, ImmArray.empty))
run(command) shouldBe a[Left[_, _]]
}
"be unable to exercise T2 (disguised as T1) by interface I2 (stopped in preprocessor)" in {
val command = ExerciseCommand(idT1, cid2, "C2", ValueRecord(None, ImmArray.empty))
preprocess(command) shouldBe a[Left[_, _]]
Expand Down Expand Up @@ -230,18 +226,16 @@ class InterfacesTest
preprocess(command) shouldBe a[Left[_, _]]
}

// TODO https://github.com/digital-asset/daml/issues/11703
// Enable these tests.
/*
"be unable to exercise T1 (disguised as T2) by interface I1 via 'exercise by interface'" in {
val command = ExerciseByInterfaceCommand(idI2, idT2, cid1, "C1", ValueRecord(None, ImmArray.empty))
run(command) shouldBe a[Left[_, _]]
}
"be unable to exercise T2 (disguised as T1) by interface I1 via 'exercise by interface'" in {
val command = ExerciseByInterfaceCommand(idI1, idT1, cid2, "C1", ValueRecord(None, ImmArray.empty))
run(command) shouldBe a[Left[_, _]]
}
*/
"be unable to exercise T1 (disguised as T2) by interface I1 via 'exercise by interface'" in {
val command =
ExerciseByInterfaceCommand(idI1, idT2, cid1, "C1", ValueRecord(None, ImmArray.empty))
run(command) shouldBe a[Left[_, _]]
}
"be unable to exercise T2 (disguised as T1) by interface I1 via 'exercise by interface'" in {
val command =
ExerciseByInterfaceCommand(idI1, idT1, cid2, "C1", ValueRecord(None, ImmArray.empty))
run(command) shouldBe a[Left[_, _]]
}
"be unable to exercise T2 (disguised as T1) by interface I2 via 'exercise by interface' (stopped in preprocessor)" in {
val command =
ExerciseByInterfaceCommand(idI2, idT1, cid2, "C2", ValueRecord(None, ImmArray.empty))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ private[lf] final class Compiler(
private val Env2 = Env1.pushVar
private val Position3 = Env2.nextPosition
private val Env3 = Env2.pushVar
private val Position4 = Env3.nextPosition
private val Env4 = Env3.pushVar

private[this] def topLevelFunction1[SDefRef <: t.SDefinitionRef: LabelModule.Allowed](
ref: SDefRef
Expand All @@ -322,6 +324,13 @@ private[lf] final class Compiler(
): (SDefRef, SDefinition) =
topLevelFunction(ref)(s.SEAbs(3, body(Position1, Position2, Position3, Env3)))

private[this] def topLevelFunction4[SDefRef <: t.SDefinitionRef: LabelModule.Allowed](
ref: SDefRef
)(
body: (Position, Position, Position, Position, Env) => s.SExpr
): (SDefRef, SDefinition) =
topLevelFunction(ref)(s.SEAbs(4, body(Position1, Position2, Position3, Position4, Env4)))

@throws[PackageNotFound]
@throws[CompilationError]
def unsafeCompile(cmds: ImmArray[Command]): t.SExpr =
Expand Down Expand Up @@ -398,9 +407,10 @@ private[lf] final class Compiler(
addDef(compileCreateInterface(identifier))
addDef(compileFetchInterface(identifier))
addDef(compileInterfacePrecond(identifier, iface.param, iface.precond))
iface.fixedChoices.values.foreach(
builder += compileFixedChoice(identifier, iface.param, _)
)
iface.fixedChoices.values.foreach { choice =>
addDef(compileInterfaceChoice(identifier, iface.param, choice))
addDef(compileInterfaceGuardedChoice(identifier, iface.param, choice))
}
}

builder.result()
Expand Down Expand Up @@ -848,8 +858,14 @@ private[lf] final class Compiler(
t.CreateDefRef(iface)(compile(env, arg))
case UpdateExercise(tmplId, chId, cidE, argE) =>
t.ChoiceDefRef(tmplId, chId)(compile(env, cidE), compile(env, argE))
case UpdateExerciseInterface(ifaceId, chId, cidE, argE) =>
case UpdateExerciseInterface(ifaceId, chId, cidE, argE, None) =>
t.ChoiceDefRef(ifaceId, chId)(compile(env, cidE), compile(env, argE))
case UpdateExerciseInterface(ifaceId, chId, cidE, argE, Some(guardE)) =>
t.GuardedChoiceDefRef(ifaceId, chId)(
compile(env, cidE),
compile(env, argE),
compile(env, guardE),
)
case UpdateExerciseByKey(tmplId, chId, keyE, argE) =>
t.ChoiceByKeyDefRef(tmplId, chId)(compile(env, keyE), compile(env, argE))
case UpdateGetTime =>
Expand Down Expand Up @@ -1078,9 +1094,33 @@ private[lf] final class Compiler(
}
}

// Apply choice guard (if given) and abort transaction if false.
// Otherwise continue with exercise.
private[this] def withChoiceGuard(
env: Env,
guardPos: Option[Position],
payloadPos: Position,
cidPos: Position,
choiceName: ChoiceName,
byInterface: Option[TypeConName],
)(body: Env => s.SExpr): s.SExpr = {
guardPos match {
case None => body(env)
case Some(guardPos) =>
let(
env,
SBApplyChoiceGuard(choiceName, byInterface)(
env.toSEVar(guardPos),
env.toSEVar(payloadPos),
env.toSEVar(cidPos),
),
) { (_, _env) => body(_env) }
}
}

// TODO https://github.com/digital-asset/daml/issues/10810:
// Try to factorise this with compileChoiceBody above.
private[this] def compileFixedChoiceBody(
private[this] def compileInterfaceChoiceBody(
env: Env,
ifaceId: TypeConName,
param: ExprVarName,
Expand All @@ -1089,38 +1129,69 @@ private[lf] final class Compiler(
choiceArgPos: Position,
cidPos: Position,
tokenPos: Position,
guardPos: Option[Position],
) =
let(env, SBUFetchInterface(ifaceId)(env.toSEVar(cidPos))) { (payloadPos, _env) =>
val env = _env.bindExprVar(param, payloadPos).bindExprVar(choice.argBinder._1, choiceArgPos)
let(
env,
SBResolveSBUBeginExercise(choice.name, choice.consuming, byKey = false, ifaceId = ifaceId)(
env.toSEVar(payloadPos),
env.toSEVar(choiceArgPos),
env.toSEVar(cidPos),
compile(env, choice.controllers),
choice.choiceObservers match {
case Some(observers) => compile(env, observers)
case None => s.SEValue.EmptyList
},
),
) { (_, _env) =>
val env = _env.bindExprVar(choice.selfBinder, cidPos)
s.SEScopeExercise(app(compile(env, choice.update), env.toSEVar(tokenPos)))
withChoiceGuard(
env = env,
guardPos = guardPos,
payloadPos = payloadPos,
cidPos = cidPos,
choiceName = choice.name,
byInterface = Some(ifaceId),
) { env =>
let(
env,
SBResolveSBUBeginExercise(
choice.name,
choice.consuming,
byKey = false,
ifaceId = ifaceId,
)(
env.toSEVar(payloadPos),
env.toSEVar(choiceArgPos),
env.toSEVar(cidPos),
compile(env, choice.controllers),
choice.choiceObservers match {
case Some(observers) => compile(env, observers)
case None => s.SEValue.EmptyList
},
),
) { (_, _env) =>
val env = _env.bindExprVar(choice.selfBinder, cidPos)
s.SEScopeExercise(app(compile(env, choice.update), env.toSEVar(tokenPos)))
}
}
}

private[this] def compileFixedChoice(
private[this] def compileInterfaceChoice(
ifaceId: TypeConName,
param: ExprVarName,
choice: TemplateChoice,
): (t.SDefinitionRef, SDefinition) =
topLevelFunction3(t.ChoiceDefRef(ifaceId, choice.name)) {
(cidPos, choiceArgPos, tokenPos, env) =>
compileFixedChoiceBody(env, ifaceId, param, choice)(
compileInterfaceChoiceBody(env, ifaceId, param, choice)(
choiceArgPos,
cidPos,
tokenPos,
None,
)
}

private[this] def compileInterfaceGuardedChoice(
ifaceId: TypeConName,
param: ExprVarName,
choice: TemplateChoice,
): (t.SDefinitionRef, SDefinition) =
topLevelFunction4(t.GuardedChoiceDefRef(ifaceId, choice.name)) {
(cidPos, choiceArgPos, guardPos, tokenPos, env) =>
compileInterfaceChoiceBody(env, ifaceId, param, choice)(
choiceArgPos,
cidPos,
tokenPos,
Some(guardPos),
)
}

Expand Down Expand Up @@ -1424,17 +1495,33 @@ private[lf] final class Compiler(
}
}

private[this] def compileExerciseByInterface(
interfaceId: TypeConName,
templateId: TypeConName,
contractId: SValue,
choiceId: ChoiceName,
argument: SValue,
): s.SExpr =
unaryFunction(Env.Empty) { (tokenPos, env) =>
let(env, SBGuardTemplateId(templateId)(s.SEValue(contractId))) { (guardPos, env) =>
t.GuardedChoiceDefRef(interfaceId, choiceId)(
s.SEValue(contractId),
s.SEValue(argument),
env.toSEVar(guardPos),
env.toSEVar(tokenPos),
)
}
}

private[this] def compileCommand(cmd: Command): s.SExpr = cmd match {
case Command.Create(templateId, argument) =>
t.CreateDefRef(templateId)(s.SEValue(argument))
case Command.CreateByInterface(interfaceId, templateId, argument) =>
t.CreateByInterfaceDefRef(templateId, interfaceId)(s.SEValue(argument))
case Command.Exercise(templateId, contractId, choiceId, argument) =>
t.ChoiceDefRef(templateId, choiceId)(s.SEValue(contractId), s.SEValue(argument))
case Command.ExerciseByInterface(interfaceId, templateId @ _, contractId, choiceId, argument) =>
// TODO https://github.com/digital-asset/daml/issues/11703
// Ensure that fetched template has expected templateId.
t.ChoiceDefRef(interfaceId, choiceId)(s.SEValue(contractId), s.SEValue(argument))
case Command.ExerciseByInterface(interfaceId, templateId, contractId, choiceId, argument) =>
compileExerciseByInterface(interfaceId, templateId, contractId, choiceId, argument)
case Command.ExerciseInterface(interfaceId, contractId, choiceId, argument) =>
t.ChoiceDefRef(interfaceId, choiceId)(s.SEValue(contractId), s.SEValue(argument))
case Command.ExerciseByKey(templateId, contractKey, choiceId, argument) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ private[lf] object Pretty {
prettyContractId(key.cids.head)
case ValueExceedsMaxNesting =>
text(s"Value exceeds maximum nesting value of 100")
case ChoiceGuardFailed(cid, templateId, choiceName, byInterface) => (
text(s"Choice guard failed for") & prettyTypeConName(templateId) &
text(s"contract") & prettyContractId(cid) &
text(s"when exercising choice $choiceName") &
(byInterface match {
case None => text("by template")
case Some(interfaceId) => text("by interface") & prettyTypeConName(interfaceId)
})
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ object Profile {
implicit val implementsDefRef: Allowed[ImplementsDefRef] = allowAll
implicit val implementsMethodDefRef: Allowed[ImplementsMethodDefRef] = allowAll
implicit val choiceDefRef: Allowed[ChoiceDefRef] = allowAll
implicit val guardedChoiceDefRef: Allowed[GuardedChoiceDefRef] = allowAll
implicit val fetchDefRef: Allowed[FetchDefRef] = allowAll
implicit val choiceByKeyDefRef: Allowed[ChoiceByKeyDefRef] = allowAll
implicit val fetchByKeyDefRef: Allowed[FetchByKeyDefRef] = allowAll
Expand Down Expand Up @@ -273,6 +274,8 @@ object Profile {
case ImplementsMethodDefRef(tmplRef, ifaceId, methodName) =>
s"implementsMethod @${tmplRef.qualifiedName} @${ifaceId.qualifiedName} ${methodName}"
case ChoiceDefRef(tmplRef, name) => s"exercise @${tmplRef.qualifiedName} ${name}"
case GuardedChoiceDefRef(tmplRef, name) =>
s"guarded exercise @${tmplRef.qualifiedName} ${name}"
case FetchDefRef(tmplRef) => s"fetch @${tmplRef.qualifiedName}"
case ChoiceByKeyDefRef(tmplRef, name) =>
s"exerciseByKey @${tmplRef.qualifiedName} ${name}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,40 @@ private[lf] object SBuiltin {
}
}

final case class SBApplyChoiceGuard(
choiceName: ChoiceName,
byInterface: Option[TypeConName],
) extends SBuiltin(3) {
override private[speedy] def execute(
args: util.ArrayList[SValue],
machine: Machine,
): Unit = {
val guard = args.get(0)
val payload = getSRecord(args, 1)
val coid = getSContractId(args, 2)
val templateId = payload.id

machine.ctrl = SEApp(SEValue(guard), Array(SEValue(payload)))
machine.pushKont(KCheckChoiceGuard(machine, coid, templateId, choiceName, byInterface))
}
}

final case class SBGuardTemplateId(
templateId: TypeConName
) extends SBuiltin(2) {
override private[speedy] def execute(
args: util.ArrayList[SValue],
machine: Machine,
): Unit = {
val coid = getSContractId(args, 0)
val record = getSRecord(args, 1)
if (record.id != templateId)
machine.ctrl = SEDamlException(IE.WronglyTypedContract(coid, templateId, record.id))
else
machine.returnValue = SBool(true)
}
}

final case class SBResolveSBUBeginExercise(
choiceName: ChoiceName,
consuming: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,8 @@ object SExpr {
final case class LfDefRef(ref: DefinitionRef) extends SDefinitionRef
// references to definitions generated by the Speedy compiler
final case class ChoiceDefRef(ref: DefinitionRef, choiceName: ChoiceName) extends SDefinitionRef
final case class GuardedChoiceDefRef(ref: DefinitionRef, choiceName: ChoiceName)
extends SDefinitionRef
final case class ChoiceByKeyDefRef(ref: DefinitionRef, choiceName: ChoiceName)
extends SDefinitionRef
final case class CreateDefRef(ref: DefinitionRef) extends SDefinitionRef
Expand Down
Loading

0 comments on commit 5c12d75

Please sign in to comment.