Skip to content

Commit

Permalink
Normalize transactions & values as a separate pass (#10524)
Browse files Browse the repository at this point in the history
* Normalize transactions & values as a separate pass. Use for simpler defintiion of isReplayedBy.

CHANGELOG_BEGIN
CHANGELOG_END

normalize transaction version

* remove stray import from bad merge which breaks scala 2_12 build

* change isReplayedBy to only norm its RIGHT (replay) argument

* add forgotton normalization for ValueEmum

* switch to use existing value normalization code (remove my newly coded duplicate code)

* normalize submittedTransaction before calling engine.validate

* dont call normalizeTx from Engine.validate

* *do* call normalizeTx from Engine.validate
  • Loading branch information
nickchapman-da authored Aug 16, 2021
1 parent 99e1d78 commit 9db5ccf
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 390 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ class Engine(val config: EngineConfig = new EngineConfig(LanguageVersion.StableV
(rtx, _) = result
validationResult <-
transaction.Validation
.isReplayedBy(tx, rtx)
.isReplayedBy(transaction.Normalization.normalizeTx(tx), rtx)
.fold(
e => ResultError(Error.Validation.ReplayMismatch(e)),
_ => ResultDone.Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import com.daml.lf.transaction.{
Transaction => Tx,
TransactionVersion => TxVersions,
}
import com.daml.lf.transaction.Validation.isReplayedBy
import com.daml.lf.transaction.{Normalization, Validation, ReplayMismatch}
import com.daml.lf.value.Value
import Value._
import com.daml.lf.speedy.{InitialSeeding, SValue, svalue}
Expand Down Expand Up @@ -526,8 +526,9 @@ class EngineTest
val Right((tx, meta)) = interpretResult
val Right(submitter) = tx.guessSubmitter
val submitters = Set(submitter)
val ntx = SubmittedTransaction(Normalization.normalizeTx(tx))
val validated = engine
.validate(submitters, tx, let, participant, meta.submissionTime, submissionSeed)
.validate(submitters, ntx, let, participant, meta.submissionTime, submissionSeed)
.consume(lookupContract, lookupPackage, lookupKey)
validated match {
case Left(e) =>
Expand Down Expand Up @@ -612,8 +613,9 @@ class EngineTest
"be validated" in {
forAll(cases) { case (templateId, signatories, submitters) =>
val Right((tx, meta)) = interpretResult(templateId, signatories, submitters)
val ntx = SubmittedTransaction(Normalization.normalizeTx(tx))
val validated = engine
.validate(submitters, tx, let, participant, meta.submissionTime, submissionSeed)
.validate(submitters, ntx, let, participant, meta.submissionTime, submissionSeed)
.consume(
lookupContract,
lookupPackage,
Expand Down Expand Up @@ -736,8 +738,9 @@ class EngineTest
}

"be validated" in {
val ntx = SubmittedTransaction(Normalization.normalizeTx(tx))
val validated = engine
.validate(Set(submitter), tx, let, participant, let, submissionSeed)
.validate(Set(submitter), ntx, let, participant, let, submissionSeed)
.consume(
lookupContract,
lookupPackage,
Expand Down Expand Up @@ -879,8 +882,9 @@ class EngineTest
}

"be validated" in {
val ntx = SubmittedTransaction(Normalization.normalizeTx(tx))
val validated = engine
.validate(submitters, tx, let, participant, let, submissionSeed)
.validate(submitters, ntx, let, participant, let, submissionSeed)
.consume(
lookupContract,
lookupPackage,
Expand Down Expand Up @@ -1140,8 +1144,9 @@ class EngineTest
}

"be validated" in {
val ntx = SubmittedTransaction(Normalization.normalizeTx(tx))
val validated = engine
.validate(Set(submitter), tx, let, participant, let, submissionSeed)
.validate(Set(submitter), ntx, let, participant, let, submissionSeed)
.consume(
lookupContract,
lookupPackage,
Expand Down Expand Up @@ -1586,7 +1591,8 @@ class EngineTest
nid -> fetch
}

fetchNodes.foreach { case (nid, n) =>
fetchNodes.foreach { case (_, n) =>
val nid = NodeId(0) //we must use node-0 so the constructed tx is normalized
val fetchTx = VersionedTransaction(n.version, Map(nid -> n), ImmArray(nid))
val Right((reinterpreted, _)) =
engine
Expand Down Expand Up @@ -2103,8 +2109,16 @@ class EngineTest
def validate(tx: SubmittedTransaction, metaData: Tx.Metadata) =
for {
submitter <- tx.guessSubmitter
ntx = SubmittedTransaction(Normalization.normalizeTx(tx))
res <- engine
.validate(Set(submitter), tx, let, participant, metaData.submissionTime, submissionSeed)
.validate(
Set(submitter),
ntx,
let,
participant,
metaData.submissionTime,
submissionSeed,
)
.consume(
_ => None,
lookupPackage,
Expand Down Expand Up @@ -2748,6 +2762,14 @@ object EngineTest {
case _ => false
}

private def isReplayedBy[Nid, Cid](
recorded: VersionedTransaction[Nid, Cid],
replayed: VersionedTransaction[Nid, Cid],
): Either[ReplayMismatch[Nid, Cid], Unit] = {
// we normalize the LEFT arg before calling isReplayedBy to mimic the effect of serialization
Validation.isReplayedBy(Normalization.normalizeTx(recorded), replayed)
}

private def reinterpret(
engine: Engine,
submitters: Set[Party],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package com.daml.lf
package transaction

import com.daml.lf.value.{Value => V}

import com.daml.lf.transaction.Node.{
KeyWithMaintainers,
GenNode,
NodeCreate,
NodeFetch,
NodeLookupByKey,
NodeExercises,
NodeRollback,
}

class Normalization[Nid, Cid] {

/** This class provides methods to normalize a transaction and embedded values.
*
* Informal spec: normalization is the result of serialization and deserialization.
*
* Here we take care of the following:
* - type information is dropped from Variant and Record values
* - field names are dropped from Records
* - values are normalized recursively
* - all values embedded in transaction nodes are normalized
* - version-specific normalization is applied to the 'byKey' fields of 'NodeFetch' and 'NodeExercises'
*
* We do not normalize the node-ids in the transaction here, but rather assume that
* aspect of normalization has already been performed (by the engine, or by
* deserialization).
*
* Eventually we would like that all aspects of normalization are achieved directly by
* the transaction which is constructed by the engine. When this is done, we will no
* longer need this separate normalization pass.
*/

private type Val = V[Cid]
private type KWM = KeyWithMaintainers[Val]
private type Node = GenNode[Nid, Cid]
private type VTX = VersionedTransaction[Nid, Cid]

def normalizeTx(vtx: VTX): VTX = {
vtx match {
case VersionedTransaction(_, nodes, roots) =>
// TODO: Normalized version calc should be shared with code in asVersionedTransaction
val version = roots.iterator.foldLeft(TransactionVersion.minVersion) { (acc, nodeId) =>
import scala.Ordering.Implicits.infixOrderingOps
nodes(nodeId).optVersion match {
case Some(version) => acc max version
case None => acc max TransactionVersion.minExceptions
}
}
VersionedTransaction(
version,
nodes.map { case (k, v) =>
(k, normNode(v))
},
vtx.roots,
)
}
}

private def normNode(
node: Node
): Node = {
import scala.Ordering.Implicits.infixOrderingOps
node match {

case old: NodeCreate[_] =>
old
.copy(arg = normValue(old.version)(old.arg))
.copy(key = old.key.map(normKWM(old.version)))

case old: NodeFetch[_] =>
(if (old.version >= TransactionVersion.minByKey) {
old
} else {
old.copy(byKey = false)
})
.copy(
key = old.key.map(normKWM(old.version))
)

case old: NodeExercises[_, _] =>
(if (old.version >= TransactionVersion.minByKey) {
old
} else {
old.copy(byKey = false)
})
.copy(
chosenValue = normValue(old.version)(old.chosenValue),
exerciseResult = old.exerciseResult.map(normValue(old.version)),
key = old.key.map(normKWM(old.version)),
)

case old: NodeLookupByKey[_] =>
old.copy(
key = normKWM(old.version)(old.key)
)

case old: NodeRollback[_] => old

}
}

private def normValue(version: TransactionVersion)(x: Val): Val = {
Util.assertNormalizeValue(x, version)
}

private def normKWM(version: TransactionVersion)(x: KWM): KWM = {
x match {
case KeyWithMaintainers(key, maintainers) =>
KeyWithMaintainers(normValue(version)(key), maintainers)
}
}

}

object Normalization {
def normalizeTx[Nid, Cid](tx: VersionedTransaction[Nid, Cid]): VersionedTransaction[Nid, Cid] = {
new Normalization().normalizeTx(tx)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ object Util {

// unsafe version of `normalize`
@throws[IllegalArgumentException]
def assertNormalizeValue(
value0: Value[ContractId],
def assertNormalizeValue[Cid](
value0: Value[Cid],
version: TransactionVersion,
): Value[ContractId] = {
): Value[Cid] = {

import Ordering.Implicits.infixOrderingOps

Expand All @@ -43,7 +43,7 @@ object Util {
x
}

def go(value: Value[ContractId]): Value[ContractId] =
def go(value: Value[Cid]): Value[Cid] =
value match {
case ValueEnum(tyCon, cons) =>
ValueEnum(handleTypeInfo(tyCon), cons)
Expand Down
Loading

0 comments on commit 9db5ccf

Please sign in to comment.