Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
package org.lfdecentralizedtrust.splice.integration.tests

import com.daml.ledger.javaapi.data.{
CreatedEvent,
CreateCommand,
DamlList,
DamlRecord,
ExerciseCommand,
ExercisedEvent,
Identifier,
Party,
Text,
Value,
}
import com.digitalasset.canton.resource.DbStorage
import com.digitalasset.canton.topology.PartyId
import com.digitalasset.daml.lf.data.Ref
import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition
import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTest
import org.lfdecentralizedtrust.splice.util.{DarUtil, WalletTestUtil}
import slick.jdbc.canton.ActionBasedSQLInterpolation.Implicits.actionBasedSQLInterpolationCanton

import java.io.File
import scala.collection.mutable
import scala.jdk.CollectionConverters.*

/** Equivalence test: Daml (real participant) == SQL (scan Postgres).
*
* Validates that the plpgsql hash functions produce identical results
* to the Daml CryptoHash module. Daml is the authority.
*
* Uses a shared environment so the dar is uploaded and proxy created once,
* then each hash function gets its own test.
*/
@org.lfdecentralizedtrust.splice.util.scalatesttags.NoDamlCompatibilityCheck
class CryptoHashEquivalenceIntegrationTest extends IntegrationTest with WalletTestUtil {
Comment thread
adetokunbo marked this conversation as resolved.

import CryptoHashEquivalenceIntegrationTest.*

override def environmentDefinition: SpliceEnvironmentDefinition =
EnvironmentDefinition
.simpleTopology1Sv(this.getClass.getSimpleName)
Comment thread
adetokunbo marked this conversation as resolved.
.withManualStart

"CryptoHash equivalence (Daml == SQL)" should {

"set up environment" in { implicit env =>
startAllSync(sv1Backend)
svParty = sv1Backend.getDsoInfo().svParty
svDb = sv1Backend.appState.storage

clue("Upload dar and create CryptoHashProxy") {
sv1Backend.participantClient.upload_dar_unless_exists(darPath)
val createArgs = new DamlRecord(
java.util.List.of(
new DamlRecord.Field("owner", new Party(svParty.toProtoPrimitive))
)
)
val createTx =
sv1Backend.participantClientWithAdminToken.ledger_api_extensions.commands
.submitJava(
actAs = Seq(svParty),
commands = Seq(new CreateCommand(templateId, createArgs)),
)
proxyContractId = createTx.getEvents.asScala
.collectFirst { case ce: CreatedEvent => ce.getContractId }
.getOrElse(fail("No CreatedEvent from CryptoHashProxy create"))
}
}

allTestCaseDefs.foreach { tcDef =>
tcDef.description in { implicit env =>
val damlHash = clue("Daml") { exerciseDaml(tcDef.op) }
intermediates(tcDef.description) = damlHash
clue("SQL should match Daml") {
val sqlHash = svDb
.query(
sql"""select #${toSqlExpr(tcDef.op, resolveRef)}""".as[String].head,
"test.sqlHash",
)
.futureValueUS
sqlHash shouldBe damlHash
}
}
}
}

/** Exercise a Daml choice and return the Text result. */
private def exerciseDaml(op: HashOp)(implicit env: FixtureParam): String = {
val (choiceName, choiceArg) = toDamlChoiceAndArg(op, resolveRef)
val cmd = new ExerciseCommand(templateId, proxyContractId, choiceName, choiceArg)
val tx = sv1Backend.participantClientWithAdminToken.ledger_api_extensions.commands
.submitJava(
actAs = Seq(svParty),
commands = Seq(cmd),
)
tx.getEventsById.values.asScala
.collectFirst { case ex: ExercisedEvent =>
ex.getExerciseResult match {
case t: Text => t.getValue
case other =>
throw new IllegalStateException(
s"Expected Text result but got ${other.getClass.getSimpleName}"
)
}
}
.getOrElse(throw new IllegalStateException("No ExercisedEvent in transaction"))
}

private def resolveRef(ref: HashRef): String = ref match {
case Lit(v) => v
case IntermediateRef(name) =>
intermediates.getOrElse(
name,
throw new IllegalStateException(s"Intermediate '$name' not found"),
)
}

// Progressive map: each test stores its Daml result for later tests to reference
private val intermediates: mutable.Map[String, String] = mutable.Map.empty

private var svParty: PartyId = _
private var proxyContractId: String = _
private var svDb: DbStorage = _

}

object CryptoHashEquivalenceIntegrationTest {

// -- Package discovery ------------------------------------------------------

private val darPath = "daml/splice-amulet-test/.daml/dist/splice-amulet-test-current.dar"
private val moduleName = "Splice.Testing.CryptoHashProxy"

private val packageId: String = {
val dar = DarUtil.readDar(new File(darPath))
val modName = Ref.ModuleName.assertFromString(moduleName)
dar.all
.collectFirst { case (pkgId, pkg) if pkg.modules.contains(modName) => pkgId }
.getOrElse(sys.error(s"Could not find package containing $moduleName"))
.toString
}

private val templateId = new Identifier(packageId, moduleName, "CryptoHashProxy")

// -- HashRef: symbolic references to intermediate hash values ---------------

sealed trait HashRef
case class Lit(value: String) extends HashRef
case class IntermediateRef(name: String) extends HashRef

// -- HashOp: test case operations (may contain HashRef) ---------------------

sealed trait HashOp
case class HashText(value: String) extends HashOp
case class HashList(elems: Seq[HashRef]) extends HashOp
case class HashVariant(tag: String, fields: Seq[HashRef]) extends HashOp
case class HashMintingAllowance(provider: String, amount: String) extends HashOp
case class HashBatchOfMintingAllowances(hashes: Seq[HashRef]) extends HashOp
case class HashBatchOfBatches(hashes: Seq[HashRef]) extends HashOp

case class TestCaseDef(description: String, op: HashOp)

// -- Derive Daml choice + arg from HashOp ----------------------------------

private def text(s: String): Value = new Text(s)
private def textList(ss: Seq[String]): Value =
DamlList.of(ss.map(s => new Text(s): Value).asJava)
private def record(fields: (String, Value)*): Value =
new DamlRecord(fields.map { case (k, v) => new DamlRecord.Field(k, v) }.asJava)

def toDamlChoiceAndArg(op: HashOp, r: HashRef => String): (String, Value) = op match {
case HashText(v) =>
("CryptoHashProxy_HashText", record("input" -> text(v)))
case HashList(elems) =>
("CryptoHashProxy_HashList", record("elems" -> textList(elems.map(r))))
case HashVariant(tag, fields) =>
(
"CryptoHashProxy_HashVariant",
record("tag" -> text(tag), "fields" -> textList(fields.map(r))),
)
case HashMintingAllowance(provider, amount) =>
(
"CryptoHashProxy_HashMintingAllowance",
record("provider" -> text(provider), "amount" -> text(amount)),
)
case HashBatchOfMintingAllowances(hashes) =>
(
"CryptoHashProxy_HashBatchOfMintingAllowances",
record("allowanceHashes" -> textList(hashes.map(r))),
)
case HashBatchOfBatches(hashes) =>
("CryptoHashProxy_HashBatchOfBatches", record("childHashes" -> textList(hashes.map(r))))
}

// -- Derive SQL expression from HashOp -------------------------------------

private def escapeSql(s: String): String = s.replace("'", "''")
private def sqlArray(elems: Seq[String]): String =
s"ARRAY[${elems.map(e => s"'$e'").mkString(", ")}]::text[]"

def toSqlExpr(op: HashOp, r: HashRef => String): String = op match {
case HashText(v) =>
s"daml_crypto_hash_text('${escapeSql(v)}')"
case HashList(elems) =>
s"daml_crypto_hash_list(${sqlArray(elems.map(r))})"
case HashVariant(tag, fields) =>
s"daml_crypto_hash_variant('${escapeSql(tag)}', ${sqlArray(fields.map(r))})"
case HashMintingAllowance(provider, amount) =>
s"hash_minting_allowance('${escapeSql(provider)}', '${escapeSql(amount)}')"
case HashBatchOfMintingAllowances(hashes) =>
s"hash_batch_of_minting_allowances(${sqlArray(hashes.map(r))})"
case HashBatchOfBatches(hashes) =>
s"hash_batch_of_batches(${sqlArray(hashes.map(r))})"
}

// -- Test case definitions (fully static) -----------------------------------

// Test cases are defined statically using HashRef (Lit/IntermediateRef) to
// reference intermediate hashes by name. Each test resolves its refs from
// a shared map populated progressively as earlier tests run.
//
// Order matters: intermediates must precede composites that reference them.
val allTestCaseDefs: Seq[TestCaseDef] = Seq(
// Primitive text hashes — used as intermediates by composite cases
TestCaseDef("hAlice", HashText("alice::provider")),
TestCaseDef("h10", HashText("10.0")),
TestCaseDef("hOnly", HashText("only")),
TestCaseDef("h1", HashText("1")),
TestCaseDef("hx", HashText("x")),

// Minting allowance hashes — used as intermediates by batch cases
TestCaseDef("maAlice", HashMintingAllowance("alice::provider", "10.0000000000")),
TestCaseDef("maBob", HashMintingAllowance("bob::provider", "0")),
TestCaseDef("maAlice5", HashMintingAllowance("alice::provider", "5.0")),
TestCaseDef("maBob3", HashMintingAllowance("bob::provider", "3.0")),
// Batch hashes — used as intermediates by batch-of-batches
TestCaseDef("leaf1", HashBatchOfMintingAllowances(Seq(IntermediateRef("maAlice5")))),
TestCaseDef("leaf2", HashBatchOfMintingAllowances(Seq(IntermediateRef("maBob3")))),

// Independent cases (no intermediate references)
TestCaseDef("hash of 'hello'", HashText("hello")),
TestCaseDef("hash of empty string", HashText("")),
TestCaseDef("hashList on empty array", HashList(Seq.empty)),

// Composite cases using intermediate hashes
TestCaseDef(
"hashList with two elements",
HashList(Seq(IntermediateRef("hAlice"), IntermediateRef("h10"))),
),
TestCaseDef("hashList on single element", HashList(Seq(IntermediateRef("hOnly")))),
TestCaseDef(
"hashVariant with tag and one field",
HashVariant("TestTag", Seq(IntermediateRef("hAlice"))),
),
TestCaseDef(
"hashRecord [hash 1, hash 'x']",
HashList(Seq(IntermediateRef("h1"), IntermediateRef("hx"))),
),
TestCaseDef(
"hashVariant 'V1' [hash 1, hash 'x']",
HashVariant("V1", Seq(IntermediateRef("h1"), IntermediateRef("hx"))),
),
TestCaseDef(
"hash_batch_of_minting_allowances with two",
HashBatchOfMintingAllowances(Seq(IntermediateRef("maAlice"), IntermediateRef("maBob"))),
),
TestCaseDef(
"hash_batch_of_batches with two leaves",
HashBatchOfBatches(Seq(IntermediateRef("leaf1"), IntermediateRef("leaf2"))),
),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
-- V065: plpgsql functions for Merkle tree hash computation over app reward batches.
-- These replicate the Daml CryptoHash module (Splice.Amulet.CryptoHash)
-- All hash functions return 64-char hex strings (text), matching Daml's Hash type.
-- Callers that store results in bytea columns (batch_hash, root_hash) must
-- convert with decode(result, 'hex').

-- ============================================================================
-- Primitive hash functions matching Daml CryptoHash encoding
-- ============================================================================

-- Hash a text scalar: sha256(value) → 64-char hex string.
-- Matches: instance Hashable Text where hash = hashText . id
CREATE FUNCTION daml_crypto_hash_text(s text) RETURNS text
RETURNS NULL ON NULL INPUT
AS $$ SELECT encode(extensions.digest(s, 'sha256'), 'hex') $$
LANGUAGE sql IMMUTABLE PARALLEL SAFE;

-- Hash a list of already-hashed elements.
-- Matches: hashListInternal ts = sha256(intercalate "|" (show(length ts) :: ts))
CREATE FUNCTION daml_crypto_hash_list(elems text[]) RETURNS text
RETURNS NULL ON NULL INPUT
AS $$
SELECT encode(extensions.digest(
array_to_string(
ARRAY[cardinality(elems)::text] || elems,
'|'
),
'sha256'
), 'hex')
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;

-- Hash a variant with a tag and field hashes.
-- Matches: hashVariant tag fields = sha256(intercalate "|" (tag :: show(length fields) :: map (.value) fields))
CREATE FUNCTION daml_crypto_hash_variant(tag text, fields text[]) RETURNS text
RETURNS NULL ON NULL INPUT
AS $$
SELECT encode(extensions.digest(
array_to_string(
ARRAY[tag, cardinality(fields)::text] || fields,
'|'
),
'sha256'
), 'hex')
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;


-- ============================================================================
-- Domain-specific hash functions for reward accounting
-- ============================================================================

-- Hash a single MintingAllowance record.
-- Matches: hash MintingAllowance{provider, amount} = hashRecord [hash provider, hash amount]
CREATE FUNCTION hash_minting_allowance(provider text, amount text) RETURNS text
RETURNS NULL ON NULL INPUT
AS $$ SELECT daml_crypto_hash_list(ARRAY[daml_crypto_hash_text(provider), daml_crypto_hash_text(amount)]) $$
LANGUAGE sql IMMUTABLE PARALLEL SAFE;

-- Hash a BatchOfMintingAllowances variant.
-- Matches: hash (BatchOfMintingAllowances allowances) = hashVariant "BatchOfMintingAllowances" [hash allowances]
-- where hash allowances = hashList (map hash allowances)
CREATE FUNCTION hash_batch_of_minting_allowances(allowance_hashes text[]) RETURNS text
RETURNS NULL ON NULL INPUT
AS $$ SELECT daml_crypto_hash_variant('BatchOfMintingAllowances', ARRAY[daml_crypto_hash_list(allowance_hashes)]) $$
LANGUAGE sql IMMUTABLE PARALLEL SAFE;

-- Hash a BatchOfBatches variant.
-- Matches: hash (BatchOfBatches batchHashes) = hashVariant "BatchOfBatches" [hash batchHashes]
-- where hash batchHashes = hashList (map hash batchHashes) (identity on Hash)
CREATE FUNCTION hash_batch_of_batches(child_hashes text[]) RETURNS text
RETURNS NULL ON NULL INPUT
AS $$ SELECT daml_crypto_hash_variant('BatchOfBatches', ARRAY[daml_crypto_hash_list(child_hashes)]) $$
LANGUAGE sql IMMUTABLE PARALLEL SAFE;
10 changes: 5 additions & 5 deletions daml/dars.lock
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ splice-amulet-name-service 0.1.6 a208aab2c4a248ab2eff352bd382f8b3bbadc92464123db
splice-amulet-name-service 0.1.7 ba7806d9b2d593eac74a050161c54ae1325d170bf175cb66a9c1e5e5ffb88c3d
splice-amulet-name-service 0.1.8 efeb3f9b2b92e55fac4ec2d6164f95407a01477240c7465e576df4e310f54bd3
splice-amulet-name-service 0.1.9 f1b5915ad45ded616f43f83c735b7ee158b5eb58abe758a721e50eee19b3e531
splice-amulet-name-service-test 0.1.21 b120f1061b3a4b278badcc27d2ab40bf9f5593a65b92fd1faa568ff02f6eba62
splice-amulet-test 0.1.20 a9850c98ef59f0550ef7e036d11464bb8b7e97c4b7e88c8a390bbd27e946cca5
splice-amulet-name-service-test 0.1.22 7be24690bd4b39a76d7245ddfae0c25a2f66c32099e17e856ddb92f5b944bea3
splice-amulet-test 0.1.21 5657829c3084a8cf5a66c8da180d30aeb5977969e41723e3493ec82830407d17
splice-api-featured-app-v1 1.0.0 7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda
splice-api-featured-app-v2 1.0.0 dd22e3e168a8c7fd0313171922dabf1f7a3b131bd9bfc9ff98e606f8c57707ea
splice-api-token-allocation-instruction-v1 1.0.0 275064aacfe99cea72ee0c80563936129563776f67415ef9f13e4297eecbc520
Expand Down Expand Up @@ -72,7 +72,7 @@ splice-dso-governance 0.1.6 4e7653cfbf7ca249de4507aca9cd3b91060e5489042a522c589d
splice-dso-governance 0.1.7 d406eba1132d464605f4dae3edf8cf5ecbbb34bd8edef0e047e7e526d328718c
splice-dso-governance 0.1.8 1790a114f83d5f290261fae1e7e46fba75a861a3dd603c6b4ef6b67b49053948
splice-dso-governance 0.1.9 9ee83bfd872f91e659b8a8439c5b4eaf240bcf6f19698f884d7d7993ab48c401
splice-dso-governance-test 0.1.27 46a2441bd370d21757a4f94fa44ba865f1cddf53ecec0e5a38bff84700977f00
splice-dso-governance-test 0.1.28 33910e178389129fd916c577fbfd0a58ce86fe33db6a358e7dc988ee2ec014e8
splice-token-standard-test 1.0.11 b641f7509aff67cdfa82b66ebccfff072b2226b39e5bd998ac118570c926fc50
splice-token-test-dummy-holding 0.0.1 1cd171c6c42ab46dc9cf12d80c6111369e00cea5cdf054924b4f26ce94b1ef5b
splice-token-test-dummy-holding 0.0.2 4f40fb033ef3db89623642c1b494e846097fa32af138b3864a63aa15937a323d
Expand Down Expand Up @@ -139,7 +139,7 @@ splice-wallet-payments 0.1.6 6124379528eeb6fa17ecdab15577c29abb33d0c0d34dc5f2680
splice-wallet-payments 0.1.7 4e3e0d9cdadf80f4bf8f3cd3660d5287c084c9a29f23c901aabce597d72fd467
splice-wallet-payments 0.1.8 e48ea337ee3335c8bb3206a2501ce947ac1a7bdb1825cee8f28bad64f5a7bc4b
splice-wallet-payments 0.1.9 7f4e081ad96f2ccded0c053b0cf5ddddae1139dfc3bb89cefcf77ea70f2cecb7
splice-wallet-test 0.1.21 c751f85e42e928f0e300d561bda15be7bb42dd19681a52a4955d839f47f6b695
splice-wallet-test 0.1.22 9d28b14cc32d128d0d6384eaae90c505f985a0c45d706caca483690b023efe60
splitwell 0.1.0 075c76de553ab88383a7c69de134afa82aacfdf8ea8fcfe8852c4b199c3b2669
splitwell 0.1.1 ccb1a0215053062202052e1a052f9214da3fdae5253a6d43e2e155ff4f57fe75
splitwell 0.1.10 d42676a366f7ca7a2409974dd3054aa4d83ab29baa3b2086ad021407b0a1a295
Expand All @@ -159,4 +159,4 @@ splitwell 0.1.6 872da0dd7986fd768930f85d6a7310a94a0ef924e7fbb7bb7a4e149f2b5feb74
splitwell 0.1.7 841d1c9c86b5c8f3a39059459ecd8febedf7703e18f117300bb0ebf4423db096
splitwell 0.1.8 63b8153a08ceb4bf40d807acc5712372c3eac548c266be4d5e92470b4f655515
splitwell 0.1.9 b6267905698d2798b9ef171e27d49fb88e052ec0ec0e0675a3a1b275c7d037d4
splitwell-test 0.1.21 7cf052802bd7c9f3dcbe7f7af893e595310f7ec98fee05362208cb4808b3b61f
splitwell-test 0.1.22 68088430da8cafaa5f8907aacdd4e257d838a6b58a3c1a29eb5555c8145483e4
2 changes: 1 addition & 1 deletion daml/splice-amulet-name-service-test/daml.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2
name: splice-amulet-name-service-test
source: daml
version: 0.1.21
version: 0.1.22
dependencies:
- daml-prim
- daml-stdlib
Expand Down
Loading
Loading