[CORDA-694] Commands visibility for Oracles (without sacrificing privacy) (#1835)

new checkCommandVisibility feature for Oracles
This commit is contained in:
Konstantinos Chalkias 2017-10-19 10:21:20 +01:00 committed by GitHub
parent 717365413d
commit 479ab9a36a
8 changed files with 521 additions and 52 deletions

View File

@ -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.
}

View File

@ -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()
}

View File

@ -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())

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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")) }
}
}

View File

@ -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

View File

@ -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