LF: Simplify transaction versionning for interface (#11744)

We revert #11626, and just change the way transaction version is
computed:

- As before, Node version is calculated from the
  package of the template ID action.

- Transaction version is the max of the version of all the nodes,
   instead of the root nodes.

CHANGELOG_BEGIN
CHANGELOG_END
This commit is contained in:
Remy 2021-11-18 22:46:46 +01:00 committed by GitHub
parent 4b59c5731c
commit 1bb2fc28a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 89 additions and 187 deletions

View File

@ -14,40 +14,17 @@ import scala.Ordering.Implicits.infixOrderingOps
* @tparam V either [[com.daml.lf.language.LanguageVersion]] or
* [[com.daml.lf.transaction.TransactionVersion]].
*/
final case class VersionRange[V] private (
final case class VersionRange[V](
min: V,
max: V,
)(implicit ordering: Ordering[V])
extends data.NoCopy {
)(implicit ordering: Ordering[V]) {
def join(that: VersionRange[V]): VersionRange[V] =
new VersionRange(
min = this.min min that.min,
max = this.max max that.max,
)
require(min <= max)
def join(version: V): VersionRange[V] = join(VersionRange(version))
private[lf] def map[W](f: V => W)(implicit ordering: Ordering[W]): VersionRange[W] =
private[lf] def map[W](f: V => W)(implicit ordering: Ordering[W]) =
VersionRange(f(min), f(max))
def contains(v: V): Boolean =
min <= v && v <= max
}
object VersionRange {
def apply[V](min: V, max: V)(implicit ordering: Ordering[V]): VersionRange[V] = {
assert(min <= max)
new VersionRange(min, max)
}
def apply[V](version: V)(implicit ordering: Ordering[V]): VersionRange[V] =
new VersionRange(version, version)
// We represent an empty Range by setting min and max to the max/min possible value
// O(n)
private[lf] def slowEmpty[V](allValues: Seq[V])(implicit ordering: Ordering[V]): VersionRange[V] =
new VersionRange(allValues.max, allValues.min)
}

View File

@ -15,7 +15,6 @@ import com.daml.lf.transaction.{
SubmittedTransaction,
Transaction => Tx,
TransactionVersion => TxVersion,
VersionedTransaction => VersionedTx,
}
import com.daml.lf.value.Value
import com.daml.nameof.NameOf
@ -85,28 +84,25 @@ private[lf] object PartialTransaction {
*/
final case class Context(
info: ContextInfo,
minChildVersion: TxVersion, // tracks the minimum version of any child within `children`
children: BackStack[NodeId],
// tracks the min and max version of any child within `children`
childrenVersions: VersionRange[TxVersion],
nextActionChildIdx: Int,
) {
// when we add a child node we must pass the minimum-version contained in that child
def addActionChild(child: NodeId, childVersions: VersionRange[TxVersion]): Context =
Context(info, children :+ child, childrenVersions join childVersions, nextActionChildIdx + 1)
def addRollbackChild(
child: NodeId,
childVersions: VersionRange[TxVersion],
nextActionChildIdx: Int,
): Context =
Context(info, children :+ child, childrenVersions join childVersions, nextActionChildIdx)
def addActionChild(child: NodeId, version: TxVersion): Context = {
Context(info, minChildVersion min version, children :+ child, nextActionChildIdx + 1)
}
def addRollbackChild(child: NodeId, version: TxVersion, nextActionChildIdx: Int): Context =
Context(info, minChildVersion min version, children :+ child, nextActionChildIdx)
// This function may be costly, it must be call at most once for each node.
def nextActionChildSeed: crypto.Hash = info.actionChildSeed(nextActionChildIdx)
}
object Context {
def apply(info: ContextInfo, nextActionChildIdx: Int = 0): Context =
Context(info, BackStack.empty, TxVersion.NoVersions, nextActionChildIdx)
def apply(info: ContextInfo): Context =
// An empty context, with no children; minChildVersion is set to the max-int.
Context(info, TxVersion.VDev, BackStack.empty, 0)
def apply(initialSeeds: InitialSeeding, committers: Set[Party]): Context =
initialSeeds match {
@ -376,7 +372,9 @@ private[speedy] case class PartialTransaction(
val tx0 = Tx(nodes, roots)
val (tx, seeds) = NormalizeRollbacks.normalizeTx(tx0)
CompleteTransaction(
SubmittedTransaction(VersionedTx(context.childrenVersions.max, tx.nodes, tx.roots)),
SubmittedTransaction(
TxVersion.asVersionedTransaction(tx)
),
locationInfo(),
seeds.zip(actionNodeSeeds.toImmArray),
)
@ -432,7 +430,7 @@ private[speedy] case class PartialTransaction(
val ptx = copy(
actionNodeLocations = actionNodeLocations :+ optLocation,
nextNodeIdx = nextNodeIdx + 1,
context = context.addActionChild(nid, VersionRange(version)),
context = context.addActionChild(nid, version),
nodes = nodes.updated(nid, createNode),
actionNodeSeeds = actionNodeSeeds :+ actionNodeSeed,
localContracts = localContracts + cid,
@ -609,14 +607,7 @@ private[speedy] case class PartialTransaction(
case _ => keys
},
),
).noteAuthFails(
nid,
CheckAuthorization.authorizeExercise(
optLocation,
makeExNode(ec, packageToTransactionVersion(templateId.packageId)),
),
auth,
)
).noteAuthFails(nid, CheckAuthorization.authorizeExercise(optLocation, makeExNode(ec)), auth)
}
/** Close normally an exercise context.
@ -626,14 +617,12 @@ private[speedy] case class PartialTransaction(
context.info match {
case ec: ExercisesContextInfo =>
val result = normValue(ec.templateId, value)
val exeVersions =
context.childrenVersions join packageToTransactionVersion(ec.templateId.packageId)
val exerciseNode =
makeExNode(ec, exeVersions.max)
.copy(children = context.children.toImmArray, exerciseResult = Some(result))
makeExNode(ec).copy(children = context.children.toImmArray, exerciseResult = Some(result))
val nodeId = ec.nodeId
copy(
context = ec.parent.addActionChild(nodeId, exeVersions),
context =
ec.parent.addActionChild(nodeId, exerciseNode.version min context.minChildVersion),
nodes = nodes.updated(nodeId, exerciseNode),
)
case _ =>
@ -649,16 +638,15 @@ private[speedy] case class PartialTransaction(
def abortExercises: PartialTransaction =
context.info match {
case ec: ExercisesContextInfo =>
val exeVersions =
context.childrenVersions join packageToTransactionVersion(ec.templateId.packageId)
val exerciseNode =
makeExNode(ec, exeVersions.max).copy(children = context.children.toImmArray)
val exerciseNode = makeExNode(ec).copy(children = context.children.toImmArray)
val nodeId = ec.nodeId
val actionNodeSeed = context.nextActionChildSeed
copy(
context = ec.parent.addActionChild(nodeId, exeVersions),
context =
ec.parent.addActionChild(nodeId, exerciseNode.version min context.minChildVersion),
nodes = nodes.updated(nodeId, exerciseNode),
actionNodeSeeds = actionNodeSeeds :+ actionNodeSeed,
actionNodeSeeds =
actionNodeSeeds :+ actionNodeSeed, //(NC) pushed by 'beginExercises'; why push again?
)
case _ =>
InternalError.runtimeException(
@ -667,7 +655,8 @@ private[speedy] case class PartialTransaction(
)
}
private[this] def makeExNode(ec: ExercisesContextInfo, version: TxVersion): Node.Exercise =
private[this] def makeExNode(ec: ExercisesContextInfo): Node.Exercise = {
val version = packageToTransactionVersion(ec.templateId.packageId)
Node.Exercise(
targetCoid = ec.targetId,
templateId = ec.templateId,
@ -685,6 +674,7 @@ private[speedy] case class PartialTransaction(
byInterface = ec.byInterface,
version = version,
)
}
/** Open a Try context.
* Must be closed by `endTry`, `abortTry`, or `rollbackTry`.
@ -694,7 +684,7 @@ private[speedy] case class PartialTransaction(
val info = TryContextInfo(nid, context, activeState, authorizers = context.info.authorizers)
copy(
nextNodeIdx = nextNodeIdx + 1,
context = Context(info, context.nextActionChildIdx),
context = Context(info).copy(nextActionChildIdx = context.nextActionChildIdx),
)
}
@ -707,7 +697,6 @@ private[speedy] case class PartialTransaction(
copy(
context = info.parent.copy(
children = info.parent.children :++ context.children.toImmArray,
childrenVersions = context.childrenVersions,
nextActionChildIdx = context.nextActionChildIdx,
)
)
@ -730,10 +719,11 @@ private[speedy] case class PartialTransaction(
*/
def rollbackTry(ex: SValue.SAny): PartialTransaction = {
// we must never create a rollback containing a node with a version pre-dating exceptions
if (context.childrenVersions.min < TxVersion.minExceptions)
if (context.minChildVersion < TxVersion.minExceptions) {
throw SError.SErrorDamlException(
interpretation.Error.UnhandledException(ex.ty, ex.value.toUnnormalizedValue)
)
}
context.info match {
case info: TryContextInfo =>
// In the case of there being no children we could drop the entire rollback node.
@ -741,7 +731,7 @@ private[speedy] case class PartialTransaction(
val rollbackNode = Node.Rollback(context.children.toImmArray)
copy(
context = info.parent
.addRollbackChild(info.nodeId, context.childrenVersions, context.nextActionChildIdx),
.addRollbackChild(info.nodeId, context.minChildVersion, context.nextActionChildIdx),
nodes = nodes.updated(info.nodeId, rollbackNode),
).resetActiveState(info.beginState)
case _ =>
@ -796,7 +786,7 @@ private[speedy] case class PartialTransaction(
copy(
actionNodeLocations = actionNodeLocations :+ optLocation,
nextNodeIdx = nextNodeIdx + 1,
context = context.addActionChild(nid, VersionRange(version)),
context = context.addActionChild(nid, version),
nodes = nodes.updated(nid, node),
)
}

View File

@ -4,7 +4,7 @@
package com.daml.lf
package speedy
import com.daml.lf.data._
import com.daml.lf.data.ImmArray
import com.daml.lf.ledger.Authorize
import com.daml.lf.speedy.PartialTransaction._
import com.daml.lf.speedy.SValue.{SValue => _, _}
@ -17,23 +17,16 @@ import org.scalatest.wordspec.AnyWordSpec
class PartialTransactionSpec extends AnyWordSpec with Matchers with Inside {
private[this] val transactionSeed = crypto.Hash.hashPrivateKey("PartialTransactionSpec")
private val templates: Map[TransactionVersion, Ref.Identifier] =
TransactionVersion.All.view
.map(v => v -> Ref.Identifier.assertFromString(s"pkg${v.protoValue}:Mod:Template"))
.toMap
private[this] val templateId = templates(TransactionVersion.StableVersions.max)
private[this] val choiceId = Ref.Name.assertFromString("choice")
private[this] val templateId = data.Ref.Identifier.assertFromString("pkg:Mod:Template")
private[this] val choiceId = data.Ref.Name.assertFromString("choice")
private[this] val cid = Value.ContractId.V1(crypto.Hash.hashPrivateKey("My contract"))
private[this] val party = Ref.Party.assertFromString("Alice")
private[this] val committers: Set[Ref.Party] = Set.empty
private[this] val pkg2TxVersion = templates.map { case (version, id) => id.packageId -> version }
private[this] val party = data.Ref.Party.assertFromString("Alice")
private[this] val committers: Set[data.Ref.Party] = Set.empty
private[this] val initialState = PartialTransaction.initial(
pkg2TxVersion,
_ => TransactionVersion.maxVersion,
ContractKeyUniquenessMode.On,
Time.Timestamp.Epoch,
data.Time.Timestamp.Epoch,
InitialSeeding.TransactionSeed(transactionSeed),
committers,
)
@ -49,7 +42,7 @@ class PartialTransactionSpec extends AnyWordSpec with Matchers with Inside {
private[this] implicit class PartialTransactionExtra(val ptx: PartialTransaction) {
def insertCreate_(templateId: Ref.Identifier = templateId): PartialTransaction =
def insertCreate_ : PartialTransaction =
ptx
.insertCreate(
Authorize(Set(party)),
@ -64,7 +57,7 @@ class PartialTransactionSpec extends AnyWordSpec with Matchers with Inside {
)
._2
def beginExercises_(templateId: Ref.Identifier = templateId): PartialTransaction =
def beginExercises_ : PartialTransaction =
ptx.beginExercises(
Authorize(Set(party)),
targetId = cid,
@ -94,13 +87,13 @@ class PartialTransactionSpec extends AnyWordSpec with Matchers with Inside {
private[this] val outputCids =
contractIdsInOrder(
initialState //
.insertCreate_() // create the contract cid_0
.beginExercises_() // open an exercise context
.insertCreate_() // create the contract cid_1_0
.insertCreate_() // create the contract cid_1_2
.insertCreate_() // create the contract cid_1_3
.insertCreate_ // create the contract cid_0
.beginExercises_ // open an exercise context
.insertCreate_ // create the contract cid_1_0
.insertCreate_ // create the contract cid_1_2
.insertCreate_ // create the contract cid_1_3
.endExercises_ // close the exercise context normally
.insertCreate_() // create the contract cid_2
.insertCreate_ // create the contract cid_2
)
val Seq(cid_0, cid_1_0, cid_1_1, cid_1_2, cid_2) = outputCids
@ -109,29 +102,29 @@ class PartialTransactionSpec extends AnyWordSpec with Matchers with Inside {
"be without effect when closed without exception" in {
def run1 = contractIdsInOrder(
initialState //
.insertCreate_() // create the contract cid_0
.beginExercises_() // open an exercise context
.insertCreate_() // create the contract cid_1_0
.insertCreate_ // create the contract cid_0
.beginExercises_ // open an exercise context
.insertCreate_ // create the contract cid_1_0
.beginTry // open a try context
.insertCreate_() // create the contract cid_1_1
.insertCreate_ // create the contract cid_1_1
.endTry // close the try context
.insertCreate_() // create the contract cid_1_2
.insertCreate_ // create the contract cid_1_2
.endExercises_ // close the exercise context normally
.insertCreate_() // create the contract cid_2
.insertCreate_ // create the contract cid_2
)
def run2 = contractIdsInOrder(
// the double slashes below tricks scalafmt
initialState //
.insertCreate_() // create the contract cid_0
.insertCreate_ // create the contract cid_0
.beginTry // open a try context
.beginExercises_() // open an exercise context
.insertCreate_() // create the contract cid_1_0
.insertCreate_() // create the contract cid_1_2
.insertCreate_() // create the contract cid_1_3
.beginExercises_ // open an exercise context
.insertCreate_ // create the contract cid_1_0
.insertCreate_ // create the contract cid_1_2
.insertCreate_ // create the contract cid_1_3
.endExercises_ // close the exercise context normally
.endTry // close the try context
.insertCreate_() // create the contract cid_2
.insertCreate_ // create the contract cid_2
)
run1 shouldBe outputCids
@ -143,46 +136,46 @@ class PartialTransactionSpec extends AnyWordSpec with Matchers with Inside {
def run1 = contractIdsInOrder(
// the double slashes below tricks scalafmt
initialState //
.insertCreate_() // create the contract cid_0
.insertCreate_ // create the contract cid_0
.beginTry // open a first try context
.beginExercises_() // open an exercise context
.insertCreate_() // create the contract cid_1_0
.insertCreate_() // create the contract cid_1_1
.insertCreate_() // create the contract cid_1_2
.beginExercises_ // open an exercise context
.insertCreate_ // create the contract cid_1_0
.insertCreate_ // create the contract cid_1_1
.insertCreate_ // create the contract cid_1_2
// an exception is thrown
.abortExercises // close abruptly the exercise due to an uncaught exception
.rollbackTry_ // the try context handles the exception
.insertCreate_() // create the contract cid_2
.insertCreate_ // create the contract cid_2
)
def run2 = contractIdsInOrder(
initialState //
.insertCreate_() // create the contract cid_0
.insertCreate_ // create the contract cid_0
.beginTry // open a first try context
.beginExercises_() // open an exercise context
.insertCreate_() // create the contract cid_1_0
.insertCreate_() // create the contract cid_1_1
.beginExercises_ // open an exercise context
.insertCreate_ // create the contract cid_1_0
.insertCreate_ // create the contract cid_1_1
.beginTry // open a second try context
.insertCreate_() // create the contract cid_1_2
.insertCreate_ // create the contract cid_1_2
// an exception is thrown
.abortTry // the second try context does not handle the exception
.abortExercises // close abruptly the exercise due to an uncaught exception
.rollbackTry_ // the first try context does handle the exception
.insertCreate_() // create the contract cid_2
.insertCreate_ // create the contract cid_2
)
def run3 = contractIdsInOrder(
// the double slashes below tricks scalafmt
initialState //
.insertCreate_() // create the contract cid_0
.beginExercises_() // open an exercise context
.insertCreate_() // create the contract cid_1_0
.insertCreate_ // create the contract cid_0
.beginExercises_ // open an exercise context
.insertCreate_ // create the contract cid_1_0
.beginTry // open a try context
.insertCreate_() // create the contract cid_1_2
.insertCreate_ // create the contract cid_1_2
.rollbackTry_ // the try context does handle the exception
.insertCreate_() // create the contract cid_1_3
.insertCreate_ // create the contract cid_1_3
.endExercises_ // close the exercise context normally
.insertCreate_() // create the contract cid_2
.insertCreate_ // create the contract cid_2
)
run1 shouldBe Seq(cid_0, cid_1_0, cid_1_1, cid_1_2, cid_2)
@ -191,59 +184,4 @@ class PartialTransactionSpec extends AnyWordSpec with Matchers with Inside {
}
}
"infers version as" should {
"be the max of the version of the children nodes" in {
import TransactionVersion._
def versionsInOrder(ptx: PartialTransaction) =
inside(ptx.finish) { case CompleteTransaction(tx, _, _) =>
tx.version -> tx.fold(Vector.empty[Option[TransactionVersion]])(_ :+ _._2.optVersion)
}
versionsInOrder(
initialState
.insertCreate_(templates(V14))
) shouldBe (V14 -> Seq(Some(V14)))
versionsInOrder(
initialState
.insertCreate_(templates(V12))
.insertCreate_(templates(V13))
) shouldBe (V13 -> Seq(Some(V12), Some(V13)))
versionsInOrder(
initialState
.beginExercises_(templates(V14))
.insertCreate_(templates(V12))
.insertCreate_(templates(V13))
.endExercises_
) shouldBe (V14 -> Seq(Some(V14), Some(V12), Some(V13)))
versionsInOrder(
initialState
.beginExercises_(templates(V11))
.insertCreate_(templates(V12))
.insertCreate_(templates(V13))
.endExercises_
) shouldBe (V13 -> Seq(Some(V13), Some(V12), Some(V13)))
versionsInOrder(
initialState.beginTry
.insertCreate_(templates(VDev))
.insertCreate_(templates(V14))
.endTry
) shouldBe (VDev -> Seq(Some(VDev), Some(V14)))
versionsInOrder(
initialState.beginTry
.insertCreate_(templates(V14))
.insertCreate_(templates(VDev))
.rollbackTry_
) shouldBe (VDev -> Seq(None, Some(V14), Some(VDev)))
}
}
}

View File

@ -67,7 +67,7 @@ object LanguageVersion {
// All versions compatible with legacy contract ID scheme.
val LegacyVersions: VersionRange[LanguageVersion] =
VersionRange(StableVersions.min, max = v1_8)
StableVersions.copy(max = v1_8)
// All the stable and preview versions
// Equals `Stable` if no preview version is available
@ -76,7 +76,7 @@ object LanguageVersion {
// All the versions
val DevVersions: VersionRange[LanguageVersion] =
VersionRange(StableVersions.min, v1_dev)
StableVersions.copy(max = v1_dev)
val defaultV1: LanguageVersion = StableVersions.max

View File

@ -26,8 +26,6 @@ object TransactionVersion {
private[daml] implicit val Ordering: scala.Ordering[TransactionVersion] =
scala.Ordering.by(_.index)
private[lf] val NoVersions: VersionRange[TransactionVersion] = VersionRange.slowEmpty(All)
private[this] val stringMapping = All.iterator.map(v => v.protoValue -> v).toMap
def fromString(vs: String): Either[String, TransactionVersion] =
@ -69,15 +67,14 @@ object TransactionVersion {
private[lf] def asVersionedTransaction(
tx: Transaction
): VersionedTransaction = tx match {
case Transaction(nodes, roots) =>
val rootVersions = roots.iterator.map(nodeId =>
nodes(nodeId) match {
case action: Node.Action => action.version
case _: Node.Rollback => minExceptions
}
)
VersionedTransaction(rootVersions.max, nodes, roots)
): VersionedTransaction = {
import scala.Ordering.Implicits.infixOrderingOps
val txVersion = tx.nodes.valuesIterator.foldLeft(TransactionVersion.minVersion) {
case (acc, action: Node.Action) => acc max action.version
case (acc, _: Node.Rollback) => acc max minExceptions
}
VersionedTransaction(txVersion, tx.nodes, tx.roots)
}
val StableVersions: VersionRange[TransactionVersion] =