mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
[CORDA-694] Commands visibility for Oracles (without sacrificing privacy) (#1835)
new checkCommandVisibility feature for Oracles
This commit is contained in:
parent
717365413d
commit
479ab9a36a
@ -10,5 +10,6 @@ enum class ComponentGroupEnum {
|
||||
COMMANDS_GROUP, // ordinal = 2.
|
||||
ATTACHMENTS_GROUP, // ordinal = 3.
|
||||
NOTARY_GROUP, // ordinal = 4.
|
||||
TIMEWINDOW_GROUP // ordinal = 5.
|
||||
TIMEWINDOW_GROUP, // ordinal = 5.
|
||||
SIGNERS_GROUP // ordinal = 6.
|
||||
}
|
||||
|
@ -158,4 +158,41 @@ class PartialMerkleTree(val root: PartialTree) {
|
||||
return false
|
||||
return (verifyRoot == merkleRootHash)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to return the index of the input leaf in the partial Merkle tree structure.
|
||||
* @param leaf the component hash to check.
|
||||
* @return leaf-index of this component (starting from zero).
|
||||
* @throws MerkleTreeException if the provided hash is not in the tree.
|
||||
*/
|
||||
@Throws(MerkleTreeException::class)
|
||||
internal fun leafIndex(leaf: SecureHash): Int {
|
||||
// Special handling if the tree consists of one node only.
|
||||
if (root is PartialTree.IncludedLeaf && root.hash == leaf) return 0
|
||||
val flagPath = mutableListOf<Boolean>()
|
||||
if (!leafIndexHelper(leaf, this.root, flagPath)) throw MerkleTreeException("The provided hash $leaf is not in the tree.")
|
||||
return indexFromFlagPath(flagPath)
|
||||
}
|
||||
|
||||
// Helper function to compute the path. False means go to the left and True to the right.
|
||||
// Because the path is updated recursively, the path is returned in reverse order.
|
||||
private fun leafIndexHelper(leaf: SecureHash, node: PartialTree, path: MutableList<Boolean>): Boolean {
|
||||
if (node is PartialTree.IncludedLeaf) {
|
||||
return node.hash == leaf
|
||||
} else if (node is PartialTree.Node) {
|
||||
if (leafIndexHelper(leaf, node.left, path)) {
|
||||
path.add(false)
|
||||
return true
|
||||
}
|
||||
if (leafIndexHelper(leaf, node.right, path)) {
|
||||
path.add(true)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Return the leaf index from the path boolean list.
|
||||
private fun indexFromFlagPath(pathList: List<Boolean>) =
|
||||
pathList.mapIndexed { index, value -> if (value) (1 shl index) else 0 }.sum()
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
|
||||
override val outputs: List<TransactionState<ContractState>> = deserialiseComponentGroup(ComponentGroupEnum.OUTPUTS_GROUP, { SerializedBytes<TransactionState<ContractState>>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) })
|
||||
|
||||
/** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */
|
||||
val commands: List<Command<*>> = deserialiseComponentGroup(ComponentGroupEnum.COMMANDS_GROUP, { SerializedBytes<Command<*>>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) })
|
||||
val commands: List<Command<*>> = deserialiseCommands()
|
||||
|
||||
override val notary: Party? = let {
|
||||
val notaries: List<Party> = deserialiseComponentGroup(ComponentGroupEnum.NOTARY_GROUP, { SerializedBytes<Party>(it).deserialize() })
|
||||
@ -74,6 +74,31 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
// Method to deserialise Commands from its two groups:
|
||||
// COMMANDS_GROUP which contains the CommandData part
|
||||
// and SIGNERS_GROUP which contains the Signers part.
|
||||
private fun deserialiseCommands(): List<Command<*>> {
|
||||
// TODO: we could avoid deserialising unrelated signers.
|
||||
// However, current approach ensures the transaction is not malformed
|
||||
// and it will throw if any of the signers objects is not List of public keys).
|
||||
val signersList = deserialiseComponentGroup(ComponentGroupEnum.SIGNERS_GROUP, { SerializedBytes<List<PublicKey>>(it).deserialize() })
|
||||
val commandDataList = deserialiseComponentGroup(ComponentGroupEnum.COMMANDS_GROUP, { SerializedBytes<CommandData>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) })
|
||||
val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal }
|
||||
if (group is FilteredComponentGroup) {
|
||||
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 leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) }
|
||||
if (leafIndices.isNotEmpty())
|
||||
check(leafIndices.max()!! < signersList.size) { "Invalid Transaction. A command with no corresponding signer detected" }
|
||||
return commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[leafIndices[index]]) }
|
||||
} else {
|
||||
// It is a WireTransaction
|
||||
// or a FilteredTransaction with no Commands (in which case group is null).
|
||||
check(commandDataList.size == signersList.size) { "Invalid Transaction. Sizes of CommandData (${commandDataList.size}) and Signers (${signersList.size}) do not match" }
|
||||
return commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[index]) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -111,11 +136,12 @@ class FilteredTransaction private constructor(
|
||||
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.
|
||||
var signersIncluded = false
|
||||
|
||||
fun <T : Any> filter(t: T, componentGroupIndex: Int, internalIndex: Int) {
|
||||
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
|
||||
// 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) {
|
||||
@ -132,6 +158,17 @@ class FilteredTransaction private constructor(
|
||||
filteredComponentNonces[componentGroupIndex]!!.add(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex])
|
||||
filteredComponentHashes[componentGroupIndex]!!.add(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex])
|
||||
}
|
||||
// If at least one command is visible, then all command-signers should be visible as well.
|
||||
// This is required for visibility purposes, see FilteredTransaction.checkAllCommandsVisible() for more details.
|
||||
if (componentGroupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal && !signersIncluded) {
|
||||
signersIncluded = true
|
||||
val signersGroupIndex = ComponentGroupEnum.SIGNERS_GROUP.ordinal
|
||||
// There exist commands, thus the signers group is not empty.
|
||||
val signersGroupComponents = wtx.componentGroups.first { it.groupIndex == signersGroupIndex }
|
||||
filteredSerialisedComponents.put(signersGroupIndex, signersGroupComponents.components.toMutableList())
|
||||
filteredComponentNonces.put(signersGroupIndex, wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList())
|
||||
filteredComponentHashes.put(signersGroupIndex, wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,6 +179,10 @@ class FilteredTransaction private constructor(
|
||||
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 is highlighted that because there is no a signers property in TraversableTransaction,
|
||||
// one cannot specifically filter them in or out.
|
||||
// The above is very important to ensure someone won't filter out the signers component group if at least one
|
||||
// command is included in a FilteredTransaction.
|
||||
|
||||
// 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.
|
||||
@ -207,7 +248,9 @@ class FilteredTransaction private constructor(
|
||||
/**
|
||||
* 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.
|
||||
* It might also be applied in Oracles or any other entity requiring [Command] visibility, but because this method
|
||||
* cannot distinguish between related and unrelated to the signer [Command]s, one should use the
|
||||
* [checkCommandVisibility] method, which is specifically designed for [Command] visibility purposes.
|
||||
* 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
|
||||
@ -229,18 +272,54 @@ class FilteredTransaction private constructor(
|
||||
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}" }
|
||||
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) { "Transaction is malformed. Top level Merkle tree cannot be verified against transaction's id" }
|
||||
}
|
||||
}
|
||||
|
||||
inline private fun verificationCheck(value: Boolean, lazyMessage: () -> Any): Unit {
|
||||
/**
|
||||
* Function that checks if all of the commands that should be signed by the input public key are visible.
|
||||
* This functionality is required from Oracles to check that all of the commands they should sign are visible.
|
||||
* This algorithm uses the [ComponentGroupEnum.SIGNERS_GROUP] to count how many commands should be signed by the
|
||||
* input [PublicKey] and it then matches it with the size of received [commands].
|
||||
* Note that this method does not throw if there are no commands for this key to sign in the original [WireTransaction].
|
||||
* @param publicKey signer's [PublicKey]
|
||||
* @throws ComponentVisibilityException if not all of the related commands are visible.
|
||||
*/
|
||||
@Throws(ComponentVisibilityException::class)
|
||||
fun checkCommandVisibility(publicKey: PublicKey) {
|
||||
val commandSigners = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.SIGNERS_GROUP.ordinal }
|
||||
val expectedNumOfCommands = expectedNumOfCommands(publicKey, commandSigners)
|
||||
val receivedForThisKeyNumOfCommands = commands.filter { publicKey in it.signers }.size
|
||||
visibilityCheck(expectedNumOfCommands == receivedForThisKeyNumOfCommands) { "$expectedNumOfCommands commands were expected, but received $receivedForThisKeyNumOfCommands" }
|
||||
}
|
||||
|
||||
// Function to return number of expected commands to sign.
|
||||
private fun expectedNumOfCommands(publicKey: PublicKey, commandSigners: ComponentGroup?): Int {
|
||||
checkAllComponentsVisible(ComponentGroupEnum.SIGNERS_GROUP)
|
||||
if (commandSigners == null) return 0
|
||||
fun signersKeys (internalIndex: Int, opaqueBytes: OpaqueBytes): List<PublicKey> {
|
||||
try {
|
||||
return SerializedBytes<List<PublicKey>>(opaqueBytes.bytes).deserialize()
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Malformed transaction, signers at index $internalIndex cannot be deserialised", e)
|
||||
}
|
||||
}
|
||||
|
||||
return commandSigners.components
|
||||
.mapIndexed { internalIndex, opaqueBytes -> signersKeys(internalIndex, opaqueBytes) }
|
||||
.filter { signers -> publicKey in signers }.size
|
||||
}
|
||||
|
||||
inline private fun verificationCheck(value: Boolean, lazyMessage: () -> Any) {
|
||||
if (!value) {
|
||||
val message = lazyMessage()
|
||||
throw FilteredTransactionVerificationException(id, message.toString())
|
||||
}
|
||||
}
|
||||
|
||||
inline private fun visibilityCheck(value: Boolean, lazyMessage: () -> Any): Unit {
|
||||
inline private fun visibilityCheck(value: Boolean, lazyMessage: () -> Any) {
|
||||
if (!value) {
|
||||
val message = lazyMessage()
|
||||
throw ComponentVisibilityException(id, message.toString())
|
||||
|
@ -6,9 +6,10 @@ import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.util.function.Predicate
|
||||
@ -213,10 +214,14 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
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() }))
|
||||
// Adding commandData only to the commands group. Signers are added in their own group.
|
||||
if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.value.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())))
|
||||
// Adding signers to their own group. This is required for command visibility purposes: a party receiving
|
||||
// a FilteredTransaction can now verify it sees all the commands it should sign.
|
||||
if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(SIGNERS_GROUP.ordinal, commands.map { it.signers.serialize() }))
|
||||
return componentGroupMap
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,9 @@
|
||||
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.crypto.*
|
||||
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.transactions.*
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
@ -34,22 +30,24 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
|
||||
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 commandGroup by lazy { ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.value.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 signersGroup by lazy { ComponentGroup(SIGNERS_GROUP.ordinal, commands.map { it.signers.serialize() }) }
|
||||
|
||||
private val newUnknownComponentGroup = ComponentGroup(20, listOf(OpaqueBytes(secureRandomBytes(4)), OpaqueBytes(secureRandomBytes(8))))
|
||||
private val newUnknownComponentEmptyGroup = ComponentGroup(21, emptyList())
|
||||
private val newUnknownComponentGroup = ComponentGroup(100, listOf(OpaqueBytes(secureRandomBytes(4)), OpaqueBytes(secureRandomBytes(8))))
|
||||
private val newUnknownComponentEmptyGroup = ComponentGroup(101, emptyList())
|
||||
|
||||
// Do not add attachments (empty list).
|
||||
private val componentGroupsA by lazy {
|
||||
listOf(
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
commandGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
commandGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
signersGroup
|
||||
)
|
||||
}
|
||||
private val wireTransactionA by lazy { WireTransaction(componentGroups = componentGroupsA, privacySalt = privacySalt) }
|
||||
@ -74,7 +72,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
commandGroup,
|
||||
attachmentGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup
|
||||
timeWindowGroup,
|
||||
signersGroup
|
||||
)
|
||||
assertFails { WireTransaction(componentGroups = componentGroupsEmptyAttachment, privacySalt = privacySalt) }
|
||||
|
||||
@ -86,7 +85,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
outputGroup,
|
||||
commandGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup
|
||||
timeWindowGroup,
|
||||
signersGroup
|
||||
)
|
||||
val wireTransaction1ShuffledInputs = WireTransaction(componentGroups = componentGroupsB, privacySalt = privacySalt)
|
||||
// The ID has changed due to change of the internal ordering in inputs.
|
||||
@ -106,7 +106,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
inputGroup,
|
||||
commandGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup
|
||||
timeWindowGroup,
|
||||
signersGroup
|
||||
)
|
||||
assertEquals(wireTransactionA, WireTransaction(componentGroups = shuffledComponentGroupsA, privacySalt = privacySalt))
|
||||
}
|
||||
@ -123,7 +124,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
commandGroup,
|
||||
ComponentGroup(ATTACHMENTS_GROUP.ordinal, inputGroup.components),
|
||||
notaryGroup,
|
||||
timeWindowGroup
|
||||
timeWindowGroup,
|
||||
signersGroup
|
||||
)
|
||||
assertFails { WireTransaction(componentGroupsB, privacySalt) }
|
||||
|
||||
@ -134,7 +136,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
commandGroup, // First commandsGroup.
|
||||
commandGroup, // Second commandsGroup.
|
||||
notaryGroup,
|
||||
timeWindowGroup
|
||||
timeWindowGroup,
|
||||
signersGroup
|
||||
)
|
||||
assertFails { WireTransaction(componentGroupsDuplicatedCommands, privacySalt) }
|
||||
|
||||
@ -144,7 +147,8 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
outputGroup,
|
||||
commandGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup
|
||||
timeWindowGroup,
|
||||
signersGroup
|
||||
)
|
||||
assertFails { WireTransaction(componentGroupsC, privacySalt) }
|
||||
|
||||
@ -154,23 +158,24 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
commandGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
newUnknownComponentGroup // A new unknown component with ordinal 20 that we cannot process.
|
||||
signersGroup,
|
||||
newUnknownComponentGroup // A new unknown component with ordinal 100 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).
|
||||
// The old client will throw if receiving an empty component (even if this is unknown).
|
||||
val componentGroupsCompatibleEmptyNew = listOf(
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
commandGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
newUnknownComponentEmptyGroup // A new unknown component with ordinal 21 that we cannot process.
|
||||
signersGroup,
|
||||
newUnknownComponentEmptyGroup // A new unknown component with ordinal 101 that we cannot process.
|
||||
)
|
||||
assertFails { WireTransaction(componentGroupsCompatibleEmptyNew, privacySalt) }
|
||||
}
|
||||
@ -179,7 +184,9 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
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.
|
||||
// Although nothing filtered, we still receive the group hashes for the top level Merkle tree.
|
||||
// Note that attachments are not sent, but group hashes include the allOnesHash flag for the attachment group hash; that's why we expect +1 group hashes.
|
||||
assertEquals(wireTransactionA.componentGroups.size + 1, ftxNothing.groupHashes.size)
|
||||
ftxNothing.verify()
|
||||
|
||||
// Include all of the components.
|
||||
@ -191,6 +198,7 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
ftxAll.checkAllComponentsVisible(ATTACHMENTS_GROUP)
|
||||
ftxAll.checkAllComponentsVisible(NOTARY_GROUP)
|
||||
ftxAll.checkAllComponentsVisible(TIMEWINDOW_GROUP)
|
||||
ftxAll.checkAllComponentsVisible(SIGNERS_GROUP)
|
||||
|
||||
// Filter inputs only.
|
||||
fun filtering(elem: Any): Boolean {
|
||||
@ -222,12 +230,14 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
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,
|
||||
val componentGroupsCompatibleA = listOf(
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
commandGroup,
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
newUnknownComponentGroup // A new unknown component with ordinal 10,000 that we cannot process.
|
||||
signersGroup,
|
||||
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
|
||||
)
|
||||
val wireTransactionCompatibleA = WireTransaction(componentGroupsCompatibleA, privacySalt)
|
||||
val ftxCompatible = wireTransactionCompatibleA.buildFilteredTransaction(Predicate(::filtering))
|
||||
@ -245,9 +255,288 @@ class CompatibleTransactionTests : TestDependencyInjectionBase() {
|
||||
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)
|
||||
// Check we received the last element that we cannot process (backwards compatibility).
|
||||
assertEquals(wireTransactionCompatibleA.componentGroups.size, ftxCompatibleAll.filteredComponentGroups.size)
|
||||
|
||||
// Hide one component group only.
|
||||
// Filter inputs only.
|
||||
fun filterOutInputs(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is StateRef -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
val ftxCompatibleNoInputs = wireTransactionCompatibleA.buildFilteredTransaction(Predicate(::filterOutInputs))
|
||||
ftxCompatibleNoInputs.verify()
|
||||
assertFailsWith<ComponentVisibilityException> { ftxCompatibleNoInputs.checkAllComponentsVisible(INPUTS_GROUP) }
|
||||
assertEquals(wireTransactionCompatibleA.componentGroups.size - 1, ftxCompatibleNoInputs.filteredComponentGroups.size)
|
||||
assertEquals(wireTransactionCompatibleA.componentGroups.map { it.groupIndex }.max()!!, ftxCompatibleNoInputs.groupHashes.size - 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Command visibility tests`() {
|
||||
// 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))
|
||||
val componentGroups = listOf(
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
ComponentGroup(COMMANDS_GROUP.ordinal, twoCommandsforKey1.map { it.value.serialize() }),
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() }),
|
||||
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
|
||||
)
|
||||
val wtx = WireTransaction(componentGroups = componentGroups, privacySalt = PrivacySalt())
|
||||
|
||||
// Filter all commands.
|
||||
fun filterCommandsOnly(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> true // Even if one Command is filtered, all signers are automatically filtered as well
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out commands only.
|
||||
fun filterOutCommands(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
// Filter KEY_1 commands.
|
||||
fun filterKEY1Commands(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> DUMMY_KEY_1.public in elem.signers
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
// Filter only one KEY_1 command.
|
||||
fun filterTwoSignersCommands(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> elem.signers.size == 2 // dummyCommand(DUMMY_KEY_1.public) is filtered out.
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
// Again filter only one KEY_1 command.
|
||||
fun filterSingleSignersCommands(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> elem.signers.size == 1 // dummyCommand(DUMMY_KEY_1.public, DUMMY_KEY_2.public) is filtered out.
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val allCommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterCommandsOnly))
|
||||
val noCommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterOutCommands))
|
||||
val key1CommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterKEY1Commands))
|
||||
val oneKey1CommandFtxA = wtx.buildFilteredTransaction(Predicate(::filterTwoSignersCommands))
|
||||
val oneKey1CommandFtxB = wtx.buildFilteredTransaction(Predicate(::filterSingleSignersCommands))
|
||||
|
||||
allCommandsFtx.checkCommandVisibility(DUMMY_KEY_1.public)
|
||||
assertFailsWith<ComponentVisibilityException> { noCommandsFtx.checkCommandVisibility(DUMMY_KEY_1.public) }
|
||||
key1CommandsFtx.checkCommandVisibility(DUMMY_KEY_1.public)
|
||||
assertFailsWith<ComponentVisibilityException> { oneKey1CommandFtxA.checkCommandVisibility(DUMMY_KEY_1.public) }
|
||||
assertFailsWith<ComponentVisibilityException> { oneKey1CommandFtxB.checkCommandVisibility(DUMMY_KEY_1.public) }
|
||||
|
||||
allCommandsFtx.checkAllComponentsVisible(SIGNERS_GROUP)
|
||||
assertFailsWith<ComponentVisibilityException> { noCommandsFtx.checkAllComponentsVisible(SIGNERS_GROUP) } // If we filter out all commands, signers are not sent as well.
|
||||
key1CommandsFtx.checkAllComponentsVisible(SIGNERS_GROUP) // If at least one Command is visible, then all Signers are visible.
|
||||
oneKey1CommandFtxA.checkAllComponentsVisible(SIGNERS_GROUP) // If at least one Command is visible, then all Signers are visible.
|
||||
oneKey1CommandFtxB.checkAllComponentsVisible(SIGNERS_GROUP) // If at least one Command is visible, then all Signers are visible.
|
||||
|
||||
// We don't send a list of signers.
|
||||
val componentGroupsCompatible = listOf(
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
ComponentGroup(COMMANDS_GROUP.ordinal, twoCommandsforKey1.map { it.value.serialize() }),
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
// ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() }),
|
||||
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
|
||||
)
|
||||
|
||||
// Invalid Transaction. Sizes of CommandData and Signers (empty) do not match.
|
||||
assertFailsWith<IllegalStateException> { WireTransaction(componentGroups = componentGroupsCompatible, privacySalt = PrivacySalt()) }
|
||||
|
||||
// We send smaller list of signers.
|
||||
val componentGroupsLessSigners = listOf(
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
ComponentGroup(COMMANDS_GROUP.ordinal, twoCommandsforKey1.map { it.value.serialize() }),
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() }.subList(0, 1)), // Send first signer only.
|
||||
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
|
||||
)
|
||||
|
||||
// Invalid Transaction. Sizes of CommandData and Signers (empty) do not match.
|
||||
assertFailsWith<IllegalStateException> { WireTransaction(componentGroups = componentGroupsLessSigners, privacySalt = PrivacySalt()) }
|
||||
|
||||
// Test if there is no command to sign.
|
||||
val commandsNoKey1= listOf(dummyCommand(DUMMY_KEY_2.public))
|
||||
|
||||
val componentGroupsNoKey1ToSign = listOf(
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
ComponentGroup(COMMANDS_GROUP.ordinal, commandsNoKey1.map { it.value.serialize() }),
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
ComponentGroup(SIGNERS_GROUP.ordinal, commandsNoKey1.map { it.signers.serialize() }),
|
||||
newUnknownComponentGroup // A new unknown component with ordinal 100 that we cannot process.
|
||||
)
|
||||
|
||||
val wtxNoKey1 = WireTransaction(componentGroups = componentGroupsNoKey1ToSign, privacySalt = PrivacySalt())
|
||||
val allCommandsNoKey1Ftx= wtxNoKey1.buildFilteredTransaction(Predicate(::filterCommandsOnly))
|
||||
allCommandsNoKey1Ftx.checkCommandVisibility(DUMMY_KEY_1.public) // This will pass, because there are indeed no commands to sign in the original transaction.
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FilteredTransaction signer manipulation tests`() {
|
||||
// Required to call the private constructor.
|
||||
val ftxConstructor = FilteredTransaction::class.java.declaredConstructors[1]
|
||||
ftxConstructor.isAccessible = true
|
||||
|
||||
// 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))
|
||||
val componentGroups = listOf(
|
||||
inputGroup,
|
||||
outputGroup,
|
||||
ComponentGroup(COMMANDS_GROUP.ordinal, twoCommandsforKey1.map { it.value.serialize() }),
|
||||
notaryGroup,
|
||||
timeWindowGroup,
|
||||
ComponentGroup(SIGNERS_GROUP.ordinal, twoCommandsforKey1.map { it.signers.serialize() })
|
||||
)
|
||||
val wtx = WireTransaction(componentGroups = componentGroups, privacySalt = PrivacySalt())
|
||||
|
||||
// Filter KEY_1 commands (commands 1 and 3).
|
||||
fun filterKEY1Commands(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> DUMMY_KEY_1.public in elem.signers
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
// Filter KEY_2 commands (commands 1 and 2).
|
||||
fun filterKEY2Commands(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> DUMMY_KEY_2.public in elem.signers
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val key1CommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterKEY1Commands))
|
||||
val key2CommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterKEY2Commands))
|
||||
|
||||
// val commandDataComponents = key1CommandsFtx.filteredComponentGroups[0].components
|
||||
val commandDataHashes = wtx.availableComponentHashes[ComponentGroupEnum.COMMANDS_GROUP.ordinal]!!
|
||||
val noLastCommandDataPMT = PartialMerkleTree.build(
|
||||
MerkleTree.getMerkleTree(commandDataHashes),
|
||||
commandDataHashes.subList(0, 1)
|
||||
)
|
||||
val noLastCommandDataComponents = key1CommandsFtx.filteredComponentGroups[0].components.subList(0, 1)
|
||||
val noLastCommandDataNonces = key1CommandsFtx.filteredComponentGroups[0].nonces.subList(0, 1)
|
||||
val noLastCommandDataGroup = FilteredComponentGroup(
|
||||
ComponentGroupEnum.COMMANDS_GROUP.ordinal,
|
||||
noLastCommandDataComponents,
|
||||
noLastCommandDataNonces,
|
||||
noLastCommandDataPMT
|
||||
)
|
||||
|
||||
val signerComponents = key1CommandsFtx.filteredComponentGroups[1].components
|
||||
val signerHashes = wtx.availableComponentHashes[ComponentGroupEnum.SIGNERS_GROUP.ordinal]!!
|
||||
val noLastSignerPMT = PartialMerkleTree.build(
|
||||
MerkleTree.getMerkleTree(signerHashes),
|
||||
signerHashes.subList(0, 2)
|
||||
)
|
||||
val noLastSignerComponents = key1CommandsFtx.filteredComponentGroups[1].components.subList(0, 2)
|
||||
val noLastSignerNonces = key1CommandsFtx.filteredComponentGroups[1].nonces.subList(0, 2)
|
||||
val noLastSignerGroup = FilteredComponentGroup(
|
||||
ComponentGroupEnum.SIGNERS_GROUP.ordinal,
|
||||
noLastSignerComponents,
|
||||
noLastSignerNonces,
|
||||
noLastSignerPMT
|
||||
)
|
||||
val noLastSignerGroupSamePartialTree = FilteredComponentGroup(
|
||||
ComponentGroupEnum.SIGNERS_GROUP.ordinal,
|
||||
noLastSignerComponents,
|
||||
noLastSignerNonces,
|
||||
key1CommandsFtx.filteredComponentGroups[1].partialMerkleTree) // We don't update that, so we can catch the index mismatch.
|
||||
|
||||
val updatedFilteredComponentsNoSignersKey2 = listOf(key2CommandsFtx.filteredComponentGroups[0], noLastSignerGroup)
|
||||
val updatedFilteredComponentsNoSignersKey2SamePMT = listOf(key2CommandsFtx.filteredComponentGroups[0], noLastSignerGroupSamePartialTree)
|
||||
|
||||
// There are only two components in key1CommandsFtx (commandData and signers).
|
||||
assertEquals(2, key1CommandsFtx.componentGroups.size)
|
||||
|
||||
// Remove last signer for which there is a pointer from a visible commandData. This is the case of Key1.
|
||||
// This will result to an invalid transaction.
|
||||
// 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.newInstance(key1CommandsFtx.id, updatedFilteredComponentsNoSignersKey1SamePMT, key1CommandsFtx.groupHashes) }
|
||||
|
||||
// Remove both last signer (KEY1) and related command.
|
||||
// Update partial Merkle tree for signers.
|
||||
val updatedFilteredComponentsNoLastCommandAndSigners = listOf(noLastCommandDataGroup, noLastSignerGroup)
|
||||
val ftxNoLastCommandAndSigners = ftxConstructor.newInstance(key1CommandsFtx.id, updatedFilteredComponentsNoLastCommandAndSigners, key1CommandsFtx.groupHashes) as FilteredTransaction
|
||||
// verify() will pass as the transaction is well-formed.
|
||||
ftxNoLastCommandAndSigners.verify()
|
||||
// checkCommandVisibility() will not pass, because checkAllComponentsVisible(ComponentGroupEnum.SIGNERS_GROUP) will fail.
|
||||
assertFailsWith<ComponentVisibilityException> { ftxNoLastCommandAndSigners.checkCommandVisibility(DUMMY_KEY_1.public) }
|
||||
|
||||
// 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.newInstance(key2CommandsFtx.id, updatedFilteredComponentsNoSignersKey2SamePMT, key2CommandsFtx.groupHashes) as FilteredTransaction
|
||||
// verify() will fail as we didn't change the partial Merkle tree.
|
||||
assertFailsWith<FilteredTransactionVerificationException> { ftxNoLastSigner.verify() }
|
||||
// checkCommandVisibility() will not pass.
|
||||
assertFailsWith<ComponentVisibilityException> { ftxNoLastSigner.checkCommandVisibility(DUMMY_KEY_2.public) }
|
||||
|
||||
// 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.newInstance(key2CommandsFtx.id, updatedFilteredComponentsNoSignersKey2, key2CommandsFtx.groupHashes) as FilteredTransaction
|
||||
// verify() will pass, the transaction is well-formed.
|
||||
ftxNoLastSignerB.verify()
|
||||
// But, checkAllComponentsVisible() will not pass.
|
||||
assertFailsWith<ComponentVisibilityException> { ftxNoLastSignerB.checkCommandVisibility(DUMMY_KEY_2.public) }
|
||||
|
||||
// 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.availableComponentHashes[ComponentGroupEnum.SIGNERS_GROUP.ordinal]!!.subList(0, 2) + componentHash(key1CommandsFtx.filteredComponentGroups[1].nonces[2], alterSignerComponents[2])
|
||||
val alterMTree = MerkleTree.getMerkleTree(alterSignersHashes)
|
||||
val alterSignerPMTK = PartialMerkleTree.build(
|
||||
alterMTree,
|
||||
alterSignersHashes
|
||||
)
|
||||
|
||||
val alterSignerGroup = FilteredComponentGroup(
|
||||
ComponentGroupEnum.SIGNERS_GROUP.ordinal,
|
||||
alterSignerComponents,
|
||||
key1CommandsFtx.filteredComponentGroups[1].nonces,
|
||||
alterSignerPMTK
|
||||
)
|
||||
val alterFilteredComponents = listOf(key1CommandsFtx.filteredComponentGroups[0], alterSignerGroup)
|
||||
|
||||
// Do not update groupHashes.
|
||||
val ftxAlterSigner = ftxConstructor.newInstance(key1CommandsFtx.id, alterFilteredComponents, key1CommandsFtx.groupHashes) as FilteredTransaction
|
||||
// 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.newInstance(key1CommandsFtx.id, alterFilteredComponents, key1CommandsFtx.groupHashes.subList(0, 6) + alterMTree.hash) as FilteredTransaction
|
||||
// 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).
|
||||
assertFailsWith<ComponentVisibilityException> { ftxAlterSignerB.checkCommandVisibility(DUMMY_KEY_1.public) }
|
||||
|
||||
ftxConstructor.isAccessible = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash.Companion.zeroHash
|
||||
import net.corda.core.identity.Party
|
||||
@ -14,10 +13,12 @@ import net.corda.testing.*
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import java.util.function.Predicate
|
||||
import java.util.stream.IntStream
|
||||
import kotlin.streams.toList
|
||||
import kotlin.test.*
|
||||
|
||||
class PartialMerkleTreeTest : TestDependencyInjectionBase() {
|
||||
val nodes = "abcdef"
|
||||
private val nodes = "abcdef"
|
||||
private val hashed = nodes.map {
|
||||
initialiseTestSerialization()
|
||||
try {
|
||||
@ -115,16 +116,18 @@ class PartialMerkleTreeTest : TestDependencyInjectionBase() {
|
||||
val d = testTx.serialize().deserialize()
|
||||
assertEquals(testTx.id, d.id)
|
||||
|
||||
val mt = testTx.buildFilteredTransaction(Predicate(::filtering))
|
||||
val ftx = testTx.buildFilteredTransaction(Predicate(::filtering))
|
||||
|
||||
assertEquals(4, mt.filteredComponentGroups.size)
|
||||
assertEquals(1, mt.inputs.size)
|
||||
assertEquals(0, mt.attachments.size)
|
||||
assertEquals(1, mt.outputs.size)
|
||||
assertEquals(1, mt.commands.size)
|
||||
assertNull(mt.notary)
|
||||
assertNotNull(mt.timeWindow)
|
||||
mt.verify()
|
||||
// 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.attachments.size)
|
||||
assertEquals(1, ftx.outputs.size)
|
||||
assertEquals(1, ftx.commands.size)
|
||||
assertNull(ftx.notary)
|
||||
assertNotNull(ftx.timeWindow)
|
||||
ftx.verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -246,4 +249,50 @@ class PartialMerkleTreeTest : TestDependencyInjectionBase() {
|
||||
privacySalt = privacySalt
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
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)
|
||||
|
||||
// Provided hashes are not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("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"))) }
|
||||
|
||||
val pmt = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("1"), SecureHash.sha256("5"), SecureHash.sha256("0"), SecureHash.sha256("19")))
|
||||
// First leaf.
|
||||
assertEquals(0, pmt.leafIndex(SecureHash.sha256("0")))
|
||||
// Second leaf.
|
||||
assertEquals(1, pmt.leafIndex(SecureHash.sha256("1")))
|
||||
// A random leaf.
|
||||
assertEquals(5, pmt.leafIndex(SecureHash.sha256("5")))
|
||||
// The last leaf.
|
||||
assertEquals(19, pmt.leafIndex(SecureHash.sha256("19")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmt.leafIndex(SecureHash.sha256("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")) }
|
||||
|
||||
val pmtFirstElementOnly = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("0")))
|
||||
assertEquals(0, pmtFirstElementOnly.leafIndex(SecureHash.sha256("0")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmtFirstElementOnly.leafIndex(SecureHash.sha256("10")) }
|
||||
|
||||
val pmtLastElementOnly = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("19")))
|
||||
assertEquals(19, pmtLastElementOnly.leafIndex(SecureHash.sha256("19")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmtLastElementOnly.leafIndex(SecureHash.sha256("10")) }
|
||||
|
||||
val pmtOneElement = PartialMerkleTree.build(merkleTree, listOf<SecureHash>(SecureHash.sha256("5")))
|
||||
assertEquals(5, pmtOneElement.leafIndex(SecureHash.sha256("5")))
|
||||
// The provided hash is not in the tree.
|
||||
assertFailsWith<MerkleTreeException> { pmtOneElement.leafIndex(SecureHash.sha256("10")) }
|
||||
|
||||
val pmtAllIncluded = PartialMerkleTree.build(merkleTree, sampleLeaves)
|
||||
for (i in 0..19) assertEquals(i, pmtAllIncluded.leafIndex(SecureHash.sha256(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")) }
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ UNRELEASED
|
||||
|
||||
* ``Cordapp`` now has a name field for identifying CorDapps and all CorDapp names are printed to console at startup.
|
||||
|
||||
* Enums now respsect the whitelist applied to the Serializer factory serializing / deserializing them. If the enum isn't
|
||||
* Enums now respect the whitelist applied to the Serializer factory serializing / deserializing them. If the enum isn't
|
||||
either annotated with the @CordaSerializable annotation or explicitly whitelisted then a NotSerializableException is
|
||||
thrown.
|
||||
|
||||
@ -54,6 +54,15 @@ UNRELEASED
|
||||
* ``TimeWindow`` now has a ``length`` property that returns the length of the time-window, or ``null`` if the
|
||||
time-window is open-ended.
|
||||
|
||||
* A new ``SIGNERS_GROUP`` with ordinal 6 has been added to ``ComponentGroupEnum`` that corresponds to the ``Command``
|
||||
signers.
|
||||
|
||||
* ``PartialMerkleTree`` is equipped with a ``leafIndex`` function that returns the index of a hash (leaf) in the
|
||||
partial Merkle tree structure.
|
||||
|
||||
* A new function ``checkCommandVisibility(publicKey: PublicKey)`` has been added to ``FilteredTransaction`` to check
|
||||
if every command that a signer should receive (e.g. an Oracle) is indeed visible.
|
||||
|
||||
.. _changelog_v1:
|
||||
|
||||
Release 1.0
|
||||
|
@ -145,7 +145,7 @@ object NodeInterestRates {
|
||||
}
|
||||
|
||||
require(ftx.checkWithFun(::check))
|
||||
|
||||
ftx.checkCommandVisibility(services.myInfo.legalIdentities.first().owningKey)
|
||||
// It all checks out, so we can return a signature.
|
||||
//
|
||||
// Note that we will happily sign an invalid transaction, as we are only being presented with a filtered
|
||||
|
Loading…
Reference in New Issue
Block a user