diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/CryptoHashEquivalenceIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/CryptoHashEquivalenceIntegrationTest.scala index ccb86e5c7e..6bc997a5ea 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/CryptoHashEquivalenceIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/CryptoHashEquivalenceIntegrationTest.scala @@ -8,15 +8,37 @@ import com.daml.ledger.javaapi.data.{ ExerciseCommand, ExercisedEvent, Identifier, + Numeric, Party, Text, Value, } +import com.daml.metrics.api.noop.NoOpMetricsFactory import com.digitalasset.canton.resource.DbStorage -import com.digitalasset.canton.topology.PartyId +import com.digitalasset.canton.topology.{ParticipantId, PartyId} import com.digitalasset.daml.lf.data.Ref +import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet.cryptohash.{Hash as DamlHash} +import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet.rewardaccountingv2.{ + Batch, + MintingAllowance, +} +import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet.rewardaccountingv2.batch.{ + BatchOfBatches, + BatchOfMintingAllowances, +} import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTest +import org.lfdecentralizedtrust.splice.migration.DomainMigrationInfo +import org.lfdecentralizedtrust.splice.scan.store.db.{ + DbAppActivityRecordStore, + DbScanAppRewardsStore, +} +import org.lfdecentralizedtrust.splice.scan.store.db.DbScanAppRewardsStore.{ + AppActivityPartyTotalT, + AppRewardPartyTotalT, +} +import org.lfdecentralizedtrust.splice.store.{HistoryMetrics, UpdateHistory} +import org.lfdecentralizedtrust.splice.store.UpdateHistory.BackfillingRequirement import org.lfdecentralizedtrust.splice.util.{DarUtil, WalletTestUtil} import slick.jdbc.canton.ActionBasedSQLInterpolation.Implicits.actionBasedSQLInterpolationCanton @@ -26,7 +48,7 @@ import scala.jdk.CollectionConverters.* /** Equivalence test: Daml (real participant) == SQL (scan Postgres). * - * Validates that the plpgsql hash functions produce identical results + * Validates that the Postgres SQL 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, @@ -49,6 +71,20 @@ class CryptoHashEquivalenceIntegrationTest extends IntegrationTest with WalletTe svParty = sv1Backend.getDsoInfo().svParty svDb = sv1Backend.appState.storage + clue("Allocate parties for MintingAllowance tests") { + Seq("alice-hash-test", "bob-hash-test").foreach { hint => + val party = scala.util + .Try(sv1Backend.participantClient.ledger_api.parties.allocate(hint).party) + .getOrElse( + sv1Backend.participantClient.ledger_api.parties + .list(filterParty = hint) + .head + .party + ) + intermediates(s"party:$hint") = party.toProtoPrimitive + } + } + clue("Upload dar and create CryptoHashProxy") { sv1Backend.participantClient.upload_dar_unless_exists(darPath) val createArgs = new DamlRecord( @@ -85,6 +121,154 @@ class CryptoHashEquivalenceIntegrationTest extends IntegrationTest with WalletTe } } + "Batch hash equivalence (DB Merkle tree == Daml)" should { + + "set up rewards store and allocate parties" in { implicit env => + import env.executionContext + val storage = sv1Backend.appState.storage match { + case db: DbStorage => db + case other => fail(s"Expected DbStorage but got ${other.getClass.getSimpleName}") + } + val participantId = + ParticipantId.tryFromProtoPrimitive("PAR::batch-hash-test::dummy") + val updateHistory = new UpdateHistory( + storage, + new DomainMigrationInfo(0L, None), + "batch_hash_equiv_test", + participantId, + svParty, + BackfillingRequirement.BackfillingNotRequired, + loggerFactory, + enableissue12777Workaround = true, + enableImportUpdateBackfill = false, + HistoryMetrics(NoOpMetricsFactory, 0L), + ) + updateHistory.ingestionSink.initialize().futureValue + val appActivityRecordStore = new DbAppActivityRecordStore( + storage, + updateHistory, + loggerFactory, + ) + rewardsStore = new DbScanAppRewardsStore( + storage, + updateHistory, + appActivityRecordStore, + loggerFactory, + ) + rewardsHistoryId = updateHistory.historyId + + // Allocate real ledger parties for batch tests (idempotent across re-runs) + testParties = (0 until MaxTestParties).map { i => + val hint = s"batch-test-$i" + scala.util + .Try( + sv1Backend.participantClient.ledger_api.parties + .allocate(hint) + .party + ) + .getOrElse( + sv1Backend.participantClient.ledger_api.parties + .list(filterParty = hint) + .head + .party + ) + } + } + + batchTestCases.foreach { tc => + tc.description in { implicit env => + val parties = tc.partyAmounts.zipWithIndex.map { case (amount, i) => + (i, testParties(i), amount) + } + + clue("Insert test data") { + rewardsStore + .insertAppActivityPartyTotals(parties.map { case (_, party, _) => + AppActivityPartyTotalT( + rewardsHistoryId, + tc.roundNumber, + 1000L, + party.toProtoPrimitive, + 1L, + ) + }) + .futureValue + rewardsStore + .insertAppRewardPartyTotals(parties.map { case (seq, party, amount) => + AppRewardPartyTotalT( + rewardsHistoryId, + tc.roundNumber, + seq, + party.toProtoPrimitive, + BigDecimal(amount), + ) + }) + .futureValue + } + + clue("Build Merkle tree in DB") { + rewardsStore.computeRewardHashes(tc.roundNumber, tc.batchSize).futureValue + } + + val dbHashes = clue("Read DB hashes") { + val batches = rewardsStore.getAppRewardBatchHashesByRound(tc.roundNumber).futureValue + batches.groupBy(_.batchLevel).map { case (lvl, bs) => + lvl -> bs.map(_.batchHash.toHex) + } + } + + val leafBatches = mkLeafBatches(parties, tc.batchSize) + + clue("Leaf hashes match") { + val damlLeaves = leafBatches.map(b => exerciseDaml(HashBatch(b.toValue))) + damlLeaves shouldBe dbHashes(0) + } + + clue("Root hash matches") { + val dbRoot = rewardsStore.getAppRewardRootHashByRound(tc.roundNumber).futureValue + dbRoot shouldBe defined + val damlRoot = exerciseDaml(HashBatch(mkRootBatch(leafBatches, tc.batchSize).toValue)) + damlRoot shouldBe dbRoot.value.rootHash.toHex + } + } + } + } + + /** Build the leaf Batch values from parties and amounts — pure data, no Daml calls. */ + private def mkLeafBatches( + parties: Seq[(Int, PartyId, String)], + batchSize: Int, + ): Seq[BatchOfMintingAllowances] = + if (parties.isEmpty) Seq(new BatchOfMintingAllowances(java.util.List.of())) + else + parties.sortBy(_._1).grouped(batchSize).toSeq.map { group => + val allowances = group.map { case (_, party, amount) => + new MintingAllowance(party.toProtoPrimitive, new java.math.BigDecimal(amount)) + } + new BatchOfMintingAllowances(allowances.asJava) + } + + /** Build the root Batch by hashing leaves and nesting into BatchOfBatches. + * For a single leaf, the leaf itself is the root. + */ + private def mkRootBatch( + leaves: Seq[BatchOfMintingAllowances], + batchSize: Int, + )(implicit env: FixtureParam): Batch = { + @annotation.tailrec + def go(batches: Seq[Batch]): Batch = + if (batches.size <= 1) batches.head + else { + val next: Seq[Batch] = batches.grouped(batchSize).toSeq.map { group => + val hashes = group.map(b => new DamlHash(exerciseDaml(HashBatch(b.toValue)))) + new BatchOfBatches(hashes.asJava): Batch + } + go(next) + } + + go(leaves.map(l => l: Batch)) + } + /** Exercise a Daml choice and return the Text result. */ private def exerciseDaml(op: HashOp)(implicit env: FixtureParam): String = { val (choiceName, choiceArg) = toDamlChoiceAndArg(op, resolveRef) @@ -122,6 +306,9 @@ class CryptoHashEquivalenceIntegrationTest extends IntegrationTest with WalletTe private var svParty: PartyId = _ private var proxyContractId: String = _ private var svDb: DbStorage = _ + private var rewardsStore: DbScanAppRewardsStore = _ + private var rewardsHistoryId: Long = _ + private var testParties: Seq[PartyId] = _ } @@ -155,9 +342,11 @@ object CryptoHashEquivalenceIntegrationTest { 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 HashMintingAllowance(provider: HashRef, amount: String) extends HashOp case class HashBatchOfMintingAllowances(hashes: Seq[HashRef]) extends HashOp case class HashBatchOfBatches(hashes: Seq[HashRef]) extends HashOp + case class HashDecimal(value: String) extends HashOp + case class HashBatch(batch: Value) extends HashOp case class TestCaseDef(description: String, op: HashOp) @@ -182,7 +371,10 @@ object CryptoHashEquivalenceIntegrationTest { case HashMintingAllowance(provider, amount) => ( "CryptoHashProxy_HashMintingAllowance", - record("provider" -> text(provider), "amount" -> text(amount)), + record( + "provider" -> new Party(r(provider)), + "amount" -> new Numeric(new java.math.BigDecimal(amount)), + ), ) case HashBatchOfMintingAllowances(hashes) => ( @@ -191,6 +383,10 @@ object CryptoHashEquivalenceIntegrationTest { ) case HashBatchOfBatches(hashes) => ("CryptoHashProxy_HashBatchOfBatches", record("childHashes" -> textList(hashes.map(r)))) + case HashDecimal(v) => + ("CryptoHashProxy_HashDecimal", record("input" -> new Numeric(new java.math.BigDecimal(v)))) + case HashBatch(batchValue) => + ("CryptoHashProxy_HashBatch", record("batch" -> batchValue)) } // -- Derive SQL expression from HashOp ------------------------------------- @@ -207,67 +403,137 @@ object CryptoHashEquivalenceIntegrationTest { 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)}')" + s"hash_minting_allowance('${escapeSql(r(provider))}', daml_numeric_to_text(${escapeSql(amount)}::decimal(38,10)))" 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))})" + case HashDecimal(v) => + s"daml_crypto_hash_text(daml_numeric_to_text(${escapeSql(v)}::decimal(38,10)))" + case HashBatch(_) => + throw new UnsupportedOperationException("HashBatch is not used in SQL expression tests") } // -- Test case definitions (fully static) ----------------------------------- + // Hand-written edge cases for hashing Decimal values. Each case verifies that + // Daml's hash(d) equals Postgres' daml_crypto_hash_text(daml_numeric_to_text(d)), + // which requires Daml's show and Postgres' daml_numeric_to_text to produce + // identical text. Each entry is (value, category, description) targeting a + // specific formatting divergence risk between the two representations. + val decimalFormatCases: Seq[(String, String, String)] = Seq( + ("10.0000000000", "trailing zeros", "10 trailing zeros are stripped to .0"), + ("0.0000000000", "all-zero fraction", "result is 0.0, not 0 or 0."), + ("5.0", "integer with .0", "trailing zeros stripped then .0 re-appended"), + ("3.14", "fractional digits", "non-zero fractional digits pass through"), + ("0", "bare integer", ".0 is appended when no decimal point"), + ("100", "bare integer", ".0 is appended to larger integer"), + ("-3.14", "negative", "sign is preserved"), + ("-0.0", "negative zero", "Daml vs Postgres agree on -0.0"), + ("0.0000000001", "smallest fraction", "10th decimal place survives"), + ("99999999999.0", "large integer part", "11-digit integer part formats correctly"), + ("1.10", "mixed trailing zeros", "single trailing zero stripped to 1.1"), + ("9999999999999999999999999999.9999999999", "max positive", "28 integer + 10 fractional nines"), + ("-9999999999999999999999999999.9999999999", "max negative", "negated max"), + ("-0.0000000001", "negative fraction", "sign + smallest fraction"), + ) + // 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"))), + val allTestCaseDefs: Seq[TestCaseDef] = + decimalFormatCases.map { case (v, category, desc) => + TestCaseDef(s"decimal $v ($category: $desc)", HashDecimal(v)) + } ++ 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(IntermediateRef("party:alice-hash-test"), "10.0000000000"), + ), + TestCaseDef( + "maBob", + HashMintingAllowance(IntermediateRef("party:bob-hash-test"), "0"), + ), + TestCaseDef( + "maAlice5", + HashMintingAllowance(IntermediateRef("party:alice-hash-test"), "5.0"), + ), + TestCaseDef( + "maBob3", + HashMintingAllowance(IntermediateRef("party:bob-hash-test"), "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"))), + ), + ) + + // -- Batch tree equivalence test data ---------------------------------------- + + val MaxTestParties = 5 + + case class BatchTestCase( + description: String, + roundNumber: Long, + batchSize: Int, + partyAmounts: Seq[String], + ) + + val batchTestCases: Seq[BatchTestCase] = Seq( + BatchTestCase("single party", 9001, 2, Seq("10.0000000000")), + BatchTestCase("two parties one batch", 9002, 2, Seq("10.0000000000", "5.0000000000")), + BatchTestCase( + "three parties two batches", + 9003, + 2, + Seq("10.0000000000", "5.0000000000", "3.0000000000"), ), - TestCaseDef( - "hash_batch_of_batches with two leaves", - HashBatchOfBatches(Seq(IntermediateRef("leaf1"), IntermediateRef("leaf2"))), + BatchTestCase( + "five parties multi-level", + 9004, + 2, + Seq("10.0000000000", "5.0000000000", "3.0000000000", "7.0000000000", "1.0000000000"), ), + BatchTestCase("empty round", 9005, 2, Seq.empty), ) } diff --git a/apps/common/src/main/resources/db/migration/canton-network/postgres/stable/V068__daml_numeric_to_text.sql b/apps/common/src/main/resources/db/migration/canton-network/postgres/stable/V068__daml_numeric_to_text.sql new file mode 100644 index 0000000000..0d5e346b8b --- /dev/null +++ b/apps/common/src/main/resources/db/migration/canton-network/postgres/stable/V068__daml_numeric_to_text.sql @@ -0,0 +1,26 @@ +-- Format a numeric so that its text representation matches Daml's +-- `show (d : Numeric n)`, which calls `Numeric.toUnscaledString`: +-- * strip trailing zeros from the fractional part +-- * drop a bare trailing "." +-- * if there is no "." after stripping, append ".0" +-- Examples (for `numeric(38,10)` input): +-- "10.0000000000" -> "10.0" +-- "23759.7510000000" -> "23759.751" +-- "0.0000000000" -> "0.0" +-- Scan stores amounts as `decimal(38,10)` whose `::text` cast preserves all +-- trailing zeros; Daml's `show` strips them. Hash inputs must match exactly, +-- so use this helper whenever hashing a numeric value. +CREATE FUNCTION daml_numeric_to_text(x numeric) RETURNS text + RETURNS NULL ON NULL INPUT + AS $$ + SELECT CASE + WHEN stripped LIKE '%.%' THEN stripped + ELSE stripped || '.0' + END + FROM ( + SELECT regexp_replace( + regexp_replace(x::text, '0+$', ''), -- strip trailing zeros + '\.$', '' -- then drop any bare trailing "." + ) AS stripped + ) s + $$ LANGUAGE sql IMMUTABLE PARALLEL SAFE; diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanAppRewardsStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanAppRewardsStore.scala index b38887a58b..9752ad76bf 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanAppRewardsStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanAppRewardsStore.scala @@ -833,7 +833,7 @@ class DbScanAppRewardsStore( * Level 1+: hash batches of batches until a single root remains. * All levels run in a single transaction. */ - private[store] def computeRewardHashes( + private[splice] def computeRewardHashes( roundNumber: Long, batchSize: Int, )(implicit tc: TraceContext): Future[Unit] = { @@ -892,7 +892,7 @@ class DbScanAppRewardsStore( min(seq_num), max(seq_num) + 1, decode(hash_batch_of_minting_allowances( array_agg( - hash_minting_allowance(party, amount::text) + hash_minting_allowance(party, daml_numeric_to_text(amount)) order by seq_num ) ), 'hex') diff --git a/daml/dars.lock b/daml/dars.lock index 991298395e..a87aaffff1 100644 --- a/daml/dars.lock +++ b/daml/dars.lock @@ -39,8 +39,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.24 06bbf6f89a0fc128f5fdf3e88119c6baccad95ead4f535046a41fcfc5a21c383 -splice-amulet-test 0.1.23 54880e06406813ead07a16a643b1a698d36b708031ad42d1bb83c0171b97ef7b +splice-amulet-name-service-test 0.1.24 cc2b70a8bcd4b145190550d1edfb3c49ffbf89b74a96798e7d46eb6cdce52079 +splice-amulet-test 0.1.23 0cd1b783c068c5571c754072f49f01b82e06a6e92cb649c65f18f5112c3c180f 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 @@ -78,7 +78,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.31 dc7d3e85609964fb7c7ac32ed247f8b28e30e79844ef1879af6732c2f7c5e5b9 +splice-dso-governance-test 0.1.31 6ea3403fc84f1a59c7e80432e6740a8f935119bed797a21a95793d42f4729f7b splice-token-standard-test 1.0.14 603d12242dfc926a4bc6096a919400d29ddbf05f14f4307b98c8f39fb285f582 splice-token-test-dummy-holding 0.0.1 1cd171c6c42ab46dc9cf12d80c6111369e00cea5cdf054924b4f26ce94b1ef5b splice-token-test-dummy-holding 0.0.2 4f40fb033ef3db89623642c1b494e846097fa32af138b3864a63aa15937a323d @@ -156,7 +156,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.24 462b1c6b5e9e43036fe1930c1db0b45f75247aaa8dd669b11df90cb3d4dc964d +splice-wallet-test 0.1.24 8edb3f61c61f4083e18eaf9af5eb9c80e3b06094aa3ccfa646d7bbdef0ad9ec9 splitwell 0.1.0 075c76de553ab88383a7c69de134afa82aacfdf8ea8fcfe8852c4b199c3b2669 splitwell 0.1.1 ccb1a0215053062202052e1a052f9214da3fdae5253a6d43e2e155ff4f57fe75 splitwell 0.1.10 d42676a366f7ca7a2409974dd3054aa4d83ab29baa3b2086ad021407b0a1a295 @@ -178,4 +178,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.24 86cfff880a2e1533d93c46567af15a656b07fad698bd2af0094473410de8b9fa \ No newline at end of file +splitwell-test 0.1.24 5f4ce96c71588e649b92ca74614da13227591d7adbd0152efb4fd14873c9235c \ No newline at end of file diff --git a/daml/splice-amulet-test/daml/Splice/Testing/CryptoHashProxy.daml b/daml/splice-amulet-test/daml/Splice/Testing/CryptoHashProxy.daml index 0c13b83c02..7993722f5a 100644 --- a/daml/splice-amulet-test/daml/Splice/Testing/CryptoHashProxy.daml +++ b/daml/splice-amulet-test/daml/Splice/Testing/CryptoHashProxy.daml @@ -3,48 +3,8 @@ module Splice.Testing.CryptoHashProxy where -import DA.Text (intercalate, sha256) - --- TODO(#4749): When Splice.Amulet.CryptoHash is merged, remove these --- test-only copies and import from the real module instead. - --- | Hash a text scalar: sha256(value). --- Matches: instance Hashable Text where hash = hashText . id -hashText : Text -> Text -hashText = sha256 - --- | Hash a list of already-hashed elements. --- Matches: hashListInternal ts = sha256(intercalate "|" (show(length ts) :: ts)) -hashList : [Text] -> Text -hashList elems = - let parts = show (length elems) :: elems - in sha256 (intercalate "|" parts) - --- | Hash a variant with a tag and field hashes. --- Matches: hashVariant tag fields = sha256(intercalate "|" (tag :: show(length fields) :: fields)) -hashVariant : Text -> [Text] -> Text -hashVariant tag fieldHashes = - let parts = tag :: show (length fieldHashes) :: fieldHashes - in sha256 (intercalate "|" parts) - --- | Hash a record as a list of field hashes. -hashRecord : [Text] -> Text -hashRecord = hashList - --- | Hash a MintingAllowance{provider, amount}. -hashMintingAllowance : Text -> Text -> Text -hashMintingAllowance provider amount = - hashRecord [hashText provider, hashText amount] - --- | Hash a BatchOfMintingAllowances variant. -hashBatchOfMintingAllowances : [Text] -> Text -hashBatchOfMintingAllowances allowanceHashes = - hashVariant "BatchOfMintingAllowances" [hashList allowanceHashes] - --- | Hash a BatchOfBatches variant. -hashBatchOfBatches : [Text] -> Text -hashBatchOfBatches childHashes = - hashVariant "BatchOfBatches" [hashList childHashes] +import Splice.Amulet.CryptoHash (Hash(..), Hashable(..), hashVariant) +import Splice.Amulet.RewardAccountingV2 (Batch, MintingAllowance(..)) -- | Proxy template exposing hash functions as exercisable choices. -- Allows Scala integration tests to call Daml hash functions via the @@ -58,36 +18,49 @@ template CryptoHashProxy with with input : Text controller owner - do pure (hashText input) + do pure (hash input).value nonconsuming choice CryptoHashProxy_HashList : Text with elems : [Text] controller owner - do pure (hashList elems) + do pure (hash (map Hash elems)).value nonconsuming choice CryptoHashProxy_HashVariant : Text with tag : Text fields : [Text] controller owner - do pure (hashVariant tag fields) + do pure (hashVariant tag (map Hash fields)).value nonconsuming choice CryptoHashProxy_HashMintingAllowance : Text with - provider : Text - amount : Text + provider : Party + amount : Decimal controller owner - do pure (hashMintingAllowance provider amount) + do pure (hash (MintingAllowance provider amount)).value nonconsuming choice CryptoHashProxy_HashBatchOfMintingAllowances : Text with allowanceHashes : [Text] controller owner - do pure (hashBatchOfMintingAllowances allowanceHashes) + do pure (hashVariant "BatchOfMintingAllowances" [hash (map Hash allowanceHashes)]).value nonconsuming choice CryptoHashProxy_HashBatchOfBatches : Text with childHashes : [Text] controller owner - do pure (hashBatchOfBatches childHashes) + do pure (hashVariant "BatchOfBatches" [hash (map Hash childHashes)]).value + + nonconsuming choice CryptoHashProxy_HashDecimal : Text + with + input : Decimal + controller owner + do pure (hash input).value + + nonconsuming choice CryptoHashProxy_HashBatch : Text + with + batch : Batch + controller owner + do pure (hash batch).value +