CORDA-521: Backwards compatible Transactions using sub-Merkle trees (#1481)

* tx backwards compatibility + rebase

* SHA256d definition
This commit is contained in:
Konstantinos Chalkias 2017-09-20 13:11:58 +01:00 committed by josecoll
parent 477ea3a5e1
commit 6887947a4d
20 changed files with 804 additions and 367 deletions

View File

@ -0,0 +1,14 @@
package net.corda.core.contracts
/**
* An enum, for which each property corresponds to a transaction component group. The position in the enum class
* declaration (ordinal) is used for component-leaf ordering when computing the Merkle tree.
*/
enum class ComponentGroupEnum {
INPUTS_GROUP, // ordinal = 0.
OUTPUTS_GROUP, // ordinal = 1.
COMMANDS_GROUP, // ordinal = 2.
ATTACHMENTS_GROUP, // ordinal = 3.
NOTARY_GROUP, // ordinal = 4.
TIMEWINDOW_GROUP // ordinal = 5.
}

View File

@ -271,7 +271,7 @@ class PrivacySalt(bytes: ByteArray) : OpaqueBytes(bytes) {
init { init {
require(bytes.size == 32) { "Privacy salt should be 32 bytes." } require(bytes.size == 32) { "Privacy salt should be 32 bytes." }
require(!bytes.all { it == 0.toByte() }) { "Privacy salt should not be all zeros." } require(bytes.any { it != 0.toByte() }) { "Privacy salt should not be all zeros." }
} }
} }

View File

@ -2,10 +2,14 @@
package net.corda.core.crypto package net.corda.core.crypto
import net.corda.core.contracts.PrivacySalt
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.serialize
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.toBase58 import net.corda.core.utilities.toBase58
import net.corda.core.utilities.toSHA256Bytes import net.corda.core.utilities.toSHA256Bytes
import java.math.BigInteger import java.math.BigInteger
import java.nio.ByteBuffer
import java.security.* import java.security.*
/** /**
@ -184,3 +188,27 @@ fun random63BitValue(): Long {
} }
} }
} }
/**
* Compute the hash of each serialised component so as to be used as Merkle tree leaf. The resultant output (leaf) is
* calculated using the SHA256d algorithm, thus SHA256(SHA256(nonce || serializedComponent)), 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 SHA256(SHA256(nonce || serializedComponent)). */
fun componentHash(nonce: SecureHash, opaqueBytes: OpaqueBytes): SecureHash = SecureHash.sha256Twice(nonce.bytes + opaqueBytes.bytes)
/** Serialise the object and return the hash of the serialized bytes. */
fun <T : Any> serializedHash(x: T): SecureHash = x.serialize(context = SerializationDefaults.P2P_CONTEXT.withoutReferences()).bytes.sha256()
/**
* 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 SHA256(SHA256(privacySalt || groupIndex || internalIndex))
*/
fun computeNonce(privacySalt: PrivacySalt, groupIndex: Int, internalIndex: Int) = SecureHash.sha256Twice(privacySalt.bytes + ByteBuffer.allocate(8).putInt(groupIndex).putInt(internalIndex).array())

View File

@ -121,6 +121,28 @@ class PartialMerkleTree(val root: PartialTree) {
} }
} }
} }
/**
* Recursive calculation of root of this partial tree.
* Modifies usedHashes to later check for inclusion with hashes provided.
* @param node the partial Merkle tree for which we want to calculate the Merkle root.
* @param usedHashes a mutable list that at the end of this recursive algorithm, it will consist of the included leaves (hashes of the visible components).
* @return the root [SecureHash] of this partial Merkle tree.
*/
fun rootAndUsedHashes(node: PartialTree, usedHashes: MutableList<SecureHash>): SecureHash {
return when (node) {
is PartialTree.IncludedLeaf -> {
usedHashes.add(node.hash)
node.hash
}
is PartialTree.Leaf -> node.hash
is PartialTree.Node -> {
val leftHash = rootAndUsedHashes(node.left, usedHashes)
val rightHash = rootAndUsedHashes(node.right, usedHashes)
return leftHash.hashConcat(rightHash)
}
}
}
} }
/** /**
@ -129,29 +151,10 @@ class PartialMerkleTree(val root: PartialTree) {
*/ */
fun verify(merkleRootHash: SecureHash, hashesToCheck: List<SecureHash>): Boolean { fun verify(merkleRootHash: SecureHash, hashesToCheck: List<SecureHash>): Boolean {
val usedHashes = ArrayList<SecureHash>() val usedHashes = ArrayList<SecureHash>()
val verifyRoot = verify(root, usedHashes) val verifyRoot = rootAndUsedHashes(root, usedHashes)
// It means that we obtained more/fewer hashes than needed or different sets of hashes. // It means that we obtained more/fewer hashes than needed or different sets of hashes.
if (hashesToCheck.groupBy { it } != usedHashes.groupBy { it }) if (hashesToCheck.groupBy { it } != usedHashes.groupBy { it })
return false return false
return (verifyRoot == merkleRootHash) return (verifyRoot == merkleRootHash)
} }
/**
* Recursive calculation of root of this partial tree.
* Modifies usedHashes to later check for inclusion with hashes provided.
*/
private fun verify(node: PartialTree, usedHashes: MutableList<SecureHash>): SecureHash {
return when (node) {
is PartialTree.IncludedLeaf -> {
usedHashes.add(node.hash)
node.hash
}
is PartialTree.Leaf -> node.hash
is PartialTree.Node -> {
val leftHash = verify(node.left, usedHashes)
val rightHash = verify(node.right, usedHashes)
return leftHash.hashConcat(rightHash)
}
}
}
} }

View File

@ -40,6 +40,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
@JvmStatic fun randomSHA256() = sha256(newSecureRandom().generateSeed(32)) @JvmStatic fun randomSHA256() = sha256(newSecureRandom().generateSeed(32))
val zeroHash = SecureHash.SHA256(ByteArray(32, { 0.toByte() })) val zeroHash = SecureHash.SHA256(ByteArray(32, { 0.toByte() }))
val allOnesHash = SecureHash.SHA256(ByteArray(32, { 255.toByte() }))
} }
// In future, maybe SHA3, truncated hashes etc. // In future, maybe SHA3, truncated hashes etc.

View File

@ -2,6 +2,6 @@ package net.corda.core.serialization
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
/** Thrown during deserialisation to indicate that an attachment needed to construct the [WireTransaction] is not found */ /** Thrown during deserialisation to indicate that an attachment needed to construct the [WireTransaction] is not found. */
@CordaSerializable @CordaSerializable
class MissingAttachmentsException(val ids: List<SecureHash>) : Exception() class MissingAttachmentsException(val ids: List<SecureHash>) : Exception()

View File

@ -3,121 +3,190 @@ package net.corda.core.transactions
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.*
import net.corda.core.serialization.SerializationFactory import net.corda.core.utilities.OpaqueBytes
import net.corda.core.serialization.serialize import java.security.PublicKey
import java.nio.ByteBuffer
import java.util.function.Predicate import java.util.function.Predicate
/** /**
* If a privacy salt is provided, the resulted output (Merkle-leaf) is computed as * Implemented by [WireTransaction] and [FilteredTransaction]. A TraversableTransaction allows you to iterate
* Hash(serializedObject || Hash(privacy_salt || obj_index_in_merkle_tree)).
*/
fun <T : Any> serializedHash(x: T, privacySalt: PrivacySalt?, index: Int): SecureHash {
return if (privacySalt != null)
serializedHash(x, computeNonce(privacySalt, index))
else
serializedHash(x)
}
fun <T : Any> serializedHash(x: T, nonce: SecureHash): SecureHash {
return if (x !is PrivacySalt) // PrivacySalt is not required to have an accompanied nonce.
(x.serialize(context = SerializationFactory.defaultFactory.defaultContext.withoutReferences()).bytes + nonce.bytes).sha256()
else
serializedHash(x)
}
fun <T : Any> serializedHash(x: T): SecureHash = x.serialize(context = SerializationFactory.defaultFactory.defaultContext.withoutReferences()).bytes.sha256()
/** The nonce is computed as Hash(privacySalt || index). */
fun computeNonce(privacySalt: PrivacySalt, index: Int) = (privacySalt.bytes + ByteBuffer.allocate(4).putInt(index).array()).sha256()
/**
* Implemented by [WireTransaction] and [FilteredLeaves]. A TraversableTransaction allows you to iterate
* over the flattened components of the underlying transaction structure, taking into account that some * over the flattened components of the underlying transaction structure, taking into account that some
* may be missing in the case of this representing a "torn" transaction. Please see the user guide section * 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. * "Transaction tear-offs" to learn more about this feature.
*
* The [availableComponents] property is used for calculation of the transaction's [MerkleTree], which is in
* turn used to derive the ID hash.
*/ */
interface TraversableTransaction { abstract class TraversableTransaction(open val componentGroups: List<ComponentGroup>) : CoreTransaction() {
val inputs: List<StateRef> /** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */
val attachments: List<SecureHash> val attachments: List<SecureHash> = deserialiseComponentGroup(ComponentGroupEnum.ATTACHMENTS_GROUP, { SerializedBytes<SecureHash>(it).deserialize() })
val outputs: List<TransactionState<ContractState>>
val commands: List<Command<*>> /** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
val notary: Party? override val inputs: List<StateRef> = deserialiseComponentGroup(ComponentGroupEnum.INPUTS_GROUP, { SerializedBytes<StateRef>(it).deserialize() })
val timeWindow: TimeWindow?
/** override val outputs: List<TransactionState<ContractState>> = deserialiseComponentGroup(ComponentGroupEnum.OUTPUTS_GROUP, { SerializedBytes<TransactionState<ContractState>>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) })
* For privacy purposes, each part of a transaction should be accompanied by a nonce.
* To avoid storing a random number (nonce) per component, an initial "salt" is the sole value utilised, /** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */
* so that all component nonces are deterministically computed in the following way: val commands: List<Command<*>> = deserialiseComponentGroup(ComponentGroupEnum.COMMANDS_GROUP, { SerializedBytes<Command<*>>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) })
* nonce1 = H(salt || 1)
* nonce2 = H(salt || 2) override val notary: Party? = let {
* val notaries: List<Party> = deserialiseComponentGroup(ComponentGroupEnum.NOTARY_GROUP, { SerializedBytes<Party>(it).deserialize() })
* Thus, all of the nonces are "independent" in the sense that knowing one or some of them, you can learn check(notaries.size <= 1) { "Invalid Transaction. More than 1 notary party detected." }
* nothing about the rest. if (notaries.isNotEmpty()) notaries[0] else null
*/ }
val privacySalt: PrivacySalt?
val timeWindow: TimeWindow? = let {
val timeWindows: List<TimeWindow> = deserialiseComponentGroup(ComponentGroupEnum.TIMEWINDOW_GROUP, { SerializedBytes<TimeWindow>(it).deserialize() })
check(timeWindows.size <= 1) { "Invalid Transaction. More than 1 time-window detected." }
if (timeWindows.isNotEmpty()) timeWindows[0] else null
}
/** /**
* Returns a flattened list of all the components that are present in the transaction, in the following order: * Returns a list of all the component groups that are present in the transaction, excluding the privacySalt,
* * in the following order (which is the same with the order in [ComponentGroupEnum]:
* - Each input that is present * - list of each input that is present
* - Each attachment that is present * - list of each output that is present
* - Each output that is present * - list of each command that is present
* - Each command that is present * - list of each attachment that is present
* - The notary [Party], if present * - The notary [Party], if present (list with one element)
* - The time-window of the transaction, if present * - The time-window of the transaction, if present (list with one element)
* - The privacy salt required for nonces, always presented in [WireTransaction] and always null in [FilteredLeaves]
*/ */
val availableComponents: List<Any> val availableComponentGroups: List<List<Any>>
// NOTE: if the order below is altered or components are added/removed in the future, one should also reflect
// this change to the indexOffsets() method in WireTransaction.
get() { get() {
// We may want to specify our own behaviour on certain tx fields. val result = mutableListOf(inputs, outputs, commands, attachments)
// Like if we include them at all, what to do with null values, if we treat list as one or not etc. for building notary?.let { result += listOf(it) }
// torn-off transaction and id calculation. timeWindow?.let { result += listOf(it) }
val result = mutableListOf(inputs, attachments, outputs, commands).flatten().toMutableList()
notary?.let { result += it }
timeWindow?.let { result += it }
privacySalt?.let { result += it }
return result return result
} }
/** // Helper function to return a meaningful exception if deserialisation of a component fails.
* Calculate the hashes of the sub-components of the transaction, that are used to build its Merkle tree. private fun <T> deserialiseComponentGroup(groupEnum: ComponentGroupEnum, deserialiseBody: (ByteArray) -> T): List<T> {
* The root of the tree is the transaction identifier. The tree structure is helpful for privacy, please val group = componentGroups.firstOrNull { it.groupIndex == groupEnum.ordinal }
* see the user-guide section "Transaction tear-offs" to learn more about this topic. return if (group != null && group.components.isNotEmpty()) {
*/ group.components.mapIndexed { internalIndex, component ->
val availableComponentHashes: List<SecureHash> get() = availableComponents.mapIndexed { index, it -> serializedHash(it, privacySalt, index) } try {
deserialiseBody(component.bytes)
} catch (e: MissingAttachmentsException) {
throw e
} catch (e: Exception) {
throw Exception("Malformed transaction, $groupEnum at index $internalIndex cannot be deserialised", e)
}
}
} else {
emptyList()
}
}
} }
/** /**
* Class that holds filtered leaves for a partial Merkle transaction. We assume mixed leaf types, notice that every * Class representing merkleized filtered transaction.
* field from [WireTransaction] can be used in [PartialMerkleTree] calculation, except for the privacySalt. * @param id Merkle tree root hash.
* A list of nonces is also required to (re)construct component hashes. * @param filteredComponentGroups list of transaction components groups remained after filters are applied to [WireTransaction].
* @param groupHashes the roots of the transaction component groups.
*/ */
@CordaSerializable @CordaSerializable
class FilteredLeaves( class FilteredTransaction private constructor(
override val inputs: List<StateRef>, override val id: SecureHash,
override val attachments: List<SecureHash>, val filteredComponentGroups: List<FilteredComponentGroup>,
override val outputs: List<TransactionState<ContractState>>, val groupHashes: List<SecureHash>
override val commands: List<Command<*>>, ) : TraversableTransaction(filteredComponentGroups) {
override val notary: Party?,
override val timeWindow: TimeWindow?, companion object {
val nonces: List<SecureHash> /**
) : TraversableTransaction { * Construction of filtered transaction with partial Merkle tree.
* @param wtx WireTransaction to be filtered.
* @param filtering filtering over the whole WireTransaction.
*/
@JvmStatic
fun buildFilteredTransaction(wtx: WireTransaction, filtering: Predicate<Any>): FilteredTransaction {
val filteredComponentGroups = filterWithFun(wtx, filtering)
return FilteredTransaction(wtx.id, filteredComponentGroups, wtx.groupHashes)
}
/** /**
* PrivacySalt should be always null for FilteredLeaves, because making it accidentally visible would expose all * Construction of partial transaction from [WireTransaction] based on filtering.
* nonces (including filtered out components) causing privacy issues, see [serializedHash] and * Note that list of nonces to be sent is updated on the fly, based on the index of the filtered tx component.
* [TraversableTransaction.privacySalt]. * @param filtering filtering over the whole WireTransaction.
* @returns a list of [FilteredComponentGroup] used in PartialMerkleTree calculation and verification.
*/ */
override val privacySalt: PrivacySalt? get() = null private fun filterWithFun(wtx: WireTransaction, filtering: Predicate<Any>): List<FilteredComponentGroup> {
val filteredSerialisedComponents: MutableMap<Int, MutableList<OpaqueBytes>> = hashMapOf()
val filteredComponentNonces: MutableMap<Int, MutableList<SecureHash>> = hashMapOf()
val filteredComponentHashes: MutableMap<Int, MutableList<SecureHash>> = hashMapOf() // Required for partial Merkle tree generation.
init { fun <T : Any> filter(t: T, componentGroupIndex: Int, internalIndex: Int) {
require(availableComponents.size == nonces.size) { "Each visible component should be accompanied by a nonce." } if (filtering.test(t)) {
val group = filteredSerialisedComponents[componentGroupIndex]
// Because the filter passed, we know there is a match. We also use first vs single as the init function
// of WireTransaction ensures there are no duplicated groups.
val serialisedComponent = wtx.componentGroups.first { it.groupIndex == componentGroupIndex }.components[internalIndex]
if (group == null) {
// As all of the helper Map structures, like availableComponentNonces, availableComponentHashes
// and groupsMerkleRoots, are computed lazily via componentGroups.forEach, there should always be
// a match on Map.get ensuring it will never return null.
filteredSerialisedComponents.put(componentGroupIndex, mutableListOf(serialisedComponent))
filteredComponentNonces.put(componentGroupIndex, mutableListOf(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex]))
filteredComponentHashes.put(componentGroupIndex, mutableListOf(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex]))
} else {
group.add(serialisedComponent)
// If the group[componentGroupIndex] existed, then we guarantee that
// filteredComponentNonces[componentGroupIndex] and filteredComponentHashes[componentGroupIndex] are not null.
filteredComponentNonces[componentGroupIndex]!!.add(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex])
filteredComponentHashes[componentGroupIndex]!!.add(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex])
}
}
}
fun updateFilteredComponents() {
wtx.inputs.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.INPUTS_GROUP.ordinal, internalIndex) }
wtx.outputs.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.OUTPUTS_GROUP.ordinal, internalIndex) }
wtx.commands.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.COMMANDS_GROUP.ordinal, internalIndex) }
wtx.attachments.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.ATTACHMENTS_GROUP.ordinal, internalIndex) }
if (wtx.notary != null) filter(wtx.notary, ComponentGroupEnum.NOTARY_GROUP.ordinal, 0)
if (wtx.timeWindow != null) filter(wtx.timeWindow, ComponentGroupEnum.TIMEWINDOW_GROUP.ordinal, 0)
// It's sometimes possible that when we receive a WireTransaction for which there is a new or more unknown component groups,
// we decide to filter and attach this field to a FilteredTransaction.
// An example would be to redact certain contract state types, but otherwise leave a transaction alone,
// including the unknown new components.
wtx.componentGroups.filter { it.groupIndex >= ComponentGroupEnum.values().size }.forEach { componentGroup -> componentGroup.components.forEachIndexed { internalIndex, component-> filter(component, componentGroup.groupIndex, internalIndex) }}
}
fun createPartialMerkleTree(componentGroupIndex: Int) = PartialMerkleTree.build(MerkleTree.getMerkleTree(wtx.availableComponentHashes[componentGroupIndex]!!), filteredComponentHashes[componentGroupIndex]!!)
fun createFilteredComponentGroups(): List<FilteredComponentGroup> {
updateFilteredComponents()
val filteredComponentGroups: MutableList<FilteredComponentGroup> = mutableListOf()
filteredSerialisedComponents.forEach { (groupIndex, value) ->
filteredComponentGroups.add(FilteredComponentGroup(groupIndex, value, filteredComponentNonces[groupIndex]!!, createPartialMerkleTree(groupIndex) ))
}
return filteredComponentGroups
}
return createFilteredComponentGroups()
}
}
/**
* Runs verification of partial Merkle branch against [id].
* Note that empty filtered transactions (with no component groups) are accepted as well,
* e.g. for Timestamp Authorities to blindly sign or any other similar case in the future
* that requires a blind signature over a transaction's [id].
* @throws FilteredTransactionVerificationException if verification fails.
*/
@Throws(FilteredTransactionVerificationException::class)
fun verify() {
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) { "Top level Merkle tree cannot be verified against transaction's id" }
// For completely blind verification (no components are included).
if (filteredComponentGroups.isEmpty()) return
// Compute partial Merkle roots for each filtered component and verify each of the partial Merkle trees.
filteredComponentGroups.forEach { (groupIndex, components, nonces, groupPartialTree) ->
verificationCheck(groupIndex < groupHashes.size ) { "There is no matching component group hash for group $groupIndex" }
val groupMerkleRoot = groupHashes[groupIndex]
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) })) { "Visible components in group $groupIndex cannot be verified against their partial Merkle tree" }
}
} }
/** /**
@ -130,113 +199,75 @@ class FilteredLeaves(
* @returns false if no elements were matched on a structure or checkingFun returned false. * @returns false if no elements were matched on a structure or checkingFun returned false.
*/ */
fun checkWithFun(checkingFun: (Any) -> Boolean): Boolean { fun checkWithFun(checkingFun: (Any) -> Boolean): Boolean {
val checkList = availableComponents.map { checkingFun(it) } val checkList = availableComponentGroups.flatten().map { checkingFun(it) }
return (!checkList.isEmpty()) && checkList.all { it } return (!checkList.isEmpty()) && checkList.all { it }
} }
override val availableComponentHashes: List<SecureHash> get() = availableComponents.mapIndexed { index, it -> serializedHash(it, nonces[index]) } /**
* Function that checks if all of the components in a particular group are visible.
* This functionality is required on non-Validating Notaries to check that all inputs are visible.
* It might also be applied in Oracles, where an Oracle should know it can see all commands.
* The logic behind this algorithm is that we check that the root of the provided group partialMerkleTree matches with the
* root of a fullMerkleTree if computed using all visible components.
* Note that this method is usually called after or before [verify], to also ensure that the provided partial Merkle
* tree corresponds to the correct leaf in the top Merkle tree.
* @param componentGroupEnum the [ComponentGroupEnum] that corresponds to the componentGroup for which we require full component visibility.
* @throws ComponentVisibilityException if not all of the components are visible or if the component group is not present in the [FilteredTransaction].
*/
@Throws(ComponentVisibilityException::class)
fun checkAllComponentsVisible(componentGroupEnum: ComponentGroupEnum) {
val group = filteredComponentGroups.firstOrNull { it.groupIndex == componentGroupEnum.ordinal }
if (group == null) {
// 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) {
"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
visibilityCheck(groupPartialRoot == groupFullRoot) { "The partial Merkle tree root does not match with the received root for group ${group.groupIndex}" }
}
}
inline private fun verificationCheck(value: Boolean, lazyMessage: () -> Any): Unit {
if (!value) {
val message = lazyMessage()
throw FilteredTransactionVerificationException(id, message.toString())
}
}
inline private fun visibilityCheck(value: Boolean, lazyMessage: () -> Any): Unit {
if (!value) {
val message = lazyMessage()
throw ComponentVisibilityException(id, message.toString())
}
}
} }
/** /**
* Class representing merkleized filtered transaction. * A FilteredComponentGroup is used to store the filtered list of transaction components of the same type in serialised form.
* @param id Merkle tree root hash. * This is similar to [ComponentGroup], but it also includes the corresponding nonce per component.
* @param filteredLeaves Leaves included in a filtered transaction.
* @param partialMerkleTree Merkle branch needed to verify filteredLeaves.
*/ */
@CordaSerializable @CordaSerializable
class FilteredTransaction private constructor( data class FilteredComponentGroup(override val groupIndex: Int, override val components: List<OpaqueBytes>, val nonces: List<SecureHash>, val partialMerkleTree: PartialMerkleTree): ComponentGroup(groupIndex, components) {
val id: SecureHash, init {
val filteredLeaves: FilteredLeaves, check(components.size == nonces.size) { "Size of transaction components and nonces do not match" }
val partialMerkleTree: PartialMerkleTree }
) { }
companion object {
/** /** Thrown when checking for visibility of all-components in a group in [FilteredTransaction.checkAllComponentsVisible].
* Construction of filtered transaction with Partial Merkle Tree. * @param id transaction's id.
* @param wtx WireTransaction to be filtered. * @param reason information about the exception.
* @param filtering filtering over the whole WireTransaction
*/ */
@JvmStatic @CordaSerializable
fun buildMerkleTransaction(wtx: WireTransaction, class ComponentVisibilityException(val id: SecureHash, val reason: String) : Exception("Component visibility error for transaction with id:$id. Reason: $reason")
filtering: Predicate<Any>
): FilteredTransaction {
val filteredLeaves = filterWithFun(wtx, filtering)
val merkleTree = wtx.merkleTree
val pmt = PartialMerkleTree.build(merkleTree, filteredLeaves.availableComponentHashes)
return FilteredTransaction(merkleTree.hash, filteredLeaves, pmt)
}
/** /** Thrown when [FilteredTransaction.verify] fails.
* Construction of partial transaction from WireTransaction based on filtering. * @param id transaction's id.
* Note that list of nonces to be sent is updated on the fly, based on the index of the filtered tx component. * @param reason information about the exception.
* @param filtering filtering over the whole WireTransaction
* @returns FilteredLeaves used in PartialMerkleTree calculation and verification.
*/ */
private fun filterWithFun(wtx: WireTransaction, filtering: Predicate<Any>): FilteredLeaves { @CordaSerializable
val nonces: MutableList<SecureHash> = mutableListOf() class FilteredTransactionVerificationException(val id: SecureHash, val reason: String) : Exception("Transaction with id:$id cannot be verified. Reason: $reason")
val offsets = indexOffsets(wtx)
fun notNullFalseAndNoncesUpdate(elem: Any?, index: Int): Any? {
return if (elem == null || !filtering.test(elem)) {
null
} else {
nonces.add(computeNonce(wtx.privacySalt, index))
elem
}
}
fun <T : Any> filterAndNoncesUpdate(t: T, index: Int): Boolean {
return if (filtering.test(t)) {
nonces.add(computeNonce(wtx.privacySalt, index))
true
} else {
false
}
}
// TODO: We should have a warning (require) if all leaves (excluding salt) are visible after filtering.
// Consider the above after refactoring FilteredTransaction to implement TraversableTransaction,
// so that a WireTransaction can be used when required to send a full tx (e.g. RatesFixFlow in Oracles).
return FilteredLeaves(
wtx.inputs.filterIndexed { index, it -> filterAndNoncesUpdate(it, index) },
wtx.attachments.filterIndexed { index, it -> filterAndNoncesUpdate(it, index + offsets[0]) },
wtx.outputs.filterIndexed { index, it -> filterAndNoncesUpdate(it, index + offsets[1]) },
wtx.commands.filterIndexed { index, it -> filterAndNoncesUpdate(it, index + offsets[2]) },
notNullFalseAndNoncesUpdate(wtx.notary, offsets[3]) as Party?,
notNullFalseAndNoncesUpdate(wtx.timeWindow, offsets[4]) as TimeWindow?,
nonces
)
}
// We use index offsets, to get the actual leaf-index per transaction component required for nonce computation.
private fun indexOffsets(wtx: WireTransaction): List<Int> {
// There is no need to add an index offset for inputs, because they are the first components in the
// transaction format and it is always zero. Thus, offsets[0] corresponds to attachments,
// offsets[1] to outputs, offsets[2] to commands and so on.
val offsets = mutableListOf(wtx.inputs.size, wtx.inputs.size + wtx.attachments.size)
offsets.add(offsets.last() + wtx.outputs.size)
offsets.add(offsets.last() + wtx.commands.size)
if (wtx.notary != null) {
offsets.add(offsets.last() + 1)
} else {
offsets.add(offsets.last())
}
if (wtx.timeWindow != null) {
offsets.add(offsets.last() + 1)
} else {
offsets.add(offsets.last())
}
// No need to add offset for privacySalt as it doesn't require a nonce.
return offsets
}
}
/**
* Runs verification of partial Merkle branch against [id].
*/
@Throws(MerkleTreeException::class)
fun verify(): Boolean {
val hashes: List<SecureHash> = filteredLeaves.availableComponentHashes
if (hashes.isEmpty())
throw MerkleTreeException("Transaction without included leaves.")
return partialMerkleTree.verify(id, hashes)
}
}

View File

@ -3,6 +3,7 @@ package net.corda.core.transactions
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.serializedHash
import net.corda.core.utilities.toBase58String import net.corda.core.utilities.toBase58String
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub

View File

@ -2,13 +2,16 @@ package net.corda.core.transactions
import co.paralleluniverse.strands.Strand import co.paralleluniverse.strands.Strand
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.* import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.FlowStateMachine
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.KeyManagementService
import java.lang.UnsupportedOperationException import java.lang.UnsupportedOperationException
import java.security.KeyPair import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationFactory
import java.security.PublicKey import java.security.PublicKey
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
@ -72,8 +75,9 @@ open class TransactionBuilder(
} }
// DOCEND 1 // DOCEND 1
fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments), fun toWireTransaction(serializationContext: SerializationContext? = null) = SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
ArrayList(outputs), ArrayList(commands), notary, window, privacySalt) WireTransaction(WireTransaction.createComponentGroups(inputs, outputs, commands, attachments, notary, window), privacySalt)
}
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class) @Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
fun toLedgerTransaction(services: ServiceHub) = toWireTransaction().toLedgerTransaction(services) fun toLedgerTransaction(services: ServiceHub) = toWireTransaction().toLedgerTransaction(services)

View File

@ -1,14 +1,13 @@
package net.corda.core.transactions package net.corda.core.transactions
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.MerkleTree import net.corda.core.contracts.ComponentGroupEnum.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.*
import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.keys
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.Emoji import net.corda.core.internal.Emoji
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.*
import net.corda.core.utilities.OpaqueBytes
import java.security.PublicKey import java.security.PublicKey
import java.security.SignatureException import java.security.SignatureException
import java.util.function.Predicate import java.util.function.Predicate
@ -17,21 +16,42 @@ import java.util.function.Predicate
* A transaction ready for serialisation, without any signatures attached. A WireTransaction is usually wrapped * A transaction ready for serialisation, without any signatures attached. A WireTransaction is usually wrapped
* by a [SignedTransaction] that carries the signatures over this payload. * by a [SignedTransaction] that carries the signatures over this payload.
* The identity of the transaction is the Merkle tree root of its components (see [MerkleTree]). * The identity of the transaction is the Merkle tree root of its components (see [MerkleTree]).
*
* For privacy purposes, each part of a transaction should be accompanied by a nonce.
* To avoid storing a random number (nonce) per component, an initial [privacySalt] is the sole value utilised,
* so that all component nonces are deterministically computed.
*
* A few notes about backwards compatibility:
* A wire transaction can be backwards compatible, in the sense that if an old client receives a [componentGroups] with
* more elements than expected, it will normally deserialise the required objects and omit any checks in the optional
* new fields. Moreover, because the Merkle tree is constructed from the received list of [ComponentGroup], which internally
* deals with bytes, any client can compute the Merkle tree and on the same time relay a [WireTransaction] object even
* if she is unable to read some of the "optional" component types. We stress that practically, a new type of
* [WireTransaction] should only be considered compatible if and only if the following rules apply:
* <p><ul>
* <li>Component-type ordering is fixed (eg. inputs, then outputs, then commands etc, see [ComponentGroupEnum] for the actual ordering).
* <li>Removing a component-type that existed in older wire transaction types is not allowed, because it will affect the Merkle tree structure.
* <li>Changing the order of existing component types is also not allowed, for the same reason.
* <li>New component types must be added at the end of the list of [ComponentGroup] and update the [ComponentGroupEnum] with the new type. After a component is added, its ordinal must never change.
* <li>A new component type should always be an "optional value", in the sense that lack of its visibility does not change the transaction and contract logic and details. An example of "optional" components could be a transaction summary or some statistics.
* </ul></p>
*/ */
@CordaSerializable @CordaSerializable
data class WireTransaction( class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: PrivacySalt = PrivacySalt()) : TraversableTransaction(componentGroups) {
/** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
override val inputs: List<StateRef>, @Deprecated("Required only in some unit-tests and for backwards compatibility purposes.", ReplaceWith("WireTransaction(val componentGroups: List<ComponentGroup>, override val privacySalt: PrivacySalt)"), DeprecationLevel.WARNING)
/** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */ constructor(inputs: List<StateRef>,
override val attachments: List<SecureHash>, attachments: List<SecureHash>,
override val outputs: List<TransactionState<ContractState>>, outputs: List<TransactionState<ContractState>>,
/** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */ commands: List<Command<*>>,
override val commands: List<Command<*>>, notary: Party?,
override val notary: Party?, timeWindow: TimeWindow?,
override val timeWindow: TimeWindow?, privacySalt: PrivacySalt = PrivacySalt()
override val privacySalt: PrivacySalt = PrivacySalt() ) : this(createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow), privacySalt)
) : CoreTransaction(), TraversableTransaction {
init { init {
check(componentGroups.all { it.components.isNotEmpty() }) { "Empty component groups are not allowed" }
check(componentGroups.map { it.groupIndex }.toSet().size == componentGroups.size) { "Duplicated component groups detected" }
checkBaseInvariants() checkBaseInvariants()
check(inputs.isNotEmpty() || outputs.isNotEmpty()) { "A transaction must contain at least one input or output state" } 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" } check(commands.isNotEmpty()) { "A transaction must contain at least one command" }
@ -44,7 +64,7 @@ data class WireTransaction(
/** Public keys that need to be fulfilled by signatures in order for the transaction to be valid. */ /** Public keys that need to be fulfilled by signatures in order for the transaction to be valid. */
val requiredSigningKeys: Set<PublicKey> get() { val requiredSigningKeys: Set<PublicKey> get() {
val commandKeys = commands.flatMap { it.signers }.toSet() val commandKeys = commands.flatMap { it.signers }.toSet()
// TODO: prevent notary field from being set if there are no inputs and no timestamp // TODO: prevent notary field from being set if there are no inputs and no timestamp.
return if (notary != null && (inputs.isNotEmpty() || timeWindow != null)) { return if (notary != null && (inputs.isNotEmpty() || timeWindow != null)) {
commandKeys + notary.owningKey commandKeys + notary.owningKey
} else { } else {
@ -97,14 +117,70 @@ data class WireTransaction(
/** /**
* Build filtered transaction using provided filtering functions. * Build filtered transaction using provided filtering functions.
*/ */
fun buildFilteredTransaction(filtering: Predicate<Any>): FilteredTransaction { fun buildFilteredTransaction(filtering: Predicate<Any>): FilteredTransaction =
return FilteredTransaction.buildMerkleTransaction(this, filtering) FilteredTransaction.buildFilteredTransaction(this, filtering)
}
/** /**
* Builds whole Merkle tree for a transaction. * Builds whole Merkle tree for a transaction.
* Briefly, each component group has its own sub Merkle tree and all of the roots of these trees are used as leaves
* in a top level Merkle tree.
* Note that ordering of elements inside a [ComponentGroup] matters when computing the Merkle root.
* On the other hand, insertion group ordering does not affect the top level Merkle tree construction, as it is
* actually an ordered Merkle tree, where its leaves are ordered based on the group ordinal in [ComponentGroupEnum].
* 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(availableComponentHashes) } val merkleTree: MerkleTree by lazy { MerkleTree.getMerkleTree(groupHashes) }
/**
* The leaves (group hashes) of the top level Merkle tree.
* If a group's Merkle root is allOnesHash, it is a flag that denotes this group is empty (if list) or null (if single object)
* in the wire transaction.
*/
internal val groupHashes: List<SecureHash> by lazy {
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.
for (i in 0..componentGroups.map { it.groupIndex }.max()!!) {
val root = groupsMerkleRoots[i] ?: SecureHash.allOnesHash
listOfLeaves.add(root)
}
listOfLeaves
}
/**
* Calculate the hashes of the existing component groups, that are used to build the transaction's Merkle tree.
* Each group has its own sub Merkle tree and the hash of the root of this sub tree works as a leaf of the top
* level Merkle tree. The root of the latter is the transaction identifier.
*
* The tree structure is helpful for preserving privacy, please
* 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()
}
/**
* Calculate nonces for every transaction component, including new fields (due to backwards compatibility support) we cannot process.
* Nonce are computed in the following way:
* nonce1 = H(salt || path_for_1st_component)
* nonce2 = H(salt || path_for_2nd_component)
* etc.
* Thus, all of the nonces are "independent" in the sense that knowing one or some of them, you can learn
* 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()
}
/**
* Calculate hashes for every transaction component. These will be used to build the full Merkle tree.
* The root of the tree is the transaction identifier. The tree structure is helpful for privacy, please
* 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()
}
/** /**
* Checks that the given signature matches one of the commands and that it is a correct signature over the tx. * Checks that the given signature matches one of the commands and that it is a correct signature over the tx.
@ -117,6 +193,28 @@ data class WireTransaction(
sig.verify(id) sig.verify(id)
} }
internal companion object {
/**
* Creating list of [ComponentGroup] used in one of the constructors of [WireTransaction] required
* for backwards compatibility purposes.
*/
fun createComponentGroups(inputs: List<StateRef>,
outputs: List<TransactionState<ContractState>>,
commands: List<Command<*>>,
attachments: List<SecureHash>,
notary: Party?,
timeWindow: TimeWindow?): List<ComponentGroup> {
val componentGroupMap: MutableList<ComponentGroup> = mutableListOf()
if (inputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(INPUTS_GROUP.ordinal, inputs.map { it.serialize() }))
if (outputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(OUTPUTS_GROUP.ordinal, outputs.map { it.serialize() }))
if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.serialize() }))
if (attachments.isNotEmpty()) componentGroupMap.add(ComponentGroup(ATTACHMENTS_GROUP.ordinal, attachments.map { it.serialize() }))
if (notary != null) componentGroupMap.add(ComponentGroup(NOTARY_GROUP.ordinal, listOf(notary.serialize())))
if (timeWindow != null) componentGroupMap.add(ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow.serialize())))
return componentGroupMap
}
}
override fun toString(): String { override fun toString(): String {
val buf = StringBuilder() val buf = StringBuilder()
buf.appendln("Transaction:") buf.appendln("Transaction:")
@ -126,4 +224,21 @@ data class WireTransaction(
for (attachment in attachments) buf.appendln("${Emoji.paperclip}ATTACHMENT: $attachment") for (attachment in attachments) buf.appendln("${Emoji.paperclip}ATTACHMENT: $attachment")
return buf.toString() return buf.toString()
} }
override fun equals(other: Any?): Boolean {
if (other is WireTransaction) {
return (this.id == other.id)
} }
return false
}
override fun hashCode(): Int = id.hashCode()
}
/**
* A ComponentGroup is used to store the full list of transaction components of the same type in serialised form.
* Practically, a group per component type of a transaction is required; thus, there will be a group for input states,
* a group for all attachments (if there are any) etc.
*/
@CordaSerializable
open class ComponentGroup(open val groupIndex: Int, open val components: List<OpaqueBytes>)

View File

@ -0,0 +1,251 @@
package net.corda.core.contracts
import net.corda.core.contracts.ComponentGroupEnum.*
import net.corda.core.crypto.MerkleTree
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.secureRandomBytes
import net.corda.core.serialization.serialize
import net.corda.core.transactions.ComponentGroup
import net.corda.core.transactions.ComponentVisibilityException
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.testing.*
import net.corda.testing.contracts.DUMMY_PROGRAM_ID
import net.corda.testing.contracts.DummyState
import org.junit.Test
import java.time.Instant
import java.util.function.Predicate
import kotlin.test.*
class CompatibleTransactionTests : TestDependencyInjectionBase() {
private val dummyOutState = TransactionState(DummyState(0), DUMMY_PROGRAM_ID, DUMMY_NOTARY)
private val stateRef1 = StateRef(SecureHash.randomSHA256(), 0)
private val stateRef2 = StateRef(SecureHash.randomSHA256(), 1)
private val stateRef3 = StateRef(SecureHash.randomSHA256(), 0)
private val inputs = listOf(stateRef1, stateRef2, stateRef3) // 3 elements.
private val outputs = listOf(dummyOutState, dummyOutState.copy(notary = BOB)) // 2 elements.
private val commands = listOf(dummyCommand(DUMMY_KEY_1.public, DUMMY_KEY_2.public)) // 1 element.
private val attachments = emptyList<SecureHash>() // Empty list.
private val notary = DUMMY_NOTARY
private val timeWindow = TimeWindow.fromOnly(Instant.now())
private val privacySalt: PrivacySalt = PrivacySalt()
private val inputGroup by lazy { ComponentGroup(INPUTS_GROUP.ordinal, inputs.map { it.serialize() }) }
private val outputGroup by lazy { ComponentGroup(OUTPUTS_GROUP.ordinal, outputs.map { it.serialize() }) }
private val commandGroup by lazy { ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.serialize() }) }
private val attachmentGroup by lazy { ComponentGroup(ATTACHMENTS_GROUP.ordinal, attachments.map { it.serialize() }) } // The list is empty.
private val notaryGroup by lazy { ComponentGroup(NOTARY_GROUP.ordinal, listOf(notary.serialize())) }
private val timeWindowGroup by lazy { ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow.serialize())) }
private val newUnknownComponentGroup = ComponentGroup(20, listOf(OpaqueBytes(secureRandomBytes(4)), OpaqueBytes(secureRandomBytes(8))))
private val newUnknownComponentEmptyGroup = ComponentGroup(21, emptyList())
// Do not add attachments (empty list).
private val componentGroupsA by lazy {
listOf(
inputGroup,
outputGroup,
commandGroup,
notaryGroup,
timeWindowGroup
)
}
private val wireTransactionA by lazy { WireTransaction(componentGroups = componentGroupsA, privacySalt = privacySalt) }
@Test
fun `Merkle root computations`() {
// Merkle tree computation is deterministic if the same salt and ordering are used.
val wireTransactionB = WireTransaction(componentGroups = componentGroupsA, privacySalt = privacySalt)
assertEquals(wireTransactionA, wireTransactionB)
// Merkle tree computation will change if privacy salt changes.
val wireTransactionOtherPrivacySalt = WireTransaction(componentGroups = componentGroupsA, privacySalt = PrivacySalt())
assertNotEquals(wireTransactionA, wireTransactionOtherPrivacySalt)
// Full Merkle root is computed from the list of Merkle roots of each component group.
assertEquals(wireTransactionA.merkleTree.hash, MerkleTree.getMerkleTree(wireTransactionA.groupHashes).hash)
// Trying to add an empty component group (not allowed), e.g. the empty attachmentGroup.
val componentGroupsEmptyAttachment = listOf(
inputGroup,
outputGroup,
commandGroup,
attachmentGroup,
notaryGroup,
timeWindowGroup
)
assertFails { WireTransaction(componentGroups = componentGroupsEmptyAttachment, privacySalt = privacySalt) }
// Ordering inside a component group matters.
val inputsShuffled = listOf(stateRef2, stateRef1, stateRef3)
val inputShuffledGroup = ComponentGroup(INPUTS_GROUP.ordinal, inputsShuffled.map { it -> it.serialize() })
val componentGroupsB = listOf(
inputShuffledGroup,
outputGroup,
commandGroup,
notaryGroup,
timeWindowGroup
)
val wireTransaction1ShuffledInputs = WireTransaction(componentGroups = componentGroupsB, privacySalt = privacySalt)
// The ID has changed due to change of the internal ordering in inputs.
assertNotEquals(wireTransaction1ShuffledInputs, wireTransactionA)
// Inputs group Merkle roots are not equal.
assertNotEquals(wireTransactionA.groupsMerkleRoots[INPUTS_GROUP.ordinal], wireTransaction1ShuffledInputs.groupsMerkleRoots[INPUTS_GROUP.ordinal])
// But outputs group Merkle leaf (and the rest) remained the same.
assertEquals(wireTransactionA.groupsMerkleRoots[OUTPUTS_GROUP.ordinal], wireTransaction1ShuffledInputs.groupsMerkleRoots[OUTPUTS_GROUP.ordinal])
assertEquals(wireTransactionA.groupsMerkleRoots[NOTARY_GROUP.ordinal], wireTransaction1ShuffledInputs.groupsMerkleRoots[NOTARY_GROUP.ordinal])
assertNull(wireTransactionA.groupsMerkleRoots[ATTACHMENTS_GROUP.ordinal])
assertNull(wireTransaction1ShuffledInputs.groupsMerkleRoots[ATTACHMENTS_GROUP.ordinal])
// Group leaves (components) ordering does not affect the id. In this case, we added outputs group before inputs.
val shuffledComponentGroupsA = listOf(
outputGroup,
inputGroup,
commandGroup,
notaryGroup,
timeWindowGroup
)
assertEquals(wireTransactionA, WireTransaction(componentGroups = shuffledComponentGroupsA, privacySalt = privacySalt))
}
@Test
fun `WireTransaction constructors and compatibility`() {
val wireTransactionOldConstructor = WireTransaction(inputs, attachments, outputs, commands, notary, timeWindow, privacySalt)
assertEquals(wireTransactionA, wireTransactionOldConstructor)
// Malformed tx - attachments is not List<SecureHash>. For this example, we mistakenly added input-state (StateRef) serialised objects with ATTACHMENTS_GROUP.ordinal.
val componentGroupsB = listOf(
inputGroup,
outputGroup,
commandGroup,
ComponentGroup(ATTACHMENTS_GROUP.ordinal, inputGroup.components),
notaryGroup,
timeWindowGroup
)
assertFails { WireTransaction(componentGroupsB, privacySalt) }
// Malformed tx - duplicated component group detected.
val componentGroupsDuplicatedCommands = listOf(
inputGroup,
outputGroup,
commandGroup, // First commandsGroup.
commandGroup, // Second commandsGroup.
notaryGroup,
timeWindowGroup
)
assertFails { WireTransaction(componentGroupsDuplicatedCommands, privacySalt) }
// Malformed tx - inputs is not a serialised object at all.
val componentGroupsC = listOf(
ComponentGroup(INPUTS_GROUP.ordinal, listOf(OpaqueBytes(ByteArray(8)))),
outputGroup,
commandGroup,
notaryGroup,
timeWindowGroup
)
assertFails { WireTransaction(componentGroupsC, privacySalt) }
val componentGroupsCompatibleA = listOf(
inputGroup,
outputGroup,
commandGroup,
notaryGroup,
timeWindowGroup,
newUnknownComponentGroup // A new unknown component with ordinal 20 that we cannot process.
)
// The old client (receiving more component types than expected) is still compatible.
val wireTransactionCompatibleA = WireTransaction(componentGroupsCompatibleA, privacySalt)
assertEquals(wireTransactionCompatibleA.availableComponentGroups, wireTransactionA.availableComponentGroups) // The known components are the same.
assertNotEquals(wireTransactionCompatibleA, wireTransactionA) // But obviously, its Merkle root has changed Vs wireTransactionA (which doesn't include this extra component).
assertEquals(6, wireTransactionCompatibleA.componentGroups.size)
// The old client will trhow if receiving an empty component (even if this unknown).
val componentGroupsCompatibleEmptyNew = listOf(
inputGroup,
outputGroup,
commandGroup,
notaryGroup,
timeWindowGroup,
newUnknownComponentEmptyGroup // A new unknown component with ordinal 21 that we cannot process.
)
assertFails { WireTransaction(componentGroupsCompatibleEmptyNew, privacySalt) }
}
@Test
fun `FilteredTransaction constructors and compatibility`() {
// Filter out all of the components.
val ftxNothing = wireTransactionA.buildFilteredTransaction(Predicate { false }) // Nothing filtered.
assertEquals(6, ftxNothing.groupHashes.size) // Although nothing filtered, we still receive the group hashes for the top level Merkle tree.
ftxNothing.verify()
// Include all of the components.
val ftxAll = wireTransactionA.buildFilteredTransaction(Predicate { true }) // All filtered.
ftxAll.verify()
ftxAll.checkAllComponentsVisible(INPUTS_GROUP)
ftxAll.checkAllComponentsVisible(OUTPUTS_GROUP)
ftxAll.checkAllComponentsVisible(COMMANDS_GROUP)
ftxAll.checkAllComponentsVisible(ATTACHMENTS_GROUP)
ftxAll.checkAllComponentsVisible(NOTARY_GROUP)
ftxAll.checkAllComponentsVisible(TIMEWINDOW_GROUP)
// Filter inputs only.
fun filtering(elem: Any): Boolean {
return when (elem) {
is StateRef -> true
else -> false
}
}
val ftxInputs = wireTransactionA.buildFilteredTransaction(Predicate(::filtering)) // Inputs only filtered.
ftxInputs.verify()
ftxInputs.checkAllComponentsVisible(INPUTS_GROUP)
assertEquals(1, ftxInputs.filteredComponentGroups.size) // We only add component groups that are not empty, thus in this case: the inputs only.
assertEquals(3, ftxInputs.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.components.size) // All 3 inputs are present.
assertEquals(3, ftxInputs.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.nonces.size) // And their corresponding nonces.
assertNotNull(ftxInputs.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.partialMerkleTree) // And the Merkle tree.
// Filter one input only.
fun filteringOneInput(elem: Any) = elem == inputs[0]
val ftxOneInput = wireTransactionA.buildFilteredTransaction(Predicate(::filteringOneInput)) // First input only filtered.
ftxOneInput.verify()
assertFailsWith<ComponentVisibilityException> { ftxOneInput.checkAllComponentsVisible(INPUTS_GROUP) }
assertEquals(1, ftxOneInput.filteredComponentGroups.size) // We only add component groups that are not empty, thus in this case: the inputs only.
assertEquals(1, ftxOneInput.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.components.size) // 1 input is present.
assertEquals(1, ftxOneInput.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.nonces.size) // And its corresponding nonce.
assertNotNull(ftxOneInput.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.partialMerkleTree) // And the Merkle tree.
// The old client (receiving more component types than expected) is still compatible.
val componentGroupsCompatibleA = listOf(inputGroup,
outputGroup,
commandGroup,
notaryGroup,
timeWindowGroup,
newUnknownComponentGroup // A new unknown component with ordinal 10,000 that we cannot process.
)
val wireTransactionCompatibleA = WireTransaction(componentGroupsCompatibleA, privacySalt)
val ftxCompatible = wireTransactionCompatibleA.buildFilteredTransaction(Predicate(::filtering))
ftxCompatible.verify()
assertEquals(ftxInputs.inputs, ftxCompatible.inputs)
assertEquals(wireTransactionCompatibleA.id, ftxCompatible.id)
assertEquals(1, ftxCompatible.filteredComponentGroups.size)
assertEquals(3, ftxCompatible.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.components.size)
assertEquals(3, ftxCompatible.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.nonces.size)
assertNotNull(ftxCompatible.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.partialMerkleTree)
// Now, let's allow everything, including the new component type that we cannot process.
val ftxCompatibleAll = wireTransactionCompatibleA.buildFilteredTransaction(Predicate { true }) // All filtered, including the unknown component.
ftxCompatibleAll.verify()
assertEquals(wireTransactionCompatibleA.id, ftxCompatibleAll.id)
// Check we received the last (6th) element that we cannot process (backwards compatibility).
assertEquals(6, ftxCompatibleAll.filteredComponentGroups.size)
}
}

View File

@ -7,6 +7,7 @@ import net.corda.testing.ALICE
import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.contracts.DUMMY_PROGRAM_ID import net.corda.testing.contracts.DUMMY_PROGRAM_ID
import net.corda.testing.contracts.DUMMY_V2_PROGRAM_ID import net.corda.testing.contracts.DUMMY_V2_PROGRAM_ID
import net.corda.testing.TestDependencyInjectionBase
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -14,7 +15,7 @@ import kotlin.test.assertTrue
/** /**
* Tests for the version 2 dummy contract, to cover ensuring upgrade transactions are built correctly. * Tests for the version 2 dummy contract, to cover ensuring upgrade transactions are built correctly.
*/ */
class DummyContractV2Tests { class DummyContractV2Tests : TestDependencyInjectionBase() {
@Test @Test
fun `upgrade from v1`() { fun `upgrade from v1`() {
val contractUpgrade = DummyContractV2() val contractUpgrade = DummyContractV2()

View File

@ -19,7 +19,7 @@ import kotlin.test.*
class PartialMerkleTreeTest : TestDependencyInjectionBase() { class PartialMerkleTreeTest : TestDependencyInjectionBase() {
val nodes = "abcdef" val nodes = "abcdef"
val hashed = nodes.map { private val hashed = nodes.map {
initialiseTestSerialization() initialiseTestSerialization()
try { try {
it.serialize().sha256() it.serialize().sha256()
@ -27,10 +27,10 @@ class PartialMerkleTreeTest : TestDependencyInjectionBase() {
resetTestSerialization() resetTestSerialization()
} }
} }
val expectedRoot = MerkleTree.getMerkleTree(hashed.toMutableList() + listOf(zeroHash, zeroHash)).hash private val expectedRoot = MerkleTree.getMerkleTree(hashed.toMutableList() + listOf(zeroHash, zeroHash)).hash
val merkleTree = MerkleTree.getMerkleTree(hashed) private val merkleTree = MerkleTree.getMerkleTree(hashed)
val testLedger = ledger { private val testLedger = ledger {
unverifiedTransaction { unverifiedTransaction {
output(CASH_PROGRAM_ID, "MEGA_CORP cash") { output(CASH_PROGRAM_ID, "MEGA_CORP cash") {
Cash.State( Cash.State(
@ -55,8 +55,8 @@ class PartialMerkleTreeTest : TestDependencyInjectionBase() {
} }
} }
val txs = testLedger.interpreter.transactionsToVerify private val txs = testLedger.interpreter.transactionsToVerify
val testTx = txs[0] private val testTx = txs[0]
// Building full Merkle Tree tests. // Building full Merkle Tree tests.
@Test @Test
@ -115,17 +115,15 @@ class PartialMerkleTreeTest : TestDependencyInjectionBase() {
assertEquals(testTx.id, d.id) assertEquals(testTx.id, d.id)
val mt = testTx.buildFilteredTransaction(Predicate(::filtering)) val mt = testTx.buildFilteredTransaction(Predicate(::filtering))
val leaves = mt.filteredLeaves
assertEquals(1, leaves.inputs.size) assertEquals(4, mt.filteredComponentGroups.size)
assertEquals(0, leaves.attachments.size) assertEquals(1, mt.inputs.size)
assertEquals(1, leaves.outputs.size) assertEquals(0, mt.attachments.size)
assertEquals(1, leaves.commands.size) assertEquals(1, mt.outputs.size)
assertNull(mt.filteredLeaves.notary) assertEquals(1, mt.commands.size)
assertNotNull(mt.filteredLeaves.timeWindow) assertNull(mt.notary)
assertNull(mt.filteredLeaves.privacySalt) assertNotNull(mt.timeWindow)
assertEquals(4, leaves.nonces.size) mt.verify()
assertTrue(mt.verify())
} }
@Test @Test
@ -140,25 +138,15 @@ class PartialMerkleTreeTest : TestDependencyInjectionBase() {
@Test @Test
fun `nothing filtered`() { fun `nothing filtered`() {
val mt = testTx.buildFilteredTransaction(Predicate { false }) val ftxNothing = testTx.buildFilteredTransaction(Predicate { false })
assertTrue(mt.filteredLeaves.attachments.isEmpty()) assertTrue(ftxNothing.componentGroups.isEmpty())
assertTrue(mt.filteredLeaves.commands.isEmpty()) assertTrue(ftxNothing.attachments.isEmpty())
assertTrue(mt.filteredLeaves.inputs.isEmpty()) assertTrue(ftxNothing.commands.isEmpty())
assertTrue(mt.filteredLeaves.outputs.isEmpty()) assertTrue(ftxNothing.inputs.isEmpty())
assertTrue(mt.filteredLeaves.timeWindow == null) assertTrue(ftxNothing.outputs.isEmpty())
assertTrue(mt.filteredLeaves.availableComponents.isEmpty()) assertNull(ftxNothing.timeWindow)
assertTrue(mt.filteredLeaves.availableComponentHashes.isEmpty()) assertTrue(ftxNothing.availableComponentGroups.flatten().isEmpty())
assertTrue(mt.filteredLeaves.nonces.isEmpty()) ftxNothing.verify() // We allow empty ftx transactions (eg from a timestamp authority that blindly signs).
assertFailsWith<MerkleTreeException> { mt.verify() }
// Including only privacySalt still results to an empty FilteredTransaction.
fun filterPrivacySalt(elem: Any): Boolean = elem is PrivacySalt
val mt2 = testTx.buildFilteredTransaction(Predicate(::filterPrivacySalt))
assertTrue(mt2.filteredLeaves.privacySalt == null)
assertTrue(mt2.filteredLeaves.availableComponents.isEmpty())
assertTrue(mt2.filteredLeaves.availableComponentHashes.isEmpty())
assertTrue(mt2.filteredLeaves.nonces.isEmpty())
assertFailsWith<MerkleTreeException> { mt2.verify() }
} }
// Partial Merkle Tree building tests. // Partial Merkle Tree building tests.

View File

@ -154,6 +154,40 @@ UNRELEASED
* Moved ``CityDatabase`` out of ``core`` and into ``finance`` * Moved ``CityDatabase`` out of ``core`` and into ``finance``
* All of the ``serializedHash`` and ``computeNonce`` functions have been removed from ``MerkleTransaction``.
The ``serializedHash(x: T)`` and ``computeNonce`` were moved to ``CryptoUtils``.
* Two overloaded methods ``componentHash(opaqueBytes: OpaqueBytes, privacySalt: PrivacySalt, componentGroupIndex: Int,
internalIndex: Int): SecureHash`` and ``componentHash(nonce: SecureHash, opaqueBytes: OpaqueBytes): SecureHash`` have
been added to ``CryptoUtils``. Similarly to ``computeNonce``, they internally use SHA256d for nonce and leaf hash
computations.
* The ``verify(node: PartialTree, usedHashes: MutableList<SecureHash>): SecureHash`` in ``PartialMerkleTree`` has been
renamed to ``rootAndUsedHashes`` and is now public, as it is required in the verify function of ``FilteredTransaction``.
* ``TraversableTransaction`` is now an abstract class extending ``CoreTransaction``. ``WireTransaction`` and
``FilteredTransaction`` now extend ``TraversableTransaction``.
* Two classes, ``ComponentGroup(open val groupIndex: Int, open val components: List<OpaqueBytes>)`` and
``FilteredComponentGroup(override val groupIndex: Int, override val components: List<OpaqueBytes>,
val nonces: List<SecureHash>, val partialMerkleTree: PartialMerkleTree): ComponentGroup(groupIndex, components)``
have been added, which are properties of the ``WireTransaction`` and ``FilteredTransaction``, respectively.
* ``checkAllComponentsVisible(componentGroupEnum: ComponentGroupEnum)`` is added to ``FilteredTransaction``, a new
function to check if all components are visible in a specific component-group.
* To allow for backwards compatibility, ``WireTransaction`` and ``FilteredTransaction`` have new fields and
constructors: ``WireTransaction(componentGroups: List<ComponentGroup>, privacySalt: PrivacySalt = PrivacySalt())``,
``FilteredTransaction private constructor(id: SecureHash,filteredComponentGroups: List<FilteredComponentGroup>,
groupHashes: List<SecureHash>``. ``FilteredTransaction`` is still built via
``buildFilteredTransaction(wtx: WireTransaction, filtering: Predicate<Any>).
* ``FilteredLeaves`` class have been removed and as a result we can directly call the components from
``FilteredTransaction``, such as ``ftx.inputs`` Vs the old ``ftx.filteredLeaves.inputs``.
* A new ``ComponentGroupEnum`` is added with the following enum items: ``INPUTS_GROUP``, ``OUTPUTS_GROUP``,
``COMMANDS_GROUP``, ``ATTACHMENTS_GROUP``, ``NOTARY_GROUP``, ``TIMEWINDOW_GROUP``.
Milestone 14 Milestone 14
------------ ------------

View File

@ -7,20 +7,15 @@ import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer
import com.esotericsoftware.kryo.serializers.FieldSerializer import com.esotericsoftware.kryo.serializers.FieldSerializer
import com.esotericsoftware.kryo.util.MapReferenceResolver import com.esotericsoftware.kryo.util.MapReferenceResolver
import net.corda.core.contracts.* import net.corda.core.contracts.PrivacySalt
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.TransactionSignature
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.nodeapi.internal.AttachmentsClassLoader
import net.corda.core.serialization.MissingAttachmentsException
import net.corda.core.serialization.SerializeAsTokenContext import net.corda.core.serialization.SerializeAsTokenContext
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.*
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.i2p.crypto.eddsa.EdDSAPrivateKey import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSAPublicKey import net.i2p.crypto.eddsa.EdDSAPublicKey
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
@ -241,42 +236,15 @@ fun Input.readBytesWithLength(): ByteArray {
@ThreadSafe @ThreadSafe
object WireTransactionSerializer : Serializer<WireTransaction>() { object WireTransactionSerializer : Serializer<WireTransaction>() {
override fun write(kryo: Kryo, output: Output, obj: WireTransaction) { override fun write(kryo: Kryo, output: Output, obj: WireTransaction) {
kryo.writeClassAndObject(output, obj.inputs) kryo.writeClassAndObject(output, obj.componentGroups)
kryo.writeClassAndObject(output, obj.attachments)
kryo.writeClassAndObject(output, obj.outputs)
kryo.writeClassAndObject(output, obj.commands)
kryo.writeClassAndObject(output, obj.notary)
kryo.writeClassAndObject(output, obj.timeWindow)
kryo.writeClassAndObject(output, obj.privacySalt) kryo.writeClassAndObject(output, obj.privacySalt)
} }
private fun attachmentsClassLoader(kryo: Kryo, attachmentHashes: List<SecureHash>): ClassLoader? {
kryo.context[attachmentsClassLoaderEnabledPropertyName] as? Boolean ?: false || return null
val serializationContext = kryo.serializationContext() ?: return null // Some tests don't set one.
val missing = ArrayList<SecureHash>()
val attachments = ArrayList<Attachment>()
attachmentHashes.forEach { id ->
serializationContext.serviceHub.attachments.openAttachment(id)?.let { attachments += it } ?: run { missing += id }
}
missing.isNotEmpty() && throw MissingAttachmentsException(missing)
return AttachmentsClassLoader(attachments)
}
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun read(kryo: Kryo, input: Input, type: Class<WireTransaction>): WireTransaction { override fun read(kryo: Kryo, input: Input, type: Class<WireTransaction>): WireTransaction {
val inputs = kryo.readClassAndObject(input) as List<StateRef> val componentGroups = kryo.readClassAndObject(input) as List<ComponentGroup>
val attachmentHashes = kryo.readClassAndObject(input) as List<SecureHash>
// If we're deserialising in the sandbox context, we use our special attachments classloader.
// Otherwise we just assume the code we need is on the classpath already.
kryo.useClassLoader(attachmentsClassLoader(kryo, attachmentHashes) ?: javaClass.classLoader) {
val outputs = kryo.readClassAndObject(input) as List<TransactionState<ContractState>>
val commands = kryo.readClassAndObject(input) as List<Command<*>>
val notary = kryo.readClassAndObject(input) as Party?
val timeWindow = kryo.readClassAndObject(input) as TimeWindow?
val privacySalt = kryo.readClassAndObject(input) as PrivacySalt val privacySalt = kryo.readClassAndObject(input) as PrivacySalt
return WireTransaction(inputs, attachmentHashes, outputs, commands, notary, timeWindow, privacySalt) return WireTransaction(componentGroups, privacySalt)
}
} }
} }
@ -410,8 +378,7 @@ inline fun <reified T> readListOfLength(kryo: Kryo, input: Input, minLen: Int =
if (elemCount < minLen) throw KryoException("Cannot deserialize list, too little elements. Minimum required: $minLen, got: $elemCount") if (elemCount < minLen) throw KryoException("Cannot deserialize list, too little elements. Minimum required: $minLen, got: $elemCount")
if (expectedLen != null && elemCount != expectedLen) if (expectedLen != null && elemCount != expectedLen)
throw KryoException("Cannot deserialize list, expected length: $expectedLen, got: $elemCount.") throw KryoException("Cannot deserialize list, expected length: $expectedLen, got: $elemCount.")
val list = (1..elemCount).map { kryo.readClassAndObject(input) as T } return (1..elemCount).map { kryo.readClassAndObject(input) as T }
return list
} }
/** /**

View File

@ -21,7 +21,6 @@ import net.corda.nodeapi.internal.serialization.withTokenContext
import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP
import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.kryoSpecific
import net.corda.testing.node.MockAttachmentStorage import net.corda.testing.node.MockAttachmentStorage
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.junit.Assert import org.junit.Assert
@ -75,7 +74,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
} }
} }
fun importJar(storage: AttachmentStorage) = ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it) } private fun importJar(storage: AttachmentStorage) = ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it) }
// These ClassLoaders work together to load 'AnotherDummyContract' in a disposable way, such that even though // These ClassLoaders work together to load 'AnotherDummyContract' in a disposable way, such that even though
// the class may be on the unit test class path (due to default IDE settings, etc), it won't be loaded into the // the class may be on the unit test class path (due to default IDE settings, etc), it won't be loaded into the
@ -83,10 +82,10 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
// ensures we have precise control over where it's loaded. // ensures we have precise control over where it's loaded.
object FilteringClassLoader : ClassLoader() { object FilteringClassLoader : ClassLoader() {
override fun loadClass(name: String, resolve: Boolean): Class<*>? { override fun loadClass(name: String, resolve: Boolean): Class<*>? {
if ("AnotherDummyContract" in name) { return if ("AnotherDummyContract" in name) {
return null null
} else } else
return super.loadClass(name, resolve) super.loadClass(name, resolve)
} }
} }
@ -102,7 +101,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value) assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
} }
fun fakeAttachment(filepath: String, content: String): ByteArray { private fun fakeAttachment(filepath: String, content: String): ByteArray {
val bs = ByteArrayOutputStream() val bs = ByteArrayOutputStream()
val js = JarOutputStream(bs) val js = JarOutputStream(bs)
js.putNextEntry(ZipEntry(filepath)) js.putNextEntry(ZipEntry(filepath))
@ -112,7 +111,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
return bs.toByteArray() return bs.toByteArray()
} }
fun readAttachment(attachment: Attachment, filepath: String): ByteArray { private fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
ByteArrayOutputStream().use { ByteArrayOutputStream().use {
attachment.extractFile(filepath, it) attachment.extractFile(filepath, it)
return it.toByteArray() return it.toByteArray()
@ -193,7 +192,6 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value) assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
} }
@Test @Test
fun `verify that contract DummyContract is in classPath`() { fun `verify that contract DummyContract is in classPath`() {
val contractClass = Class.forName("net.corda.nodeapi.AttachmentsClassLoaderTests\$AttachmentDummyContract") val contractClass = Class.forName("net.corda.nodeapi.AttachmentsClassLoaderTests\$AttachmentDummyContract")
@ -202,7 +200,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
assertNotNull(contract) assertNotNull(contract)
} }
fun createContract2Cash(): Contract { private fun createContract2Cash(): Contract {
val cl = ClassLoaderForTests() val cl = ClassLoaderForTests()
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, cl) val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, cl)
return contractClass.newInstance() as Contract return contractClass.newInstance() as Contract
@ -320,8 +318,8 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
val bytes = run { val bytes = run {
val attachmentRef = importJar(storage) val attachmentRef = importJar(storage)
tx.addAttachment(storage.openAttachment(attachmentRef)!!.id) tx.addAttachment(storage.openAttachment(attachmentRef)!!.id)
val wireTransaction = tx.toWireTransaction() val wireTransaction = tx.toWireTransaction(serializationContext = context)
wireTransaction.serialize(context = context) wireTransaction.serialize()
} }
val copiedWireTransaction = bytes.deserialize(context = context) val copiedWireTransaction = bytes.deserialize(context = context)
assertEquals(1, copiedWireTransaction.outputs.size) assertEquals(1, copiedWireTransaction.outputs.size)
@ -334,12 +332,15 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
@Test @Test
fun `test deserialize of WireTransaction where contract cannot be found`() { fun `test deserialize of WireTransaction where contract cannot be found`() {
kryoSpecific<AttachmentsClassLoaderTests>("Kryo verifies/loads attachments on deserialization, whereas AMQP currently does not") {
val child = ClassLoaderForTests() val child = ClassLoaderForTests()
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, child) val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, child)
val contract = contractClass.newInstance() as DummyContractBackdoor val contract = contractClass.newInstance() as DummyContractBackdoor
val tx = contract.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY) val tx = contract.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY)
val storage = MockAttachmentStorage() val storage = MockAttachmentStorage()
val context = SerializationFactory.defaultFactory.defaultContext.withWhitelisted(contract.javaClass)
.withWhitelisted(Class.forName("net.corda.finance.contracts.isolated.AnotherDummyContract\$State", true, child))
.withWhitelisted(Class.forName("net.corda.finance.contracts.isolated.AnotherDummyContract\$Commands\$Create", true, child))
.withAttachmentStorage(storage)
// todo - think about better way to push attachmentStorage down to serializer // todo - think about better way to push attachmentStorage down to serializer
val attachmentRef = importJar(storage) val attachmentRef = importJar(storage)
@ -347,9 +348,8 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
tx.addAttachment(storage.openAttachment(attachmentRef)!!.id) tx.addAttachment(storage.openAttachment(attachmentRef)!!.id)
val wireTransaction = tx.toWireTransaction() val wireTransaction = tx.toWireTransaction(serializationContext = context)
wireTransaction.serialize()
wireTransaction.serialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentStorage(storage))
} }
// use empty attachmentStorage // use empty attachmentStorage
@ -363,7 +363,6 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
} }
assertEquals(attachmentRef, e.ids.single()) assertEquals(attachmentRef, e.ids.single())
} }
}
@Test @Test
fun `test loading a class from attachment during deserialization`() { fun `test loading a class from attachment during deserialization`() {
@ -395,10 +394,10 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
val storage = MockAttachmentStorage() val storage = MockAttachmentStorage()
val attachmentRef = SecureHash.randomSHA256() val attachmentRef = SecureHash.randomSHA256()
val outboundContext = SerializationFactory.defaultFactory.defaultContext.withClassLoader(child) val outboundContext = SerializationFactory.defaultFactory.defaultContext.withClassLoader(child)
// Serialize with custom context to avoid populating the default context with the specially loaded class // Serialize with custom context to avoid populating the default context with the specially loaded class.
val serialized = contract.serialize(context = outboundContext) val serialized = contract.serialize(context = outboundContext)
// Then deserialize with the attachment class loader associated with the attachment // Then deserialize with the attachment class loader associated with the attachment.
val e = assertFailsWith(MissingAttachmentsException::class) { val e = assertFailsWith(MissingAttachmentsException::class) {
// We currently ignore annotations in attachments, so manually whitelist. // We currently ignore annotations in attachments, so manually whitelist.
val inboundContext = SerializationFactory val inboundContext = SerializationFactory

View File

@ -138,9 +138,9 @@ class BFTNonValidatingNotaryService(override val services: ServiceHubInternal, c
fun verifyAndCommitTx(ftx: FilteredTransaction, callerIdentity: Party): BFTSMaRt.ReplicaResponse { fun verifyAndCommitTx(ftx: FilteredTransaction, callerIdentity: Party): BFTSMaRt.ReplicaResponse {
return try { return try {
val id = ftx.id val id = ftx.id
val inputs = ftx.filteredLeaves.inputs val inputs = ftx.inputs
validateTimeWindow(ftx.filteredLeaves.timeWindow) validateTimeWindow(ftx.timeWindow)
commitInputStates(inputs, id, callerIdentity) commitInputStates(inputs, id, callerIdentity)
log.debug { "Inputs committed successfully, signing $id" } log.debug { "Inputs committed successfully, signing $id" }
BFTSMaRt.ReplicaResponse.Signature(sign(ftx)) BFTSMaRt.ReplicaResponse.Signature(sign(ftx))

View File

@ -1,6 +1,7 @@
package net.corda.node.services.transactions package net.corda.node.services.transactions
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.ComponentGroupEnum
import net.corda.core.flows.NotaryFlow import net.corda.core.flows.NotaryFlow
import net.corda.core.flows.TransactionParts import net.corda.core.flows.TransactionParts
import net.corda.core.identity.Party import net.corda.core.identity.Party
@ -24,7 +25,9 @@ class NonValidatingNotaryFlow(otherSide: Party, service: TrustedAuthorityNotaryS
when (it) { when (it) {
is FilteredTransaction -> { is FilteredTransaction -> {
it.verify() it.verify()
TransactionParts(it.id, it.filteredLeaves.inputs, it.filteredLeaves.timeWindow) it.checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP)
it.checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP)
TransactionParts(it.id, it.inputs, it.timeWindow)
} }
is NotaryChangeWireTransaction -> TransactionParts(it.id, it.inputs, null) is NotaryChangeWireTransaction -> TransactionParts(it.id, it.inputs, null)
else -> { else -> {

View File

@ -129,10 +129,8 @@ object NodeInterestRates {
// It will be fixed by adding partial signatures later. // It will be fixed by adding partial signatures later.
// DOCSTART 1 // DOCSTART 1
fun sign(ftx: FilteredTransaction): TransactionSignature { fun sign(ftx: FilteredTransaction): TransactionSignature {
if (!ftx.verify()) { ftx.verify()
throw MerkleTreeException("Rate Fix Oracle: Couldn't verify partial Merkle tree.") // Performing validation of obtained filtered components.
}
// Performing validation of obtained FilteredLeaves.
fun commandValidator(elem: Command<*>): Boolean { fun commandValidator(elem: Command<*>): Boolean {
require(services.myInfo.legalIdentities.first().owningKey in elem.signers && elem.value is Fix) { require(services.myInfo.legalIdentities.first().owningKey in elem.signers && elem.value is Fix) {
"Oracle received unknown command (not in signers or not Fix)." "Oracle received unknown command (not in signers or not Fix)."
@ -151,8 +149,7 @@ object NodeInterestRates {
} }
} }
val leaves = ftx.filteredLeaves require(ftx.checkWithFun(::check))
require(leaves.checkWithFun(::check))
// It all checks out, so we can return a signature. // It all checks out, so we can return a signature.
// //

View File

@ -193,7 +193,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() {
val tx = makeFullTx() val tx = makeFullTx()
val wtx = tx.toWireTransaction() val wtx = tx.toWireTransaction()
val ftx = wtx.buildFilteredTransaction(Predicate { false }) val ftx = wtx.buildFilteredTransaction(Predicate { false })
assertFailsWith<MerkleTreeException> { oracle.sign(ftx) } assertFailsWith<IllegalArgumentException> { oracle.sign(ftx) } // It throws failed requirement (as it is empty there is no command to check and sign).
} }
@Test @Test