mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
[DRAFT] feat/CORDA-3823-hash-agility-qa-ready (#6789)
* wip * wip * wip (need to review IEE comments) * wip * wip * Small refactoring, fixed network-verifier's TestNotaryFlow * Added command line option to explicitly enable hash agility support * wip-do-not-push * wip * wip * wip * aligned merkletree/transaction hash algorithms * wip * Added mixed algorithm support for nodes vs leaves and corrected mixed algorithm tests * moved global computeNonce and componentHash to DigestService * added comment for failing test to fix * wip * Minor cleanups, added deprecated componentHash/computeNonce * restored exploratory changes to failing SignedTransaction test * cleaned up and minor rafactoring * Fixed some tests with hardcoded hash algorithm * some changes and cleanups following code review * WIP commit before large change * WIP Fixed 3 tests * WIP removed direct references to randomSHA256() and sha256() * Updated/added liquibase migrations to support larger hash algorithms * Reviewed, cleanups, comments, fixes * removing direct references to sha256() * WIP verifying obligations test errors * reviewing obligation/attachment issues with sha3_256 * Full review before PR - intermediate commits * Reviewed and cleaned up * Futher cleanup * Fixed partial tree backward compatible json and cleanups * all tests passing * Removed couple of unused imports * Reworked global componentHash function to avoid deprecated warnings * replaced SHA3s with some alternate SHA2s * Removed SHA3-256 and SHA3-512 references * fixed some tests using non ubiquitous hash algorithms * Fixed ABI compatibility (not for TransactionBuilder) * Fixed ABI compatibility to TransactionBuilder * couple of fixes * fixed DigestService's randomHash * Removed constructor with loosely typed args for private constructor of LedgerTransaction class (API removal) * re-introduced LedgerTransaction deprecated ctor for deserialization * Add possibility to load CustomMessageDigest bypassing JCA (#6798) * Change api-current for DigestAlgorithm * disable flaky tests Co-authored-by: Denis Rekalov <denis.rekalov@r3.com>
This commit is contained in:
parent
74c5470627
commit
82a114a329
@ -1849,6 +1849,15 @@ public final class net.corda.core.crypto.CryptoUtils extends java.lang.Object
|
||||
public static final boolean verify(java.security.PublicKey, byte[], net.corda.core.crypto.DigitalSignature)
|
||||
public static final boolean verify(java.security.PublicKey, byte[], byte[])
|
||||
##
|
||||
public interface net.corda.core.crypto.DigestAlgorithm
|
||||
@NotNull
|
||||
public abstract byte[] digest(byte[])
|
||||
@NotNull
|
||||
public abstract String getAlgorithm()
|
||||
public abstract int getDigestLength()
|
||||
@NotNull
|
||||
public abstract byte[] preImageResistantDigest(byte[])
|
||||
##
|
||||
@CordaSerializable
|
||||
public class net.corda.core.crypto.DigitalSignature extends net.corda.core.utilities.OpaqueBytes
|
||||
public <init>(byte[])
|
||||
@ -6408,7 +6417,6 @@ public abstract class net.corda.core.transactions.FullTransaction extends net.co
|
||||
public final class net.corda.core.transactions.LedgerTransaction extends net.corda.core.transactions.FullTransaction
|
||||
public <init>(java.util.List<? extends net.corda.core.contracts.StateAndRef<? extends net.corda.core.contracts.ContractState>>, java.util.List<? extends net.corda.core.contracts.TransactionState<? extends net.corda.core.contracts.ContractState>>, java.util.List<? extends net.corda.core.contracts.CommandWithParties<? extends net.corda.core.contracts.CommandData>>, java.util.List<? extends net.corda.core.contracts.Attachment>, net.corda.core.crypto.SecureHash, net.corda.core.identity.Party, net.corda.core.contracts.TimeWindow, net.corda.core.contracts.PrivacySalt)
|
||||
public <init>(java.util.List<? extends net.corda.core.contracts.StateAndRef<? extends net.corda.core.contracts.ContractState>>, java.util.List<? extends net.corda.core.contracts.TransactionState<? extends net.corda.core.contracts.ContractState>>, java.util.List<? extends net.corda.core.contracts.CommandWithParties<? extends net.corda.core.contracts.CommandData>>, java.util.List<? extends net.corda.core.contracts.Attachment>, net.corda.core.crypto.SecureHash, net.corda.core.identity.Party, net.corda.core.contracts.TimeWindow, net.corda.core.contracts.PrivacySalt, net.corda.core.node.NetworkParameters)
|
||||
public <init>(java.util.List, java.util.List, java.util.List, java.util.List, net.corda.core.crypto.SecureHash, net.corda.core.identity.Party, net.corda.core.contracts.TimeWindow, net.corda.core.contracts.PrivacySalt, net.corda.core.node.NetworkParameters, java.util.List, java.util.List, java.util.List, java.util.List, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function2, net.corda.core.serialization.internal.AttachmentsClassLoaderCache, kotlin.jvm.internal.DefaultConstructorMarker)
|
||||
@NotNull
|
||||
public final java.util.List<net.corda.core.contracts.Command<T>> commandsOfType(Class<T>)
|
||||
@NotNull
|
||||
|
@ -544,7 +544,7 @@ task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
|
||||
fileTree(dir: it,
|
||||
// these exclusions are necessary because jacoco gets confused by same class names
|
||||
// which occur due to deterministic versions of non deterministic classes
|
||||
exclude: ['**/net/corda/core/crypto/SHA256DigestSupplier**',
|
||||
exclude: ['**/net/corda/core/crypto/DigestSupplier**',
|
||||
'**/net/corda/core/crypto/DelegatingSecureRandomService',
|
||||
'**/net/corda/core/internal/ThreadLocalToggleField**',
|
||||
'**/net/corda/core/internal/InheritableThreadLocalToggleField**',
|
||||
|
@ -396,7 +396,7 @@ object JacksonSupport {
|
||||
class SecureHashDeserializer<T : SecureHash> : JsonDeserializer<T>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): T {
|
||||
try {
|
||||
return uncheckedCast(SecureHash.parse(parser.text))
|
||||
return uncheckedCast(SecureHash.create(parser.text))
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException(parser, "Invalid hash ${parser.text}: ${e.message}")
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ import net.corda.client.jackson.JacksonSupport
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.PartialMerkleTree.PartialTree
|
||||
import net.corda.core.crypto.SecureHash.Companion.SHA2_256
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.identity.*
|
||||
import net.corda.core.internal.DigitalSignatureWithCert
|
||||
@ -80,8 +81,9 @@ class CordaModule : SimpleModule("corda-core") {
|
||||
context.setMixInAnnotations(Party::class.java, PartyMixin::class.java)
|
||||
context.setMixInAnnotations(PublicKey::class.java, PublicKeyMixin::class.java)
|
||||
context.setMixInAnnotations(ByteSequence::class.java, ByteSequenceMixin::class.java)
|
||||
context.setMixInAnnotations(SecureHash.SHA256::class.java, SecureHashSHA256Mixin::class.java)
|
||||
context.setMixInAnnotations(SecureHash::class.java, SecureHashSHA256Mixin::class.java)
|
||||
context.setMixInAnnotations(SecureHash.SHA256::class.java, SecureHashMixin::class.java)
|
||||
context.setMixInAnnotations(SecureHash.HASH::class.java, SecureHashMixin::class.java)
|
||||
context.setMixInAnnotations(SecureHash::class.java, SecureHashMixin::class.java)
|
||||
context.setMixInAnnotations(SerializedBytes::class.java, SerializedBytesMixin::class.java)
|
||||
context.setMixInAnnotations(DigitalSignature.WithKey::class.java, ByteSequenceWithPropertiesMixin::class.java)
|
||||
context.setMixInAnnotations(DigitalSignatureWithCert::class.java, ByteSequenceWithPropertiesMixin::class.java)
|
||||
@ -97,6 +99,7 @@ class CordaModule : SimpleModule("corda-core") {
|
||||
context.setMixInAnnotations(PartialTree::class.java, PartialTreeMixin::class.java)
|
||||
context.setMixInAnnotations(NodeInfo::class.java, NodeInfoMixin::class.java)
|
||||
context.setMixInAnnotations(StateMachineRunId::class.java, StateMachineRunIdMixin::class.java)
|
||||
context.setMixInAnnotations(DigestService::class.java, DigestServiceMixin::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@ -209,6 +212,7 @@ private interface WireTransactionMixin
|
||||
private class WireTransactionSerializer : JsonSerializer<WireTransaction>() {
|
||||
override fun serialize(value: WireTransaction, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
gen.writeObject(WireTransactionJson(
|
||||
value.digestService,
|
||||
value.id,
|
||||
value.notary,
|
||||
value.inputs,
|
||||
@ -236,11 +240,12 @@ private class WireTransactionDeserializer : JsonDeserializer<WireTransaction>()
|
||||
wrapper.references,
|
||||
wrapper.networkParametersHash
|
||||
)
|
||||
return WireTransaction(componentGroups, wrapper.privacySalt)
|
||||
return WireTransaction(componentGroups, wrapper.privacySalt, wrapper.digestService ?: DigestService.sha2_256)
|
||||
}
|
||||
}
|
||||
|
||||
private class WireTransactionJson(val id: SecureHash,
|
||||
private class WireTransactionJson(@get:JsonInclude(Include.NON_NULL) val digestService: DigestService?,
|
||||
val id: SecureHash,
|
||||
val notary: Party?,
|
||||
val inputs: List<StateRef>,
|
||||
val outputs: List<TransactionState<*>>,
|
||||
@ -335,7 +340,7 @@ private class PartialTreeSerializer : JsonSerializer<PartialTree>() {
|
||||
return when (tree) {
|
||||
is PartialTree.IncludedLeaf -> PartialTreeJson(includedLeaf = tree.hash)
|
||||
is PartialTree.Leaf -> PartialTreeJson(leaf = tree.hash)
|
||||
is PartialTree.Node -> PartialTreeJson(left = convert(tree.left), right = convert(tree.right))
|
||||
is PartialTree.Node -> PartialTreeJson(left = convert(tree.left), right = convert(tree.right), hashAlgorithm = tree.hashAlgorithm)
|
||||
else -> throw IllegalArgumentException("Don't know how to serialize $tree")
|
||||
}
|
||||
}
|
||||
@ -351,7 +356,7 @@ private class PartialTreeDeserializer : JsonDeserializer<PartialTree>() {
|
||||
when {
|
||||
includedLeaf != null -> PartialTree.IncludedLeaf(includedLeaf)
|
||||
leaf != null -> PartialTree.Leaf(leaf)
|
||||
else -> PartialTree.Node(convert(left!!), convert(right!!))
|
||||
else -> PartialTree.Node(convert(left!!), convert(right!!), hashAlgorithm ?: SHA2_256)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -361,7 +366,8 @@ private class PartialTreeDeserializer : JsonDeserializer<PartialTree>() {
|
||||
private class PartialTreeJson(val includedLeaf: SecureHash? = null,
|
||||
val leaf: SecureHash? = null,
|
||||
val left: PartialTreeJson? = null,
|
||||
val right: PartialTreeJson? = null) {
|
||||
val right: PartialTreeJson? = null,
|
||||
val hashAlgorithm: String? = null) {
|
||||
init {
|
||||
if (includedLeaf != null) {
|
||||
require(leaf == null && left == null && right == null) { "Invalid JSON structure" }
|
||||
@ -440,7 +446,7 @@ private interface NodeInfoMixin
|
||||
|
||||
@ToStringSerialize
|
||||
@JsonDeserialize(using = JacksonSupport.SecureHashDeserializer::class)
|
||||
private interface SecureHashSHA256Mixin
|
||||
private interface SecureHashMixin
|
||||
|
||||
@JsonSerialize(using = JacksonSupport.PublicKeySerializer::class)
|
||||
@JsonDeserialize(using = JacksonSupport.PublicKeyDeserializer::class)
|
||||
@ -541,6 +547,25 @@ private class AmountDeserializer(delegate: JsonDeserializer<*>) : DelegatingDese
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerialize(using = DigestServiceSerializer::class)
|
||||
@JsonDeserialize(using = DigestServiceDeserializer::class)
|
||||
private interface DigestServiceMixin
|
||||
|
||||
private class DigestServiceSerializer : JsonSerializer<DigestService>() {
|
||||
override fun serialize(value: DigestService, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
gen.writeObject(DigestServiceJson(value.hashAlgorithm))
|
||||
}
|
||||
}
|
||||
|
||||
private class DigestServiceDeserializer : JsonDeserializer<DigestService>() {
|
||||
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): DigestService {
|
||||
val wrapper = parser.readValueAs<DigestServiceJson>()
|
||||
return DigestService(wrapper.hashAlgorithm)
|
||||
}
|
||||
}
|
||||
|
||||
private class DigestServiceJson(val hashAlgorithm: String)
|
||||
|
||||
@JsonDeserialize(using = JacksonSupport.OpaqueBytesDeserializer::class)
|
||||
private interface ByteSequenceMixin {
|
||||
@Suppress("unused")
|
||||
|
@ -50,6 +50,7 @@ import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.TestFactory
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.junit.runners.Parameterized.Parameters
|
||||
@ -236,6 +237,29 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
assertThat(mapper.convertValue<TransactionSignature>(json)).isEqualTo(transactionSignature)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `TransactionSignature with SHA3`() {
|
||||
val signatureMetadata = SignatureMetadata(1, 1)
|
||||
val partialMerkleTree = PartialMerkleTree(PartialTree.Node(
|
||||
left = PartialTree.Leaf(SecureHash.random(SecureHash.SHA2_384)),
|
||||
right = PartialTree.IncludedLeaf(SecureHash.random(SecureHash.SHA2_384)),
|
||||
hashAlgorithm = SecureHash.SHA2_384
|
||||
))
|
||||
val transactionSignature = TransactionSignature(secureRandomBytes(128), BOB_PUBKEY, signatureMetadata, partialMerkleTree)
|
||||
val json = mapper.valueToTree<ObjectNode>(transactionSignature)
|
||||
val (bytesJson, byJson, signatureMetadataJson, partialMerkleTreeJson) = json.assertHasOnlyFields(
|
||||
"bytes",
|
||||
"by",
|
||||
"signatureMetadata",
|
||||
"partialMerkleTree"
|
||||
)
|
||||
assertThat(bytesJson.binaryValue()).isEqualTo(transactionSignature.bytes)
|
||||
assertThat(byJson.valueAs<PublicKey>(mapper)).isEqualTo(BOB_PUBKEY)
|
||||
assertThat(signatureMetadataJson.valueAs<SignatureMetadata>(mapper)).isEqualTo(signatureMetadata)
|
||||
assertThat(partialMerkleTreeJson.valueAs<PartialMerkleTree>(mapper).root).isEqualTo(partialMerkleTree.root)
|
||||
assertThat(mapper.convertValue<TransactionSignature>(json)).isEqualTo(transactionSignature)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `SignedTransaction (WireTransaction)`() {
|
||||
val attachmentId = SecureHash.randomSHA256()
|
||||
@ -267,7 +291,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
println(mapper.writeValueAsString(json))
|
||||
val (wtxJson, signaturesJson) = json.assertHasOnlyFields("wire", "signatures")
|
||||
assertThat(signaturesJson.childrenAs<TransactionSignature>(mapper)).isEqualTo(stx.sigs)
|
||||
val wtxFields = wtxJson.assertHasOnlyFields("id", "notary", "inputs", "attachments", "outputs", "commands", "timeWindow", "references", "privacySalt", "networkParametersHash")
|
||||
val wtxFields = wtxJson.assertHasOnlyFields("id", "notary", "inputs", "attachments", "outputs", "commands", "timeWindow", "references", "privacySalt", "networkParametersHash", "digestService")
|
||||
assertThat(wtxFields[0].valueAs<SecureHash>(mapper)).isEqualTo(wtx.id)
|
||||
assertThat(wtxFields[1].valueAs<Party>(mapper)).isEqualTo(wtx.notary)
|
||||
assertThat(wtxFields[2].childrenAs<StateRef>(mapper)).isEqualTo(wtx.inputs)
|
||||
@ -277,6 +301,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
assertThat(wtxFields[6].valueAs<TimeWindow>(mapper)).isEqualTo(wtx.timeWindow)
|
||||
assertThat(wtxFields[7].childrenAs<StateRef>(mapper)).isEqualTo(wtx.references)
|
||||
assertThat(wtxFields[8].valueAs<PrivacySalt>(mapper)).isEqualTo(wtx.privacySalt)
|
||||
assertThat(wtxFields[10].valueAs<DigestService>(mapper)).isEqualTo(wtx.digestService)
|
||||
assertThat(mapper.convertValue<WireTransaction>(wtxJson)).isEqualTo(wtx)
|
||||
assertThat(mapper.convertValue<SignedTransaction>(json)).isEqualTo(stx)
|
||||
}
|
||||
@ -379,14 +404,47 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
right = PartialTree.IncludedLeaf(SecureHash.randomSHA256())
|
||||
)
|
||||
val json = mapper.valueToTree<ObjectNode>(node)
|
||||
println(mapper.writeValueAsString(json))
|
||||
val (leftJson, rightJson) = json.assertHasOnlyFields("left", "right")
|
||||
val (leftJson, rightJson, algorithm) = json.assertHasOnlyFields("left", "right", "hashAlgorithm")
|
||||
assertThat(leftJson.valueAs<PartialTree>(mapper)).isEqualTo(node.left)
|
||||
assertThat(rightJson.valueAs<PartialTree>(mapper)).isEqualTo(node.right)
|
||||
assertThat(algorithm.valueAs<String>(mapper)).isEqualTo(node.hashAlgorithm)
|
||||
assertThat(mapper.convertValue<PartialTree.Node>(json)).isEqualTo(node)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `simple PartialTree Node with SHA3`() {
|
||||
val node = PartialTree.Node(
|
||||
left = PartialTree.Leaf(SecureHash.random(SecureHash.SHA2_384)),
|
||||
right = PartialTree.IncludedLeaf(SecureHash.random(SecureHash.SHA2_384)),
|
||||
hashAlgorithm = SecureHash.SHA2_384
|
||||
)
|
||||
val json = mapper.valueToTree<ObjectNode>(node)
|
||||
val (leftJson, rightJson, algorithm) = json.assertHasOnlyFields("left", "right", "hashAlgorithm")
|
||||
assertThat(leftJson.valueAs<PartialTree>(mapper)).isEqualTo(node.left)
|
||||
assertThat(rightJson.valueAs<PartialTree>(mapper)).isEqualTo(node.right)
|
||||
assertThat(algorithm.valueAs<String>(mapper)).isEqualTo(node.hashAlgorithm)
|
||||
assertThat(mapper.convertValue<PartialTree.Node>(json)).isEqualTo(node)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `PartialTree Node backward compatible deserialize`() {
|
||||
val left = SecureHash.randomSHA256()
|
||||
val right = SecureHash.randomSHA256()
|
||||
val node = PartialTree.Node(
|
||||
left = PartialTree.Leaf(left),
|
||||
right = PartialTree.IncludedLeaf(right),
|
||||
hashAlgorithm = SecureHash.SHA2_256
|
||||
)
|
||||
val legacyJson = mapper.readTree("{\"left\":{\"leaf\":\"$left\"},\"right\":{\"includedLeaf\":\"$right\"}}")
|
||||
val (leftJson, rightJson) = legacyJson.assertHasOnlyFields("left", "right")
|
||||
|
||||
assertThat(leftJson.valueAs<PartialTree>(mapper)).isEqualTo(node.left)
|
||||
assertThat(rightJson.valueAs<PartialTree>(mapper)).isEqualTo(node.right)
|
||||
assertThat(mapper.convertValue<PartialTree.Node>(legacyJson)).isEqualTo(node)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@TestFactory()
|
||||
fun `complex PartialTree Node`() {
|
||||
val node = PartialTree.Node(
|
||||
left = PartialTree.IncludedLeaf(SecureHash.randomSHA256()),
|
||||
|
@ -13,7 +13,7 @@ class StringToMethodCallParserTest {
|
||||
fun simple() = "simple"
|
||||
fun string(noteTextWord: String) = noteTextWord
|
||||
fun twoStrings(a: String, b: String) = a + b
|
||||
fun simpleObject(hash: SecureHash.SHA256) = hash.toString()
|
||||
fun simpleObject(hash: SecureHash) = hash.toString()
|
||||
fun complexObject(pair: Pair<Int, String>) = pair
|
||||
fun complexNestedObject(pairs: Pair<Int, Deque<Char>>) = pairs
|
||||
|
||||
|
@ -68,7 +68,7 @@ def patchCore = tasks.register('patchCore', Zip) {
|
||||
from(processResources)
|
||||
from(zipTree(originalJar)) {
|
||||
exclude 'net/corda/core/crypto/DelegatingSecureRandomService*.class'
|
||||
exclude 'net/corda/core/crypto/SHA256DigestSupplier.class'
|
||||
exclude 'net/corda/core/crypto/DigestSupplier.class'
|
||||
exclude 'net/corda/core/internal/*ToggleField*.class'
|
||||
exclude 'net/corda/core/serialization/*SerializationFactory*.class'
|
||||
exclude 'net/corda/core/serialization/internal/AttachmentsHolderImpl.class'
|
||||
|
@ -0,0 +1,10 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.crypto.internal.DigestAlgorithmFactory
|
||||
import java.util.function.Supplier
|
||||
|
||||
@Suppress("unused")
|
||||
private class DigestSupplier(private val algorithm: String) : Supplier<DigestAlgorithm> {
|
||||
override fun get(): DigestAlgorithm = DigestAlgorithmFactory.create(algorithm)
|
||||
val digestLength: Int by lazy { get().digestLength }
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.util.function.Supplier
|
||||
|
||||
@Suppress("unused")
|
||||
private class SHA256DigestSupplier : Supplier<MessageDigest> {
|
||||
override fun get(): MessageDigest = MessageDigest.getInstance("SHA-256")
|
||||
}
|
@ -21,14 +21,14 @@ class PrivacySaltTest {
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun testTooShortPrivacySalt() {
|
||||
fun testTooShortPrivacySaltForSHA256() {
|
||||
val ex = assertFailsWith<IllegalArgumentException> { PrivacySalt(ByteArray(SALT_SIZE - 1) { 0x7f }) }
|
||||
assertEquals("Privacy salt should be 32 bytes.", ex.message)
|
||||
assertEquals("Privacy salt should be at least 32 bytes.", ex.message)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun testTooLongPrivacySalt() {
|
||||
val ex = assertFailsWith<IllegalArgumentException> { PrivacySalt(ByteArray(SALT_SIZE + 1) { 0x7f }) }
|
||||
assertEquals("Privacy salt should be 32 bytes.", ex.message)
|
||||
fun testTooShortPrivacySaltForSHA512() {
|
||||
val ex = assertFailsWith<IllegalArgumentException> { PrivacySalt(ByteArray(SALT_SIZE) { 0x7f }).apply { validateFor("SHA-512") } }
|
||||
assertEquals("Privacy salt should be at least 64 bytes for SHA-512.", ex.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,50 @@
|
||||
package net.corda.deterministic.crypto
|
||||
|
||||
import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.MerkleTree
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class MerkleTreeTest {
|
||||
private fun leafs(algorithm : String) : List<SecureHash> =
|
||||
listOf(SecureHash.allOnesHashFor(algorithm), SecureHash.zeroHashFor(algorithm))
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun testCreate() {
|
||||
val merkle = MerkleTree.getMerkleTree(listOf(SecureHash.allOnesHash, SecureHash.zeroHash))
|
||||
assertEquals(SecureHash.parse("A5DE9B714ACCD8AFAAABF1CBD6E1014C9D07FF95C2AE154D91EC68485B31E7B5"), merkle.hash)
|
||||
val merkle = MerkleTree.getMerkleTree(leafs(SecureHash.SHA2_256), DigestService.sha2_256)
|
||||
assertEquals(SecureHash.create("A5DE9B714ACCD8AFAAABF1CBD6E1014C9D07FF95C2AE154D91EC68485B31E7B5"), merkle.hash)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `test create SHA2-384`() {
|
||||
val merkle = MerkleTree.getMerkleTree(leafs(SecureHash.SHA2_384), DigestService.sha2_384)
|
||||
assertEquals(SecureHash.create("SHA-384:2B83D37859E3665D7C239964D769CF950EE6478C13E4CA2D6643C23B6C4EAE035C88F654D22E0D65E7CA40BAE4F3718F"), merkle.hash)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `test create SHA2-256 to SHA2-384`() {
|
||||
val merkle = MerkleTree.getMerkleTree(leafs(SecureHash.SHA2_256), DigestService.sha2_384)
|
||||
assertEquals(SecureHash.create("SHA-384:02A4E8EA5AA4BBAFE80C0E7127B15994B84030BE8616EA2A0127D85203CF34221403635C08084A6BDDB1DB06333F0A49"), merkle.hash)
|
||||
}
|
||||
|
||||
// @Test(timeout=300_000)
|
||||
// fun testCreateSHA3256() {
|
||||
// val merkle = MerkleTree.getMerkleTree(listOf(SecureHash.allOnesHashFor(SecureHash.SHA3_256),
|
||||
// SecureHash.zeroHashFor(SecureHash.SHA3_256)), DigestService.sha3_256)
|
||||
// assertEquals(SecureHash.create("SHA3-256:80673DBEEC8F6761ACBB121E7E45F61D4279CCD8B8E2231741ECD0716F4C9EDC"), merkle.hash)
|
||||
// }
|
||||
//
|
||||
// @Test(timeout=300_000)
|
||||
// fun testCreateSHA2256toSHA3256() {
|
||||
// val merkle = MerkleTree.getMerkleTree(listOf(SecureHash.allOnesHash, SecureHash.zeroHash), DigestService.sha3_256)
|
||||
// assertEquals(SecureHash.create("SHA3-256:80673DBEEC8F6761ACBB121E7E45F61D4279CCD8B8E2231741ECD0716F4C9EDC"), merkle.hash)
|
||||
// }
|
||||
//
|
||||
// @Test(timeout=300_000)
|
||||
// fun testCreateSHA3256toSHA2256() {
|
||||
// val merkle = MerkleTree.getMerkleTree(listOf(SecureHash.allOnesHashFor(SecureHash.SHA3_256),
|
||||
// SecureHash.zeroHashFor(SecureHash.SHA3_256)), DigestService.sha2_256)
|
||||
// assertEquals(SecureHash.create("A5DE9B714ACCD8AFAAABF1CBD6E1014C9D07FF95C2AE154D91EC68485B31E7B5"), merkle.hash)
|
||||
// }
|
||||
}
|
@ -10,7 +10,7 @@ class SecureHashTest {
|
||||
@Test(timeout=300_000)
|
||||
fun testSHA256() {
|
||||
val hash = SecureHash.sha256(byteArrayOf(0x64, -0x13, 0x42, 0x3a))
|
||||
assertEquals(SecureHash.parse("6D1687C143DF792A011A1E80670A4E4E0C25D0D87A39514409B1ABFC2043581F"), hash)
|
||||
assertEquals(SecureHash.create("6D1687C143DF792A011A1E80670A4E4E0C25D0D87A39514409B1ABFC2043581F"), hash)
|
||||
assertEquals("6D1687C143DF792A011A1E80670A4E4E0C25D0D87A39514409B1ABFC2043581F", hash.toString())
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,7 @@ class TransactionSignatureTest {
|
||||
val txSignature = signMultipleTx(txIds, keyPair)
|
||||
|
||||
// The hash of all txIds are used as leaves.
|
||||
val merkleTree = MerkleTree.getMerkleTree(txIds.map { it.sha256() })
|
||||
val merkleTree = MerkleTree.getMerkleTree(txIds.map { it.sha256() }, DigestService.default)
|
||||
|
||||
// We haven't added the partial tree yet.
|
||||
assertNull(txSignature.partialMerkleTree)
|
||||
@ -128,7 +128,7 @@ class TransactionSignatureTest {
|
||||
|
||||
// Returns a TransactionSignature over the Merkle root, but the partial tree is null.
|
||||
private fun signMultipleTx(txIds: List<SecureHash>, keyPair: KeyPair): TransactionSignature {
|
||||
val merkleTreeRoot = MerkleTree.getMerkleTree(txIds.map { it.sha256() }).hash
|
||||
val merkleTreeRoot = MerkleTree.getMerkleTree(txIds.map { it.sha256() }, DigestService.default).hash
|
||||
return signOneTx(merkleTreeRoot, keyPair)
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.canBeTransitionedFrom
|
||||
import net.corda.core.internal.inputStream
|
||||
import net.corda.core.internal.requireSupportedHashType
|
||||
import net.corda.core.internal.toPath
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.node.services.IdentityService
|
||||
@ -372,6 +373,7 @@ class ConstraintsPropagationTests {
|
||||
}
|
||||
|
||||
private fun MockServices.recordTransaction(wireTransaction: WireTransaction) {
|
||||
requireSupportedHashType(wireTransaction)
|
||||
val nodeKey = ALICE_PUBKEY
|
||||
val sigs = listOf(keyManagementService.sign(
|
||||
SignableData(wireTransaction.id, SignatureMetadata(4, Crypto.findSignatureScheme(nodeKey).schemeNumberID)), nodeKey))
|
||||
|
@ -204,6 +204,18 @@ class TransactionVerificationExceptionSerialisationTests {
|
||||
assertEquals(exc.message, exc2.message)
|
||||
}
|
||||
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun unsupportedHashTypeExceptionTest() {
|
||||
val exc = TransactionVerificationException.UnsupportedHashTypeException(txid)
|
||||
|
||||
val exc2 = DeserializationInput(factory).deserialize(
|
||||
SerializationOutput(factory).serialize(exc, context),
|
||||
context)
|
||||
|
||||
assertEquals(exc.message, exc2.message)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun transactionNetworkParameterOrderingExceptionTest() {
|
||||
val exception = TransactionVerificationException.TransactionNetworkParameterOrderingException(
|
||||
|
@ -5,7 +5,6 @@ import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.SecureHash.Companion.zeroHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NotaryInfo
|
||||
@ -31,13 +30,16 @@ import net.corda.testing.node.ledger
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import java.security.PublicKey
|
||||
import java.util.function.Predicate
|
||||
import java.util.stream.IntStream
|
||||
import kotlin.streams.toList
|
||||
import kotlin.test.*
|
||||
|
||||
class PartialMerkleTreeTest {
|
||||
@RunWith(Parameterized::class)
|
||||
class PartialMerkleTreeTest(private var digestService: DigestService) {
|
||||
private companion object {
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
||||
@ -46,6 +48,14 @@ class PartialMerkleTreeTest {
|
||||
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
|
||||
val MINI_CORP get() = miniCorp.party
|
||||
val MINI_CORP_PUBKEY get() = miniCorp.publicKey
|
||||
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters
|
||||
fun data(): Collection<DigestService> = listOf(
|
||||
DigestService.sha2_256,
|
||||
DigestService.sha2_384,
|
||||
DigestService.sha2_512
|
||||
)
|
||||
}
|
||||
|
||||
@Rule
|
||||
@ -53,7 +63,7 @@ class PartialMerkleTreeTest {
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
private val nodes = "abcdef"
|
||||
private lateinit var hashed: List<SecureHash.SHA256>
|
||||
private lateinit var hashed: List<SecureHash>
|
||||
private lateinit var expectedRoot: SecureHash
|
||||
private lateinit var merkleTree: MerkleTree
|
||||
private lateinit var testLedger: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>
|
||||
@ -62,9 +72,10 @@ class PartialMerkleTreeTest {
|
||||
|
||||
@Before
|
||||
fun init() {
|
||||
hashed = nodes.map { it.serialize().sha256() }
|
||||
expectedRoot = MerkleTree.getMerkleTree(hashed.toMutableList() + listOf(zeroHash, zeroHash)).hash
|
||||
merkleTree = MerkleTree.getMerkleTree(hashed)
|
||||
digestService = DigestService.default
|
||||
hashed = nodes.map { digestService.hash(it.serialize().bytes) }
|
||||
expectedRoot = MerkleTree.getMerkleTree(hashed.toMutableList() + listOf(digestService.zeroHash, digestService.zeroHash), digestService).hash
|
||||
merkleTree = MerkleTree.getMerkleTree(hashed, digestService)
|
||||
|
||||
testLedger = MockServices(
|
||||
cordappPackages = emptyList(),
|
||||
@ -107,29 +118,29 @@ class PartialMerkleTreeTest {
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `building Merkle tree - no hashes`() {
|
||||
assertFailsWith<MerkleTreeException> { MerkleTree.getMerkleTree(emptyList()) }
|
||||
assertFailsWith<MerkleTreeException> { MerkleTree.getMerkleTree(emptyList(), digestService) }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `building Merkle tree one node`() {
|
||||
val node = 'a'.serialize().sha256()
|
||||
val mt = MerkleTree.getMerkleTree(listOf(node))
|
||||
val mt = MerkleTree.getMerkleTree(listOf(node), digestService)
|
||||
assertEquals(node, mt.hash)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `building Merkle tree odd number of nodes`() {
|
||||
val odd = hashed.subList(0, 3)
|
||||
val h1 = hashed[0].hashConcat(hashed[1])
|
||||
val h2 = hashed[2].hashConcat(zeroHash)
|
||||
val expected = h1.hashConcat(h2)
|
||||
val mt = MerkleTree.getMerkleTree(odd)
|
||||
val h1 = hashed[0].concatenate(hashed[1])
|
||||
val h2 = hashed[2].concatenate(digestService.zeroHash)
|
||||
val expected = h1.concatenate(h2)
|
||||
val mt = MerkleTree.getMerkleTree(odd, digestService)
|
||||
assertEquals(mt.hash, expected)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `check full tree`() {
|
||||
val h = SecureHash.randomSHA256()
|
||||
val h = digestService.randomHash()
|
||||
val left = MerkleTree.Node(h, MerkleTree.Node(h, MerkleTree.Leaf(h), MerkleTree.Leaf(h)),
|
||||
MerkleTree.Node(h, MerkleTree.Leaf(h), MerkleTree.Leaf(h)))
|
||||
val right = MerkleTree.Node(h, MerkleTree.Leaf(h), MerkleTree.Leaf(h))
|
||||
@ -226,7 +237,7 @@ class PartialMerkleTreeTest {
|
||||
fun `build Partial Merkle Tree - only duplicate leaves, less included failure`() {
|
||||
val leaves = "aaa"
|
||||
val hashes = leaves.map { it.serialize().hash }
|
||||
val mt = MerkleTree.getMerkleTree(hashes)
|
||||
val mt = MerkleTree.getMerkleTree(hashes, digestService)
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(mt, hashes.subList(0, 1)) }
|
||||
}
|
||||
|
||||
@ -248,7 +259,7 @@ class PartialMerkleTreeTest {
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `verify Partial Merkle Tree - duplicate leaves failure`() {
|
||||
val mt = MerkleTree.getMerkleTree(hashed.subList(0, 5)) // Odd number of leaves. Last one is duplicated.
|
||||
val mt = MerkleTree.getMerkleTree(hashed.subList(0, 5), digestService) // Odd number of leaves. Last one is duplicated.
|
||||
val inclHashes = arrayListOf(hashed[3], hashed[4])
|
||||
val pmt = PartialMerkleTree.build(mt, inclHashes)
|
||||
inclHashes.add(hashed[4])
|
||||
@ -266,7 +277,7 @@ class PartialMerkleTreeTest {
|
||||
fun `verify Partial Merkle Tree - wrong root`() {
|
||||
val inclHashes = listOf(hashed[3], hashed[5])
|
||||
val pmt = PartialMerkleTree.build(merkleTree, inclHashes)
|
||||
val wrongRoot = hashed[3].hashConcat(hashed[5])
|
||||
val wrongRoot = hashed[3].concatenate(hashed[5])
|
||||
assertFalse(pmt.verify(wrongRoot, inclHashes))
|
||||
}
|
||||
|
||||
@ -289,54 +300,55 @@ class PartialMerkleTreeTest {
|
||||
commands = testTx.commands,
|
||||
notary = notary,
|
||||
timeWindow = timeWindow,
|
||||
privacySalt = privacySalt
|
||||
privacySalt = privacySalt,
|
||||
digestService = digestService
|
||||
)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `Find leaf index`() {
|
||||
// A Merkle tree with 20 leaves.
|
||||
val sampleLeaves = IntStream.rangeClosed(0, 19).toList().map { SecureHash.sha256(it.toString()) }
|
||||
val merkleTree = MerkleTree.getMerkleTree(sampleLeaves)
|
||||
val sampleLeaves = IntStream.rangeClosed(0, 19).toList().map { digestService.hash(it.toString()) }
|
||||
val merkleTree = MerkleTree.getMerkleTree(sampleLeaves, digestService)
|
||||
|
||||
// Provided hashes are not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("20"))) }
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(merkleTree, listOf<SecureHash>(digestService.hash("20"))) }
|
||||
// One of the provided hashes is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("20"), SecureHash.sha256("1"), SecureHash.sha256("5"))) }
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(merkleTree, listOf<SecureHash>(digestService.hash("20"), digestService.hash("1"), digestService.hash("5"))) }
|
||||
|
||||
val pmt = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("1"), SecureHash.sha256("5"), SecureHash.sha256("0"), SecureHash.sha256("19")))
|
||||
val pmt = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(digestService.hash("1"), digestService.hash("5"), digestService.hash("0"), digestService.hash("19")))
|
||||
// First leaf.
|
||||
assertEquals(0, pmt.leafIndex(SecureHash.sha256("0")))
|
||||
assertEquals(0, pmt.leafIndex(digestService.hash("0")))
|
||||
// Second leaf.
|
||||
assertEquals(1, pmt.leafIndex(SecureHash.sha256("1")))
|
||||
assertEquals(1, pmt.leafIndex(digestService.hash("1")))
|
||||
// A random leaf.
|
||||
assertEquals(5, pmt.leafIndex(SecureHash.sha256("5")))
|
||||
assertEquals(5, pmt.leafIndex(digestService.hash("5")))
|
||||
// The last leaf.
|
||||
assertEquals(19, pmt.leafIndex(SecureHash.sha256("19")))
|
||||
assertEquals(19, pmt.leafIndex(digestService.hash("19")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmt.leafIndex(SecureHash.sha256("10")) }
|
||||
assertFailsWith<MerkleTreeException> { pmt.leafIndex(digestService.hash("10")) }
|
||||
// The provided hash is not in the tree (using a leaf that didn't exist in the original Merkle tree).
|
||||
assertFailsWith<MerkleTreeException> { pmt.leafIndex(SecureHash.sha256("30")) }
|
||||
assertFailsWith<MerkleTreeException> { pmt.leafIndex(digestService.hash("30")) }
|
||||
|
||||
val pmtFirstElementOnly = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("0")))
|
||||
assertEquals(0, pmtFirstElementOnly.leafIndex(SecureHash.sha256("0")))
|
||||
val pmtFirstElementOnly = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(digestService.hash("0")))
|
||||
assertEquals(0, pmtFirstElementOnly.leafIndex(digestService.hash("0")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmtFirstElementOnly.leafIndex(SecureHash.sha256("10")) }
|
||||
assertFailsWith<MerkleTreeException> { pmtFirstElementOnly.leafIndex(digestService.hash("10")) }
|
||||
|
||||
val pmtLastElementOnly = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("19")))
|
||||
assertEquals(19, pmtLastElementOnly.leafIndex(SecureHash.sha256("19")))
|
||||
val pmtLastElementOnly = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(digestService.hash("19")))
|
||||
assertEquals(19, pmtLastElementOnly.leafIndex(digestService.hash("19")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmtLastElementOnly.leafIndex(SecureHash.sha256("10")) }
|
||||
assertFailsWith<MerkleTreeException> { pmtLastElementOnly.leafIndex(digestService.hash("10")) }
|
||||
|
||||
val pmtOneElement = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("5")))
|
||||
assertEquals(5, pmtOneElement.leafIndex(SecureHash.sha256("5")))
|
||||
val pmtOneElement = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(digestService.hash("5")))
|
||||
assertEquals(5, pmtOneElement.leafIndex(digestService.hash("5")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmtOneElement.leafIndex(SecureHash.sha256("10")) }
|
||||
assertFailsWith<MerkleTreeException> { pmtOneElement.leafIndex(digestService.hash("10")) }
|
||||
|
||||
val pmtAllIncluded = PartialMerkleTree.build(merkleTree, sampleLeaves)
|
||||
for (i in 0..19) assertEquals(i, pmtAllIncluded.leafIndex(SecureHash.sha256(i.toString())))
|
||||
for (i in 0..19) assertEquals(i, pmtAllIncluded.leafIndex(digestService.hash(i.toString())))
|
||||
// The provided hash is not in the tree (using a leaf that didn't exist in the original Merkle tree).
|
||||
assertFailsWith<MerkleTreeException> { pmtAllIncluded.leafIndex(SecureHash.sha256("30")) }
|
||||
assertFailsWith<MerkleTreeException> { pmtAllIncluded.leafIndex(digestService.hash("30")) }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
|
@ -0,0 +1,384 @@
|
||||
package net.corda.coretests.crypto
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.crypto.MerkleTree
|
||||
import net.corda.core.crypto.MerkleTreeException
|
||||
import net.corda.core.crypto.PartialMerkleTree
|
||||
import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SecureHash.Companion.SHA2_384
|
||||
import net.corda.core.crypto.SecureHash.Companion.hashAs
|
||||
import net.corda.core.crypto.hashAs
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.ReferenceStateRef
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.`issued by`
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.dsl.LedgerDSL
|
||||
import net.corda.testing.dsl.TestLedgerDSLInterpreter
|
||||
import net.corda.testing.dsl.TestTransactionDSLInterpreter
|
||||
import net.corda.coretesting.internal.TEST_TX_TIME
|
||||
import net.corda.testing.internal.createWireTransaction
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.ledger
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import java.security.PublicKey
|
||||
import java.util.function.Predicate
|
||||
import java.util.stream.IntStream
|
||||
import kotlin.streams.toList
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
@Suppress("FunctionNaming")
|
||||
class PartialMerkleTreeWithNamedHashMultiAlgTreeTest {
|
||||
private companion object {
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
||||
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
||||
val MEGA_CORP get() = megaCorp.party
|
||||
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
|
||||
val MINI_CORP get() = miniCorp.party
|
||||
val MINI_CORP_PUBKEY get() = miniCorp.publicKey
|
||||
|
||||
fun sha2_384(str: String): SecureHash {
|
||||
return hashAs(SHA2_384, str.toByteArray())
|
||||
}
|
||||
|
||||
fun OpaqueBytes.sha2_384(): SecureHash = hashAs(SHA2_384)
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
private val nodes = "abcdef"
|
||||
private lateinit var hashed: List<SecureHash>
|
||||
private lateinit var expectedRoot: SecureHash
|
||||
private lateinit var merkleTree: MerkleTree
|
||||
private lateinit var testLedger: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>
|
||||
private lateinit var txs: List<WireTransaction>
|
||||
private lateinit var testTx: WireTransaction
|
||||
|
||||
@Before
|
||||
fun init() {
|
||||
hashed = nodes.map { it.serialize().sha2_384() }
|
||||
val zeroHash = SecureHash.zeroHashFor(SHA2_384)
|
||||
expectedRoot = MerkleTree.getMerkleTree(hashed.toMutableList() + listOf(zeroHash, zeroHash), DigestService.sha2_256).hash
|
||||
merkleTree = MerkleTree.getMerkleTree(hashed, DigestService.sha2_256)
|
||||
|
||||
testLedger = MockServices(
|
||||
cordappPackages = emptyList(),
|
||||
initialIdentity = TestIdentity(MEGA_CORP.name),
|
||||
identityService = mock<IdentityService>().also {
|
||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||
},
|
||||
networkParameters = testNetworkParameters(minimumPlatformVersion = 4, notaries = listOf(NotaryInfo(DUMMY_NOTARY, true)))
|
||||
).ledger(DUMMY_NOTARY) {
|
||||
unverifiedTransaction {
|
||||
attachments(Cash.PROGRAM_ID)
|
||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash",
|
||||
Cash.State(
|
||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||
owner = MEGA_CORP))
|
||||
output(Cash.PROGRAM_ID, "dummy cash 1",
|
||||
Cash.State(
|
||||
amount = 900.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||
owner = MINI_CORP))
|
||||
}
|
||||
transaction {
|
||||
attachments(Cash.PROGRAM_ID)
|
||||
input("MEGA_CORP cash")
|
||||
reference("dummy cash 1")
|
||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash".output<Cash.State>().copy(owner = MINI_CORP))
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
txs = testLedger.interpreter.transactionsToVerify
|
||||
testTx = txs[0]
|
||||
}
|
||||
|
||||
// Building full Merkle Tree tests.
|
||||
@Test(timeout=300_000)
|
||||
fun `building Merkle tree with 6 nodes - no rightmost nodes`() {
|
||||
assertEquals(expectedRoot, merkleTree.hash)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `building Merkle tree - no hashes`() {
|
||||
assertFailsWith<MerkleTreeException> { MerkleTree.getMerkleTree(emptyList(), DigestService.sha2_256) }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `building Merkle tree one node`() {
|
||||
val node = 'a'.serialize().sha2_384()
|
||||
val mt = MerkleTree.getMerkleTree(listOf(node), DigestService.sha2_256)
|
||||
assertEquals(node, mt.hash)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `building Merkle tree odd number of nodes`() {
|
||||
val odd = hashed.subList(0, 3)
|
||||
val h1 = hashed[0].concatenateAs(SecureHash.SHA2_256, hashed[1])
|
||||
val h2 = hashed[2].concatenateAs(SecureHash.SHA2_256, SecureHash.zeroHashFor(SHA2_384))
|
||||
val expected = h1.concatenateAs(SecureHash.SHA2_256, h2)
|
||||
val mt = MerkleTree.getMerkleTree(odd, DigestService.sha2_256)
|
||||
assertEquals(mt.hash, expected)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `check full tree`() {
|
||||
val h = SecureHash.random(SHA2_384)
|
||||
val left = MerkleTree.Node(h, MerkleTree.Node(h, MerkleTree.Leaf(h), MerkleTree.Leaf(h)),
|
||||
MerkleTree.Node(h, MerkleTree.Leaf(h), MerkleTree.Leaf(h)))
|
||||
val right = MerkleTree.Node(h, MerkleTree.Leaf(h), MerkleTree.Leaf(h))
|
||||
val tree = MerkleTree.Node(h, left, right)
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(tree, listOf(h)) }
|
||||
PartialMerkleTree.build(right, listOf(h, h)) // Node and two leaves.
|
||||
PartialMerkleTree.build(MerkleTree.Leaf(h), listOf(h)) // Just a leaf.
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `building Merkle tree for a tx and nonce test`() {
|
||||
fun filtering(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is StateRef -> true
|
||||
is TransactionState<*> -> elem.data.participants[0].owningKey.keys == MINI_CORP_PUBKEY.keys
|
||||
is Command<*> -> MEGA_CORP_PUBKEY in elem.signers
|
||||
is TimeWindow -> true
|
||||
is PublicKey -> elem == MEGA_CORP_PUBKEY
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val d = testTx.serialize().deserialize()
|
||||
assertEquals(testTx.id, d.id)
|
||||
|
||||
val ftx = testTx.buildFilteredTransaction(Predicate(::filtering))
|
||||
|
||||
// We expect 5 and not 4 component groups, because there is at least one command in the ftx and thus,
|
||||
// the signers component is also sent (required for visibility purposes).
|
||||
assertEquals(5, ftx.filteredComponentGroups.size)
|
||||
assertEquals(1, ftx.inputs.size)
|
||||
assertEquals(0, ftx.references.size)
|
||||
assertEquals(0, ftx.attachments.size)
|
||||
assertEquals(1, ftx.outputs.size)
|
||||
assertEquals(1, ftx.commands.size)
|
||||
assertNull(ftx.notary)
|
||||
assertNotNull(ftx.timeWindow)
|
||||
assertNull(ftx.networkParametersHash)
|
||||
ftx.verify()
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `same transactions with different notaries have different ids`() {
|
||||
// We even use the same privacySalt, and thus the only difference between the two transactions is the notary party.
|
||||
val privacySalt = PrivacySalt()
|
||||
val wtx1 = makeSimpleCashWtx(DUMMY_NOTARY, privacySalt)
|
||||
val wtx2 = makeSimpleCashWtx(MEGA_CORP, privacySalt)
|
||||
assertEquals(wtx1.privacySalt, wtx2.privacySalt)
|
||||
assertNotEquals(wtx1.id, wtx2.id)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `nothing filtered`() {
|
||||
val ftxNothing = testTx.buildFilteredTransaction(Predicate { false })
|
||||
assertTrue(ftxNothing.componentGroups.isEmpty())
|
||||
assertTrue(ftxNothing.attachments.isEmpty())
|
||||
assertTrue(ftxNothing.commands.isEmpty())
|
||||
assertTrue(ftxNothing.inputs.isEmpty())
|
||||
assertTrue(ftxNothing.references.isEmpty())
|
||||
assertTrue(ftxNothing.outputs.isEmpty())
|
||||
assertNull(ftxNothing.timeWindow)
|
||||
assertTrue(ftxNothing.availableComponentGroups.flatten().isEmpty())
|
||||
assertNull(ftxNothing.networkParametersHash)
|
||||
ftxNothing.verify() // We allow empty ftx transactions (eg from a timestamp authority that blindly signs).
|
||||
}
|
||||
|
||||
// Partial Merkle Tree building tests.
|
||||
@Test(timeout=300_000)
|
||||
fun `build Partial Merkle Tree, only left nodes branch`() {
|
||||
val inclHashes = listOf(hashed[3], hashed[5])
|
||||
val pmt = PartialMerkleTree.build(merkleTree, inclHashes)
|
||||
assertTrue(pmt.verify(merkleTree.hash, inclHashes))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `build Partial Merkle Tree, include zero leaves`() {
|
||||
val pmt = PartialMerkleTree.build(merkleTree, emptyList())
|
||||
assertTrue(pmt.verify(merkleTree.hash, emptyList()))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `build Partial Merkle Tree, include all leaves`() {
|
||||
val pmt = PartialMerkleTree.build(merkleTree, hashed)
|
||||
assertTrue(pmt.verify(merkleTree.hash, hashed))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `build Partial Merkle Tree - duplicate leaves failure`() {
|
||||
val inclHashes = arrayListOf(hashed[3], hashed[5], hashed[3], hashed[5])
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(merkleTree, inclHashes) }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `build Partial Merkle Tree - only duplicate leaves, less included failure`() {
|
||||
val leaves = "aaa"
|
||||
val hashes = leaves.map { it.serialize().hash }
|
||||
val mt = MerkleTree.getMerkleTree(hashes, DigestService.sha2_256)
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(mt, hashes.subList(0, 1)) }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `verify Partial Merkle Tree - too many leaves failure`() {
|
||||
val inclHashes = arrayListOf(hashed[3], hashed[5])
|
||||
val pmt = PartialMerkleTree.build(merkleTree, inclHashes)
|
||||
inclHashes.add(hashed[0])
|
||||
assertFalse(pmt.verify(merkleTree.hash, inclHashes))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `verify Partial Merkle Tree - too little leaves failure`() {
|
||||
val inclHashes = arrayListOf(hashed[3], hashed[5], hashed[0])
|
||||
val pmt = PartialMerkleTree.build(merkleTree, inclHashes)
|
||||
inclHashes.remove(hashed[0])
|
||||
assertFalse(pmt.verify(merkleTree.hash, inclHashes))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `verify Partial Merkle Tree - duplicate leaves failure`() {
|
||||
val mt = MerkleTree.getMerkleTree(hashed.subList(0, 5), DigestService.sha2_256) // Odd number of leaves. Last one is duplicated.
|
||||
val inclHashes = arrayListOf(hashed[3], hashed[4])
|
||||
val pmt = PartialMerkleTree.build(mt, inclHashes)
|
||||
inclHashes.add(hashed[4])
|
||||
assertFalse(pmt.verify(mt.hash, inclHashes))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `verify Partial Merkle Tree - different leaves failure`() {
|
||||
val inclHashes = arrayListOf(hashed[3], hashed[5])
|
||||
val pmt = PartialMerkleTree.build(merkleTree, inclHashes)
|
||||
assertFalse(pmt.verify(merkleTree.hash, listOf(hashed[2], hashed[4])))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `verify Partial Merkle Tree - wrong root`() {
|
||||
val inclHashes = listOf(hashed[3], hashed[5])
|
||||
val pmt = PartialMerkleTree.build(merkleTree, inclHashes)
|
||||
val wrongRoot = hashed[3].concatenate(hashed[5])
|
||||
assertFalse(pmt.verify(wrongRoot, inclHashes))
|
||||
}
|
||||
|
||||
@Test(expected = Exception::class, timeout=300_000)
|
||||
fun `hash map serialization not allowed`() {
|
||||
val hm1 = hashMapOf("a" to 1, "b" to 2, "c" to 3, "e" to 4)
|
||||
hm1.serialize()
|
||||
}
|
||||
|
||||
private fun makeSimpleCashWtx(
|
||||
notary: Party,
|
||||
privacySalt: PrivacySalt = PrivacySalt(),
|
||||
timeWindow: TimeWindow? = null,
|
||||
attachments: List<SecureHash> = emptyList()
|
||||
): WireTransaction {
|
||||
return createWireTransaction(
|
||||
inputs = testTx.inputs,
|
||||
attachments = attachments,
|
||||
outputs = testTx.outputs,
|
||||
commands = testTx.commands,
|
||||
notary = notary,
|
||||
timeWindow = timeWindow,
|
||||
privacySalt = privacySalt
|
||||
)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `Find leaf index`() {
|
||||
// A Merkle tree with 20 leaves.
|
||||
val sampleLeaves = IntStream.rangeClosed(0, 19).toList().map { sha2_384(it.toString()) }
|
||||
val merkleTree = MerkleTree.getMerkleTree(sampleLeaves, DigestService.sha2_256)
|
||||
|
||||
// Provided hashes are not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(merkleTree, listOf(sha2_384("20"))) }
|
||||
// One of the provided hashes is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(merkleTree, listOf(sha2_384("20"), sha2_384("1"), sha2_384("5"))) }
|
||||
|
||||
val pmt = PartialMerkleTree.build(merkleTree, listOf(sha2_384("1"), sha2_384("5"), sha2_384("0"), sha2_384("19")))
|
||||
// First leaf.
|
||||
assertEquals(0, pmt.leafIndex(sha2_384("0")))
|
||||
// Second leaf.
|
||||
assertEquals(1, pmt.leafIndex(sha2_384("1")))
|
||||
// A random leaf.
|
||||
assertEquals(5, pmt.leafIndex(sha2_384("5")))
|
||||
// The last leaf.
|
||||
assertEquals(19, pmt.leafIndex(sha2_384("19")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmt.leafIndex(sha2_384("10")) }
|
||||
// The provided hash is not in the tree (using a leaf that didn't exist in the original Merkle tree).
|
||||
assertFailsWith<MerkleTreeException> { pmt.leafIndex(sha2_384("30")) }
|
||||
|
||||
val pmtFirstElementOnly = PartialMerkleTree.build(merkleTree, listOf(sha2_384("0")))
|
||||
assertEquals(0, pmtFirstElementOnly.leafIndex(sha2_384("0")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmtFirstElementOnly.leafIndex(sha2_384("10")) }
|
||||
|
||||
val pmtLastElementOnly = PartialMerkleTree.build(merkleTree, listOf(sha2_384("19")))
|
||||
assertEquals(19, pmtLastElementOnly.leafIndex(sha2_384("19")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmtLastElementOnly.leafIndex(sha2_384("10")) }
|
||||
|
||||
val pmtOneElement = PartialMerkleTree.build(merkleTree, listOf(sha2_384("5")))
|
||||
assertEquals(5, pmtOneElement.leafIndex(sha2_384("5")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmtOneElement.leafIndex(sha2_384("10")) }
|
||||
|
||||
val pmtAllIncluded = PartialMerkleTree.build(merkleTree, sampleLeaves)
|
||||
for (i in 0..19) assertEquals(i, pmtAllIncluded.leafIndex(sha2_384(i.toString())))
|
||||
// The provided hash is not in the tree (using a leaf that didn't exist in the original Merkle tree).
|
||||
assertFailsWith<MerkleTreeException> { pmtAllIncluded.leafIndex(sha2_384("30")) }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `building Merkle for reference states only`() {
|
||||
fun filtering(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is ReferenceStateRef -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val ftx = testTx.buildFilteredTransaction(Predicate(::filtering))
|
||||
|
||||
assertEquals(1, ftx.filteredComponentGroups.size)
|
||||
assertEquals(0, ftx.inputs.size)
|
||||
assertEquals(1, ftx.references.size)
|
||||
ftx.verify()
|
||||
}
|
||||
}
|
@ -0,0 +1,384 @@
|
||||
package net.corda.coretests.crypto
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.crypto.MerkleTree
|
||||
import net.corda.core.crypto.MerkleTreeException
|
||||
import net.corda.core.crypto.PartialMerkleTree
|
||||
import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SecureHash.Companion.SHA2_384
|
||||
import net.corda.core.crypto.SecureHash.Companion.hashAs
|
||||
import net.corda.core.crypto.hashAs
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.ReferenceStateRef
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.`issued by`
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.dsl.LedgerDSL
|
||||
import net.corda.testing.dsl.TestLedgerDSLInterpreter
|
||||
import net.corda.testing.dsl.TestTransactionDSLInterpreter
|
||||
import net.corda.coretesting.internal.TEST_TX_TIME
|
||||
import net.corda.testing.internal.createWireTransaction
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.ledger
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import java.security.PublicKey
|
||||
import java.util.function.Predicate
|
||||
import java.util.stream.IntStream
|
||||
import kotlin.streams.toList
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
@Suppress("FunctionNaming")
|
||||
class PartialMerkleTreeWithNamedHashTest {
|
||||
private companion object {
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
||||
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
||||
val MEGA_CORP get() = megaCorp.party
|
||||
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
|
||||
val MINI_CORP get() = miniCorp.party
|
||||
val MINI_CORP_PUBKEY get() = miniCorp.publicKey
|
||||
|
||||
fun sha2_384(str: String): SecureHash {
|
||||
return hashAs(SHA2_384, str.toByteArray())
|
||||
}
|
||||
|
||||
fun OpaqueBytes.sha2_384(): SecureHash = hashAs(SHA2_384)
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
private val nodes = "abcdef"
|
||||
private lateinit var hashed: List<SecureHash>
|
||||
private lateinit var expectedRoot: SecureHash
|
||||
private lateinit var merkleTree: MerkleTree
|
||||
private lateinit var testLedger: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>
|
||||
private lateinit var txs: List<WireTransaction>
|
||||
private lateinit var testTx: WireTransaction
|
||||
|
||||
@Before
|
||||
fun init() {
|
||||
hashed = nodes.map { it.serialize().sha2_384() }
|
||||
val zeroHash = SecureHash.zeroHashFor(SHA2_384)
|
||||
expectedRoot = MerkleTree.getMerkleTree(hashed.toMutableList() + listOf(zeroHash, zeroHash), DigestService.sha2_384).hash
|
||||
merkleTree = MerkleTree.getMerkleTree(hashed, DigestService.sha2_384)
|
||||
|
||||
testLedger = MockServices(
|
||||
cordappPackages = emptyList(),
|
||||
initialIdentity = TestIdentity(MEGA_CORP.name),
|
||||
identityService = mock<IdentityService>().also {
|
||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||
},
|
||||
networkParameters = testNetworkParameters(minimumPlatformVersion = 4, notaries = listOf(NotaryInfo(DUMMY_NOTARY, true)))
|
||||
).ledger(DUMMY_NOTARY) {
|
||||
unverifiedTransaction {
|
||||
attachments(Cash.PROGRAM_ID)
|
||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash",
|
||||
Cash.State(
|
||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||
owner = MEGA_CORP))
|
||||
output(Cash.PROGRAM_ID, "dummy cash 1",
|
||||
Cash.State(
|
||||
amount = 900.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||
owner = MINI_CORP))
|
||||
}
|
||||
transaction {
|
||||
attachments(Cash.PROGRAM_ID)
|
||||
input("MEGA_CORP cash")
|
||||
reference("dummy cash 1")
|
||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash".output<Cash.State>().copy(owner = MINI_CORP))
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
txs = testLedger.interpreter.transactionsToVerify
|
||||
testTx = txs[0]
|
||||
}
|
||||
|
||||
// Building full Merkle Tree tests.
|
||||
@Test(timeout=300_000)
|
||||
fun `building Merkle tree with 6 nodes - no rightmost nodes`() {
|
||||
assertEquals(expectedRoot, merkleTree.hash)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `building Merkle tree - no hashes`() {
|
||||
assertFailsWith<MerkleTreeException> { MerkleTree.getMerkleTree(emptyList(), DigestService.sha2_384) }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `building Merkle tree one node`() {
|
||||
val node = 'a'.serialize().sha2_384()
|
||||
val mt = MerkleTree.getMerkleTree(listOf(node), DigestService.sha2_384)
|
||||
assertEquals(node, mt.hash)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `building Merkle tree odd number of nodes`() {
|
||||
val odd = hashed.subList(0, 3)
|
||||
val h1 = hashed[0].concatenate(hashed[1])
|
||||
val h2 = hashed[2].concatenate(SecureHash.zeroHashFor(SHA2_384))
|
||||
val expected = h1.concatenate(h2)
|
||||
val mt = MerkleTree.getMerkleTree(odd, DigestService.sha2_384)
|
||||
assertEquals(mt.hash, expected)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `check full tree`() {
|
||||
val h = SecureHash.random(SHA2_384)
|
||||
val left = MerkleTree.Node(h, MerkleTree.Node(h, MerkleTree.Leaf(h), MerkleTree.Leaf(h)),
|
||||
MerkleTree.Node(h, MerkleTree.Leaf(h), MerkleTree.Leaf(h)))
|
||||
val right = MerkleTree.Node(h, MerkleTree.Leaf(h), MerkleTree.Leaf(h))
|
||||
val tree = MerkleTree.Node(h, left, right)
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(tree, listOf(h)) }
|
||||
PartialMerkleTree.build(right, listOf(h, h)) // Node and two leaves.
|
||||
PartialMerkleTree.build(MerkleTree.Leaf(h), listOf(h)) // Just a leaf.
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `building Merkle tree for a tx and nonce test`() {
|
||||
fun filtering(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is StateRef -> true
|
||||
is TransactionState<*> -> elem.data.participants[0].owningKey.keys == MINI_CORP_PUBKEY.keys
|
||||
is Command<*> -> MEGA_CORP_PUBKEY in elem.signers
|
||||
is TimeWindow -> true
|
||||
is PublicKey -> elem == MEGA_CORP_PUBKEY
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val d = testTx.serialize().deserialize()
|
||||
assertEquals(testTx.id, d.id)
|
||||
|
||||
val ftx = testTx.buildFilteredTransaction(Predicate(::filtering))
|
||||
|
||||
// We expect 5 and not 4 component groups, because there is at least one command in the ftx and thus,
|
||||
// the signers component is also sent (required for visibility purposes).
|
||||
assertEquals(5, ftx.filteredComponentGroups.size)
|
||||
assertEquals(1, ftx.inputs.size)
|
||||
assertEquals(0, ftx.references.size)
|
||||
assertEquals(0, ftx.attachments.size)
|
||||
assertEquals(1, ftx.outputs.size)
|
||||
assertEquals(1, ftx.commands.size)
|
||||
assertNull(ftx.notary)
|
||||
assertNotNull(ftx.timeWindow)
|
||||
assertNull(ftx.networkParametersHash)
|
||||
ftx.verify()
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `same transactions with different notaries have different ids`() {
|
||||
// We even use the same privacySalt, and thus the only difference between the two transactions is the notary party.
|
||||
val privacySalt = PrivacySalt()
|
||||
val wtx1 = makeSimpleCashWtx(DUMMY_NOTARY, privacySalt)
|
||||
val wtx2 = makeSimpleCashWtx(MEGA_CORP, privacySalt)
|
||||
assertEquals(wtx1.privacySalt, wtx2.privacySalt)
|
||||
assertNotEquals(wtx1.id, wtx2.id)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `nothing filtered`() {
|
||||
val ftxNothing = testTx.buildFilteredTransaction(Predicate { false })
|
||||
assertTrue(ftxNothing.componentGroups.isEmpty())
|
||||
assertTrue(ftxNothing.attachments.isEmpty())
|
||||
assertTrue(ftxNothing.commands.isEmpty())
|
||||
assertTrue(ftxNothing.inputs.isEmpty())
|
||||
assertTrue(ftxNothing.references.isEmpty())
|
||||
assertTrue(ftxNothing.outputs.isEmpty())
|
||||
assertNull(ftxNothing.timeWindow)
|
||||
assertTrue(ftxNothing.availableComponentGroups.flatten().isEmpty())
|
||||
assertNull(ftxNothing.networkParametersHash)
|
||||
ftxNothing.verify() // We allow empty ftx transactions (eg from a timestamp authority that blindly signs).
|
||||
}
|
||||
|
||||
// Partial Merkle Tree building tests.
|
||||
@Test(timeout=300_000)
|
||||
fun `build Partial Merkle Tree, only left nodes branch`() {
|
||||
val inclHashes = listOf(hashed[3], hashed[5])
|
||||
val pmt = PartialMerkleTree.build(merkleTree, inclHashes)
|
||||
assertTrue(pmt.verify(merkleTree.hash, inclHashes))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `build Partial Merkle Tree, include zero leaves`() {
|
||||
val pmt = PartialMerkleTree.build(merkleTree, emptyList())
|
||||
assertTrue(pmt.verify(merkleTree.hash, emptyList()))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `build Partial Merkle Tree, include all leaves`() {
|
||||
val pmt = PartialMerkleTree.build(merkleTree, hashed)
|
||||
assertTrue(pmt.verify(merkleTree.hash, hashed))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `build Partial Merkle Tree - duplicate leaves failure`() {
|
||||
val inclHashes = arrayListOf(hashed[3], hashed[5], hashed[3], hashed[5])
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(merkleTree, inclHashes) }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `build Partial Merkle Tree - only duplicate leaves, less included failure`() {
|
||||
val leaves = "aaa"
|
||||
val hashes = leaves.map { it.serialize().hash }
|
||||
val mt = MerkleTree.getMerkleTree(hashes, DigestService.sha2_384)
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(mt, hashes.subList(0, 1)) }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `verify Partial Merkle Tree - too many leaves failure`() {
|
||||
val inclHashes = arrayListOf(hashed[3], hashed[5])
|
||||
val pmt = PartialMerkleTree.build(merkleTree, inclHashes)
|
||||
inclHashes.add(hashed[0])
|
||||
assertFalse(pmt.verify(merkleTree.hash, inclHashes))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `verify Partial Merkle Tree - too little leaves failure`() {
|
||||
val inclHashes = arrayListOf(hashed[3], hashed[5], hashed[0])
|
||||
val pmt = PartialMerkleTree.build(merkleTree, inclHashes)
|
||||
inclHashes.remove(hashed[0])
|
||||
assertFalse(pmt.verify(merkleTree.hash, inclHashes))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `verify Partial Merkle Tree - duplicate leaves failure`() {
|
||||
val mt = MerkleTree.getMerkleTree(hashed.subList(0, 5), DigestService.sha2_384) // Odd number of leaves. Last one is duplicated.
|
||||
val inclHashes = arrayListOf(hashed[3], hashed[4])
|
||||
val pmt = PartialMerkleTree.build(mt, inclHashes)
|
||||
inclHashes.add(hashed[4])
|
||||
assertFalse(pmt.verify(mt.hash, inclHashes))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `verify Partial Merkle Tree - different leaves failure`() {
|
||||
val inclHashes = arrayListOf(hashed[3], hashed[5])
|
||||
val pmt = PartialMerkleTree.build(merkleTree, inclHashes)
|
||||
assertFalse(pmt.verify(merkleTree.hash, listOf(hashed[2], hashed[4])))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `verify Partial Merkle Tree - wrong root`() {
|
||||
val inclHashes = listOf(hashed[3], hashed[5])
|
||||
val pmt = PartialMerkleTree.build(merkleTree, inclHashes)
|
||||
val wrongRoot = hashed[3].concatenate(hashed[5])
|
||||
assertFalse(pmt.verify(wrongRoot, inclHashes))
|
||||
}
|
||||
|
||||
@Test(expected = Exception::class, timeout=300_000)
|
||||
fun `hash map serialization not allowed`() {
|
||||
val hm1 = hashMapOf("a" to 1, "b" to 2, "c" to 3, "e" to 4)
|
||||
hm1.serialize()
|
||||
}
|
||||
|
||||
private fun makeSimpleCashWtx(
|
||||
notary: Party,
|
||||
privacySalt: PrivacySalt = PrivacySalt(),
|
||||
timeWindow: TimeWindow? = null,
|
||||
attachments: List<SecureHash> = emptyList()
|
||||
): WireTransaction {
|
||||
return createWireTransaction(
|
||||
inputs = testTx.inputs,
|
||||
attachments = attachments,
|
||||
outputs = testTx.outputs,
|
||||
commands = testTx.commands,
|
||||
notary = notary,
|
||||
timeWindow = timeWindow,
|
||||
privacySalt = privacySalt
|
||||
)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `Find leaf index`() {
|
||||
// A Merkle tree with 20 leaves.
|
||||
val sampleLeaves = IntStream.rangeClosed(0, 19).toList().map { sha2_384(it.toString()) }
|
||||
val merkleTree = MerkleTree.getMerkleTree(sampleLeaves, DigestService.sha2_384)
|
||||
|
||||
// Provided hashes are not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(merkleTree, listOf(sha2_384("20"))) }
|
||||
// One of the provided hashes is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(merkleTree, listOf(sha2_384("20"), sha2_384("1"), sha2_384("5"))) }
|
||||
|
||||
val pmt = PartialMerkleTree.build(merkleTree, listOf(sha2_384("1"), sha2_384("5"), sha2_384("0"), sha2_384("19")))
|
||||
// First leaf.
|
||||
assertEquals(0, pmt.leafIndex(sha2_384("0")))
|
||||
// Second leaf.
|
||||
assertEquals(1, pmt.leafIndex(sha2_384("1")))
|
||||
// A random leaf.
|
||||
assertEquals(5, pmt.leafIndex(sha2_384("5")))
|
||||
// The last leaf.
|
||||
assertEquals(19, pmt.leafIndex(sha2_384("19")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmt.leafIndex(sha2_384("10")) }
|
||||
// The provided hash is not in the tree (using a leaf that didn't exist in the original Merkle tree).
|
||||
assertFailsWith<MerkleTreeException> { pmt.leafIndex(sha2_384("30")) }
|
||||
|
||||
val pmtFirstElementOnly = PartialMerkleTree.build(merkleTree, listOf(sha2_384("0")))
|
||||
assertEquals(0, pmtFirstElementOnly.leafIndex(sha2_384("0")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmtFirstElementOnly.leafIndex(sha2_384("10")) }
|
||||
|
||||
val pmtLastElementOnly = PartialMerkleTree.build(merkleTree, listOf(sha2_384("19")))
|
||||
assertEquals(19, pmtLastElementOnly.leafIndex(sha2_384("19")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmtLastElementOnly.leafIndex(sha2_384("10")) }
|
||||
|
||||
val pmtOneElement = PartialMerkleTree.build(merkleTree, listOf(sha2_384("5")))
|
||||
assertEquals(5, pmtOneElement.leafIndex(sha2_384("5")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmtOneElement.leafIndex(sha2_384("10")) }
|
||||
|
||||
val pmtAllIncluded = PartialMerkleTree.build(merkleTree, sampleLeaves)
|
||||
for (i in 0..19) assertEquals(i, pmtAllIncluded.leafIndex(sha2_384(i.toString())))
|
||||
// The provided hash is not in the tree (using a leaf that didn't exist in the original Merkle tree).
|
||||
assertFailsWith<MerkleTreeException> { pmtAllIncluded.leafIndex(sha2_384("30")) }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `building Merkle for reference states only`() {
|
||||
fun filtering(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is ReferenceStateRef -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val ftx = testTx.buildFilteredTransaction(Predicate(::filtering))
|
||||
|
||||
assertEquals(1, ftx.filteredComponentGroups.size)
|
||||
assertEquals(0, ftx.inputs.size)
|
||||
assertEquals(1, ftx.references.size)
|
||||
ftx.verify()
|
||||
}
|
||||
}
|
@ -50,13 +50,14 @@ class TransactionSignatureTest {
|
||||
@Test(timeout=300_000)
|
||||
fun `Verify multi-tx signature`() {
|
||||
val keyPair = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, BigInteger.valueOf(1234567890L))
|
||||
|
||||
// Deterministically create 5 txIds.
|
||||
val txIds: List<SecureHash> = IntRange(0, 4).map { byteArrayOf(it.toByte()).sha256() }
|
||||
// Multi-tx signature.
|
||||
val txSignature = signMultipleTx(txIds, keyPair)
|
||||
|
||||
// The hash of all txIds are used as leaves.
|
||||
val merkleTree = MerkleTree.getMerkleTree(txIds.map { it.sha256() })
|
||||
val merkleTree = MerkleTree.getMerkleTree(txIds.map { it.reHash() })
|
||||
|
||||
// We haven't added the partial tree yet.
|
||||
assertNull(txSignature.partialMerkleTree)
|
||||
@ -64,7 +65,7 @@ class TransactionSignatureTest {
|
||||
assertFailsWith<SignatureException> { Crypto.doVerify(txIds[3], txSignature) }
|
||||
|
||||
// Create a partial tree for one tx.
|
||||
val pmt = PartialMerkleTree.build(merkleTree, listOf(txIds[0].sha256()))
|
||||
val pmt = PartialMerkleTree.build(merkleTree, listOf(txIds[0].reHash()))
|
||||
// Add the partial Merkle tree to the tx signature.
|
||||
val txSignatureWithTree = TransactionSignature(txSignature.bytes, txSignature.by, txSignature.signatureMetadata, pmt)
|
||||
|
||||
@ -84,7 +85,7 @@ class TransactionSignatureTest {
|
||||
|
||||
// What if we send the Full tree. This could be used if notaries didn't want to create a per tx partial tree.
|
||||
// Create a partial tree for all txs, thus all leaves are included.
|
||||
val pmtFull = PartialMerkleTree.build(merkleTree, txIds.map { it.sha256() })
|
||||
val pmtFull = PartialMerkleTree.build(merkleTree, txIds.map { it.reHash() })
|
||||
// Add the partial Merkle tree to the tx.
|
||||
val txSignatureWithFullTree = TransactionSignature(txSignature.bytes, txSignature.by, txSignature.signatureMetadata, pmtFull)
|
||||
|
||||
|
@ -417,7 +417,9 @@ class CompatibleTransactionTests {
|
||||
@Test(timeout=300_000)
|
||||
fun `FilteredTransaction signer manipulation tests`() {
|
||||
// Required to call the private constructor.
|
||||
val ftxConstructor = FilteredTransaction::class.constructors.first()
|
||||
//val ftxConstructor = FilteredTransaction::class.constructors.first()
|
||||
// TODO(iee): verify if it was a hard requirement that the constructor must be the first()
|
||||
val ftxConstructor = FilteredTransaction::class.constructors.last()
|
||||
|
||||
// 1st and 3rd commands require a signature from KEY_1.
|
||||
val twoCommandsforKey1 = listOf(dummyCommand(DUMMY_KEY_1.public, DUMMY_KEY_2.public), dummyCommand(DUMMY_KEY_2.public), dummyCommand(DUMMY_KEY_1.public))
|
||||
@ -429,7 +431,7 @@ class CompatibleTransactionTests {
|
||||
timeWindowGroup,
|
||||
ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() })
|
||||
)
|
||||
val wtx = WireTransaction(componentGroups = componentGroups, privacySalt = PrivacySalt())
|
||||
val wtx = WireTransaction(componentGroups = componentGroups, privacySalt = PrivacySalt(), digestService = DigestService.default)
|
||||
|
||||
// Filter KEY_1 commands (commands 1 and 3).
|
||||
fun filterKEY1Commands(elem: Any): Boolean {
|
||||
@ -453,7 +455,7 @@ class CompatibleTransactionTests {
|
||||
// val commandDataComponents = key1CommandsFtx.filteredComponentGroups[0].components
|
||||
val commandDataHashes = wtx.accessAvailableComponentHashes()[ComponentGroupEnum.COMMANDS_GROUP.ordinal]!!
|
||||
val noLastCommandDataPMT = PartialMerkleTree.build(
|
||||
MerkleTree.getMerkleTree(commandDataHashes),
|
||||
MerkleTree.getMerkleTree(commandDataHashes, wtx.digestService),
|
||||
commandDataHashes.subList(0, 1)
|
||||
)
|
||||
val noLastCommandDataComponents = key1CommandsFtx.filteredComponentGroups[0].components.subList(0, 1)
|
||||
@ -468,7 +470,7 @@ class CompatibleTransactionTests {
|
||||
val signerComponents = key1CommandsFtx.filteredComponentGroups[1].components
|
||||
val signerHashes = wtx.accessAvailableComponentHashes()[ComponentGroupEnum.SIGNERS_GROUP.ordinal]!!
|
||||
val noLastSignerPMT = PartialMerkleTree.build(
|
||||
MerkleTree.getMerkleTree(signerHashes),
|
||||
MerkleTree.getMerkleTree(signerHashes, wtx.digestService),
|
||||
signerHashes.subList(0, 2)
|
||||
)
|
||||
val noLastSignerComponents = key1CommandsFtx.filteredComponentGroups[1].components.subList(0, 2)
|
||||
@ -496,12 +498,12 @@ class CompatibleTransactionTests {
|
||||
// A command with no corresponding signer detected
|
||||
// because the pointer of CommandData (3rd leaf) cannot find a corresponding (3rd) signer.
|
||||
val updatedFilteredComponentsNoSignersKey1SamePMT = listOf(key1CommandsFtx.filteredComponentGroups[0], noLastSignerGroupSamePartialTree)
|
||||
assertFails { ftxConstructor.call(key1CommandsFtx.id, updatedFilteredComponentsNoSignersKey1SamePMT, key1CommandsFtx.groupHashes) }
|
||||
assertFails { ftxConstructor.call(key1CommandsFtx.id, updatedFilteredComponentsNoSignersKey1SamePMT, key1CommandsFtx.groupHashes, wtx.digestService) }
|
||||
|
||||
// Remove both last signer (KEY1) and related command.
|
||||
// Update partial Merkle tree for signers.
|
||||
val updatedFilteredComponentsNoLastCommandAndSigners = listOf(noLastCommandDataGroup, noLastSignerGroup)
|
||||
val ftxNoLastCommandAndSigners = ftxConstructor.call(key1CommandsFtx.id, updatedFilteredComponentsNoLastCommandAndSigners, key1CommandsFtx.groupHashes)
|
||||
val ftxNoLastCommandAndSigners = ftxConstructor.call(key1CommandsFtx.id, updatedFilteredComponentsNoLastCommandAndSigners, key1CommandsFtx.groupHashes, wtx.digestService)
|
||||
// verify() will pass as the transaction is well-formed.
|
||||
ftxNoLastCommandAndSigners.verify()
|
||||
// checkCommandVisibility() will not pass, because checkAllComponentsVisible(ComponentGroupEnum.SIGNERS_GROUP) will fail.
|
||||
@ -510,7 +512,7 @@ class CompatibleTransactionTests {
|
||||
// Remove last signer for which there is no pointer from a visible commandData. This is the case of Key2.
|
||||
// Do not change partial Merkle tree for signers.
|
||||
// This time the object can be constructed as there is no pointer mismatch.
|
||||
val ftxNoLastSigner = ftxConstructor.call(key2CommandsFtx.id, updatedFilteredComponentsNoSignersKey2SamePMT, key2CommandsFtx.groupHashes)
|
||||
val ftxNoLastSigner = ftxConstructor.call(key2CommandsFtx.id, updatedFilteredComponentsNoSignersKey2SamePMT, key2CommandsFtx.groupHashes, wtx.digestService)
|
||||
// verify() will fail as we didn't change the partial Merkle tree.
|
||||
assertFailsWith<FilteredTransactionVerificationException> { ftxNoLastSigner.verify() }
|
||||
// checkCommandVisibility() will not pass.
|
||||
@ -518,7 +520,7 @@ class CompatibleTransactionTests {
|
||||
|
||||
// Remove last signer for which there is no pointer from a visible commandData. This is the case of Key2.
|
||||
// Update partial Merkle tree for signers.
|
||||
val ftxNoLastSignerB = ftxConstructor.call(key2CommandsFtx.id, updatedFilteredComponentsNoSignersKey2, key2CommandsFtx.groupHashes)
|
||||
val ftxNoLastSignerB = ftxConstructor.call(key2CommandsFtx.id, updatedFilteredComponentsNoSignersKey2, key2CommandsFtx.groupHashes, wtx.digestService)
|
||||
// verify() will pass, the transaction is well-formed.
|
||||
ftxNoLastSignerB.verify()
|
||||
// But, checkAllComponentsVisible() will not pass.
|
||||
@ -527,8 +529,8 @@ class CompatibleTransactionTests {
|
||||
// Modify last signer (we have a pointer from commandData).
|
||||
// Update partial Merkle tree for signers.
|
||||
val alterSignerComponents = signerComponents.subList(0, 2) + signerComponents[1] // Third one is removed and the 2nd command is added twice.
|
||||
val alterSignersHashes = wtx.accessAvailableComponentHashes()[ComponentGroupEnum.SIGNERS_GROUP.ordinal]!!.subList(0, 2) + componentHash(key1CommandsFtx.filteredComponentGroups[1].nonces[2], alterSignerComponents[2])
|
||||
val alterMTree = MerkleTree.getMerkleTree(alterSignersHashes)
|
||||
val alterSignersHashes = wtx.accessAvailableComponentHashes()[ComponentGroupEnum.SIGNERS_GROUP.ordinal]!!.subList(0, 2) + wtx.digestService.componentHash(key1CommandsFtx.filteredComponentGroups[1].nonces[2], alterSignerComponents[2])
|
||||
val alterMTree = MerkleTree.getMerkleTree(alterSignersHashes, wtx.digestService)
|
||||
val alterSignerPMTK = PartialMerkleTree.build(
|
||||
alterMTree,
|
||||
alterSignersHashes
|
||||
@ -543,14 +545,14 @@ class CompatibleTransactionTests {
|
||||
val alterFilteredComponents = listOf(key1CommandsFtx.filteredComponentGroups[0], alterSignerGroup)
|
||||
|
||||
// Do not update groupHashes.
|
||||
val ftxAlterSigner = ftxConstructor.call(key1CommandsFtx.id, alterFilteredComponents, key1CommandsFtx.groupHashes)
|
||||
val ftxAlterSigner = ftxConstructor.call(key1CommandsFtx.id, alterFilteredComponents, key1CommandsFtx.groupHashes, wtx.digestService)
|
||||
// Visible components in signers group cannot be verified against their partial Merkle tree.
|
||||
assertFailsWith<FilteredTransactionVerificationException> { ftxAlterSigner.verify() }
|
||||
// Also, checkAllComponentsVisible() will not pass (groupHash matching will fail).
|
||||
assertFailsWith<ComponentVisibilityException> { ftxAlterSigner.checkCommandVisibility(DUMMY_KEY_1.public) }
|
||||
|
||||
// Update groupHashes.
|
||||
val ftxAlterSignerB = ftxConstructor.call(key1CommandsFtx.id, alterFilteredComponents, key1CommandsFtx.groupHashes.subList(0, 6) + alterMTree.hash)
|
||||
val ftxAlterSignerB = ftxConstructor.call(key1CommandsFtx.id, alterFilteredComponents, key1CommandsFtx.groupHashes.subList(0, 6) + alterMTree.hash, wtx.digestService)
|
||||
// Visible components in signers group cannot be verified against their partial Merkle tree.
|
||||
assertFailsWith<FilteredTransactionVerificationException> { ftxAlterSignerB.verify() }
|
||||
// Also, checkAllComponentsVisible() will not pass (top level Merkle tree cannot be verified against transaction's id).
|
||||
|
@ -6,10 +6,13 @@ import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AbstractAttachment
|
||||
import net.corda.core.internal.HashAgility
|
||||
import net.corda.core.internal.PLATFORM_VERSION
|
||||
import net.corda.core.internal.digestService
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.ZoneVersionTooLowException
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
@ -27,6 +30,7 @@ import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
@ -249,4 +253,50 @@ class TransactionBuilderTest {
|
||||
assertThat(builder.commands()).hasSize(1)
|
||||
assertThat(builder.referenceStates()).hasSize(1)
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test(timeout=300_000, expected = TransactionVerificationException.UnsupportedHashTypeException::class)
|
||||
fun `throws with non-default hash algorithm`() {
|
||||
HashAgility.init()
|
||||
try {
|
||||
val outputState = TransactionState(
|
||||
data = DummyState(),
|
||||
contract = DummyContract.PROGRAM_ID,
|
||||
notary = notary,
|
||||
constraint = HashAttachmentConstraint(contractAttachmentId)
|
||||
)
|
||||
val builder = TransactionBuilder(
|
||||
//privacySalt = DigestService.sha2_384.privacySalt,
|
||||
privacySalt = PrivacySalt.createFor(DigestService.sha2_384.hashAlgorithm))
|
||||
.addOutputState(outputState)
|
||||
.addCommand(DummyCommandData, notary.owningKey)
|
||||
|
||||
builder.toWireTransaction(services)
|
||||
} finally {
|
||||
HashAgility.init()
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000, expected = Test.None::class)
|
||||
fun `allows non-default hash algorithm`() {
|
||||
HashAgility.init(txHashAlgoName = DigestService.sha2_384.hashAlgorithm)
|
||||
assertThat(services.digestService).isEqualTo(DigestService.sha2_384)
|
||||
try {
|
||||
val outputState = TransactionState(
|
||||
data = DummyState(),
|
||||
contract = DummyContract.PROGRAM_ID,
|
||||
notary = notary,
|
||||
constraint = HashAttachmentConstraint(contractAttachmentId)
|
||||
)
|
||||
val builder = TransactionBuilder(
|
||||
//privacySalt = DigestService.sha2_384.privacySalt,
|
||||
privacySalt = PrivacySalt.createFor(DigestService.sha2_384.hashAlgorithm))
|
||||
.addOutputState(outputState)
|
||||
.addCommand(DummyCommandData, notary.owningKey)
|
||||
|
||||
assertThat(builder.toWireTransaction(services).digestService).isEqualTo(DigestService.sha2_384)
|
||||
} finally {
|
||||
HashAgility.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AbstractAttachment
|
||||
import net.corda.core.internal.HashAgility
|
||||
import net.corda.core.internal.TESTDSL_UPLOADER
|
||||
import net.corda.core.internal.createLedgerTransaction
|
||||
import net.corda.core.node.NotaryInfo
|
||||
@ -20,8 +21,11 @@ import net.corda.testing.internal.createWireTransaction
|
||||
import net.corda.testing.internal.fakeAttachment
|
||||
import net.corda.coretesting.internal.rigorousMock
|
||||
import net.corda.testing.internal.TestingNamedCacheFactory
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
@ -29,7 +33,8 @@ import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotEquals
|
||||
|
||||
class TransactionTests {
|
||||
@RunWith(Parameterized::class)
|
||||
class TransactionTests(private val digestService : DigestService) {
|
||||
private companion object {
|
||||
val DUMMY_KEY_1 = generateKeyPair()
|
||||
val DUMMY_KEY_2 = generateKeyPair()
|
||||
@ -39,12 +44,30 @@ class TransactionTests {
|
||||
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
|
||||
val DUMMY_NOTARY get() = dummyNotary.party
|
||||
val DUMMY_NOTARY_KEY get() = dummyNotary.keyPair
|
||||
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters
|
||||
fun data(): Collection<DigestService> = listOf(
|
||||
DigestService.sha2_256
|
||||
// DigestService.sha2_384,
|
||||
// DigestService.sha2_512
|
||||
)
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
HashAgility.init(txHashAlgoName = digestService.hashAlgorithm)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun after() {
|
||||
HashAgility.init()
|
||||
}
|
||||
|
||||
private fun makeSigned(wtx: WireTransaction, vararg keys: KeyPair, notarySig: Boolean = true): SignedTransaction {
|
||||
val keySigs = keys.map { it.sign(SignableData(wtx.id, SignatureMetadata(1, Crypto.findSignatureScheme(it.public).schemeNumberID))) }
|
||||
val sigs = if (notarySig) {
|
||||
@ -130,7 +153,7 @@ class TransactionTests {
|
||||
doReturn(SecureHash.zeroHash).whenever(it).id
|
||||
doReturn(fakeAttachment("nothing", "nada").inputStream()).whenever(it).open()
|
||||
}, DummyContract.PROGRAM_ID, uploader = "app"))
|
||||
val id = SecureHash.randomSHA256()
|
||||
val id = digestService.randomHash()
|
||||
val timeWindow: TimeWindow? = null
|
||||
val privacySalt = PrivacySalt()
|
||||
val attachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory())
|
||||
@ -146,7 +169,8 @@ class TransactionTests {
|
||||
testNetworkParameters(),
|
||||
emptyList(),
|
||||
isAttachmentTrusted = { true },
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache,
|
||||
digestService = digestService
|
||||
)
|
||||
|
||||
transaction.verify()
|
||||
@ -173,6 +197,7 @@ class TransactionTests {
|
||||
val inState = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DummyContract.PROGRAM_ID, notary)
|
||||
val outState = inState.copy(notary = ALICE)
|
||||
val inputs = listOf(StateAndRef(inState, StateRef(SecureHash.randomSHA256(), 0)))
|
||||
|
||||
val outputs = listOf(outState)
|
||||
val commands = emptyList<CommandWithParties<CommandData>>()
|
||||
val attachments = listOf(object : AbstractAttachment({
|
||||
@ -184,9 +209,9 @@ class TransactionTests {
|
||||
override val size: Int = 1234
|
||||
override val id: SecureHash = SecureHash.zeroHash
|
||||
})
|
||||
val id = SecureHash.randomSHA256()
|
||||
val id = digestService.randomHash()
|
||||
val timeWindow: TimeWindow? = null
|
||||
val privacySalt = PrivacySalt()
|
||||
val privacySalt = PrivacySalt(digestService.digestLength)
|
||||
val attachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory())
|
||||
|
||||
fun buildTransaction() = createLedgerTransaction(
|
||||
@ -201,7 +226,8 @@ class TransactionTests {
|
||||
testNetworkParameters(notaries = listOf(NotaryInfo(DUMMY_NOTARY, true))),
|
||||
emptyList(),
|
||||
isAttachmentTrusted = { true },
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache,
|
||||
digestService = digestService
|
||||
)
|
||||
|
||||
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction().verify() }
|
||||
@ -217,7 +243,8 @@ class TransactionTests {
|
||||
commands = listOf(dummyCommand(DUMMY_KEY_1.public, DUMMY_KEY_2.public)),
|
||||
notary = null,
|
||||
timeWindow = null,
|
||||
privacySalt = PrivacySalt() // Randomly-generated – used for calculating the id
|
||||
privacySalt = PrivacySalt(digestService.digestLength), // Randomly-generated – used for calculating the id
|
||||
digestService = digestService
|
||||
)
|
||||
|
||||
val issueTx1 = buildTransaction()
|
||||
|
@ -153,6 +153,9 @@ interface SchedulableState : ContractState {
|
||||
/** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */
|
||||
fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bytes)
|
||||
|
||||
/** Returns the hash of the serialised contents of this state (not cached!) */
|
||||
fun ContractState.hash(algorithm: String): SecureHash = SecureHash.hashAs(algorithm, serialize().bytes)
|
||||
|
||||
/**
|
||||
* A stateref is a pointer (reference) to a state, this is an equivalent of an "outpoint" in Bitcoin. It records which
|
||||
* transaction defined the state and where in that transaction it was.
|
||||
@ -324,13 +327,32 @@ interface UpgradedContractWithLegacyConstraint<in OldState : ContractState, out
|
||||
@CordaSerializable
|
||||
@KeepForDJVM
|
||||
class PrivacySalt(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
/** Constructs a salt with a randomly-generated saltLength byte value. */
|
||||
@DeleteForDJVM
|
||||
constructor(saltLength: Int) : this(secureRandomBytes(saltLength))
|
||||
|
||||
/** Constructs a salt with a randomly-generated 32 byte value. */
|
||||
@DeleteForDJVM
|
||||
constructor() : this(secureRandomBytes(32))
|
||||
constructor() : this(MINIMUM_SIZE)
|
||||
|
||||
init {
|
||||
require(bytes.size == 32) { "Privacy salt should be 32 bytes." }
|
||||
require(bytes.any { it != 0.toByte() }) { "Privacy salt should not be all zeros." }
|
||||
require(bytes.size >= MINIMUM_SIZE) { "Privacy salt should be at least $MINIMUM_SIZE bytes." }
|
||||
}
|
||||
|
||||
fun validateFor(algorithm: String) {
|
||||
val digestLength = SecureHash.digestLengthFor(algorithm)
|
||||
require(bytes.size >= digestLength) { "Privacy salt should be at least $digestLength bytes for $algorithm." }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MINIMUM_SIZE = 32
|
||||
|
||||
@DeleteForDJVM
|
||||
@JvmStatic
|
||||
fun createFor(algorithm: String): PrivacySalt {
|
||||
return PrivacySalt(SecureHash.digestLengthFor(algorithm))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -342,6 +342,10 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
|
||||
"At this time these are not loadable because the DJVM sandbox has not yet been integrated. " +
|
||||
"You will need to manually install the CorDapp to whitelist it for use.")
|
||||
|
||||
// TODO(iee): verify if this is an acceptable exception
|
||||
@KeepForDJVM
|
||||
class UnsupportedHashTypeException(txId: SecureHash) : TransactionVerificationException(txId, "The transaction Id is defined by an unsupported hash type", null);
|
||||
|
||||
/*
|
||||
If you add a new class extending [TransactionVerificationException], please add a test in `TransactionVerificationExceptionSerializationTests`
|
||||
proving that it can actually be serialised. As a rule, exceptions intended to be serialised _must_ have a corresponding readable property
|
||||
|
@ -1067,7 +1067,7 @@ object Crypto {
|
||||
return if (partialMerkleTree != null) {
|
||||
val usedHashes = mutableListOf<SecureHash>()
|
||||
val root = PartialMerkleTree.rootAndUsedHashes(partialMerkleTree.root, usedHashes)
|
||||
require(txId.sha256() in usedHashes) { "Transaction with id:$txId is not a leaf in the provided partial Merkle tree" }
|
||||
require(txId.reHash() in usedHashes) { "Transaction with id:$txId is not a leaf in the provided partial Merkle tree" }
|
||||
root
|
||||
} else {
|
||||
txId
|
||||
|
@ -265,10 +265,12 @@ fun random63BitValue(): Long {
|
||||
* calculated using the SHA256d algorithm, thus SHA256(SHA256(nonce || serializedComponent)), where nonce is computed
|
||||
* from [computeNonce].
|
||||
*/
|
||||
@Deprecated("This has been moved to DigestService")
|
||||
fun componentHash(opaqueBytes: OpaqueBytes, privacySalt: PrivacySalt, componentGroupIndex: Int, internalIndex: Int): SecureHash =
|
||||
componentHash(computeNonce(privacySalt, componentGroupIndex, internalIndex), opaqueBytes)
|
||||
@Suppress("DEPRECATION") componentHash(computeNonce(privacySalt, componentGroupIndex, internalIndex), opaqueBytes)
|
||||
|
||||
/** Return the SHA256(SHA256(nonce || serializedComponent)). */
|
||||
@Deprecated("This has been moved to DigestService")
|
||||
fun componentHash(nonce: SecureHash, opaqueBytes: OpaqueBytes): SecureHash = SecureHash.sha256Twice(nonce.bytes + opaqueBytes.bytes)
|
||||
|
||||
/**
|
||||
@ -276,6 +278,7 @@ fun componentHash(nonce: SecureHash, opaqueBytes: OpaqueBytes): SecureHash = Sec
|
||||
* across platform versions: serialization can produce different values if any of the types being serialized have changed,
|
||||
* or if the version of serialization specified by the context changes.
|
||||
*/
|
||||
@Deprecated("This has been moved to DigestService")
|
||||
fun <T : Any> serializedHash(x: T): SecureHash = x.serialize(context = SerializationDefaults.P2P_CONTEXT.withoutReferences()).bytes.sha256()
|
||||
|
||||
/**
|
||||
@ -286,5 +289,5 @@ fun <T : Any> serializedHash(x: T): SecureHash = x.serialize(context = Serializa
|
||||
* @param internalIndex the internal index of this object in its corresponding components list.
|
||||
* @return SHA256(SHA256(privacySalt || groupIndex || internalIndex))
|
||||
*/
|
||||
@Deprecated("This has been moved to DigestService")
|
||||
fun computeNonce(privacySalt: PrivacySalt, groupIndex: Int, internalIndex: Int) = SecureHash.sha256Twice(privacySalt.bytes + ByteBuffer.allocate(8).putInt(groupIndex).putInt(internalIndex).array())
|
||||
|
||||
|
@ -0,0 +1,32 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.KeepForDJVM
|
||||
|
||||
/**
|
||||
* Interface for injecting custom digest implementation bypassing JCA.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
interface DigestAlgorithm {
|
||||
/**
|
||||
* Algorithm identifier.
|
||||
*/
|
||||
val algorithm: String
|
||||
|
||||
/**
|
||||
* The length of the digest in bytes.
|
||||
*/
|
||||
val digestLength: Int
|
||||
|
||||
/**
|
||||
* Computes the digest of the [ByteArray].
|
||||
*
|
||||
* @param bytes The [ByteArray] to hash.
|
||||
*/
|
||||
fun digest(bytes: ByteArray): ByteArray
|
||||
|
||||
/**
|
||||
* Computes the digest of the [ByteArray] which is resistant to pre-image attacks.
|
||||
* Default implementation provides double hashing, but can it be changed to single hashing or something else for better performance.
|
||||
*/
|
||||
fun preImageResistantDigest(bytes: ByteArray): ByteArray = digest(digest(bytes))
|
||||
}
|
121
core/src/main/kotlin/net/corda/core/crypto/DigestService.kt
Normal file
121
core/src/main/kotlin/net/corda/core/crypto/DigestService.kt
Normal file
@ -0,0 +1,121 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* The DigestService class is a service that offers the main crypto methods for calculating transaction hashes and
|
||||
* building Merkle trees. The [default] instance is passed by default to instances of classes like TransactionBuilder
|
||||
* and as a parameter to MerkleTree.getMerkleTree(...) method. In future the [default] instance can be parametrized
|
||||
* to initialize with the network default hash algorithm or just a more secure algorithm (e.g. SHA3_256). While the
|
||||
* SHA2_256 is vulnerable to pre-image attacks, the computeNonce and componentHash methods behaviour is defined by
|
||||
* the hashTwiceNonce and hashTwiceComponent; with SHA2_256 they both must be set to true to ensure pre-image attack
|
||||
* won't work (and for backward compatibility), but for other algorithms like SHA3_256 that are not affected, they
|
||||
* can and should be set to false as hashing twice would not improve security but affect performance.
|
||||
*
|
||||
* @param hashAlgorithm the name of the hash algorithm to be used for the instance
|
||||
*/
|
||||
@CordaSerializable
|
||||
@KeepForDJVM
|
||||
data class DigestService(val hashAlgorithm: String) {
|
||||
init {
|
||||
require(hashAlgorithm.isNotEmpty()) { "Hash algorithm name unavailable or not specified" }
|
||||
}
|
||||
|
||||
@KeepForDJVM
|
||||
companion object {
|
||||
private const val NONCE_SIZE = 8
|
||||
/**
|
||||
* The [default] instance will be parametrized and initialized at runtime. It would be probably useful to assume an override
|
||||
* priority order.
|
||||
*/
|
||||
val default : DigestService by lazy { sha2_256 }
|
||||
val sha2_256: DigestService by lazy { DigestService(SecureHash.SHA2_256) }
|
||||
val sha2_384: DigestService by lazy { DigestService(SecureHash.SHA2_384) }
|
||||
val sha2_512: DigestService by lazy { DigestService(SecureHash.SHA2_512) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the WORD size for the given hash algorithm.
|
||||
*/
|
||||
val digestLength: Int
|
||||
get() = SecureHash.digestLengthFor(hashAlgorithm)
|
||||
|
||||
/**
|
||||
* Computes the digest of the [ByteArray].
|
||||
*
|
||||
* @param bytes The [ByteArray] to hash.
|
||||
*/
|
||||
fun hash(bytes: ByteArray): SecureHash = SecureHash.hashAs(hashAlgorithm, bytes)
|
||||
|
||||
/**
|
||||
* Computes the digest of the [String]'s UTF-8 byte contents.
|
||||
*
|
||||
* @param str [String] whose UTF-8 contents will be hashed.
|
||||
*/
|
||||
fun hash(str: String): SecureHash = hash(str.toByteArray())
|
||||
|
||||
/**
|
||||
* A digest value consisting of 0xFF bytes.
|
||||
*/
|
||||
val allOnesHash: SecureHash
|
||||
get() = SecureHash.allOnesHashFor(hashAlgorithm)
|
||||
|
||||
/**
|
||||
* A hash value consisting of 0x00 bytes.
|
||||
*/
|
||||
val zeroHash: SecureHash
|
||||
get() = SecureHash.zeroHashFor(hashAlgorithm)
|
||||
|
||||
// val privacySalt: PrivacySalt
|
||||
// get() = PrivacySalt.createFor(hashAlgorithm)
|
||||
|
||||
/**
|
||||
* Compute the hash of each serialised component so as to be used as Merkle tree leaf. The resultant output (leaf) is
|
||||
* calculated using the service's hash algorithm, thus HASH(HASH(nonce || serializedComponent)) for SHA2-256 and other
|
||||
* algorithms loaded via JCA [MessageDigest], or DigestAlgorithm.preImageResistantDigest(nonce || serializedComponent)
|
||||
* otherwise, where nonce is computed from [computeNonce].
|
||||
*/
|
||||
fun componentHash(opaqueBytes: OpaqueBytes, privacySalt: PrivacySalt, componentGroupIndex: Int, internalIndex: Int): SecureHash =
|
||||
componentHash(computeNonce(privacySalt, componentGroupIndex, internalIndex), opaqueBytes)
|
||||
|
||||
/** Return the HASH(HASH(nonce || serializedComponent)) for SHA2-256 and other algorithms loaded via JCA [MessageDigest],
|
||||
* otherwise it's defined by DigestAlgorithm.preImageResistantDigest(nonce || serializedComponent). */
|
||||
fun componentHash(nonce: SecureHash, opaqueBytes: OpaqueBytes): SecureHash {
|
||||
val data = nonce.bytes + opaqueBytes.bytes
|
||||
return SecureHash.preImageResistantHashAs(hashAlgorithm, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialise the object and return the hash of the serialized bytes. Note that the resulting hash may not be deterministic
|
||||
* across platform versions: serialization can produce different values if any of the types being serialized have changed,
|
||||
* or if the version of serialization specified by the context changes.
|
||||
*/
|
||||
fun <T : Any> serializedHash(x: T): SecureHash =
|
||||
SecureHash.hashAs(hashAlgorithm, x.serialize(context = SerializationDefaults.P2P_CONTEXT.withoutReferences()).bytes)
|
||||
|
||||
/**
|
||||
* Method to compute a nonce based on privacySalt, component group index and component internal index.
|
||||
* SHA256d (double SHA256) is used to prevent length extension attacks.
|
||||
* @param privacySalt a [PrivacySalt].
|
||||
* @param groupIndex the fixed index (ordinal) of this component group.
|
||||
* @param internalIndex the internal index of this object in its corresponding components list.
|
||||
* @return HASH(HASH(privacySalt || groupIndex || internalIndex)) for SHA2-256 and other algorithms loaded via JCA [MessageDigest],
|
||||
* otherwise it's defined by DigestAlgorithm.preImageResistantDigest(privacySalt || groupIndex || internalIndex).
|
||||
*/
|
||||
fun computeNonce(privacySalt: PrivacySalt, groupIndex: Int, internalIndex: Int) : SecureHash {
|
||||
val data = (privacySalt.bytes + ByteBuffer.allocate(NONCE_SIZE).putInt(groupIndex).putInt(internalIndex).array())
|
||||
return SecureHash.preImageResistantHashAs(hashAlgorithm, data)
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteForDJVM
|
||||
fun DigestService.randomHash(): SecureHash = SecureHash.random(this.hashAlgorithm)
|
@ -21,24 +21,34 @@ sealed class MerkleTree {
|
||||
companion object {
|
||||
private fun isPow2(num: Int): Boolean = num and (num - 1) == 0
|
||||
|
||||
@Throws(MerkleTreeException::class)
|
||||
fun getMerkleTree(allLeavesHashes: List<SecureHash>): MerkleTree {
|
||||
return getMerkleTree(allLeavesHashes, DigestService.sha2_256);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merkle tree building using hashes, with zero hash padding to full power of 2.
|
||||
*/
|
||||
@Throws(MerkleTreeException::class)
|
||||
fun getMerkleTree(allLeavesHashes: List<SecureHash>): MerkleTree {
|
||||
fun getMerkleTree(allLeavesHashes: List<SecureHash>, nodeDigestService: DigestService): MerkleTree {
|
||||
if (allLeavesHashes.isEmpty())
|
||||
throw MerkleTreeException("Cannot calculate Merkle root on empty hash list.")
|
||||
val algorithms = allLeavesHashes.mapTo(HashSet(), SecureHash::algorithm)
|
||||
require(algorithms.size == 1) {
|
||||
"Cannot build Merkle tree with multiple hash algorithms: $algorithms"
|
||||
}
|
||||
val leaves = padWithZeros(allLeavesHashes).map { Leaf(it) }
|
||||
return buildMerkleTree(leaves)
|
||||
return buildMerkleTree(leaves, nodeDigestService)
|
||||
}
|
||||
|
||||
// If number of leaves in the tree is not a power of 2, we need to pad it with zero hashes.
|
||||
private fun padWithZeros(allLeavesHashes: List<SecureHash>): List<SecureHash> {
|
||||
var n = allLeavesHashes.size
|
||||
if (isPow2(n)) return allLeavesHashes
|
||||
val paddedHashes = ArrayList<SecureHash>(allLeavesHashes)
|
||||
val paddedHashes = ArrayList(allLeavesHashes)
|
||||
val zeroHash = SecureHash.zeroHashFor(paddedHashes[0].algorithm)
|
||||
while (!isPow2(n++)) {
|
||||
paddedHashes.add(SecureHash.zeroHash)
|
||||
paddedHashes.add(zeroHash)
|
||||
}
|
||||
return paddedHashes
|
||||
}
|
||||
@ -48,7 +58,7 @@ sealed class MerkleTree {
|
||||
* @param lastNodesList MerkleTree nodes from previous level.
|
||||
* @return Tree root.
|
||||
*/
|
||||
private tailrec fun buildMerkleTree(lastNodesList: List<MerkleTree>): MerkleTree {
|
||||
private tailrec fun buildMerkleTree(lastNodesList: List<MerkleTree>, nodeDigestService: DigestService): MerkleTree {
|
||||
return if (lastNodesList.size == 1) {
|
||||
lastNodesList[0] // Root reached.
|
||||
} else {
|
||||
@ -58,11 +68,10 @@ sealed class MerkleTree {
|
||||
for (i in 0..n - 2 step 2) {
|
||||
val left = lastNodesList[i]
|
||||
val right = lastNodesList[i + 1]
|
||||
val newHash = left.hash.hashConcat(right.hash)
|
||||
val combined = Node(newHash, left, right)
|
||||
newLevelHashes.add(combined)
|
||||
val node = Node(nodeDigestService.hash(left.hash.bytes + right.hash.bytes), left, right)
|
||||
newLevelHashes.add(node)
|
||||
}
|
||||
buildMerkleTree(newLevelHashes)
|
||||
buildMerkleTree(newLevelHashes, nodeDigestService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ package net.corda.core.crypto
|
||||
import net.corda.core.CordaException
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.crypto.SecureHash.Companion.zeroHash
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import java.util.*
|
||||
|
||||
@KeepForDJVM
|
||||
@ -59,7 +59,20 @@ class PartialMerkleTree(val root: PartialTree) {
|
||||
sealed class PartialTree {
|
||||
@KeepForDJVM data class IncludedLeaf(val hash: SecureHash) : PartialTree()
|
||||
@KeepForDJVM data class Leaf(val hash: SecureHash) : PartialTree()
|
||||
@KeepForDJVM data class Node(val left: PartialTree, val right: PartialTree) : PartialTree()
|
||||
@KeepForDJVM data class Node(val left: PartialTree, val right: PartialTree, val hashAlgorithm: String? = SecureHash.SHA2_256) : PartialTree(){
|
||||
/**
|
||||
* Old version of [PartialTree.Node] constructor for ABI compatibility.
|
||||
*/
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor(left: PartialTree, right: PartialTree) : this(left, right, SecureHash.SHA2_256)
|
||||
|
||||
/**
|
||||
* Old version of [PartialTree.Node.copy] for ABI compatibility.
|
||||
*/
|
||||
fun copy(left: PartialTree, right: PartialTree) : Node {
|
||||
return Node(left, right, SecureHash.SHA2_256)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -70,13 +83,14 @@ class PartialMerkleTree(val root: PartialTree) {
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, MerkleTreeException::class)
|
||||
fun build(merkleRoot: MerkleTree, includeHashes: List<SecureHash>): PartialMerkleTree {
|
||||
val usedHashes = ArrayList<SecureHash>()
|
||||
require(zeroHash !in includeHashes) { "Zero hashes shouldn't be included in partial tree." }
|
||||
require(includeHashes.none(SecureHash::isZero)) { "Zero hashes shouldn't be included in partial tree." }
|
||||
checkFull(merkleRoot) // Throws MerkleTreeException if it is not a full binary tree.
|
||||
val usedHashes = ArrayList<SecureHash>()
|
||||
val tree = buildPartialTree(merkleRoot, includeHashes, usedHashes)
|
||||
// Too many included hashes or different ones.
|
||||
if (includeHashes.size != usedHashes.size)
|
||||
if (includeHashes.size != usedHashes.size) {
|
||||
throw MerkleTreeException("Some of the provided hashes are not in the tree.")
|
||||
}
|
||||
return PartialMerkleTree(tree.second)
|
||||
}
|
||||
|
||||
@ -116,7 +130,7 @@ class PartialMerkleTree(val root: PartialTree) {
|
||||
val rightNode = buildPartialTree(root.right, includeHashes, usedHashes)
|
||||
if (leftNode.first or rightNode.first) {
|
||||
// This node is on a path to some included leaves. Don't store hash.
|
||||
val newTree = PartialTree.Node(leftNode.second, rightNode.second)
|
||||
val newTree = PartialTree.Node(leftNode.second, rightNode.second, root.hash.algorithm)
|
||||
Pair(true, newTree)
|
||||
} else {
|
||||
// This node has no included leaves below. Cut the tree here and store a hash as a Leaf.
|
||||
@ -144,7 +158,7 @@ class PartialMerkleTree(val root: PartialTree) {
|
||||
is PartialTree.Node -> {
|
||||
val leftHash = rootAndUsedHashes(node.left, usedHashes)
|
||||
val rightHash = rootAndUsedHashes(node.right, usedHashes)
|
||||
leftHash.hashConcat(rightHash)
|
||||
leftHash.concatenateAs(node.hashAlgorithm!!, rightHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,19 @@
|
||||
@file:Suppress("TooManyFunctions", "MagicNumber")
|
||||
@file:KeepForDJVM
|
||||
package net.corda.core.crypto
|
||||
|
||||
import io.netty.util.concurrent.FastThreadLocal
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.crypto.internal.DigestAlgorithmFactory
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.parseAsHex
|
||||
import net.corda.core.utilities.toHexString
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentMap
|
||||
import java.util.function.Supplier
|
||||
|
||||
/**
|
||||
@ -18,7 +22,9 @@ import java.util.function.Supplier
|
||||
*/
|
||||
@KeepForDJVM
|
||||
@CordaSerializable
|
||||
sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
sealed class SecureHash(val algorithm: String, bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
constructor(bytes: ByteArray) : this(SHA2_256, bytes)
|
||||
|
||||
/** SHA-256 is part of the SHA-2 hash function family. Generated hash is fixed size, 256-bits (32-bytes). */
|
||||
class SHA256(bytes: ByteArray) : SecureHash(bytes) {
|
||||
init {
|
||||
@ -27,7 +33,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
if (other !is SHA256) return false
|
||||
if (!super.equals(other)) return false
|
||||
return true
|
||||
}
|
||||
@ -35,18 +41,44 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
// This is an efficient hashCode, because there is no point in performing a hash calculation on a cryptographic hash.
|
||||
// It just takes the first 4 bytes and transforms them into an Int.
|
||||
override fun hashCode() = ByteBuffer.wrap(bytes).int
|
||||
|
||||
/**
|
||||
* Convert the hash value to an uppercase hexadecimal [String].
|
||||
*/
|
||||
override fun toString() = toHexString()
|
||||
|
||||
override fun generate(data: ByteArray): SecureHash {
|
||||
return data.sha256()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the hash value to an uppercase hexadecimal [String].
|
||||
*/
|
||||
override fun toString(): String = bytes.toHexString()
|
||||
class HASH(algorithm: String, bytes: ByteArray) : SecureHash(algorithm, bytes) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return when {
|
||||
this === other -> true
|
||||
other !is HASH -> false
|
||||
else -> algorithm == other.algorithm && super.equals(other)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode() = ByteBuffer.wrap(bytes).int
|
||||
|
||||
override fun generate(data: ByteArray): SecureHash {
|
||||
return HASH(algorithm, digestAs(algorithm, data))
|
||||
}
|
||||
}
|
||||
|
||||
fun toHexString(): String = bytes.toHexString()
|
||||
|
||||
override fun toString(): String {
|
||||
return "$algorithm$DELIMITER${toHexString()}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first [prefixLen] hexadecimal digits of the [SecureHash] value.
|
||||
* @param prefixLen The number of characters in the prefix.
|
||||
*/
|
||||
fun prefixChars(prefixLen: Int = 6) = toString().substring(0, prefixLen)
|
||||
fun prefixChars(prefixLen: Int = 6) = toHexString().substring(0, prefixLen)
|
||||
|
||||
/**
|
||||
* Append a second hash value to this hash value, and then compute the SHA-256 hash of the result.
|
||||
@ -54,8 +86,82 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
*/
|
||||
fun hashConcat(other: SecureHash) = (this.bytes + other.bytes).sha256()
|
||||
|
||||
/**
|
||||
* Append a second hash value to this hash value, and then compute the hash of the result.
|
||||
* @param other The hash to append to this one.
|
||||
*/
|
||||
fun concatenate(other: SecureHash): SecureHash {
|
||||
require(algorithm == other.algorithm) {
|
||||
"Cannot concatenate $algorithm with ${other.algorithm}"
|
||||
}
|
||||
return generate(this.bytes + other.bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a second hash value to this hash value, and then compute the hash of the result using the specified algorithm.
|
||||
* @param other The hash to append to this one.
|
||||
* @param concatAlgorithm The hash algorithm to use for the resulting hash.
|
||||
*/
|
||||
fun concatenateAs(concatAlgorithm: String, other: SecureHash): SecureHash {
|
||||
require(algorithm == other.algorithm) {
|
||||
"Cannot concatenate $algorithm with ${other.algorithm}"
|
||||
}
|
||||
val concatBytes = this.bytes + other.bytes
|
||||
return if(concatAlgorithm == SHA2_256) {
|
||||
concatBytes.sha256()
|
||||
} else {
|
||||
HASH(concatAlgorithm, digestAs(concatAlgorithm, concatBytes))
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun generate(data: ByteArray): SecureHash {
|
||||
throw UnsupportedOperationException("Not implemented for $algorithm")
|
||||
}
|
||||
|
||||
fun reHash() : SecureHash = hashAs(algorithm, bytes)
|
||||
|
||||
// Like static methods in Java, except the 'companion' is a singleton that can have state.
|
||||
companion object {
|
||||
const val SHA2_256 = "SHA-256"
|
||||
const val SHA2_384 = "SHA-384"
|
||||
const val SHA2_512 = "SHA-512"
|
||||
const val DELIMITER = ':'
|
||||
|
||||
/**
|
||||
* Converts a SecureHash hash value represented as a {algorithm:}hexadecimal [String] into a [SecureHash].
|
||||
* @param str An optional algorithm id followed by a delimiter and the sequence of hexadecimal digits that represents a hash value.
|
||||
* @throws IllegalArgumentException The input string does not contain the expected number of hexadecimal digits, or it contains incorrectly-encoded characters.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun create(str: String?): SecureHash {
|
||||
val txt = str ?: throw IllegalArgumentException("Provided string is null")
|
||||
val idx = txt.indexOf(DELIMITER)
|
||||
return if (idx == -1) {
|
||||
parse(txt)
|
||||
} else {
|
||||
val algorithm = txt.substring(0, idx)
|
||||
val value = txt.substring(idx + 1)
|
||||
if (algorithm == SHA2_256) {
|
||||
parse(value)
|
||||
} else {
|
||||
decode(algorithm, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param algorithm [MessageDigest] algorithm name, in uppercase.
|
||||
* @param value Hash value as a hexadecimal string.
|
||||
*/
|
||||
private fun decode(algorithm: String, value: String): SecureHash {
|
||||
val digestLength = digestFor(algorithm).digestLength
|
||||
val data = value.parseAsHex()
|
||||
return when (data.size) {
|
||||
digestLength -> HASH(algorithm, data)
|
||||
else -> throw IllegalArgumentException("Provided string is ${data.size} bytes not $digestLength bytes in hex: $value")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a SHA-256 hash value represented as a hexadecimal [String] into a [SecureHash].
|
||||
* @param str A sequence of 64 hexadecimal digits that represents a SHA-256 hash value.
|
||||
@ -71,14 +177,61 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
} ?: throw IllegalArgumentException("Provided string is null")
|
||||
}
|
||||
|
||||
private val sha256MessageDigest = SHA256DigestSupplier()
|
||||
private val messageDigests: ConcurrentMap<String, DigestSupplier> = ConcurrentHashMap()
|
||||
|
||||
private fun digestFor(algorithm: String): DigestSupplier {
|
||||
return messageDigests.getOrPut(algorithm) { DigestSupplier(algorithm) }
|
||||
}
|
||||
|
||||
private fun digestAs(algorithm: String, bytes: ByteArray): ByteArray = digestFor(algorithm).get().digest(bytes)
|
||||
|
||||
/**
|
||||
* @param algorithm The [MessageDigest] algorithm to query.
|
||||
* @return The length in bytes of this [MessageDigest].
|
||||
*/
|
||||
fun digestLengthFor(algorithm: String): Int {
|
||||
return digestFor(algorithm).digestLength
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the hash value of the [ByteArray].
|
||||
* @param algorithm Java provider name of the digest algorithm.
|
||||
* @param bytes The [ByteArray] to hash.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun hashAs(algorithm: String, bytes: ByteArray): SecureHash {
|
||||
val hashBytes = digestAs(algorithm, bytes)
|
||||
return if (algorithm == SHA2_256) {
|
||||
SHA256(hashBytes)
|
||||
} else {
|
||||
HASH(algorithm, hashBytes)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the digest of the [ByteArray] which is resistant to pre-image attacks.
|
||||
* It computes the hash of the hash for SHA2-256 and other algorithms loaded via JCA [MessageDigest].
|
||||
* For custom algorithms the strategy can be modified via [DigestAlgorithm].
|
||||
* @param algorithm The [MessageDigest] algorithm to use.
|
||||
* @param bytes The [ByteArray] to hash.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun preImageResistantHashAs(algorithm: String, bytes: ByteArray): SecureHash {
|
||||
return if (algorithm == SHA2_256) {
|
||||
sha256Twice(bytes)
|
||||
} else {
|
||||
val digest = digestFor(algorithm).get()
|
||||
val firstHash = digest.preImageResistantDigest(bytes)
|
||||
HASH(algorithm, digest.digest(firstHash))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the SHA-256 hash value of the [ByteArray].
|
||||
* @param bytes The [ByteArray] to hash.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun sha256(bytes: ByteArray) = SHA256(sha256MessageDigest.get().digest(bytes))
|
||||
fun sha256(bytes: ByteArray) = SHA256(digestAs(SHA2_256, bytes))
|
||||
|
||||
/**
|
||||
* Computes the SHA-256 hash of the [ByteArray], and then computes the SHA-256 hash of the hash.
|
||||
@ -101,12 +254,26 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
@JvmStatic
|
||||
fun randomSHA256() = sha256(secureRandomBytes(32))
|
||||
|
||||
/**
|
||||
* Generates a random hash value.
|
||||
*/
|
||||
@DeleteForDJVM
|
||||
@JvmStatic
|
||||
fun random(algorithm: String): SecureHash {
|
||||
return if (algorithm == SHA2_256) {
|
||||
randomSHA256()
|
||||
} else {
|
||||
val digest = digestFor(algorithm)
|
||||
HASH(algorithm, digest.get().digest(secureRandomBytes(digest.digestLength)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A SHA-256 hash value consisting of 32 0x00 bytes.
|
||||
* This field provides more intuitive access from Java.
|
||||
*/
|
||||
@JvmField
|
||||
val zeroHash: SHA256 = SecureHash.SHA256(ByteArray(32) { 0.toByte() })
|
||||
val zeroHash: SHA256 = SHA256(ByteArray(32) { 0.toByte() })
|
||||
|
||||
/**
|
||||
* A SHA-256 hash value consisting of 32 0x00 bytes.
|
||||
@ -120,7 +287,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
* This field provides more intuitive access from Java.
|
||||
*/
|
||||
@JvmField
|
||||
val allOnesHash: SHA256 = SecureHash.SHA256(ByteArray(32) { 255.toByte() })
|
||||
val allOnesHash: SHA256 = SHA256(ByteArray(32) { 255.toByte() })
|
||||
|
||||
/**
|
||||
* A SHA-256 hash value consisting of 32 0xFF bytes.
|
||||
@ -128,11 +295,45 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
*/
|
||||
@Suppress("Unused")
|
||||
fun getAllOnesHash(): SHA256 = allOnesHash
|
||||
|
||||
private val hashConstants: ConcurrentMap<String, HashConstants> = ConcurrentHashMap()
|
||||
init {
|
||||
hashConstants[SHA2_256] = HashConstants(zeroHash, allOnesHash)
|
||||
}
|
||||
|
||||
private fun getConstantsFor(algorithm: String): HashConstants {
|
||||
return hashConstants.getOrPut(algorithm) {
|
||||
val digestLength = digestFor(algorithm).digestLength
|
||||
HashConstants(
|
||||
zero = HASH(algorithm, ByteArray(digestLength)),
|
||||
allOnes = HASH(algorithm, ByteArray(digestLength) { 255.toByte() })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun zeroHashFor(algorithm: String): SecureHash {
|
||||
return getConstantsFor(algorithm).zero
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun allOnesHashFor(algorithm: String): SecureHash {
|
||||
return getConstantsFor(algorithm).allOnes
|
||||
}
|
||||
}
|
||||
|
||||
// In future, maybe SHA3, truncated hashes etc.
|
||||
}
|
||||
|
||||
val OpaqueBytes.isZero: Boolean get() {
|
||||
for (b in bytes) {
|
||||
if (b != 0.toByte()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the SHA-256 hash for the contents of the [ByteArray].
|
||||
*/
|
||||
@ -143,17 +344,30 @@ fun ByteArray.sha256(): SecureHash.SHA256 = SecureHash.sha256(this)
|
||||
*/
|
||||
fun OpaqueBytes.sha256(): SecureHash.SHA256 = SecureHash.sha256(this.bytes)
|
||||
|
||||
/**
|
||||
* Compute the [algorithm] hash for the contents of the [ByteArray].
|
||||
*/
|
||||
fun ByteArray.hashAs(algorithm: String): SecureHash = SecureHash.hashAs(algorithm, this)
|
||||
|
||||
/**
|
||||
* Compute the [algorithm] hash for the contents of the [OpaqueBytes].
|
||||
*/
|
||||
fun OpaqueBytes.hashAs(algorithm: String): SecureHash = SecureHash.hashAs(algorithm, bytes)
|
||||
|
||||
/**
|
||||
* Hide the [FastThreadLocal] class behind a [Supplier] interface
|
||||
* so that we can remove it for core-deterministic.
|
||||
*/
|
||||
private class SHA256DigestSupplier : Supplier<MessageDigest> {
|
||||
private val threadLocalSha256MessageDigest = LocalSHA256Digest()
|
||||
override fun get(): MessageDigest = threadLocalSha256MessageDigest.get()
|
||||
private class DigestSupplier(algorithm: String) : Supplier<DigestAlgorithm> {
|
||||
private val threadLocalMessageDigest = LocalDigest(algorithm)
|
||||
override fun get(): DigestAlgorithm = threadLocalMessageDigest.get()
|
||||
val digestLength: Int = get().digestLength
|
||||
}
|
||||
|
||||
// Declaring this as "object : FastThreadLocal<>" would have
|
||||
// created an extra public class in the API definition.
|
||||
private class LocalSHA256Digest : FastThreadLocal<MessageDigest>() {
|
||||
override fun initialValue(): MessageDigest = MessageDigest.getInstance("SHA-256")
|
||||
private class LocalDigest(private val algorithm: String) : FastThreadLocal<DigestAlgorithm>() {
|
||||
override fun initialValue() = DigestAlgorithmFactory.create(algorithm)
|
||||
}
|
||||
|
||||
private class HashConstants(val zero: SecureHash, val allOnes: SecureHash)
|
||||
|
@ -0,0 +1,70 @@
|
||||
package net.corda.core.crypto.internal
|
||||
|
||||
import net.corda.core.crypto.DigestAlgorithm
|
||||
import java.lang.reflect.Constructor
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
sealed class DigestAlgorithmFactory {
|
||||
abstract fun create(): DigestAlgorithm
|
||||
abstract val algorithm: String
|
||||
|
||||
private class MessageDigestFactory(override val algorithm: String) : DigestAlgorithmFactory() {
|
||||
override fun create(): DigestAlgorithm {
|
||||
try {
|
||||
val messageDigest = MessageDigest.getInstance(algorithm)
|
||||
return MessageDigestWrapper(messageDigest, algorithm)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw IllegalArgumentException("Unknown hash algorithm $algorithm")
|
||||
}
|
||||
}
|
||||
|
||||
private class MessageDigestWrapper(val messageDigest: MessageDigest, override val algorithm: String) : DigestAlgorithm {
|
||||
override val digestLength = messageDigest.digestLength
|
||||
override fun digest(bytes: ByteArray): ByteArray = messageDigest.digest(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomAlgorithmFactory(className: String) : DigestAlgorithmFactory() {
|
||||
val constructor: Constructor<out DigestAlgorithm> = javaClass
|
||||
.classLoader
|
||||
.loadClass(className)
|
||||
.asSubclass(DigestAlgorithm::class.java)
|
||||
.getConstructor()
|
||||
override val algorithm: String = constructor.newInstance().algorithm
|
||||
|
||||
override fun create(): DigestAlgorithm {
|
||||
return constructor.newInstance()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SHA2_256 = "SHA-256"
|
||||
private val BANNED: Set<String> = Collections.unmodifiableSet(setOf("MD5", "MD2", "SHA-1"))
|
||||
private val sha256Factory = MessageDigestFactory(SHA2_256)
|
||||
private val factories = ConcurrentHashMap<String, DigestAlgorithmFactory>()
|
||||
|
||||
private fun check(algorithm: String) {
|
||||
require(algorithm.toUpperCase() == algorithm) { "Hash algorithm name $this must be in the upper case" }
|
||||
require(algorithm !in BANNED) { "$algorithm is forbidden!" }
|
||||
}
|
||||
|
||||
fun registerClass(className: String): String {
|
||||
val factory = CustomAlgorithmFactory(className)
|
||||
check(factory.algorithm)
|
||||
require(factory.algorithm != SHA2_256) { "Standard algorithm name is not allowed in $className" }
|
||||
factories.putIfAbsent(factory.algorithm, factory)
|
||||
return factory.algorithm
|
||||
}
|
||||
|
||||
fun create(algorithm: String): DigestAlgorithm {
|
||||
check(algorithm)
|
||||
return when (algorithm) {
|
||||
SHA2_256 -> sha256Factory.create()
|
||||
else -> factories[algorithm]?.create()?: MessageDigestFactory(algorithm).create()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import net.corda.core.crypto.SignableData
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.NotaryChangeTransactionBuilder
|
||||
import net.corda.core.internal.digestService
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
|
||||
@ -34,7 +35,8 @@ class NotaryChangeFlow<out T : ContractState>(
|
||||
inputs.map { it.ref },
|
||||
originalState.state.notary,
|
||||
modification,
|
||||
serviceHub.networkParametersService.currentHash
|
||||
serviceHub.networkParametersService.currentHash,
|
||||
serviceHub.digestService
|
||||
).build()
|
||||
|
||||
val participantKeys = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet()
|
||||
|
@ -175,7 +175,7 @@ class NotaryFlow {
|
||||
*/
|
||||
private fun generateRequestSignature(): NotarisationRequestSignature {
|
||||
// TODO: This is not required any more once our AMQP serialization supports turning off object referencing.
|
||||
val notarisationRequest = NotarisationRequest(stx.inputs.map { it.copy(txhash = SecureHash.parse(it.txhash.toString())) }, stx.id)
|
||||
val notarisationRequest = NotarisationRequest(stx.inputs.map { it.copy(txhash = SecureHash.create(it.txhash.toString())) }, stx.id)
|
||||
return notarisationRequest.generateSignature(serviceHub)
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,8 @@ object ContractUpgradeUtils {
|
||||
upgradedContractClass.name,
|
||||
upgradedContractAttachmentId,
|
||||
privacySalt,
|
||||
networkParametersHash
|
||||
networkParametersHash,
|
||||
services.digestService
|
||||
).build()
|
||||
}
|
||||
|
||||
|
@ -2,11 +2,13 @@ package net.corda.core.internal
|
||||
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.componentHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.crypto.hashAs
|
||||
import net.corda.core.crypto.internal.DigestAlgorithmFactory
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.transactions.*
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
@ -18,10 +20,12 @@ import kotlin.reflect.KClass
|
||||
class NotaryChangeTransactionBuilder(val inputs: List<StateRef>,
|
||||
val notary: Party,
|
||||
val newNotary: Party,
|
||||
val networkParametersHash: SecureHash) {
|
||||
val networkParametersHash: SecureHash,
|
||||
val digestService: DigestService = DigestService.sha2_256) {
|
||||
|
||||
fun build(): NotaryChangeWireTransaction {
|
||||
val components = listOf(inputs, notary, newNotary, networkParametersHash).map { it.serialize() }
|
||||
return NotaryChangeWireTransaction(components)
|
||||
return NotaryChangeWireTransaction(components, digestService)
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,21 +36,32 @@ class ContractUpgradeTransactionBuilder(
|
||||
val legacyContractAttachmentId: SecureHash,
|
||||
val upgradedContractClassName: ContractClassName,
|
||||
val upgradedContractAttachmentId: SecureHash,
|
||||
val privacySalt: PrivacySalt = PrivacySalt(),
|
||||
val networkParametersHash: SecureHash) {
|
||||
privacySalt: PrivacySalt = PrivacySalt(),
|
||||
val networkParametersHash: SecureHash,
|
||||
val digestService: DigestService = DigestService.sha2_256) {
|
||||
var privacySalt: PrivacySalt = privacySalt
|
||||
private set
|
||||
|
||||
fun build(): ContractUpgradeWireTransaction {
|
||||
val components = listOf(inputs, notary, legacyContractAttachmentId, upgradedContractClassName, upgradedContractAttachmentId, networkParametersHash).map { it.serialize() }
|
||||
return ContractUpgradeWireTransaction(components, privacySalt)
|
||||
return ContractUpgradeWireTransaction(components, privacySalt, digestService)
|
||||
}
|
||||
}
|
||||
|
||||
/** Concatenates the hash components into a single [ByteArray] and returns its hash. */
|
||||
fun combinedHash(components: Iterable<SecureHash>): SecureHash {
|
||||
fun combinedHash(components: Iterable<SecureHash>/*, digestService: DigestService = DigestService.default*/): SecureHash {
|
||||
val stream = ByteArrayOutputStream()
|
||||
components.forEach {
|
||||
stream.write(it.bytes)
|
||||
}
|
||||
return stream.toByteArray().sha256()
|
||||
// TODO(iee): need to re-visit and review this code to understand which is the right
|
||||
// way to combine the hashes. Is this meant to match a pre-existing tx id,
|
||||
// or create a new tx id with the [default] hash algorithm from whatever
|
||||
// components are passed in and independently from their algorithm?
|
||||
// This is used to build the tx id of ContractUpgradeFilteredTransaction and
|
||||
// ContractUpgradeWireTransaction
|
||||
return stream.toByteArray().hashAs(components.first().algorithm)
|
||||
//return digestService.hash(stream.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,7 +121,8 @@ fun deserialiseCommands(
|
||||
componentGroups: List<ComponentGroup>,
|
||||
forceDeserialize: Boolean = false,
|
||||
factory: SerializationFactory = SerializationFactory.defaultFactory,
|
||||
@Suppress("UNUSED_PARAMETER") context: SerializationContext = factory.defaultContext
|
||||
@Suppress("UNUSED_PARAMETER") context: SerializationContext = factory.defaultContext,
|
||||
digestService: DigestService = DigestService.sha2_256
|
||||
): List<Command<*>> {
|
||||
// TODO: we could avoid deserialising unrelated signers.
|
||||
// However, current approach ensures the transaction is not malformed
|
||||
@ -118,7 +134,7 @@ fun deserialiseCommands(
|
||||
check(commandDataList.size <= signersList.size) {
|
||||
"Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects"
|
||||
}
|
||||
val componentHashes = group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }
|
||||
val componentHashes = group.components.mapIndexed { index, component -> digestService.componentHash(group.nonces[index], component) }
|
||||
val leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) }
|
||||
if (leafIndices.isNotEmpty())
|
||||
check(leafIndices.max()!! < signersList.size) { "Invalid Transaction. A command with no corresponding signer detected" }
|
||||
@ -191,3 +207,45 @@ fun FlowLogic<*>.checkParameterHash(networkParametersHash: SecureHash?) {
|
||||
val SignedTransaction.dependencies: Set<SecureHash>
|
||||
get() = (inputs.asSequence() + references.asSequence()).map { it.txhash }.toSet()
|
||||
|
||||
class HashAgility {
|
||||
companion object {
|
||||
@Volatile
|
||||
internal var digestService = DigestService.sha2_256
|
||||
private set
|
||||
|
||||
fun init(txHashAlgoName: String? = null, txHashAlgoClass: String? = null) {
|
||||
digestService = DigestService.sha2_256
|
||||
txHashAlgoName?.let {
|
||||
// Verify that algorithm exists.
|
||||
DigestAlgorithmFactory.create(it)
|
||||
digestService = DigestService(it)
|
||||
}
|
||||
txHashAlgoClass?.let {
|
||||
val algorithm = DigestAlgorithmFactory.registerClass(it)
|
||||
digestService = DigestService(algorithm)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun isAlgorithmSupported(algorithm: String): Boolean {
|
||||
return algorithm == SecureHash.SHA2_256 || algorithm == digestService.hashAlgorithm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The configured instance of DigestService which is passed by default to instances of classes like TransactionBuilder
|
||||
* and as a parameter to MerkleTree.getMerkleTree(...) method. Default: SHA2_256.
|
||||
*/
|
||||
val ServicesForResolution.digestService get() = HashAgility.digestService
|
||||
|
||||
fun ServicesForResolution.requireSupportedHashType(hash: NamedByHash) {
|
||||
require(HashAgility.isAlgorithmSupported(hash.id.algorithm)) {
|
||||
"Tried to record a transaction with non-standard hash algorithm ${hash.id.algorithm} (experimental mode off)"
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTransaction.checkSupportedHashType() {
|
||||
if (!HashAgility.isAlgorithmSupported(id.algorithm)) {
|
||||
throw TransactionVerificationException.UnsupportedHashTypeException(id)
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ abstract class Verifier(val ltx: LedgerTransaction, protected val transactionCla
|
||||
// list, the contents of which need to be deserialized under the correct classloader.
|
||||
checkNoNotaryChange()
|
||||
checkEncumbrancesValid()
|
||||
ltx.checkSupportedHashType()
|
||||
|
||||
// The following checks ensure the integrity of the current transaction and also of the future chain.
|
||||
// See: https://docs.corda.net/head/api-contract-constraints.html
|
||||
|
@ -174,7 +174,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
fun constraintInfo(type: Type, data: ByteArray?): ConstraintInfo {
|
||||
return when (type) {
|
||||
Type.ALWAYS_ACCEPT -> ConstraintInfo(AlwaysAcceptAttachmentConstraint)
|
||||
Type.HASH -> ConstraintInfo(HashAttachmentConstraint(SecureHash.parse(data!!.toHexString())))
|
||||
Type.HASH -> ConstraintInfo(HashAttachmentConstraint(SecureHash.create(data!!.toHexString())))
|
||||
Type.CZ_WHITELISTED -> ConstraintInfo(WhitelistedByZoneAttachmentConstraint)
|
||||
Type.SIGNATURE -> ConstraintInfo(SignatureAttachmentConstraint(Crypto.decodePublicKey(data!!)))
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.toHexString
|
||||
import org.hibernate.annotations.Immutable
|
||||
import java.io.Serializable
|
||||
import javax.persistence.Column
|
||||
@ -93,13 +92,13 @@ class PersistentState(@EmbeddedId override var stateRef: PersistentStateRef? = n
|
||||
|
||||
data class PersistentStateRef(
|
||||
@Suppress("MagicNumber") // column width
|
||||
@Column(name = "transaction_id", length = 64, nullable = false)
|
||||
@Column(name = "transaction_id", length = 144, nullable = false)
|
||||
var txId: String,
|
||||
|
||||
@Column(name = "output_index", nullable = false)
|
||||
var index: Int
|
||||
) : Serializable {
|
||||
constructor(stateRef: StateRef) : this(stateRef.txhash.bytes.toHexString(), stateRef.index)
|
||||
constructor(stateRef: StateRef) : this(stateRef.txhash.toString(), stateRef.index)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,10 +3,9 @@ package net.corda.core.transactions
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.componentHash
|
||||
import net.corda.core.crypto.computeNonce
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AttachmentWithContext
|
||||
import net.corda.core.internal.ServiceHubCoreInternal
|
||||
@ -14,6 +13,7 @@ import net.corda.core.internal.combinedHash
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
||||
@ -42,8 +42,13 @@ data class ContractUpgradeWireTransaction(
|
||||
*/
|
||||
val serializedComponents: List<OpaqueBytes>,
|
||||
/** Required for hiding components in [ContractUpgradeFilteredTransaction]. */
|
||||
val privacySalt: PrivacySalt = PrivacySalt()
|
||||
val privacySalt: PrivacySalt,
|
||||
val digestService: DigestService
|
||||
) : CoreTransaction() {
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor(serializedComponents: List<OpaqueBytes>, privacySalt: PrivacySalt = PrivacySalt())
|
||||
: this(serializedComponents, privacySalt, DigestService.sha2_256)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Runs the explicit upgrade logic.
|
||||
@ -83,6 +88,14 @@ data class ContractUpgradeWireTransaction(
|
||||
init {
|
||||
check(inputs.isNotEmpty()) { "A contract upgrade transaction must have inputs" }
|
||||
checkBaseInvariants()
|
||||
privacySalt.validateFor(digestService.hashAlgorithm)
|
||||
}
|
||||
|
||||
/**
|
||||
* Old version of [ContractUpgradeWireTransaction.copy] for sake of ABI compatibility.
|
||||
*/
|
||||
fun copy(serializedComponents: List<OpaqueBytes>, privacySalt: PrivacySalt): ContractUpgradeWireTransaction {
|
||||
return ContractUpgradeWireTransaction(serializedComponents, privacySalt, digestService)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -99,14 +112,14 @@ data class ContractUpgradeWireTransaction(
|
||||
|
||||
override val id: SecureHash by lazy {
|
||||
val componentHashes = serializedComponents.mapIndexed { index, component ->
|
||||
componentHash(nonces[index], component)
|
||||
digestService.componentHash(nonces[index], component)
|
||||
}
|
||||
combinedHash(componentHashes)
|
||||
combinedHash(componentHashes/*, digestService*/)
|
||||
}
|
||||
|
||||
/** Required for filtering transaction components. */
|
||||
private val nonces = (0 until serializedComponents.size).map {
|
||||
computeNonce(privacySalt, it, 0)
|
||||
private val nonces = serializedComponents.indices.map {
|
||||
digestService.computeNonce(privacySalt, it, 0)
|
||||
}
|
||||
|
||||
/** Resolves input states and contract attachments, and builds a ContractUpgradeLedgerTransaction. */
|
||||
@ -171,11 +184,11 @@ data class ContractUpgradeWireTransaction(
|
||||
PARAMETERS_HASH.ordinal to FilteredComponent(serializedComponents[PARAMETERS_HASH.ordinal], nonces[PARAMETERS_HASH.ordinal])
|
||||
)
|
||||
val hiddenComponents = (totalComponents - visibleComponents.keys).map { index ->
|
||||
val hash = componentHash(nonces[index], serializedComponents[index])
|
||||
val hash = digestService.componentHash(nonces[index], serializedComponents[index])
|
||||
index to hash
|
||||
}.toMap()
|
||||
|
||||
return ContractUpgradeFilteredTransaction(visibleComponents, hiddenComponents)
|
||||
return ContractUpgradeFilteredTransaction(visibleComponents, hiddenComponents, digestService)
|
||||
}
|
||||
|
||||
enum class Component {
|
||||
@ -197,8 +210,24 @@ data class ContractUpgradeFilteredTransaction(
|
||||
* Hashes of the transaction components that are not revealed in this transaction.
|
||||
* Required for computing the transaction id.
|
||||
*/
|
||||
val hiddenComponents: Map<Int, SecureHash>
|
||||
val hiddenComponents: Map<Int, SecureHash>,
|
||||
val digestService: DigestService = DigestService.sha2_256
|
||||
) : CoreTransaction() {
|
||||
|
||||
/**
|
||||
* Old version of [ContractUpgradeFilteredTransaction] constructor for ABI compatibility.
|
||||
*/
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor(visibleComponents: Map<Int, FilteredComponent>, hiddenComponents: Map<Int, SecureHash>)
|
||||
: this(visibleComponents, hiddenComponents, DigestService.sha2_256)
|
||||
|
||||
/**
|
||||
* Old version of [ContractUpgradeFilteredTransaction.copy] for ABI compatibility.
|
||||
*/
|
||||
fun copy(visibleComponents: Map<Int, FilteredComponent>, hiddenComponents: Map<Int, SecureHash>) : ContractUpgradeFilteredTransaction {
|
||||
return ContractUpgradeFilteredTransaction(visibleComponents, hiddenComponents, DigestService.sha2_256)
|
||||
}
|
||||
|
||||
override val inputs: List<StateRef> by lazy {
|
||||
visibleComponents[INPUTS.ordinal]?.component?.deserialize<List<StateRef>>()
|
||||
?: throw IllegalArgumentException("Inputs not specified")
|
||||
@ -215,13 +244,13 @@ data class ContractUpgradeFilteredTransaction(
|
||||
val hashList = (0 until totalComponents).map { i ->
|
||||
when {
|
||||
visibleComponents.containsKey(i) -> {
|
||||
componentHash(visibleComponents[i]!!.nonce, visibleComponents[i]!!.component)
|
||||
digestService.componentHash(visibleComponents[i]!!.nonce, visibleComponents[i]!!.component)
|
||||
}
|
||||
hiddenComponents.containsKey(i) -> hiddenComponents[i]!!
|
||||
else -> throw IllegalStateException("Missing component hashes")
|
||||
}
|
||||
}
|
||||
combinedHash(hashList)
|
||||
combinedHash(hashList/*, digestService*/)
|
||||
}
|
||||
override val outputs: List<TransactionState<ContractState>> get() = emptyList()
|
||||
override val references: List<StateRef> get() = emptyList()
|
||||
|
@ -14,6 +14,7 @@ import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.Party
|
||||
@ -26,6 +27,7 @@ import net.corda.core.internal.deserialiseComponentGroup
|
||||
import net.corda.core.internal.isUploaderTrusted
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderCache
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
||||
import net.corda.core.utilities.contextLogger
|
||||
@ -89,9 +91,42 @@ private constructor(
|
||||
private val serializedReferences: List<SerializedStateAndRef>?,
|
||||
private val isAttachmentTrusted: (Attachment) -> Boolean,
|
||||
private val verifierFactory: (LedgerTransaction, ClassLoader) -> Verifier,
|
||||
private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache?
|
||||
private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache?,
|
||||
val digestService: DigestService = DigestService.sha2_256
|
||||
) : FullTransaction() {
|
||||
|
||||
/**
|
||||
* Old version of [LedgerTransaction] constructor for ABI compatibility.
|
||||
*/
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
private constructor(
|
||||
inputs: List<StateAndRef<ContractState>>,
|
||||
outputs: List<TransactionState<ContractState>>,
|
||||
commands: List<CommandWithParties<CommandData>>,
|
||||
attachments: List<Attachment>,
|
||||
id: SecureHash,
|
||||
notary: Party?,
|
||||
timeWindow: TimeWindow?,
|
||||
privacySalt: PrivacySalt,
|
||||
networkParameters: NetworkParameters?,
|
||||
references: List<StateAndRef<ContractState>>,
|
||||
componentGroups: List<ComponentGroup>?,
|
||||
serializedInputs: List<SerializedStateAndRef>?,
|
||||
serializedReferences: List<SerializedStateAndRef>?,
|
||||
isAttachmentTrusted: (Attachment) -> Boolean,
|
||||
verifierFactory: (LedgerTransaction, ClassLoader) -> Verifier,
|
||||
attachmentsClassLoaderCache: AttachmentsClassLoaderCache?) : this(
|
||||
inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt,
|
||||
networkParameters, references, componentGroups, serializedInputs, serializedReferences,
|
||||
isAttachmentTrusted, verifierFactory, attachmentsClassLoaderCache, DigestService.sha2_256)
|
||||
|
||||
// TODO(iee): add missing => Removed from API txt
|
||||
// public <init>(java.util.List, java.util.List, java.util.List, java.util.List, net.corda.core.crypto.SecureHash,
|
||||
// net.corda.core.identity.Party, net.corda.core.contracts.TimeWindow, net.corda.core.contracts.PrivacySalt,
|
||||
// net.corda.core.node.NetworkParameters, java.util.List, java.util.List, java.util.List, java.util.List,
|
||||
// kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function2,
|
||||
// net.corda.core.serialization.internal.AttachmentsClassLoaderCache, kotlin.jvm.internal.DefaultConstructorMarker)
|
||||
|
||||
init {
|
||||
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }
|
||||
checkNotaryWhitelisted()
|
||||
@ -127,7 +162,8 @@ private constructor(
|
||||
serializedInputs: List<SerializedStateAndRef>? = null,
|
||||
serializedReferences: List<SerializedStateAndRef>? = null,
|
||||
isAttachmentTrusted: (Attachment) -> Boolean,
|
||||
attachmentsClassLoaderCache: AttachmentsClassLoaderCache?
|
||||
attachmentsClassLoaderCache: AttachmentsClassLoaderCache?,
|
||||
digestService: DigestService
|
||||
): LedgerTransaction {
|
||||
return LedgerTransaction(
|
||||
inputs = inputs,
|
||||
@ -145,7 +181,8 @@ private constructor(
|
||||
serializedReferences = protect(serializedReferences),
|
||||
isAttachmentTrusted = isAttachmentTrusted,
|
||||
verifierFactory = ::BasicVerifier,
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache,
|
||||
digestService = digestService
|
||||
)
|
||||
}
|
||||
|
||||
@ -164,7 +201,8 @@ private constructor(
|
||||
timeWindow: TimeWindow?,
|
||||
privacySalt: PrivacySalt,
|
||||
networkParameters: NetworkParameters,
|
||||
references: List<StateAndRef<ContractState>>): LedgerTransaction {
|
||||
references: List<StateAndRef<ContractState>>,
|
||||
digestService: DigestService): LedgerTransaction {
|
||||
return LedgerTransaction(
|
||||
inputs = inputs,
|
||||
outputs = outputs,
|
||||
@ -181,7 +219,8 @@ private constructor(
|
||||
serializedReferences = null,
|
||||
isAttachmentTrusted = { true },
|
||||
verifierFactory = ::BasicVerifier,
|
||||
attachmentsClassLoaderCache = null
|
||||
attachmentsClassLoaderCache = null,
|
||||
digestService = digestService
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -261,7 +300,8 @@ private constructor(
|
||||
serializedReferences = serializedReferences,
|
||||
isAttachmentTrusted = isAttachmentTrusted,
|
||||
verifierFactory = alternateVerifier,
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache,
|
||||
digestService = digestService
|
||||
)
|
||||
|
||||
// Read network parameters with backwards compatibility goo.
|
||||
@ -305,7 +345,7 @@ private constructor(
|
||||
val deserializedInputs = serializedInputs.map { it.toStateAndRef() }
|
||||
val deserializedReferences = serializedReferences.map { it.toStateAndRef() }
|
||||
val deserializedOutputs = deserialiseComponentGroup(componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true)
|
||||
val deserializedCommands = deserialiseCommands(componentGroups, forceDeserialize = true)
|
||||
val deserializedCommands = deserialiseCommands(componentGroups, forceDeserialize = true, digestService = digestService)
|
||||
val authenticatedDeserializedCommands = deserializedCommands.map { cmd ->
|
||||
@Suppress("DEPRECATION") // Deprecated feature.
|
||||
val parties = commands.find { it.value.javaClass.name == cmd.value.javaClass.name }!!.signingParties
|
||||
@ -328,7 +368,8 @@ private constructor(
|
||||
serializedReferences = serializedReferences,
|
||||
isAttachmentTrusted = isAttachmentTrusted,
|
||||
verifierFactory = verifierFactory,
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache,
|
||||
digestService = digestService
|
||||
)
|
||||
} else {
|
||||
// This branch is only present for backwards compatibility.
|
||||
@ -772,7 +813,8 @@ private constructor(
|
||||
serializedReferences = serializedReferences,
|
||||
isAttachmentTrusted = isAttachmentTrusted,
|
||||
verifierFactory = verifierFactory,
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache,
|
||||
digestService = digestService
|
||||
)
|
||||
}
|
||||
|
||||
@ -803,7 +845,8 @@ private constructor(
|
||||
serializedReferences = serializedReferences,
|
||||
isAttachmentTrusted = isAttachmentTrusted,
|
||||
verifierFactory = verifierFactory,
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache
|
||||
attachmentsClassLoaderCache = attachmentsClassLoaderCache,
|
||||
digestService = digestService
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.deserialiseCommands
|
||||
import net.corda.core.internal.deserialiseComponentGroup
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
@ -21,7 +22,14 @@ import java.util.function.Predicate
|
||||
* may be missing in the case of this representing a "torn" transaction. Please see the user guide section
|
||||
* "Transaction tear-offs" to learn more about this feature.
|
||||
*/
|
||||
abstract class TraversableTransaction(open val componentGroups: List<ComponentGroup>) : CoreTransaction() {
|
||||
abstract class TraversableTransaction(open val componentGroups: List<ComponentGroup>, val digestService: DigestService) : CoreTransaction() {
|
||||
|
||||
/**
|
||||
* Old version of [TraversableTransaction] constructor for ABI compatibility.
|
||||
*/
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor(componentGroups: List<ComponentGroup>) : this(componentGroups, DigestService.sha2_256)
|
||||
|
||||
/** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */
|
||||
val attachments: List<SecureHash> = deserialiseComponentGroup(componentGroups, SecureHash::class, ATTACHMENTS_GROUP)
|
||||
|
||||
@ -34,7 +42,7 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
|
||||
override val outputs: List<TransactionState<ContractState>> = deserialiseComponentGroup(componentGroups, TransactionState::class, OUTPUTS_GROUP)
|
||||
|
||||
/** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */
|
||||
val commands: List<Command<*>> = deserialiseCommands(componentGroups)
|
||||
val commands: List<Command<*>> = deserialiseCommands(componentGroups, digestService = digestService)
|
||||
|
||||
override val notary: Party? = let {
|
||||
val notaries: List<Party> = deserialiseComponentGroup(componentGroups, Party::class, NOTARY_GROUP)
|
||||
@ -87,8 +95,16 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
|
||||
class FilteredTransaction internal constructor(
|
||||
override val id: SecureHash,
|
||||
val filteredComponentGroups: List<FilteredComponentGroup>,
|
||||
val groupHashes: List<SecureHash>
|
||||
) : TraversableTransaction(filteredComponentGroups) {
|
||||
val groupHashes: List<SecureHash>,
|
||||
digestService: DigestService
|
||||
) : TraversableTransaction(filteredComponentGroups, digestService) {
|
||||
|
||||
/**
|
||||
* Old version of [FilteredTransaction] constructor for ABI compatibility.
|
||||
*/
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
internal constructor(id: SecureHash, filteredComponentGroups: List<FilteredComponentGroup>, groupHashes: List<SecureHash>)
|
||||
: this(id, filteredComponentGroups, groupHashes, DigestService.sha2_256)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
@ -99,7 +115,7 @@ class FilteredTransaction internal constructor(
|
||||
@JvmStatic
|
||||
fun buildFilteredTransaction(wtx: WireTransaction, filtering: Predicate<Any>): FilteredTransaction {
|
||||
val filteredComponentGroups = filterWithFun(wtx, filtering)
|
||||
return FilteredTransaction(wtx.id, filteredComponentGroups, wtx.groupHashes)
|
||||
return FilteredTransaction(wtx.id, filteredComponentGroups, wtx.groupHashes, wtx.digestService)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -176,7 +192,7 @@ class FilteredTransaction internal constructor(
|
||||
|
||||
fun createPartialMerkleTree(componentGroupIndex: Int): PartialMerkleTree {
|
||||
return PartialMerkleTree.build(
|
||||
MerkleTree.getMerkleTree(wtx.availableComponentHashes[componentGroupIndex]!!),
|
||||
MerkleTree.getMerkleTree(wtx.availableComponentHashes[componentGroupIndex]!!, wtx.digestService),
|
||||
filteredComponentHashes[componentGroupIndex]!!
|
||||
)
|
||||
}
|
||||
@ -206,7 +222,7 @@ class FilteredTransaction internal constructor(
|
||||
verificationCheck(groupHashes.isNotEmpty()) { "At least one component group hash is required" }
|
||||
// Verify the top level Merkle tree (group hashes are its leaves, including allOnesHash for empty list or null
|
||||
// components in WireTransaction).
|
||||
verificationCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) {
|
||||
verificationCheck(MerkleTree.getMerkleTree(groupHashes, digestService).hash == id) {
|
||||
"Top level Merkle tree cannot be verified against transaction's id"
|
||||
}
|
||||
|
||||
@ -220,7 +236,7 @@ class FilteredTransaction internal constructor(
|
||||
verificationCheck(groupMerkleRoot == PartialMerkleTree.rootAndUsedHashes(groupPartialTree.root, mutableListOf())) {
|
||||
"Partial Merkle tree root and advertised full Merkle tree root for component group $groupIndex do not match"
|
||||
}
|
||||
verificationCheck(groupPartialTree.verify(groupMerkleRoot, components.mapIndexed { index, component -> componentHash(nonces[index], component) })) {
|
||||
verificationCheck(groupPartialTree.verify(groupMerkleRoot, components.mapIndexed { index, component -> digestService.componentHash(nonces[index], component) })) {
|
||||
"Visible components in group $groupIndex cannot be verified against their partial Merkle tree"
|
||||
}
|
||||
}
|
||||
@ -260,16 +276,16 @@ class FilteredTransaction internal constructor(
|
||||
// If we don't receive elements of a particular component, check if its ordinal is bigger that the
|
||||
// groupHashes.size or if the group hash is allOnesHash,
|
||||
// to ensure there were indeed no elements in the original wire transaction.
|
||||
visibilityCheck(componentGroupEnum.ordinal >= groupHashes.size || groupHashes[componentGroupEnum.ordinal] == SecureHash.allOnesHash) {
|
||||
visibilityCheck(componentGroupEnum.ordinal >= groupHashes.size || groupHashes[componentGroupEnum.ordinal] == SecureHash.allOnesHashFor(id.algorithm)) {
|
||||
"Did not receive components for group ${componentGroupEnum.ordinal} and cannot verify they didn't exist in the original wire transaction"
|
||||
}
|
||||
} else {
|
||||
visibilityCheck(group.groupIndex < groupHashes.size) { "There is no matching component group hash for group ${group.groupIndex}" }
|
||||
val groupPartialRoot = groupHashes[group.groupIndex]
|
||||
val groupFullRoot = MerkleTree.getMerkleTree(group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }).hash
|
||||
val groupFullRoot = MerkleTree.getMerkleTree(group.components.mapIndexed { index, component -> digestService.componentHash(group.nonces[index], component) }, digestService).hash
|
||||
visibilityCheck(groupPartialRoot == groupFullRoot) { "Some components for group ${group.groupIndex} are not visible" }
|
||||
// Verify the top level Merkle tree from groupHashes.
|
||||
visibilityCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) {
|
||||
visibilityCheck(MerkleTree.getMerkleTree(groupHashes, digestService).hash == id) {
|
||||
"Transaction is malformed. Top level Merkle tree cannot be verified against transaction's id"
|
||||
}
|
||||
}
|
||||
|
@ -4,14 +4,15 @@ import net.corda.core.CordaInternal
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
@ -33,8 +34,30 @@ data class NotaryChangeWireTransaction(
|
||||
* This is used for calculating the transaction id in a deterministic fashion, since re-serializing properties
|
||||
* may result in a different byte sequence depending on the serialization context.
|
||||
*/
|
||||
val serializedComponents: List<OpaqueBytes>
|
||||
val serializedComponents: List<OpaqueBytes>,
|
||||
val digestService: DigestService
|
||||
) : CoreTransaction() {
|
||||
/**
|
||||
* Old version of [NotaryChangeWireTransaction] constructor for ABI compatibility.
|
||||
*/
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor(serializedComponents: List<OpaqueBytes>) : this(serializedComponents, DigestService.sha2_256)
|
||||
|
||||
/**
|
||||
* Old version of [NotaryChangeWireTransaction.copy] for ABI compatibility.
|
||||
*/
|
||||
fun copy(serializedComponents: List<OpaqueBytes>): NotaryChangeWireTransaction {
|
||||
return NotaryChangeWireTransaction(serializedComponents, DigestService.sha2_256)
|
||||
}
|
||||
|
||||
// TODO(iee): add missing:
|
||||
// public <init>(net.corda.core.identity.Party, java.util.UUID, java.util.List<net.corda.core.contracts.StateRef>,
|
||||
// java.util.List<net.corda.core.crypto.SecureHash>,
|
||||
// java.util.List<net.corda.core.contracts.TransactionState<net.corda.core.contracts.ContractState>>,
|
||||
// java.util.List<net.corda.core.contracts.Command<?>>, net.corda.core.contracts.TimeWindow,
|
||||
// net.corda.core.contracts.PrivacySalt, java.util.List<net.corda.core.contracts.StateRef>,
|
||||
// net.corda.core.node.ServiceHub)
|
||||
|
||||
override val inputs: List<StateRef> = serializedComponents[INPUTS.ordinal].deserialize()
|
||||
override val references: List<StateRef> = emptyList()
|
||||
override val notary: Party = serializedComponents[NOTARY.ordinal].deserialize()
|
||||
@ -68,9 +91,9 @@ data class NotaryChangeWireTransaction(
|
||||
*/
|
||||
override val id: SecureHash by lazy {
|
||||
serializedComponents.map { component ->
|
||||
component.bytes.sha256()
|
||||
digestService.hash(component.bytes)
|
||||
}.reduce { combinedHash, componentHash ->
|
||||
combinedHash.hashConcat(componentHash)
|
||||
combinedHash.concatenate(componentHash)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,6 +138,7 @@ open class TransactionBuilder(
|
||||
*/
|
||||
@Throws(MissingContractAttachments::class)
|
||||
fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services, null)
|
||||
.apply { checkSupportedHashType() }
|
||||
|
||||
@CordaInternal
|
||||
internal fun toWireTransactionWithContext(
|
||||
@ -176,7 +177,8 @@ open class TransactionBuilder(
|
||||
window,
|
||||
referenceStates,
|
||||
services.networkParametersService.currentHash),
|
||||
privacySalt
|
||||
privacySalt,
|
||||
services.digestService
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderCache
|
||||
import net.corda.core.serialization.serialize
|
||||
@ -48,10 +49,16 @@ import java.util.function.Predicate
|
||||
*/
|
||||
@CordaSerializable
|
||||
@KeepForDJVM
|
||||
class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: PrivacySalt = PrivacySalt()) : TraversableTransaction(componentGroups) {
|
||||
class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: PrivacySalt, digestService: DigestService) : TraversableTransaction(componentGroups, digestService) {
|
||||
@DeleteForDJVM
|
||||
constructor(componentGroups: List<ComponentGroup>) : this(componentGroups, PrivacySalt())
|
||||
|
||||
/**
|
||||
* Old version of [WireTransaction] constructor for ABI compatibility.
|
||||
*/
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor(componentGroups: List<ComponentGroup>, privacySalt: PrivacySalt = PrivacySalt()) : this(componentGroups, privacySalt, DigestService.sha2_256)
|
||||
|
||||
@Deprecated("Required only in some unit-tests and for backwards compatibility purposes.",
|
||||
ReplaceWith("WireTransaction(val componentGroups: List<ComponentGroup>, override val privacySalt: PrivacySalt)"), DeprecationLevel.WARNING)
|
||||
@DeleteForDJVM
|
||||
@ -64,7 +71,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
notary: Party?,
|
||||
timeWindow: TimeWindow?,
|
||||
privacySalt: PrivacySalt = PrivacySalt()
|
||||
) : this(createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList(), null), privacySalt)
|
||||
) : this(createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList(), null), privacySalt, DigestService.sha2_256)
|
||||
|
||||
init {
|
||||
check(componentGroups.all { it.components.isNotEmpty() }) { "Empty component groups are not allowed" }
|
||||
@ -73,6 +80,8 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
check(inputs.isNotEmpty() || outputs.isNotEmpty()) { "A transaction must contain at least one input or output state" }
|
||||
check(commands.isNotEmpty()) { "A transaction must contain at least one command" }
|
||||
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }
|
||||
// TODO(iee): review salt validation - Should we just warn when privacy bytes already >= 32 but <= digestLength?
|
||||
privacySalt.validateFor(digestService.hashAlgorithm)
|
||||
}
|
||||
|
||||
/** The transaction id is represented by the root hash of Merkle tree over the transaction components. */
|
||||
@ -213,7 +222,8 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
serializedResolvedInputs,
|
||||
serializedResolvedReferences,
|
||||
isAttachmentTrusted,
|
||||
attachmentsClassLoaderCache
|
||||
attachmentsClassLoaderCache,
|
||||
digestService
|
||||
)
|
||||
|
||||
checkTransactionSize(ltx, resolvedNetworkParameters.maxTransactionSize, serializedResolvedInputs, serializedResolvedReferences)
|
||||
@ -269,7 +279,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
* If any of the groups is an empty list or a null object, then [SecureHash.allOnesHash] is used as its hash.
|
||||
* Also, [privacySalt] is not a Merkle tree leaf, because it is already "inherently" included via the component nonces.
|
||||
*/
|
||||
val merkleTree: MerkleTree by lazy { MerkleTree.getMerkleTree(groupHashes) }
|
||||
val merkleTree: MerkleTree by lazy { MerkleTree.getMerkleTree(groupHashes, digestService) }
|
||||
|
||||
/**
|
||||
* The leaves (group hashes) of the top level Merkle tree.
|
||||
@ -280,8 +290,9 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
val listOfLeaves = mutableListOf<SecureHash>()
|
||||
// Even if empty and not used, we should at least send oneHashes for each known
|
||||
// or received but unknown (thus, bigger than known ordinal) component groups.
|
||||
val allOnesHash = digestService.allOnesHash
|
||||
for (i in 0..componentGroups.map { it.groupIndex }.max()!!) {
|
||||
val root = groupsMerkleRoots[i] ?: SecureHash.allOnesHash
|
||||
val root = groupsMerkleRoots[i] ?: allOnesHash
|
||||
listOfLeaves.add(root)
|
||||
}
|
||||
listOfLeaves
|
||||
@ -296,7 +307,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
* see the user-guide section "Transaction tear-offs" to learn more about this topic.
|
||||
*/
|
||||
internal val groupsMerkleRoots: Map<Int, SecureHash> by lazy {
|
||||
availableComponentHashes.map { Pair(it.key, MerkleTree.getMerkleTree(it.value).hash) }.toMap()
|
||||
availableComponentHashes.entries.associate { it.key to MerkleTree.getMerkleTree(it.value, digestService).hash }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -309,7 +320,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
* nothing about the rest.
|
||||
*/
|
||||
internal val availableComponentNonces: Map<Int, List<SecureHash>> by lazy {
|
||||
componentGroups.map { Pair(it.groupIndex, it.components.mapIndexed { internalIndex, internalIt -> componentHash(internalIt, privacySalt, it.groupIndex, internalIndex) }) }.toMap()
|
||||
componentGroups.associate { it.groupIndex to it.components.mapIndexed { internalIndex, internalIt -> digestService.componentHash(internalIt, privacySalt, it.groupIndex, internalIndex) } }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -318,7 +329,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
* see the user-guide section "Transaction tear-offs" to learn more about this topic.
|
||||
*/
|
||||
internal val availableComponentHashes: Map<Int, List<SecureHash>> by lazy {
|
||||
componentGroups.map { Pair(it.groupIndex, it.components.mapIndexed { internalIndex, internalIt -> componentHash(availableComponentNonces[it.groupIndex]!![internalIndex], internalIt) }) }.toMap()
|
||||
componentGroups.associate { it.groupIndex to it.components.mapIndexed { internalIndex, internalIt -> digestService.componentHash(availableComponentNonces[it.groupIndex]!![internalIndex], internalIt) } }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -362,7 +373,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when (coreTransaction) {
|
||||
is WireTransaction -> coreTransaction.componentGroups
|
||||
.firstOrNull { it.groupIndex == ComponentGroupEnum.OUTPUTS_GROUP.ordinal }
|
||||
.firstOrNull { it.groupIndex == OUTPUTS_GROUP.ordinal }
|
||||
?.components
|
||||
?.get(stateRef.index) as SerializedBytes<TransactionState<ContractState>>?
|
||||
is ContractUpgradeWireTransaction -> coreTransaction.resolveOutputComponent(services, stateRef, params)
|
||||
|
@ -0,0 +1,67 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.crypto.internal.DigestAlgorithmFactory
|
||||
import org.bouncycastle.crypto.digests.Blake2sDigest
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class Blake2s256DigestServiceTest {
|
||||
class BLAKE2s256DigestService : DigestAlgorithm {
|
||||
override val algorithm = "BLAKE_TEST"
|
||||
|
||||
override val digestLength = 32
|
||||
|
||||
override fun digest(bytes: ByteArray): ByteArray {
|
||||
val blake2s256 = Blake2sDigest(null, digestLength, null, "12345678".toByteArray())
|
||||
blake2s256.reset()
|
||||
blake2s256.update(bytes, 0, bytes.size)
|
||||
val hash = ByteArray(digestLength)
|
||||
blake2s256.doFinal(hash, 0)
|
||||
return hash
|
||||
}
|
||||
}
|
||||
|
||||
private val service = DigestService("BLAKE_TEST")
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
DigestAlgorithmFactory.registerClass(BLAKE2s256DigestService::class.java.name)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun testBlankHash() {
|
||||
assertEquals(
|
||||
"C59F682376D137F3F255E671E207D1F2374EBE504E9314208A52D9F88D69E8C8",
|
||||
service.hash(byteArrayOf()).toHexString()
|
||||
)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun testHashBytes() {
|
||||
val hash = service.hash(byteArrayOf(0x64, -0x13, 0x42, 0x3a))
|
||||
assertEquals("9EEA14092257E759ADAA56539A7A88DA1F68F03ABE3D9552A21D4731F4E6ECA0", hash.toHexString())
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun testHashString() {
|
||||
val hash = service.hash("test")
|
||||
assertEquals("AB76E8F7EEA1968C183D343B756EC812E47D4BC7A3F061F4DDE8948B3E05DAF2", hash.toHexString())
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun testGetAllOnesHash() {
|
||||
assertArrayEquals(service.allOnesHash.bytes, ByteArray(32) { 0xFF.toByte() })
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun testGetZeroHash() {
|
||||
assertArrayEquals(service.zeroHash.bytes, ByteArray(32))
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `Blake2s256 does not retain state between same-thread invocations`() {
|
||||
assertEquals(service.hash("abc"), service.hash("abc"))
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class SHA2256DigestServiceTest {
|
||||
private val service: DigestService = DigestService.sha2_256
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test digest service hash and no prefix`() {
|
||||
Assert.assertEquals(
|
||||
"6D1687C143DF792A011A1E80670A4E4E0C25D0D87A39514409B1ABFC2043581F",
|
||||
service.hash(byteArrayOf(0x64, -0x13, 0x42, 0x3a)).toString()
|
||||
)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test digest service hash blank and no prefix`() {
|
||||
Assert.assertEquals(
|
||||
"E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
|
||||
service.hash(byteArrayOf()).toString()
|
||||
)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test sha2-256 digest service hash length`() {
|
||||
Assert.assertEquals(32, service.digestLength)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test sha2-256 digest service hash name`() {
|
||||
Assert.assertEquals("SHA-256", service.hashAlgorithm)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test sha2-256 digest service zero hash and no prefix`() {
|
||||
Assert.assertEquals("0000000000000000000000000000000000000000000000000000000000000000",
|
||||
service.zeroHash.toString())
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test sha2-256 digest service ones hash and no prefix`() {
|
||||
Assert.assertEquals("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
|
||||
service.allOnesHash.toString())
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class SHA2384DigestServiceTest {
|
||||
private val service: DigestService = DigestService.sha2_384
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test digest service hash and prefix`() {
|
||||
Assert.assertEquals(
|
||||
"SHA-384:5E3DBD33BEC467F625E28D4C5DF90CAACEA722F2DBB2AE9EF9C59EF4FB0FA31A070F5911156713F6AA0FCB09186B78FF",
|
||||
service.hash(byteArrayOf(0x64, -0x13, 0x42, 0x3a)).toString()
|
||||
)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test digest service hash blank and prefix`() {
|
||||
Assert.assertEquals(
|
||||
"SHA-384:38B060A751AC96384CD9327EB1B1E36A21FDB71114BE07434C0CC7BF63F6E1DA274EDEBFE76F65FBD51AD2F14898B95B",
|
||||
service.hash(byteArrayOf()).toString()
|
||||
)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test sha3-256 digest service hash length`() {
|
||||
Assert.assertEquals(48, service.digestLength)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test sha3-256 digest service hash name`() {
|
||||
Assert.assertEquals("SHA-384", service.hashAlgorithm)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test sha3-256 digest service zero hash and prefix`() {
|
||||
Assert.assertEquals("SHA-384:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
service.zeroHash.toString())
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test sha3-256 digest service ones hash and prefix`() {
|
||||
Assert.assertEquals("SHA-384:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
|
||||
service.allOnesHash.toString())
|
||||
}
|
||||
}
|
@ -1,6 +1,12 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.crypto.SecureHash.Companion.SHA2_256
|
||||
import net.corda.core.internal.JavaVersion
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Assume
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import java.lang.IllegalArgumentException
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class SecureHashTest {
|
||||
@ -8,4 +14,65 @@ class SecureHashTest {
|
||||
fun `sha256 does not retain state between same-thread invocations`() {
|
||||
assertEquals(SecureHash.sha256("abc"), SecureHash.sha256("abc"))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `new sha256 does not retain state between same-thread invocations`() {
|
||||
assertEquals(SecureHash.hashAs("SHA-256", "abc".toByteArray()), SecureHash.hashAs("SHA-256", "abc".toByteArray()))
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test new sha256 secure hash`() {
|
||||
val hash = SecureHash.hashAs("SHA-256", byteArrayOf(0x64, -0x13, 0x42, 0x3a))
|
||||
assertEquals(SecureHash.create("SHA-256:6D1687C143DF792A011A1E80670A4E4E0C25D0D87A39514409B1ABFC2043581F"), hash)
|
||||
assertEquals("6D1687C143DF792A011A1E80670A4E4E0C25D0D87A39514409B1ABFC2043581F", hash.toString())
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test new sha3-256 secure hash`() {
|
||||
Assume.assumeTrue(JavaVersion.isVersionAtLeast(JavaVersion.Java_11))
|
||||
val hash = SecureHash.hashAs("SHA3-256", byteArrayOf(0x64, -0x13, 0x42, 0x3a))
|
||||
assertEquals(SecureHash.create("SHA3-256:A243D53F7273F4C92ED901A14F11B372FDF6FF69583149AFD4AFA24BF17A8880"), hash)
|
||||
assertEquals("SHA3-256:A243D53F7273F4C92ED901A14F11B372FDF6FF69583149AFD4AFA24BF17A8880", hash.toString())
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test sha2-256 equivalence`() {
|
||||
val data = byteArrayOf(0x64, -0x13, 0x42, 0x3a)
|
||||
val oldHash = SecureHash.sha256(data)
|
||||
val newHash = SecureHash.hashAs("SHA-256", data)
|
||||
assertEquals(oldHash.hashCode(), newHash.hashCode())
|
||||
assertEquals(oldHash, newHash)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test unsafe sha-1 secure hash is banned`() {
|
||||
val ex = assertThrows<IllegalArgumentException> {
|
||||
SecureHash.hashAs("SHA-1", byteArrayOf(0x64, -0x13, 0x42, 0x3a))
|
||||
}
|
||||
assertThat(ex).hasMessage("SHA-1 is forbidden!")
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test hash concatenation is equivalent`() {
|
||||
val data = byteArrayOf(0x45, 0x33, -0x63, 0x2a, 0x76, -0x64, 0x01, 0x5f)
|
||||
val oldHash = data.sha256()
|
||||
val newHash = data.hashAs(SHA2_256)
|
||||
val expectedHash = oldHash.hashConcat(oldHash)
|
||||
assertEquals(expectedHash, newHash.concatenate(newHash))
|
||||
assertEquals(expectedHash, newHash.concatenate(oldHash))
|
||||
assertEquals(expectedHash, oldHash.concatenate(newHash))
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `test lowercase letters are prohibited in the algorithm name`() {
|
||||
assertThrows<IllegalArgumentException> {
|
||||
SecureHash.hashAs("sha-256", "abc".toByteArray())
|
||||
}
|
||||
assertThrows<IllegalArgumentException> {
|
||||
SecureHash.hashAs("Sha-256", "abc".toByteArray())
|
||||
}
|
||||
assertThrows<IllegalArgumentException> {
|
||||
SecureHash.hashAs("sha3-256", "abc".toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -127,7 +127,7 @@ open class InternalUtilsTest {
|
||||
fun `test SHA-256 hash for InputStream`() {
|
||||
val contents = arrayOfJunk(DEFAULT_BUFFER_SIZE * 2 + DEFAULT_BUFFER_SIZE / 2)
|
||||
assertThat(contents.inputStream().hash())
|
||||
.isEqualTo(SecureHash.parse("A4759E7AA20338328866A2EA17EAF8C7FE4EC6BBE3BB71CEE7DF7C0461B3C22F"))
|
||||
.isEqualTo(SecureHash.create("A4759E7AA20338328866A2EA17EAF8C7FE4EC6BBE3BB71CEE7DF7C0461B3C22F"))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NetworkParameters
|
||||
@ -34,8 +35,9 @@ fun createLedgerTransaction(
|
||||
serializedInputs: List<SerializedStateAndRef>? = null,
|
||||
serializedReferences: List<SerializedStateAndRef>? = null,
|
||||
isAttachmentTrusted: (Attachment) -> Boolean,
|
||||
attachmentsClassLoaderCache: AttachmentsClassLoaderCache
|
||||
): LedgerTransaction = LedgerTransaction.create(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references, componentGroups, serializedInputs, serializedReferences, isAttachmentTrusted, attachmentsClassLoaderCache)
|
||||
attachmentsClassLoaderCache: AttachmentsClassLoaderCache,
|
||||
digestService: DigestService = DigestService.default
|
||||
): LedgerTransaction = LedgerTransaction.create(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references, componentGroups, serializedInputs, serializedReferences, isAttachmentTrusted, attachmentsClassLoaderCache, digestService)
|
||||
|
||||
fun createContractCreationError(txId: SecureHash, contractClass: String, cause: Throwable) = TransactionVerificationException.ContractCreationError(txId, contractClass, cause)
|
||||
fun createContractRejection(txId: SecureHash, contract: Contract, cause: Throwable) = TransactionVerificationException.ContractRejection(txId, contract, cause)
|
||||
|
@ -184,7 +184,6 @@
|
||||
<ID>EmptyCatchBlock:PersistentUniquenessProvider.kt$PersistentUniquenessProvider${ }</ID>
|
||||
<ID>EmptyCatchBlock:RPCClientProxyHandler.kt$RPCClientProxyHandler${}</ID>
|
||||
<ID>EmptyCatchBlock:RPCStabilityTests.kt$RPCStabilityTests${}</ID>
|
||||
<ID>EmptyCatchBlock:ScheduledFlowIntegrationTests.kt$ScheduledFlowIntegrationTests${ }</ID>
|
||||
<ID>EmptyCatchBlock:TransactionCallbackTest.kt$TransactionCallbackTest${ }</ID>
|
||||
<ID>EmptyCatchBlock:WebServer.kt$WebServer${ }</ID>
|
||||
<ID>EmptyClassBlock:CordaRPCClient.kt$CordaRPCClient$Companion</ID>
|
||||
@ -192,7 +191,6 @@
|
||||
<ID>EmptyDefaultConstructor:FlowRetryTest.kt$RetryFlow$()</ID>
|
||||
<ID>EmptyDefaultConstructor:FlowRetryTest.kt$ThrowingFlow$()</ID>
|
||||
<ID>EmptyIfBlock:ContentSignerBuilder.kt$ContentSignerBuilder.SignatureOutputStream$if (alreadySigned) throw IllegalStateException("Cannot write to already signed object")</ID>
|
||||
<ID>EmptyIfBlock:InMemoryIdentityService.kt$InMemoryIdentityService${ }</ID>
|
||||
<ID>EmptyKtFile:KryoHook.kt$.KryoHook.kt</ID>
|
||||
<ID>EmptyKtFile:ValidatingNotaryService.kt$.ValidatingNotaryService.kt</ID>
|
||||
<ID>EnumNaming:LoginView.kt$LoginView.LoginStatus$exception</ID>
|
||||
@ -645,7 +643,6 @@
|
||||
<ID>LongParameterList:IdenticonRenderer.kt$IdenticonRenderer$(g: GraphicsContext, x: Double, y: Double, patchIndex: Int, turn: Int, patchSize: Double, _invert: Boolean, color: PatchColor)</ID>
|
||||
<ID>LongParameterList:Injectors.kt$( metricRegistry: MetricRegistry, parallelism: Int, overallDuration: Duration, injectionRate: Rate, queueSizeMetricName: String = "QueueSize", workDurationMetricName: String = "WorkDuration", work: () -> Unit )</ID>
|
||||
<ID>LongParameterList:InteractiveShell.kt$InteractiveShell$(nameFragment: String, inputData: String, output: RenderPrintWriter, rpcOps: CordaRPCOps, ansiProgressRenderer: ANSIProgressRenderer, inputObjectMapper: ObjectMapper = createYamlInputMapper(rpcOps))</ID>
|
||||
<ID>LongParameterList:InternalTestUtils.kt$(inputs: List<StateRef>, attachments: List<SecureHash>, outputs: List<TransactionState<*>>, commands: List<Command<*>>, notary: Party?, timeWindow: TimeWindow?, privacySalt: PrivacySalt = PrivacySalt())</ID>
|
||||
<ID>LongParameterList:JarSignatureTestUtils.kt$JarSignatureTestUtils$(alias: String = "Test", storePassword: String = "secret!", name: String = CODE_SIGNER.toString(), keyalg: String = "RSA", keyPassword: String = storePassword, storeName: String = "_teststore")</ID>
|
||||
<ID>LongParameterList:MockServices.kt$MockServices.Companion$( cordappLoader: CordappLoader, identityService: IdentityService, networkParameters: NetworkParameters, initialIdentity: TestIdentity, moreKeys: Set<KeyPair>, keyManagementService: KeyManagementService, schemaService: SchemaService, persistence: CordaPersistence )</ID>
|
||||
<ID>LongParameterList:MockServices.kt$MockServices.Companion$( cordappPackages: List<String>, initialIdentity: TestIdentity, networkParameters: NetworkParameters = testNetworkParameters(modifiedTime = Instant.MIN), moreKeys: Set<KeyPair>, moreIdentities: Set<PartyAndCertificate>, cacheFactory: TestingNamedCacheFactory = TestingNamedCacheFactory() )</ID>
|
||||
@ -1095,8 +1092,6 @@
|
||||
<ID>MagicNumber:SearchField.kt$SearchField$5.0</ID>
|
||||
<ID>MagicNumber:SecureArtemisConfiguration.kt$SecureArtemisConfiguration$128</ID>
|
||||
<ID>MagicNumber:SecureArtemisConfiguration.kt$SecureArtemisConfiguration$16</ID>
|
||||
<ID>MagicNumber:SecureHash.kt$SecureHash.Companion$32</ID>
|
||||
<ID>MagicNumber:SecureHash.kt$SecureHash.SHA256$32</ID>
|
||||
<ID>MagicNumber:SerializationEnvironmentHelper.kt$SerializationEnvironmentHelper$128</ID>
|
||||
<ID>MagicNumber:ShutdownManager.kt$ShutdownManager$5</ID>
|
||||
<ID>MagicNumber:ShutdownManager.kt$ShutdownManager$60</ID>
|
||||
@ -1106,7 +1101,6 @@
|
||||
<ID>MagicNumber:StaffedFlowHospital.kt$StaffedFlowHospital$10</ID>
|
||||
<ID>MagicNumber:StandaloneShell.kt$StandaloneShell$7</ID>
|
||||
<ID>MagicNumber:StateRevisionFlow.kt$StateRevisionFlow.Requester$30</ID>
|
||||
<ID>MagicNumber:Structures.kt$PrivacySalt$32</ID>
|
||||
<ID>MagicNumber:TestNodeInfoBuilder.kt$TestNodeInfoBuilder$1234</ID>
|
||||
<ID>MagicNumber:TestUtils.kt$10000</ID>
|
||||
<ID>MagicNumber:TestUtils.kt$30000</ID>
|
||||
@ -1541,7 +1535,6 @@
|
||||
<ID>TooGenericExceptionCaught:ReconnectingObservable.kt$ReconnectingObservable.ReconnectingSubscriber$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:RpcServerObservableSerializerTests.kt$RpcServerObservableSerializerTests$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:SSLHelper.kt$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:ScheduledFlowIntegrationTests.kt$ScheduledFlowIntegrationTests$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:SerializationOutputTests.kt$SerializationOutputTests$t: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:ShutdownManager.kt$ShutdownManager$t: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:SimpleAMQPClient.kt$SimpleAMQPClient$e: Exception</ID>
|
||||
|
@ -58,6 +58,8 @@ class ObligationTests {
|
||||
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
|
||||
val MINI_CORP get() = miniCorp.party
|
||||
val MINI_CORP_PUBKEY get() = miniCorp.publicKey
|
||||
// TODO(iee): Obligations fail to find attachments if we use anything else than sha2_256. All attachment logic is still
|
||||
// using direct calls to .sha256() which still needs to be replaced with DigestService.default
|
||||
}
|
||||
|
||||
@Rule
|
||||
@ -324,7 +326,6 @@ class ObligationTests {
|
||||
private inline fun <reified T : ContractState> getStateAndRef(state: T, contractClassName: ContractClassName): StateAndRef<T> {
|
||||
val txState = TransactionState(state, contractClassName, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint)
|
||||
return StateAndRef(txState, StateRef(SecureHash.randomSHA256(), 0))
|
||||
|
||||
}
|
||||
|
||||
/** Test generating a transaction to mark outputs as having defaulted. */
|
||||
|
@ -128,7 +128,7 @@ abstract class AbstractCashSelection(private val maxRetries : Int = 8, private v
|
||||
var totalPennies = 0L
|
||||
val stateRefs = mutableSetOf<StateRef>()
|
||||
while (rs.next()) {
|
||||
val txHash = SecureHash.parse(rs.getString(1))
|
||||
val txHash = SecureHash.create(rs.getString(1))
|
||||
val index = rs.getInt(2)
|
||||
val pennies = rs.getLong(3)
|
||||
totalPennies = rs.getLong(4)
|
||||
|
@ -6,5 +6,6 @@
|
||||
|
||||
<include file="migration/cash.changelog-init.xml"/>
|
||||
<include file="migration/cash.changelog-v1.xml"/>
|
||||
<include file="migration/cash.changelog-v2.xml"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
@ -2,8 +2,7 @@
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"
|
||||
logicalFilePath="migration/node-services.changelog-init.xml">
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
|
||||
|
||||
<changeSet id="1525793504" author="R3.Corda">
|
||||
<!-- drop indexes before adding not null constraints to the underlying table, recreating index immediately after -->
|
||||
@ -24,4 +23,4 @@
|
||||
<column name="pennies"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
</databaseChangeLog>
|
||||
|
@ -0,0 +1,11 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
|
||||
<changeSet id="extend_cash_states_transaction_id" author="R3.Corda">
|
||||
<modifyDataType tableName="contract_cash_states"
|
||||
columnName="transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
@ -6,5 +6,6 @@
|
||||
|
||||
<include file="migration/commercial-paper.changelog-init.xml"/>
|
||||
<include file="migration/commercial-paper.changelog-v1.xml"/>
|
||||
<include file="migration/commercial-paper.changelog-v2.xml"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
@ -0,0 +1,11 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
|
||||
<changeSet id="extend_commercial_paper_transaction_id" author="R3.Corda">
|
||||
<modifyDataType tableName="cp_states"
|
||||
columnName="transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
@ -130,7 +130,7 @@ const val DEV_CA_TRUST_STORE_PRIVATE_KEY_PASS: String = "trustpasskeypass"
|
||||
// https://github.com/corda/corda-gradle-plugins/blob/master/cordapp/src/main/resources/certificates/cordadevcodesign.jks
|
||||
const val DEV_CORDAPP_CODE_SIGNING_STR = "AA59D829F2CA8FDDF5ABEA40D815F937E3E54E572B65B93B5C216AE6594E7D6B"
|
||||
|
||||
val DEV_PUB_KEY_HASHES: List<SecureHash.SHA256> get() = listOf(DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate).map { it.publicKey.hash.sha256() } + SecureHash.parse(DEV_CORDAPP_CODE_SIGNING_STR).sha256()
|
||||
val DEV_PUB_KEY_HASHES: List<SecureHash> get() = listOf(DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate).map { it.publicKey.hash.sha256() } + SecureHash.create(DEV_CORDAPP_CODE_SIGNING_STR).sha256()
|
||||
|
||||
// We need a class so that we can get hold of the class loader
|
||||
internal object DevCaHelper {
|
||||
|
@ -10,6 +10,7 @@ import com.esotericsoftware.kryo.util.MapReferenceResolver
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.internal.LazyMappedList
|
||||
@ -214,12 +215,15 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: WireTransaction) {
|
||||
kryo.writeClassAndObject(output, obj.componentGroups)
|
||||
kryo.writeClassAndObject(output, obj.privacySalt)
|
||||
kryo.writeClassAndObject(output, obj.digestService)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<WireTransaction>): WireTransaction {
|
||||
val componentGroups: List<ComponentGroup> = uncheckedCast(kryo.readClassAndObject(input))
|
||||
val privacySalt = kryo.readClassAndObject(input) as PrivacySalt
|
||||
return WireTransaction(componentGroups, privacySalt)
|
||||
// TODO(iee): handle backward compatibility when deserializing old version of WTX
|
||||
val digestService = kryo.readClassAndObject(input) as? DigestService
|
||||
return WireTransaction(componentGroups, privacySalt, digestService ?: DigestService.sha2_256)
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,11 +231,14 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
|
||||
object NotaryChangeWireTransactionSerializer : Serializer<NotaryChangeWireTransaction>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: NotaryChangeWireTransaction) {
|
||||
kryo.writeClassAndObject(output, obj.serializedComponents)
|
||||
kryo.writeClassAndObject(output, obj.digestService)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<NotaryChangeWireTransaction>): NotaryChangeWireTransaction {
|
||||
val components: List<OpaqueBytes> = uncheckedCast(kryo.readClassAndObject(input))
|
||||
return NotaryChangeWireTransaction(components)
|
||||
// TODO(iee): handle backward compatibility when deserializing old version of NCWTX
|
||||
val digestService = kryo.readClassAndObject(input) as? DigestService
|
||||
return NotaryChangeWireTransaction(components, digestService ?: DigestService.sha2_256)
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,13 +247,15 @@ object ContractUpgradeWireTransactionSerializer : Serializer<ContractUpgradeWire
|
||||
override fun write(kryo: Kryo, output: Output, obj: ContractUpgradeWireTransaction) {
|
||||
kryo.writeClassAndObject(output, obj.serializedComponents)
|
||||
kryo.writeClassAndObject(output, obj.privacySalt)
|
||||
kryo.writeClassAndObject(output, obj.digestService)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<ContractUpgradeWireTransaction>): ContractUpgradeWireTransaction {
|
||||
val components: List<OpaqueBytes> = uncheckedCast(kryo.readClassAndObject(input))
|
||||
val privacySalt = kryo.readClassAndObject(input) as PrivacySalt
|
||||
|
||||
return ContractUpgradeWireTransaction(components, privacySalt)
|
||||
// TODO(iee): handle backward compatibility when deserializing old version of WTX
|
||||
val digestService = kryo.readClassAndObject(input) as? DigestService
|
||||
return ContractUpgradeWireTransaction(components, privacySalt, digestService ?: DigestService.sha2_256)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NetworkParameters
|
||||
@ -26,6 +27,7 @@ private const val TX_TIME_WINDOW = 6
|
||||
private const val TX_PRIVACY_SALT = 7
|
||||
private const val TX_NETWORK_PARAMETERS = 8
|
||||
private const val TX_REFERENCES = 9
|
||||
private const val TX_DIGEST_SERVICE = 10
|
||||
|
||||
class LtxFactory : Function<Array<out Any?>, LedgerTransaction> {
|
||||
|
||||
@ -41,7 +43,8 @@ class LtxFactory : Function<Array<out Any?>, LedgerTransaction> {
|
||||
timeWindow = txArgs[TX_TIME_WINDOW] as? TimeWindow,
|
||||
privacySalt = txArgs[TX_PRIVACY_SALT] as PrivacySalt,
|
||||
networkParameters = txArgs[TX_NETWORK_PARAMETERS] as NetworkParameters,
|
||||
references = (txArgs[TX_REFERENCES] as Array<Array<out Any?>>).map { it.toStateAndRef() }
|
||||
references = (txArgs[TX_REFERENCES] as Array<Array<out Any?>>).map { it.toStateAndRef() },
|
||||
digestService = if (txArgs.size > TX_DIGEST_SERVICE) (txArgs[TX_DIGEST_SERVICE] as DigestService) else DigestService.sha2_256
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -850,10 +850,10 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseSecureHashConfiguration(unparsedConfig: List<String>, errorMessage: (String) -> String): List<SecureHash.SHA256> {
|
||||
private fun parseSecureHashConfiguration(unparsedConfig: List<String>, errorMessage: (String) -> String): List<SecureHash> {
|
||||
return unparsedConfig.map {
|
||||
try {
|
||||
SecureHash.parse(it)
|
||||
SecureHash.create(it)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
log.error("${errorMessage(it)} due to - ${e.message}", e)
|
||||
throw e
|
||||
|
@ -45,7 +45,7 @@ class DBNetworkParametersStorage(
|
||||
toPersistentEntityKey = { it.toString() },
|
||||
fromPersistentEntity = {
|
||||
Pair(
|
||||
SecureHash.parse(it.hash),
|
||||
SecureHash.create(it.hash),
|
||||
it.signedNetworkParameters
|
||||
)
|
||||
},
|
||||
|
@ -175,6 +175,11 @@ open class NodeStartup : NodeStartupLogging {
|
||||
// This needs to go after initLogging(netty clashes with our logging)
|
||||
Crypto.registerProviders()
|
||||
|
||||
// Temp Step. Enable experimental hash agility feature allowing to override default transaction hash algorithm.
|
||||
val txHashAlgoName = System.getProperty("corda.experimental.txHashAlgoName")
|
||||
val txHashAlgoClass = System.getProperty("corda.experimental.txHashAlgoClass")
|
||||
HashAgility.init(txHashAlgoName, txHashAlgoClass)
|
||||
|
||||
// Step 4. Print banner and basic node info.
|
||||
val versionInfo = getVersionInfo()
|
||||
drawBanner(versionInfo)
|
||||
|
@ -100,7 +100,7 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
|
||||
)
|
||||
}
|
||||
} catch (faee: java.nio.file.FileAlreadyExistsException) {
|
||||
AttachmentId.parse(faee.message!!)
|
||||
AttachmentId.create(faee.message!!)
|
||||
}
|
||||
} to cordapp.jarPath
|
||||
}.toMap()
|
||||
@ -151,7 +151,7 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
|
||||
private fun parseIds(ids: String): Set<AttachmentId> {
|
||||
return ids.split(",").map(String::trim)
|
||||
.filterNot(String::isEmpty)
|
||||
.mapTo(LinkedHashSet(), SecureHash.Companion::parse)
|
||||
.mapTo(LinkedHashSet(), SecureHash.Companion::create)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,7 +49,7 @@ import kotlin.streams.toList
|
||||
class JarScanningCordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>,
|
||||
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN,
|
||||
extraCordapps: List<CordappImpl>,
|
||||
private val signerKeyFingerprintBlacklist: List<SecureHash.SHA256> = emptyList()) : CordappLoaderTemplate() {
|
||||
private val signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()) : CordappLoaderTemplate() {
|
||||
init {
|
||||
if (cordappJarPaths.isEmpty()) {
|
||||
logger.info("No CorDapp paths provided")
|
||||
@ -75,7 +75,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
fun fromDirectories(cordappDirs: Collection<Path>,
|
||||
versionInfo: VersionInfo = VersionInfo.UNKNOWN,
|
||||
extraCordapps: List<CordappImpl> = emptyList(),
|
||||
signerKeyFingerprintBlacklist: List<SecureHash.SHA256> = emptyList()): JarScanningCordappLoader {
|
||||
signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader {
|
||||
logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}")
|
||||
val paths = cordappDirs.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() }
|
||||
return JarScanningCordappLoader(paths, versionInfo, extraCordapps, signerKeyFingerprintBlacklist)
|
||||
@ -87,7 +87,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
* @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection.
|
||||
*/
|
||||
fun fromJarUrls(scanJars: List<URL>, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List<CordappImpl> = emptyList(),
|
||||
cordappsSignerKeyFingerprintBlacklist: List<SecureHash.SHA256> = emptyList()): JarScanningCordappLoader {
|
||||
cordappsSignerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader {
|
||||
val paths = scanJars.map { it.restricted() }
|
||||
return JarScanningCordappLoader(paths, versionInfo, extraCordapps, cordappsSignerKeyFingerprintBlacklist)
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
package net.corda.node.internal.djvm
|
||||
|
||||
import net.corda.core.contracts.ComponentGroupEnum
|
||||
import net.corda.core.crypto.componentHash
|
||||
import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.transactions.ComponentGroup
|
||||
import net.corda.core.transactions.FilteredComponentGroup
|
||||
import net.corda.djvm.rewiring.SandboxClassLoader
|
||||
@ -29,10 +29,10 @@ class ComponentFactory(
|
||||
))
|
||||
}
|
||||
|
||||
fun calculateLeafIndicesFor(groupType: ComponentGroupEnum): IntArray? {
|
||||
fun calculateLeafIndicesFor(groupType: ComponentGroupEnum, digestService: DigestService): IntArray? {
|
||||
val componentGroup = componentGroups.firstOrNull(groupType::isSameType) as? FilteredComponentGroup ?: return null
|
||||
val componentHashes = componentGroup.components.mapIndexed { index, component ->
|
||||
componentHash(componentGroup.nonces[index], component)
|
||||
digestService.componentHash(componentGroup.nonces[index], component)
|
||||
}
|
||||
return componentHashes.map { componentGroup.partialMerkleTree.leafIndex(it) }.toIntArray()
|
||||
}
|
||||
|
@ -91,6 +91,7 @@ class DeterministicVerifier(
|
||||
val timeWindowData = ltx.timeWindow?.serialize()
|
||||
val privacySaltData = ltx.privacySalt.serialize()
|
||||
val networkingParametersData = ltx.networkParameters?.serialize()
|
||||
val digestServiceData = ltx.digestService.serialize()
|
||||
|
||||
val createSandboxTx = taskFactory.apply(LtxFactory::class.java)
|
||||
createSandboxTx.apply(arrayOf(
|
||||
@ -99,7 +100,7 @@ class DeterministicVerifier(
|
||||
CommandFactory(taskFactory).toSandbox(
|
||||
componentFactory.toSandbox(SIGNERS_GROUP, List::class.java),
|
||||
componentFactory.toSandbox(COMMANDS_GROUP, CommandData::class.java),
|
||||
componentFactory.calculateLeafIndicesFor(COMMANDS_GROUP)
|
||||
componentFactory.calculateLeafIndicesFor(COMMANDS_GROUP, digestService = ltx.digestService)
|
||||
),
|
||||
attachmentFactory.toSandbox(ltx.attachments),
|
||||
serializer.deserialize(idData),
|
||||
@ -107,7 +108,8 @@ class DeterministicVerifier(
|
||||
serializer.deserialize(timeWindowData),
|
||||
serializer.deserialize(privacySaltData),
|
||||
serializer.deserialize(networkingParametersData),
|
||||
serializer.deserialize(serializedReferences)
|
||||
serializer.deserialize(serializedReferences),
|
||||
serializer.deserialize(digestServiceData)
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,11 @@ object NodeInfoSchemaV1 : MappedSchema(
|
||||
@Column(name = "node_info_id", nullable = false)
|
||||
var id: Int,
|
||||
|
||||
// TODO(iee): this field receives a hardcoded SHA2_256 string that comes from SerializationAPI's
|
||||
// SerializedBytes' hash method that calls directly to .sha256(). It appears to be used
|
||||
// only for serialized NodeInfo instances. Requires review and discussion to determine if
|
||||
// it should follow the node/system hash algorithm or remain sha256 (in case it does change,
|
||||
// it will be necessary to change length from 64 to 144.
|
||||
@Suppress("MagicNumber") // database column width
|
||||
@Column(name = "node_info_hash", length = 64, nullable = false)
|
||||
val hash: String,
|
||||
|
@ -61,7 +61,7 @@ class VaultStateMigration : CordaMigration() {
|
||||
private fun getStateAndRef(persistentState: VaultSchemaV1.VaultStates): StateAndRef<ContractState> {
|
||||
val persistentStateRef = persistentState.stateRef ?:
|
||||
throw VaultStateMigrationException("Persistent state ref missing from state")
|
||||
val txHash = SecureHash.parse(persistentStateRef.txId)
|
||||
val txHash = SecureHash.create(persistentStateRef.txId)
|
||||
val stateRef = StateRef(txHash, persistentStateRef.index)
|
||||
val state = try {
|
||||
servicesForResolution.loadState(stateRef)
|
||||
|
@ -156,6 +156,7 @@ interface ServiceHubInternal : ServiceHubCoreInternal {
|
||||
val cacheFactory: NamedCacheFactory
|
||||
|
||||
override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>) {
|
||||
txs.forEach { requireSupportedHashType(it) }
|
||||
recordTransactions(
|
||||
statesToRecord,
|
||||
txs as? Collection ?: txs.toList(), // We can't change txs to a Collection as it's now part of the public API
|
||||
@ -247,7 +248,7 @@ interface WritableTransactionStorage : TransactionStorage {
|
||||
/**
|
||||
* Add a new *verified* transaction to the store, or convert the existing unverified transaction into a verified one.
|
||||
* @param transaction The transaction to be recorded.
|
||||
* @return true if the transaction was recorded as a *new verified* transcation, false if the transaction already exists.
|
||||
* @return true if the transaction was recorded as a *new verified* transaction, false if the transaction already exists.
|
||||
*/
|
||||
// TODO: Throw an exception if trying to add a transaction with fewer signatures than an existing entry.
|
||||
fun addTransaction(transaction: SignedTransaction): Boolean
|
||||
|
@ -27,7 +27,7 @@ class PersistentScheduledFlowRepository(val database: CordaPersistence) : Schedu
|
||||
private fun fromPersistentEntity(scheduledStateRecord: NodeSchedulerService.PersistentScheduledState): Pair<StateRef, ScheduledStateRef> {
|
||||
val txId = scheduledStateRecord.output.txId
|
||||
val index = scheduledStateRecord.output.index
|
||||
return Pair(StateRef(SecureHash.parse(txId), index), ScheduledStateRef(StateRef(SecureHash.parse(txId), index), scheduledStateRecord.scheduledAt))
|
||||
return Pair(StateRef(SecureHash.create(txId), index), ScheduledStateRef(StateRef(SecureHash.create(txId), index), scheduledStateRecord.scheduledAt))
|
||||
}
|
||||
|
||||
override fun delete(key: StateRef): Boolean {
|
||||
|
@ -75,7 +75,7 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory,
|
||||
select(get<String>(NodeInfoSchemaV1.PersistentNodeInfo::hash.name))
|
||||
}
|
||||
}
|
||||
session.createQuery(query).resultList.map { SecureHash.parse(it) }
|
||||
session.createQuery(query).resultList.map { SecureHash.create(it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ class DBTransactionMappingStorage(private val database: CordaPersistence) : Stat
|
||||
val from = cq.from(DBTransactionStorage.DBTransaction::class.java)
|
||||
cq.multiselect(from.get<String>(DBTransactionStorage.DBTransaction::stateMachineRunId.name), from.get<String>(DBTransactionStorage.DBTransaction::txId.name))
|
||||
cq.where(cb.isNotNull(from.get<String>(DBTransactionStorage.DBTransaction::stateMachineRunId.name)))
|
||||
val flowIds = session.createQuery(cq).resultList.map { StateMachineTransactionMapping(StateMachineRunId(UUID.fromString(it[0] as String)), SecureHash.parse(it[1] as String)) }
|
||||
val flowIds = session.createQuery(cq).resultList.map { StateMachineTransactionMapping(StateMachineRunId(UUID.fromString(it[0] as String)), SecureHash.create(it[1] as String)) }
|
||||
DataFeed(flowIds, updates.bufferUntilSubscribed().wrapWithDatabaseTransaction())
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory:
|
||||
@Table(name = "${NODE_DATABASE_PREFIX}transactions")
|
||||
class DBTransaction(
|
||||
@Id
|
||||
@Column(name = "tx_id", length = 64, nullable = false)
|
||||
@Column(name = "tx_id", length = 144, nullable = false)
|
||||
val txId: String,
|
||||
|
||||
@Column(name = "state_machine_run_id", length = 36, nullable = true)
|
||||
@ -120,7 +120,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory:
|
||||
name = "DBTransactionStorage_transactions",
|
||||
toPersistentEntityKey = SecureHash::toString,
|
||||
fromPersistentEntity = {
|
||||
SecureHash.parse(it.txId) to TxCacheValue(
|
||||
SecureHash.create(it.txId) to TxCacheValue(
|
||||
it.transaction.deserialize(context = contextToUse()),
|
||||
it.status)
|
||||
},
|
||||
|
@ -102,7 +102,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
throw SecurityException("Signed jar has been tampered with. Files ${allManifestEntries} have been removed.")
|
||||
}
|
||||
val extraSignableFiles = extraFilesNotFoundInEntries.filterNot { JarSignatureCollector.isNotSignable(it) }
|
||||
if (extraSignableFiles.size > 0) {
|
||||
if (extraSignableFiles.isNotEmpty()) {
|
||||
throw SecurityException("Signed jar has been tampered with. Files ${extraSignableFiles} have been added to the JAR.")
|
||||
}
|
||||
}
|
||||
@ -173,7 +173,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
* this will provide an additional safety check against user error.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
class HashCheckingStream(val expected: SecureHash.SHA256,
|
||||
class HashCheckingStream(val expected: SecureHash,
|
||||
val expectedSize: Int,
|
||||
input: InputStream,
|
||||
private val counter: CountingInputStream = CountingInputStream(input),
|
||||
@ -288,7 +288,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
|
||||
private fun createAttachmentFromDatabase(attachment: DBAttachment): Attachment {
|
||||
val attachmentImpl = AttachmentImpl(
|
||||
id = SecureHash.parse(attachment.attId),
|
||||
id = SecureHash.create(attachment.attId),
|
||||
dataLoader = { attachment.content },
|
||||
checkOnLoad = checkAttachmentsOnLoad,
|
||||
uploader = attachment.uploader,
|
||||
@ -358,7 +358,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
return try {
|
||||
import(jar, uploader, filename)
|
||||
} catch (faee: java.nio.file.FileAlreadyExistsException) {
|
||||
AttachmentId.parse(faee.message!!)
|
||||
AttachmentId.create(faee.message!!)
|
||||
}
|
||||
}
|
||||
|
||||
@ -454,7 +454,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
return try {
|
||||
import(jar, UNKNOWN_UPLOADER, null)
|
||||
} catch (faee: java.nio.file.FileAlreadyExistsException) {
|
||||
AttachmentId.parse(faee.message!!)
|
||||
AttachmentId.create(faee.message!!)
|
||||
}
|
||||
}
|
||||
|
||||
@ -464,7 +464,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
createAttachmentsIdsQuery(
|
||||
criteria,
|
||||
sorting
|
||||
).resultList.map { AttachmentId.parse(it) }
|
||||
).resultList.map { AttachmentId.create(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -477,7 +477,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
|
||||
val criteriaQuery = criteriaBuilder.createQuery(String::class.java)
|
||||
val root = criteriaQuery.from(DBAttachment::class.java)
|
||||
criteriaQuery.select(root.get("${DBAttachment::attId.name}"))
|
||||
criteriaQuery.select(root.get(DBAttachment::attId.name))
|
||||
|
||||
val criteriaParser = HibernateAttachmentQueryCriteriaParser<DBAttachment,String>(
|
||||
criteriaBuilder,
|
||||
@ -562,12 +562,12 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
private fun makeAttachmentIds(it: Map.Entry<Int, List<DBAttachment>>, contractClassName: String): Pair<Version, AttachmentIds> {
|
||||
val signed = it.value.filter { it.signers?.isNotEmpty() ?: false }.map { AttachmentId.parse(it.attId) }
|
||||
val signed = it.value.filter { it.signers?.isNotEmpty() ?: false }.map { AttachmentId.create(it.attId) }
|
||||
if (!devMode)
|
||||
check(signed.size <= 1) //sanity check
|
||||
else
|
||||
log.warn("(Dev Mode) Multiple signed attachments ${signed.map { it.toString() }} for contract $contractClassName version '${it.key}'.")
|
||||
val unsigned = it.value.filter { it.signers?.isEmpty() ?: true }.map { AttachmentId.parse(it.attId) }
|
||||
val unsigned = it.value.filter { it.signers?.isEmpty() ?: true }.map { AttachmentId.create(it.attId) }
|
||||
if (unsigned.size > 1)
|
||||
log.warn("Selecting attachment ${unsigned.first()} from duplicated, unsigned attachments ${unsigned.map { it.toString() }} for contract $contractClassName version '${it.key}'.")
|
||||
return it.key to AttachmentIds(signed.firstOrNull(), unsigned.firstOrNull())
|
||||
|
@ -5,7 +5,6 @@ import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.NotarisationRequestSignature
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.StateConsumptionDetails
|
||||
@ -88,7 +87,7 @@ class PersistentUniquenessProvider(val clock: Clock, val database: CordaPersiste
|
||||
@javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}notary_committed_txs")
|
||||
class CommittedTransaction(
|
||||
@Id
|
||||
@Column(name = "transaction_id", nullable = false, length = 64)
|
||||
@Column(name = "transaction_id", nullable = false, length = 144)
|
||||
val transactionId: String
|
||||
)
|
||||
|
||||
@ -161,8 +160,8 @@ class PersistentUniquenessProvider(val clock: Clock, val database: CordaPersiste
|
||||
val txId = it.id.txId
|
||||
val index = it.id.index
|
||||
Pair(
|
||||
StateRef(txhash = SecureHash.parse(txId), index = index),
|
||||
SecureHash.parse(it.consumingTxHash)
|
||||
StateRef(txhash = SecureHash.create(txId), index = index),
|
||||
SecureHash.create(it.consumingTxHash)
|
||||
)
|
||||
|
||||
},
|
||||
@ -218,7 +217,7 @@ class PersistentUniquenessProvider(val clock: Clock, val database: CordaPersiste
|
||||
fun checkConflicts(toCheck: List<StateRef>, type: StateConsumptionDetails.ConsumedStateType) {
|
||||
return toCheck.forEach { stateRef ->
|
||||
val consumingTx = commitLog[stateRef]
|
||||
if (consumingTx != null) conflictingStates[stateRef] = StateConsumptionDetails(consumingTx.sha256(), type)
|
||||
if (consumingTx != null) conflictingStates[stateRef] = StateConsumptionDetails(consumingTx.reHash(), type)
|
||||
}
|
||||
}
|
||||
|
||||
@ -266,7 +265,8 @@ class PersistentUniquenessProvider(val clock: Clock, val database: CordaPersiste
|
||||
}
|
||||
|
||||
private fun handleConflicts(txId: SecureHash, conflictingStates: LinkedHashMap<StateRef, StateConsumptionDetails>) {
|
||||
if (isConsumedByTheSameTx(txId.sha256(), conflictingStates)) {
|
||||
// TODO(iee): is the use of reHash correct here? Check backward/forward compatibility
|
||||
if (isConsumedByTheSameTx(txId.reHash(), conflictingStates)) {
|
||||
log.info("Transaction $txId already notarised. TxId: $txId")
|
||||
return
|
||||
} else {
|
||||
|
@ -473,7 +473,7 @@ class NodeVaultService(
|
||||
val session = currentDBSession()
|
||||
val criteriaBuilder = session.criteriaBuilder
|
||||
fun execute(configure: Root<*>.(CriteriaUpdate<*>, Array<Predicate>) -> Any?) = criteriaBuilder.executeUpdate(session, null) { update, _ ->
|
||||
val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) }
|
||||
val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.toString(), it.index) }
|
||||
val compositeKey = get<PersistentStateRef>(VaultSchemaV1.VaultStates::stateRef.name)
|
||||
val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs))
|
||||
configure(update, arrayOf(stateRefsPredicate))
|
||||
@ -715,7 +715,7 @@ class NodeVaultService(
|
||||
if (!paging.isDefault && index == paging.pageSize) // skip last result if paged
|
||||
return@forEachIndexed
|
||||
val vaultState = result[0] as VaultSchemaV1.VaultStates
|
||||
val stateRef = StateRef(SecureHash.parse(vaultState.stateRef!!.txId), vaultState.stateRef!!.index)
|
||||
val stateRef = StateRef(SecureHash.create(vaultState.stateRef!!.txId), vaultState.stateRef!!.index)
|
||||
stateRefs.add(stateRef)
|
||||
statesMeta.add(Vault.StateMetadata(stateRef,
|
||||
vaultState.contractStateClassName,
|
||||
@ -869,7 +869,7 @@ private fun CriteriaBuilder.executeUpdate(
|
||||
// Increase SQL server performance by, processing updates in chunks allowing the database's optimizer to make use of the index.
|
||||
var updatedRows = 0
|
||||
it.asSequence()
|
||||
.map { stateRef -> PersistentStateRef(stateRef.txhash.bytes.toHexString(), stateRef.index) }
|
||||
.map { stateRef -> PersistentStateRef(stateRef.txhash.toString(), stateRef.index) }
|
||||
.chunked(NodeVaultService.DEFAULT_SOFT_LOCKING_SQL_IN_CLAUSE_SIZE)
|
||||
.forEach { persistentStateRefs ->
|
||||
updatedRows += doUpdate(persistentStateRefs)
|
||||
|
@ -151,7 +151,7 @@ object VaultSchemaV1 : MappedSchema(
|
||||
@Column(name = "seq_no", nullable = false)
|
||||
var seqNo: Int,
|
||||
|
||||
@Column(name = "transaction_id", length = 64, nullable = true)
|
||||
@Column(name = "transaction_id", length = 144, nullable = true)
|
||||
var txId: String?,
|
||||
|
||||
@Column(name = "note", nullable = true)
|
||||
|
@ -7,8 +7,8 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SignableData
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.internal.digestService
|
||||
import net.corda.core.node.ServiceHub
|
||||
import java.security.PublicKey
|
||||
|
||||
@ -20,7 +20,17 @@ fun signBatch(
|
||||
notaryIdentityKey: PublicKey,
|
||||
services: ServiceHub
|
||||
): BatchSignature {
|
||||
val merkleTree = MerkleTree.getMerkleTree(txIds.map { it.sha256() })
|
||||
val algorithms = txIds.mapTo(HashSet(), SecureHash::algorithm)
|
||||
require(algorithms.size > 0) {
|
||||
"Cannot sign an empty batch"
|
||||
}
|
||||
// TODO(iee): too strict? will be valid in the future?
|
||||
require(algorithms.size == 1) {
|
||||
"Cannot sign a batch with multiple hash algorithms: $algorithms"
|
||||
}
|
||||
// TODO(iee): assuming this is running on a notary node, and therefore using only the default
|
||||
// hash algorithm. Review and discuss.
|
||||
val merkleTree = MerkleTree.getMerkleTree(txIds.map { it.reHash() }, services.digestService)
|
||||
val merkleTreeRoot = merkleTree.hash
|
||||
val signableData = SignableData(
|
||||
merkleTreeRoot,
|
||||
@ -44,11 +54,14 @@ data class BatchSignature(
|
||||
val fullMerkleTree: MerkleTree) {
|
||||
/** Extracts a signature with a partial Merkle tree for the specified leaf in the batch signature. */
|
||||
fun forParticipant(txId: SecureHash): TransactionSignature {
|
||||
require(fullMerkleTree.hash.algorithm == txId.algorithm) {
|
||||
"The leaf hash algorithm ${txId.algorithm} does not match the Merkle tree hash algorithm ${fullMerkleTree.hash.algorithm}"
|
||||
}
|
||||
return TransactionSignature(
|
||||
rootSignature.bytes,
|
||||
rootSignature.by,
|
||||
rootSignature.signatureMetadata,
|
||||
PartialMerkleTree.build(fullMerkleTree, listOf(txId.sha256()))
|
||||
PartialMerkleTree.build(fullMerkleTree, listOf(txId.reHash()))
|
||||
)
|
||||
}
|
||||
}
|
@ -230,7 +230,7 @@ object BFTSmart {
|
||||
type: StateConsumptionDetails.ConsumedStateType
|
||||
) {
|
||||
states.forEach { stateRef ->
|
||||
commitLog[stateRef]?.let { conflictingStates[stateRef] = StateConsumptionDetails(it.sha256(), type) }
|
||||
commitLog[stateRef]?.let { conflictingStates[stateRef] = StateConsumptionDetails(it.reHash(), type) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -277,7 +277,7 @@ object BFTSmart {
|
||||
}
|
||||
|
||||
private fun handleConflicts(txId: SecureHash, conflictingStates: LinkedHashMap<StateRef, StateConsumptionDetails>) {
|
||||
if (isConsumedByTheSameTx(txId.sha256(), conflictingStates)) {
|
||||
if (isConsumedByTheSameTx(txId.reHash(), conflictingStates)) {
|
||||
log.debug { "Transaction $txId already notarised" }
|
||||
return
|
||||
} else {
|
||||
|
@ -132,7 +132,7 @@ class BFTSmartNotaryService(
|
||||
@Table(name = "${NODE_DATABASE_PREFIX}bft_committed_txs")
|
||||
class CommittedTransaction(
|
||||
@Id
|
||||
@Column(name = "transaction_id", nullable = false, length = 64)
|
||||
@Column(name = "transaction_id", nullable = false, length = 144)
|
||||
val transactionId: String
|
||||
)
|
||||
|
||||
@ -150,8 +150,8 @@ class BFTSmartNotaryService(
|
||||
val txId = it.id.txId
|
||||
val index = it.id.index
|
||||
Pair(
|
||||
StateRef(txhash = SecureHash.parse(txId), index = index),
|
||||
SecureHash.parse(it.consumingTxHash)
|
||||
StateRef(txhash = SecureHash.create(txId), index = index),
|
||||
SecureHash.create(it.consumingTxHash)
|
||||
)
|
||||
},
|
||||
toPersistentEntity = { (txHash, index): StateRef, id: SecureHash ->
|
||||
|
@ -14,7 +14,6 @@ import io.atomix.copycat.server.storage.snapshot.SnapshotWriter
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.StateConsumptionDetails
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
@ -79,7 +78,7 @@ class RaftTransactionCommitLog<E, EK>(
|
||||
val conflictingStates = LinkedHashMap<StateRef, StateConsumptionDetails>()
|
||||
|
||||
fun checkConflict(states: List<StateRef>, type: StateConsumptionDetails.ConsumedStateType) = states.forEach { stateRef ->
|
||||
map[stateRef]?.let { conflictingStates[stateRef] = StateConsumptionDetails(it.second.sha256(), type) }
|
||||
map[stateRef]?.let { conflictingStates[stateRef] = StateConsumptionDetails(it.second.reHash(), type) }
|
||||
}
|
||||
|
||||
raftCommit.use {
|
||||
@ -116,7 +115,7 @@ class RaftTransactionCommitLog<E, EK>(
|
||||
}
|
||||
|
||||
private fun handleConflicts(txId: SecureHash, conflictingStates: java.util.LinkedHashMap<StateRef, StateConsumptionDetails>): NotaryError? {
|
||||
return if (isConsumedByTheSameTx(txId.sha256(), conflictingStates)) {
|
||||
return if (isConsumedByTheSameTx(txId.reHash(), conflictingStates)) {
|
||||
log.debug { "Transaction $txId already notarised" }
|
||||
null
|
||||
} else {
|
||||
|
@ -92,7 +92,13 @@ class RaftUniquenessProvider(
|
||||
)
|
||||
|
||||
fun StateRef.encoded() = "$txhash:$index"
|
||||
fun String.parseStateRef() = split(":").let { StateRef(SecureHash.parse(it[0]), it[1].toInt()) }
|
||||
fun String.parseStateRef(): StateRef {
|
||||
val idx = lastIndexOf(':')
|
||||
require(idx != -1) {
|
||||
"Encoding error for StateRef '$this'"
|
||||
}
|
||||
return StateRef(SecureHash.create(substring(0, idx)), substring(idx + 1).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
@Entity
|
||||
@ -115,7 +121,7 @@ class RaftUniquenessProvider(
|
||||
@Table(name = "${NODE_DATABASE_PREFIX}raft_committed_txs")
|
||||
class CommittedTransaction(
|
||||
@Id
|
||||
@Column(name = "transaction_id", nullable = false, length = 64)
|
||||
@Column(name = "transaction_id", nullable = false, length = 144)
|
||||
val transactionId: String
|
||||
)
|
||||
|
||||
|
@ -5,7 +5,6 @@ import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.NotarisationRequestSignature
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.StateConsumptionDetails
|
||||
@ -68,7 +67,7 @@ class JPAUniquenessProvider(
|
||||
@Column(nullable = true, length = 76)
|
||||
var id: String? = null,
|
||||
|
||||
@Column(name = "consuming_transaction_id", nullable = true, length = 64)
|
||||
@Column(name = "consuming_transaction_id", nullable = true, length = 144)
|
||||
val consumingTxHash: String?,
|
||||
|
||||
@Column(name = "requesting_party_name", nullable = true, length = 255)
|
||||
@ -102,14 +101,14 @@ class JPAUniquenessProvider(
|
||||
class CommittedState(
|
||||
@EmbeddedId
|
||||
val id: PersistentStateRef,
|
||||
@Column(name = "consuming_transaction_id", nullable = false, length = 64)
|
||||
@Column(name = "consuming_transaction_id", nullable = false, length = 144)
|
||||
val consumingTxHash: String)
|
||||
|
||||
@Entity
|
||||
@javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}notary_committed_txs")
|
||||
class CommittedTransaction(
|
||||
@Id
|
||||
@Column(name = "transaction_id", nullable = false, length = 64)
|
||||
@Column(name = "transaction_id", nullable = false, length = 144)
|
||||
val transactionId: String
|
||||
)
|
||||
|
||||
@ -145,7 +144,7 @@ class JPAUniquenessProvider(
|
||||
}
|
||||
|
||||
fun decodeStateRef(s: PersistentStateRef): StateRef {
|
||||
return StateRef(txhash = SecureHash.parse(s.txId), index = s.index)
|
||||
return StateRef(txhash = SecureHash.create(s.txId), index = s.index)
|
||||
}
|
||||
}
|
||||
|
||||
@ -217,12 +216,12 @@ class JPAUniquenessProvider(
|
||||
}
|
||||
|
||||
return committedStates.map {
|
||||
val stateRef = StateRef(txhash = SecureHash.parse(it.id.txId), index = it.id.index)
|
||||
val consumingTxId = SecureHash.parse(it.consumingTxHash)
|
||||
val stateRef = StateRef(txhash = SecureHash.create(it.id.txId), index = it.id.index)
|
||||
val consumingTxId = SecureHash.create(it.consumingTxHash)
|
||||
if (stateRef in references) {
|
||||
stateRef to StateConsumptionDetails(consumingTxId.sha256(), type = StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE)
|
||||
stateRef to StateConsumptionDetails(consumingTxId.reHash(), type = StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE)
|
||||
} else {
|
||||
stateRef to StateConsumptionDetails(consumingTxId.sha256())
|
||||
stateRef to StateConsumptionDetails(consumingTxId.reHash())
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
@ -286,7 +285,7 @@ class JPAUniquenessProvider(
|
||||
session: Session
|
||||
): InternalResult {
|
||||
return when {
|
||||
isConsumedByTheSameTx(request.txId.sha256(), stateConflicts) -> {
|
||||
isConsumedByTheSameTx(request.txId.reHash(), stateConflicts) -> {
|
||||
InternalResult.Success
|
||||
}
|
||||
request.states.isEmpty() && isPreviouslyNotarised(session, request.txId) -> {
|
||||
@ -326,7 +325,7 @@ class JPAUniquenessProvider(
|
||||
} else {
|
||||
// Mark states as consumed to capture conflicting transactions in the same batch
|
||||
request.states.forEach {
|
||||
consumedStates[it] = StateConsumptionDetails(request.txId.sha256())
|
||||
consumedStates[it] = StateConsumptionDetails(request.txId.reHash())
|
||||
}
|
||||
toCommit.add(request)
|
||||
InternalResult.Success
|
||||
|
@ -31,6 +31,8 @@
|
||||
<!-- This must run after node-core.changelog-init.xml, to prevent database columns being created twice. -->
|
||||
<include file="migration/vault-schema.changelog-v9.xml"/>
|
||||
|
||||
<include file="migration/node-core.changelog-v17.xml"/>
|
||||
|
||||
<include file="migration/node-core.changelog-v19.xml"/>
|
||||
<include file="migration/node-core.changelog-v19-postgres.xml"/>
|
||||
<include file="migration/node-core.changelog-v19-keys.xml"/>
|
||||
|
@ -0,0 +1,14 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
|
||||
|
||||
<changeSet author="R3.Corda" id="node_transaction_id_size">
|
||||
<modifyDataType tableName="node_transactions"
|
||||
columnName="tx_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
<modifyDataType tableName="node_scheduled_states"
|
||||
columnName="transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
@ -11,5 +11,6 @@
|
||||
<include file="migration/node-notary.changelog-committed-transactions-table.xml" />
|
||||
<include file="migration/node-notary.changelog-v3.xml" />
|
||||
<include file="migration/node-notary.changelog-worker-logging.xml" />
|
||||
<include file="migration/node-notary.changelog-v100.xml"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
@ -0,0 +1,20 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
|
||||
|
||||
<changeSet author="R3.Corda" id="node_notary_transaction_id_size">
|
||||
<modifyDataType tableName="node_notary_committed_txs"
|
||||
columnName="transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
<modifyDataType tableName="node_notary_committed_states"
|
||||
columnName="transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
<modifyDataType tableName="node_notary_committed_states"
|
||||
columnName="consuming_transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
<modifyDataType tableName="node_notary_request_log"
|
||||
columnName="consuming_transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
@ -7,4 +7,5 @@
|
||||
<include file="migration/notary-bft-smart.changelog-v1.xml"/>
|
||||
<include file="migration/notary-bft-smart.changelog-pkey.xml"/>
|
||||
<include file="migration/notary-bft-smart.changelog-committed-transactions-table.xml"/>
|
||||
<include file="migration/notary-bft-smart.changelog-v2.xml"/>
|
||||
</databaseChangeLog>
|
||||
|
@ -0,0 +1,17 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
|
||||
|
||||
<changeSet author="R3.Corda" id="node_bft_transaction_id_size">
|
||||
<modifyDataType tableName="node_bft_committed_states"
|
||||
columnName="transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
<modifyDataType tableName="node_bft_committed_states"
|
||||
columnName="consuming_transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
<modifyDataType tableName="node_notary_request_log"
|
||||
columnName="consuming_transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
@ -6,4 +6,5 @@
|
||||
<include file="migration/notary-raft.changelog-init.xml"/>
|
||||
<include file="migration/notary-raft.changelog-v1.xml"/>
|
||||
<include file="migration/notary-raft.changelog-committed-transactions-table.xml" />
|
||||
<include file="migration/notary-raft.changelog-v2.xml"/>
|
||||
</databaseChangeLog>
|
||||
|
@ -0,0 +1,19 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"
|
||||
logicalFilePath="migration/node-services.changelog-init.xml">
|
||||
<changeSet author="R3.Corda" id="nullability">
|
||||
<addNotNullConstraint tableName="node_raft_committed_states" columnName="state_index" columnDataType="BIGINT"/>
|
||||
<addNotNullConstraint tableName="node_raft_committed_states" columnName="state_value" columnDataType="BLOB"/>
|
||||
</changeSet>
|
||||
<changeSet author="R3.Corda" id="node_raft_transaction_id_size">
|
||||
<modifyDataType tableName="node_raft_committed_txs"
|
||||
columnName="transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
<modifyDataType tableName="node_notary_request_log"
|
||||
columnName="consuming_transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
@ -12,4 +12,5 @@
|
||||
<include file="migration/vault-schema.changelog-v7.xml"/>
|
||||
<include file="migration/vault-schema.changelog-v8.xml"/>
|
||||
<include file="migration/vault-schema.changelog-v11.xml"/>
|
||||
<include file="migration/vault-schema.changelog-v12.xml"/>
|
||||
</databaseChangeLog>
|
||||
|
@ -0,0 +1,29 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
|
||||
|
||||
<changeSet author="R3.Corda" id="vault_transaction_id_size">
|
||||
<modifyDataType tableName="vault_fungible_states"
|
||||
columnName="transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
<modifyDataType tableName="vault_fungible_states_parts"
|
||||
columnName="transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
<modifyDataType tableName="vault_linear_states"
|
||||
columnName="transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
<modifyDataType tableName="vault_linear_states_parts"
|
||||
columnName="transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
<modifyDataType tableName="vault_states"
|
||||
columnName="transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
<modifyDataType tableName="vault_transaction_notes"
|
||||
columnName="transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
<modifyDataType tableName="state_party"
|
||||
columnName="transaction_id"
|
||||
newDataType="NVARCHAR(144)"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
@ -2,7 +2,9 @@ package net.corda.node.services.events
|
||||
|
||||
import com.nhaarman.mockito_kotlin.*
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.randomHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowLogicRef
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
@ -283,7 +285,7 @@ class NodeSchedulerPersistenceTest : NodeSchedulerServiceTestBase() {
|
||||
val database = configureDatabase(dataSourceProps, databaseConfig, { null }, { null })
|
||||
database.transaction {
|
||||
val repo = PersistentScheduledFlowRepository(database)
|
||||
val stateRef = StateRef(SecureHash.randomSHA256(), 0)
|
||||
val stateRef = StateRef(DigestService.default.randomHash(), 0)
|
||||
val ssr = ScheduledStateRef(stateRef, mark)
|
||||
repo.merge(ssr)
|
||||
|
||||
|
@ -953,7 +953,7 @@ class HibernateConfigurationTest {
|
||||
// DOCEND JdbcSession
|
||||
var count = 0
|
||||
while (rs.next()) {
|
||||
val stateRef = StateRef(SecureHash.parse(rs.getString(1)), rs.getInt(2))
|
||||
val stateRef = StateRef(SecureHash.create(rs.getString(1)), rs.getInt(2))
|
||||
Assert.assertTrue(cashStates.map { it.ref }.contains(stateRef))
|
||||
count++
|
||||
}
|
||||
@ -962,7 +962,7 @@ class HibernateConfigurationTest {
|
||||
}
|
||||
|
||||
private fun toStateRef(pStateRef: PersistentStateRef): StateRef {
|
||||
return StateRef(SecureHash.parse(pStateRef.txId), pStateRef.index)
|
||||
return StateRef(SecureHash.create(pStateRef.txId), pStateRef.index)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
|
@ -8,7 +8,9 @@ import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.randomHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.internal.*
|
||||
@ -128,7 +130,7 @@ class NodeAttachmentServiceTest {
|
||||
val id = testJar.read { storage.importAttachment(it, "test", null) }
|
||||
assertEquals(expectedHash, id)
|
||||
|
||||
assertNull(storage.openAttachment(SecureHash.randomSHA256()))
|
||||
assertNull(storage.openAttachment(DigestService.default.randomHash()))
|
||||
val stream = storage.openAttachment(expectedHash)!!.openAsJAR()
|
||||
val e1 = stream.nextJarEntry!!
|
||||
assertEquals("test1.txt", e1.name)
|
||||
|
@ -213,8 +213,8 @@ class NonValidatingNotaryServiceTests {
|
||||
assertEquals(notaryError.txId, doubleSpendTx.id)
|
||||
with(notaryError) {
|
||||
assertEquals(consumedStates.size, 2)
|
||||
assertEquals(consumedStates[firstState.ref]!!.hashOfTransactionId, firstSpendTx.id.sha256())
|
||||
assertEquals(consumedStates[secondState.ref]!!.hashOfTransactionId, secondSpendTx.id.sha256())
|
||||
assertEquals(consumedStates[firstState.ref]!!.hashOfTransactionId, firstSpendTx.id.reHash())
|
||||
assertEquals(consumedStates[secondState.ref]!!.hashOfTransactionId, secondSpendTx.id.reHash())
|
||||
}
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user