diff --git a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt index 36d12a1924..d03767176c 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt @@ -1,7 +1,7 @@ package net.corda.core.crypto import net.corda.core.transactions.MerkleTree -import net.corda.core.transactions.hashConcat +import net.corda.core.crypto.SecureHash.Companion.zeroHash import java.util.* @@ -19,14 +19,12 @@ class MerkleTreeException(val reason: String) : Exception() { * / \ * h14 h55 * / \ / \ - * h12 h34 h5->d(h5) - * / \ / \ / \ - * l1 l2 l3 l4 l5->d(l5) + * h12 h34 h50 h00 + * / \ / \ / \ / \ + * l1 l2 l3 l4 l5 0 0 0 * - * l* denote hashes of leaves, h* - hashes of nodes below. - * h5->d(h5) denotes duplication of the left hand side node. These nodes are kept in a full tree as DuplicatedLeaf. - * When filtering the tree for l5, we don't want to keep both l5 and its duplicate (it can also be solved using null - * values in a tree, but this solution is clearer). + * l* denote hashes of leaves, h* - hashes of nodes below. 0 denotes zero hash, we use it to pad not full binary trees, + * so the number of leaves is always a power of 2. * * Example of Partial tree based on the tree above. * @@ -34,13 +32,13 @@ class MerkleTreeException(val reason: String) : Exception() { * / \ * _ _ * / \ / \ - * h12 _ _ d(h5) + * h12 _ _ h00 * / \ / \ - * I3 l4 I5 d(l5) + * I3 l4 I5 0 * * We want to check l3 and l5 - now turned into IncudedLeaf (I3 and I5 above). To verify that these two leaves belong to * the tree with a hash root h15 we need to provide a Merkle branch (or partial tree). In our case we need hashes: - * h12, l4, d(l5) and d(h5). Verification is done by hashing the partial tree to obtain the root and checking it against + * h12, l4, 0 and h00. Verification is done by hashing the partial tree to obtain the root and checking it against * the obtained h15 hash. Additionally we store included hashes used in calculation and compare them to leaves hashes we got * (there can be a difference in obtained leaves ordering - that's why it's a set comparison not hashing leaves into a tree). * If both equalities hold, we can assume that l3 and l5 belong to the transaction with root h15. @@ -54,7 +52,7 @@ class PartialMerkleTree(val root: PartialTree) { * transaction and leaves that just keep hashes needed for calculation. Reason for this approach: during verification * it's easier to extract hashes used as a base for this tree. */ - sealed class PartialTree() { + sealed class PartialTree { class IncludedLeaf(val hash: SecureHash) : PartialTree() class Leaf(val hash: SecureHash) : PartialTree() class Node(val left: PartialTree, val right: PartialTree) : PartialTree() @@ -66,15 +64,31 @@ class PartialMerkleTree(val root: PartialTree) { * @param includeHashes Hashes that should be included in a partial tree. * @return Partial Merkle tree root. */ + @Throws(IllegalArgumentException::class, MerkleTreeException::class) fun build(merkleRoot: MerkleTree, includeHashes: List): PartialMerkleTree { val usedHashes = ArrayList() + require(zeroHash !in includeHashes) { "Zero hashes shouldn't be included in partial tree." } + checkFull(merkleRoot) // Throws MerkleTreeException if it is not a full binary tree. val tree = buildPartialTree(merkleRoot, includeHashes, usedHashes) - //Too much included hashes or different ones. + // Too many included hashes or different ones. if (includeHashes.size != usedHashes.size) throw MerkleTreeException("Some of the provided hashes are not in the tree.") return PartialMerkleTree(tree.second) } + // Check if a MerkleTree is full binary tree. Returns the height of the tree if full, otherwise throws exception. + private fun checkFull(tree: MerkleTree, level: Int = 0): Int { + return when (tree) { + is MerkleTree.Leaf -> level + is MerkleTree.Node -> { + val l1 = checkFull(tree.left, level+1) + val l2 = checkFull(tree.right, level+1) + if (l1 != l2) throw MerkleTreeException("Got not full binary tree.") + l1 + } + } + } + /** * @param root Root of full Merkle tree which is a base for a partial one. * @param includeHashes Hashes of leaves to be included in this partial tree. @@ -93,18 +107,17 @@ class PartialMerkleTree(val root: PartialTree) { usedHashes.add(root.value) Pair(true, PartialTree.IncludedLeaf(root.value)) } else Pair(false, PartialTree.Leaf(root.value)) - is MerkleTree.DuplicatedLeaf -> Pair(false, PartialTree.Leaf(root.value)) is MerkleTree.Node -> { val leftNode = buildPartialTree(root.left, includeHashes, usedHashes) val rightNode = buildPartialTree(root.right, includeHashes, usedHashes) if (leftNode.first or rightNode.first) { - //This node is on a path to some included leaves. Don't store hash. + // This node is on a path to some included leaves. Don't store hash. val newTree = PartialTree.Node(leftNode.second, rightNode.second) - return Pair(true, newTree) + Pair(true, newTree) } else { - //This node has no included leaves below. Cut the tree here and store a hash as a Leaf. + // This node has no included leaves below. Cut the tree here and store a hash as a Leaf. val newTree = PartialTree.Leaf(root.value) - return Pair(false, newTree) + Pair(false, newTree) } } } @@ -118,7 +131,7 @@ class PartialMerkleTree(val root: PartialTree) { fun verify(merkleRootHash: SecureHash, hashesToCheck: List): Boolean { val usedHashes = ArrayList() val verifyRoot = verify(root, usedHashes) - //It means that we obtained more/less 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 }) return false return (verifyRoot == merkleRootHash) diff --git a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt index f5bb961c72..b16d17ef89 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt @@ -19,6 +19,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) { override fun toString() = BaseEncoding.base16().encode(bytes) fun prefixChars(prefixLen: Int = 6) = toString().substring(0, prefixLen) + fun hashConcat(other: SecureHash) = (this.bytes + other.bytes).sha256() // Like static methods in Java, except the 'companion' is a singleton that can have state. companion object { @@ -35,6 +36,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) { @JvmStatic fun sha256(str: String) = sha256(str.toByteArray()) @JvmStatic fun randomSHA256() = sha256(newSecureRandom().generateSeed(32)) + val zeroHash = SecureHash.SHA256(ByteArray(32, { 0.toByte() })) } // In future, maybe SHA3, truncated hashes etc. diff --git a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt index 1eacf56258..c0198ec456 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -1,39 +1,15 @@ package net.corda.core.transactions -import net.corda.core.contracts.Command -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionState -import net.corda.core.crypto.MerkleTreeException -import net.corda.core.crypto.PartialMerkleTree -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.sha256 +import net.corda.core.contracts.* +import net.corda.core.crypto.* +import net.corda.core.crypto.SecureHash.Companion.zeroHash import net.corda.core.serialization.createKryo import net.corda.core.serialization.extendKryoHash import net.corda.core.serialization.serialize import java.util.* -/** - * Build filtered transaction using provided filtering functions. - */ -fun WireTransaction.buildFilteredTransaction(filterFuns: FilterFuns): FilteredTransaction { - return FilteredTransaction.buildMerkleTransaction(this, filterFuns) -} - -/** - * Calculation of all leaves hashes that are needed for calculation of transaction id and partial Merkle branches. - */ -fun WireTransaction.calculateLeavesHashes(): List { - val resultHashes = ArrayList() - val entries = listOf(inputs, outputs, attachments, commands) - entries.forEach { it.mapTo(resultHashes, { x -> serializedHash(x) }) } - return resultHashes -} - -fun SecureHash.hashConcat(other: SecureHash) = (this.bytes + other.bytes).sha256() - fun serializedHash(x: T): SecureHash { - val kryo = extendKryoHash(createKryo()) //Dealing with HashMaps inside states. + val kryo = extendKryoHash(createKryo()) // Dealing with HashMaps inside states. return x.serialize(kryo).hash } @@ -42,55 +18,58 @@ fun serializedHash(x: T): SecureHash { * * See: https://en.wikipedia.org/wiki/Merkle_tree * - * Transaction is split into following blocks: inputs, outputs, commands, attachments' refs. Merkle Tree is kept in - * a recursive data structure. Building is done bottom up, from all leaves' hashes. - * If a row in a tree has an odd number of elements - the final hash is hashed with itself. + * Transaction is split into following blocks: inputs, attachments' refs, outputs, commands, notary, + * signers, tx type, timestamp. Merkle Tree is kept in a recursive data structure. Building is done bottom up, + * from all leaves' hashes. If number of leaves is not a power of two, the tree is padded with zero hashes. */ sealed class MerkleTree(val hash: SecureHash) { class Leaf(val value: SecureHash) : MerkleTree(value) class Node(val value: SecureHash, val left: MerkleTree, val right: MerkleTree) : MerkleTree(value) - //DuplicatedLeaf is storing a hash of the rightmost node that had to be duplicated to obtain the tree. - //That duplication can cause problems while building and verifying partial tree (especially for trees with duplicate - //attachments or commands). - class DuplicatedLeaf(val value: SecureHash) : MerkleTree(value) - - fun hashNodes(right: MerkleTree): MerkleTree { - val newHash = this.hash.hashConcat(right.hash) - return Node(newHash, this, right) - } companion object { + private fun isPow2(num: Int): Boolean = num and (num-1) == 0 + /** - * Merkle tree building using hashes. + * Merkle tree building using hashes, with zero hash padding to full power of 2. */ + @Throws(IllegalArgumentException::class) fun getMerkleTree(allLeavesHashes: List): MerkleTree { - val leaves = allLeavesHashes.map { MerkleTree.Leaf(it) } + val leaves = padWithZeros(allLeavesHashes).map { MerkleTree.Leaf(it) } return buildMerkleTree(leaves) } + // If number of leaves in the tree is not a power of 2, we need to pad it with zero hashes. + private fun padWithZeros(allLeavesHashes: List): List { + var n = allLeavesHashes.size + if (isPow2(n)) return allLeavesHashes + val paddedHashes = ArrayList(allLeavesHashes) + while (!isPow2(n)) { + paddedHashes.add(zeroHash) + n++ + } + return paddedHashes + } + /** * Tailrecursive function for building a tree bottom up. * @param lastNodesList MerkleTree nodes from previous level. * @return Tree root. */ private tailrec fun buildMerkleTree(lastNodesList: List): MerkleTree { - if (lastNodesList.size < 1) + if (lastNodesList.isEmpty()) throw MerkleTreeException("Cannot calculate Merkle root on empty hash list.") if (lastNodesList.size == 1) { return lastNodesList[0] //Root reached. } else { val newLevelHashes: MutableList = ArrayList() var i = 0 - while (i < lastNodesList.size) { + val n = lastNodesList.size + while (i < n) { val left = lastNodesList[i] - val n = lastNodesList.size - // If there is an odd number of elements at this level, - // the last element is hashed with itself and stored as a Leaf. - val right = when { - i + 1 > n - 1 -> MerkleTree.DuplicatedLeaf(lastNodesList[n - 1].hash) - else -> lastNodesList[i + 1] - } - val combined = left.hashNodes(right) + require(i+1 <= n-1) { "Sanity check: number of nodes should be even." } + val right = lastNodesList[i+1] + val newHash = left.hash.hashConcat(right.hash) + val combined = Node(newHash, left, right) newLevelHashes.add(combined) i += 2 } @@ -101,40 +80,67 @@ sealed class MerkleTree(val hash: SecureHash) { } /** - * Class that holds filtered leaves for a partial Merkle transaction. We assume mixed leaves types. + * Interface implemented by WireTransaction and FilteredLeaves. + * Property traversableList assures that we always calculate hashes in the same order, lets us define which + * fields of WireTransaction will be included in id calculation or partial merkle tree building. */ -class FilteredLeaves( - val inputs: List, - val outputs: List>, - val attachments: List, - val commands: List -) { - fun getFilteredHashes(): List { - val resultHashes = ArrayList() - val entries = listOf(inputs, outputs, attachments, commands) - entries.forEach { it.mapTo(resultHashes, { x -> serializedHash(x) }) } - return resultHashes - } +interface TraversableTransaction { + val inputs: List + val attachments: List + val outputs: List> + val commands: List + val notary: Party? + val mustSign: List + val type: TransactionType? + val timestamp: Timestamp? + + /** + * Traversing transaction fields with a list function over transaction contents. Used for leaves hashes calculation + * and user provided filtering and checking of filtered transaction. + */ + // We may want to specify our own behaviour on certain tx fields. + // Like if we include them at all, what to do with null values, if we treat list as one or not etc. for building + // torn-off transaction and id calculation. + val traversableList: List + get() { + val traverseList = mutableListOf(inputs, attachments, outputs, commands).flatten().toMutableList() + if (notary != null) traverseList.add(notary!!) + traverseList.addAll(mustSign) + if (type != null) traverseList.add(type!!) + if (timestamp != null) traverseList.add(timestamp!!) + return traverseList + } + + // Calculation of all leaves hashes that are needed for calculation of transaction id and partial Merkle branches. + fun calculateLeavesHashes(): List = traversableList.map { serializedHash(it) } } /** - * Holds filter functions on transactions fields. - * Functions are used to build a partial tree only out of some subset of original transaction fields. + * Class that holds filtered leaves for a partial Merkle transaction. We assume mixed leaf types, notice that every + * field from WireTransaction can be used in PartialMerkleTree calculation. */ -class FilterFuns( - val filterInputs: (StateRef) -> Boolean = { false }, - val filterOutputs: (TransactionState) -> Boolean = { false }, - val filterAttachments: (SecureHash) -> Boolean = { false }, - val filterCommands: (Command) -> Boolean = { false } -) { - fun genericFilter(elem: T): Boolean { - return when (elem) { - is StateRef -> filterInputs(elem) - is TransactionState<*> -> filterOutputs(elem) - is SecureHash -> filterAttachments(elem) - is Command -> filterCommands(elem) - else -> throw IllegalArgumentException("Wrong argument type: ${elem.javaClass}") - } +class FilteredLeaves( + override val inputs: List, + override val attachments: List, + override val outputs: List>, + override val commands: List, + override val notary: Party?, + override val mustSign: List, + override val type: TransactionType?, + override val timestamp: Timestamp? +) : TraversableTransaction { + /** + * Function that checks the whole filtered structure. + * Force type checking on a structure that we obtained, so we don't sign more than expected. + * Example: Oracle is implemented to check only for commands, if it gets an attachment and doesn't expect it - it can sign + * over a transaction with the attachment that wasn't verified. Of course it depends on how you implement it, but else -> false + * should solve a problem with possible later extensions to WireTransaction. + * @param checkingFun function that performs type checking on the structure fields and provides verification logic accordingly. + * @returns false if no elements were matched on a structure or checkingFun returned false. + */ + fun checkWithFun(checkingFun: (Any) -> Boolean): Boolean { + val checkList = traversableList.map { checkingFun(it) } + return (!checkList.isEmpty()) && checkList.all { true } } } @@ -151,18 +157,14 @@ class FilteredTransaction( /** * Construction of filtered transaction with Partial Merkle Tree. * @param wtx WireTransaction to be filtered. - * @param filterFuns filtering functions for inputs, outputs, attachments, commands. + * @param filtering filtering over the whole WireTransaction */ fun buildMerkleTransaction(wtx: WireTransaction, - filterFuns: FilterFuns + filtering: (Any) -> Boolean ): FilteredTransaction { - val filteredInputs = wtx.inputs.filter { filterFuns.genericFilter(it) } - val filteredOutputs = wtx.outputs.filter { filterFuns.genericFilter(it) } - val filteredAttachments = wtx.attachments.filter { filterFuns.genericFilter(it) } - val filteredCommands = wtx.commands.filter { filterFuns.genericFilter(it) } - val filteredLeaves = FilteredLeaves(filteredInputs, filteredOutputs, filteredAttachments, filteredCommands) - - val pmt = PartialMerkleTree.build(wtx.merkleTree, filteredLeaves.getFilteredHashes()) + val filteredLeaves = wtx.filterWithFun(filtering) + val merkleTree = wtx.getMerkleTree() + val pmt = PartialMerkleTree.build(merkleTree, filteredLeaves.calculateLeavesHashes()) return FilteredTransaction(filteredLeaves, pmt) } } @@ -170,10 +172,19 @@ class FilteredTransaction( /** * Runs verification of Partial Merkle Branch with merkleRootHash. */ + @Throws(MerkleTreeException::class) fun verify(merkleRootHash: SecureHash): Boolean { - val hashes: List = filteredLeaves.getFilteredHashes() - if (hashes.size == 0) + val hashes: List = filteredLeaves.calculateLeavesHashes() + if (hashes.isEmpty()) throw MerkleTreeException("Transaction without included leaves.") return partialMerkleTree.verify(merkleRootHash, hashes) } + + /** + * Runs verification of Partial Merkle Branch with merkleRootHash. Checks filteredLeaves with provided checkingFun. + */ + @Throws(MerkleTreeException::class) + fun verifyWithFunction(merkleRootHash: SecureHash, checkingFun: (Any) -> Boolean): Boolean { + return verify(merkleRootHash) && filteredLeaves.checkWithFun { checkingFun(it) } + } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index eb67965b46..1a8a40ab04 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -25,15 +25,15 @@ class WireTransaction( /** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */ override val inputs: List, /** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */ - val attachments: List, + override val attachments: List, outputs: List>, /** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */ - val commands: List, + override val commands: List, notary: Party?, signers: List, type: TransactionType, timestamp: Timestamp? -) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp) { +) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp), TraversableTransaction { init { checkInvariants() } @@ -42,14 +42,7 @@ class WireTransaction( @Volatile @Transient private var cachedBytes: SerializedBytes? = null val serialized: SerializedBytes get() = cachedBytes ?: serialize().apply { cachedBytes = this } - //We need cashed leaves hashes and whole tree for an id and Partial Merkle Tree calculation. - @Volatile @Transient private var cachedLeavesHashes: List? = null - val allLeavesHashes: List get() = cachedLeavesHashes ?: calculateLeavesHashes().apply { cachedLeavesHashes = this } - - @Volatile @Transient var cachedTree: MerkleTree? = null - val merkleTree: MerkleTree get() = cachedTree ?: MerkleTree.getMerkleTree(allLeavesHashes).apply { cachedTree = this } - - override val id: SecureHash get() = merkleTree.hash + override val id: SecureHash by lazy { getMerkleTree().hash } companion object { fun deserialize(data: SerializedBytes, kryo: Kryo = THREAD_LOCAL_KRYO.get()): WireTransaction { @@ -91,6 +84,39 @@ class WireTransaction( return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, mustSign, timestamp, type) } + /** + * Build filtered transaction using provided filtering functions. + */ + fun buildFilteredTransaction(filtering: (Any) -> Boolean): FilteredTransaction { + return FilteredTransaction.buildMerkleTransaction(this, filtering) + } + + /** + * Builds whole Merkle tree for a transaction. + */ + fun getMerkleTree(): MerkleTree { + return MerkleTree.getMerkleTree(calculateLeavesHashes()) + } + + /** + * Construction of partial transaction from WireTransaction based on filtering. + * @param filtering filtering over the whole WireTransaction + * @returns FilteredLeaves used in PartialMerkleTree calculation and verification. + */ + fun filterWithFun(filtering: (Any) -> Boolean): FilteredLeaves { + fun notNullFalse(elem: Any?): Any? = if(elem == null || !filtering(elem)) null else elem + return FilteredLeaves( + inputs.filter { filtering(it) }, + attachments.filter { filtering(it) }, + outputs.filter { filtering(it) }, + commands.filter { filtering(it) }, + notNullFalse(notary) as Party?, + mustSign.filter { filtering(it) }, + notNullFalse(type) as TransactionType?, + notNullFalse(timestamp) as Timestamp? + ) + } + override fun toString(): String { val buf = StringBuilder() buf.appendln("Transaction $id:") diff --git a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt index 53ac8e0f33..b8b0b3bc9a 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt @@ -3,26 +3,22 @@ package net.corda.core.crypto import com.esotericsoftware.kryo.serializers.MapSerializer import net.corda.contracts.asset.Cash -import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.`issued by` +import net.corda.core.contracts.* +import net.corda.core.crypto.SecureHash.Companion.zeroHash import net.corda.core.serialization.* import net.corda.core.transactions.* -import net.corda.core.utilities.DUMMY_PUBKEY_1 -import net.corda.testing.ALICE_PUBKEY +import net.corda.core.utilities.* import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP_PUBKEY import net.corda.testing.ledger import org.junit.Test import java.util.* -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertTrue +import kotlin.test.* class PartialMerkleTreeTest { val nodes = "abcdef" val hashed = nodes.map { it.serialize().sha256() } - val root = SecureHash.parse("F6D8FB3720114F8D040D64F633B0D9178EB09A55AA7D62FAE1A070D1BF561051") + val expectedRoot = MerkleTree.getMerkleTree(hashed.toMutableList() + listOf(zeroHash, zeroHash)).hash val merkleTree = MerkleTree.getMerkleTree(hashed) val testLedger = ledger { @@ -33,22 +29,30 @@ class PartialMerkleTreeTest { owner = MEGA_CORP_PUBKEY ) } + output("dummy cash 1") { + Cash.State( + amount = 900.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = DUMMY_PUBKEY_1 + ) + } } transaction { input("MEGA_CORP cash") output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_1)) command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } + timestamp(TEST_TX_TIME) this.verifies() } } - val testTx = testLedger.interpreter.transactionsToVerify[0] + val txs = testLedger.interpreter.transactionsToVerify + val testTx = txs[0] - //Building full Merkle Tree tests. + // Building full Merkle Tree tests. @Test fun `building Merkle tree with 6 nodes - no rightmost nodes`() { - assertEquals(root, merkleTree.hash) + assertEquals(expectedRoot, merkleTree.hash) } @Test @@ -67,25 +71,70 @@ class PartialMerkleTreeTest { fun `building Merkle tree odd number of nodes`() { val odd = hashed.subList(0, 3) val h1 = hashed[0].hashConcat(hashed[1]) - val h2 = hashed[2].hashConcat(hashed[2]) + val h2 = hashed[2].hashConcat(zeroHash) val expected = h1.hashConcat(h2) val mt = MerkleTree.getMerkleTree(odd) assertEquals(mt.hash, expected) } + @Test + fun `check full tree`() { + val h = SecureHash.randomSHA256() + val left = MerkleTree.Node(h, MerkleTree.Node(h, MerkleTree.Leaf(h), MerkleTree.Leaf(h)), + MerkleTree.Node(h, MerkleTree.Leaf(h), MerkleTree.Leaf(h))) + val right = MerkleTree.Node(h, MerkleTree.Leaf(h), MerkleTree.Leaf(h)) + val tree = MerkleTree.Node(h, left, right) + assertFailsWith { PartialMerkleTree.build(tree, listOf(h)) } + PartialMerkleTree.build(right, listOf(h, h)) // Node and two leaves. + PartialMerkleTree.build(MerkleTree.Leaf(h), listOf(h)) // Just a leaf. + } + @Test fun `building Merkle tree for a transaction`() { - val filterFuns = FilterFuns( - filterCommands = { x -> ALICE_PUBKEY in x.signers }, - filterOutputs = { true }, - filterInputs = { true }) - val mt = testTx.buildFilteredTransaction(filterFuns) + fun filtering(elem: Any): Boolean { + return when (elem) { + is StateRef -> true + is TransactionState<*> -> elem.data.participants[0].keys == DUMMY_PUBKEY_1.keys + is Command -> MEGA_CORP_PUBKEY in elem.signers + is Timestamp -> true + is CompositeKey -> elem == MEGA_CORP_PUBKEY + else -> false + } + } + val mt = testTx.buildFilteredTransaction(::filtering) + val leaves = mt.filteredLeaves val d = WireTransaction.deserialize(testTx.serialized) assertEquals(testTx.id, d.id) + assertEquals(1, leaves.commands.size) + assertEquals(1, leaves.outputs.size) + assertEquals(1, leaves.inputs.size) + assertEquals(1, leaves.mustSign.size) + assertEquals(0, leaves.attachments.size) + assertTrue(mt.filteredLeaves.timestamp != null) + assertEquals(null, mt.filteredLeaves.type) + assertEquals(null, mt.filteredLeaves.notary) assert(mt.verify(testTx.id)) } - //Partial Merkle Tree building tests + @Test + fun `same transactions with different notaries have different ids`() { + val wtx1 = makeSimpleCashWtx(DUMMY_NOTARY) + val wtx2 = makeSimpleCashWtx(MEGA_CORP) + assertNotEquals(wtx1.id, wtx2.id) + } + + @Test + fun `nothing filtered`() { + val mt = testTx.buildFilteredTransaction( {false} ) + assertTrue(mt.filteredLeaves.attachments.isEmpty()) + assertTrue(mt.filteredLeaves.commands.isEmpty()) + assertTrue(mt.filteredLeaves.inputs.isEmpty()) + assertTrue(mt.filteredLeaves.outputs.isEmpty()) + assertTrue(mt.filteredLeaves.timestamp == null) + assertFailsWith { mt.verify(testTx.id) } + } + + // Partial Merkle Tree building tests @Test fun `build Partial Merkle Tree, only left nodes branch`() { val inclHashes = listOf(hashed[3], hashed[5]) @@ -137,7 +186,7 @@ class PartialMerkleTreeTest { @Test fun `verify Partial Merkle Tree - duplicate leaves failure`() { - val mt = MerkleTree.getMerkleTree(hashed.subList(0, 5)) //Odd number of leaves. Last one is duplicated. + val mt = MerkleTree.getMerkleTree(hashed.subList(0, 5)) // Odd number of leaves. Last one is duplicated. val inclHashes = arrayListOf(hashed[3], hashed[4]) val pmt = PartialMerkleTree.build(mt, inclHashes) inclHashes.add(hashed[4]) @@ -162,11 +211,24 @@ class PartialMerkleTreeTest { @Test fun `hash map serialization`() { val hm1 = hashMapOf("a" to 1, "b" to 2, "c" to 3, "e" to 4) - assert(serializedHash(hm1) == serializedHash(hm1.serialize().deserialize())) //It internally uses the ordered HashMap extension. + assert(serializedHash(hm1) == serializedHash(hm1.serialize().deserialize())) // It internally uses the ordered HashMap extension. val kryo = extendKryoHash(createKryo()) assertTrue(kryo.getSerializer(HashMap::class.java) is OrderedSerializer) assertTrue(kryo.getSerializer(LinkedHashMap::class.java) is MapSerializer) val hm2 = hm1.serialize(kryo).deserialize(kryo) assert(hm1.hashCode() == hm2.hashCode()) } + + private fun makeSimpleCashWtx(notary: Party, timestamp: Timestamp? = null, attachments: List = emptyList()): WireTransaction { + return WireTransaction( + inputs = testTx.inputs, + attachments = attachments, + outputs = testTx.outputs, + commands = testTx.commands, + notary = notary, + signers = listOf(MEGA_CORP_PUBKEY, DUMMY_PUBKEY_1), + type = TransactionType.General(), + timestamp = timestamp + ) + } } diff --git a/docs/source/merkle-trees.rst b/docs/source/merkle-trees.rst index 23b1b9e149..6c8378fa1f 100644 --- a/docs/source/merkle-trees.rst +++ b/docs/source/merkle-trees.rst @@ -15,17 +15,17 @@ You can read more on the concept `here Boolean``. .. container:: codeset .. sourcecode:: kotlin - val partialTx = ... + val partialTx = ... val oracle: Party = ... - fun filterCommands(c: Command) = oracle.owningKey in c.signers && c.value is Fix - val filterFuns = FilterFuns(filterCommands = ::filterCommands) + fun filtering(elem: Any): Boolean { + return when (elem) { + is Command -> oracleParty.owningKey in elem.signers && elem.value is Fix + else -> false + } + } Assuming that we already assembled partialTx with some commands and know the identity of Oracle service, -we pass filtering function over commands - ``filterCommands`` to ``FilterFuns``. It filters only -commands of type ``Fix`` as in IRSDemo example. Then we can construct ``FilteredTransaction``: +we construct filtering function over commands - ``filtering``. It performs type checking and filters only ``Fix`` commands +as in IRSDemo example. Then we can construct ``FilteredTransaction``: .. container:: codeset .. sourcecode:: kotlin val wtx: WireTransaction = partialTx.toWireTransaction() - val ftx = FilteredTransaction.buildMerkleTransaction(wtx, filterFuns) + val ftx: FilteredTransaction = wtx.buildFilteredTransaction(filtering) -In the Oracle example this step takes place in ``RatesFixFlow``: +In the Oracle example this step takes place in ``RatesFixFlow`` by overriding ``filtering`` function, see: :ref:`filtering_ref` -.. container:: codeset - - .. sourcecode:: kotlin - - val flow = RatesFixFlow(partialTx, filterFuns, oracle, fixOf, "0.675".bd, "0.1".bd) ``FilteredTransaction`` holds ``filteredLeaves`` (data that we wanted to reveal) and Merkle branch for them. @@ -87,14 +87,21 @@ In the Oracle example this step takes place in ``RatesFixFlow``: .. sourcecode:: kotlin - // Getting included commands, inputs, outputs, attachments. + // Direct accsess to included commands, inputs, outputs, attachments etc. val cmds: List = ftx.filteredLeaves.commands val ins: List = ftx.filteredLeaves.inputs - val outs: List> = ftx.filteredLeaves.outputs - val attchs: List = ftx.filteredLeaves.attachments + val timestamp: Timestamp? = ftx.filteredLeaves.timestamp + ... +.. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 -If you want to verify obtained ``FilteredTransaction`` all you need is the root hash of the full transaction: +Above code snippet is taken from ``NodeInterestRates.kt`` file and implements a signing part of an Oracle. +You can check only leaves using ``leaves.checkWithFun { check(it) }`` and then verify obtained ``FilteredTransaction`` +to see if data from ``PartialMerkleTree`` belongs to ``WireTransaction`` with provided ``id``. All you need is the root hash +of the full transaction: .. container:: codeset @@ -104,6 +111,13 @@ If you want to verify obtained ``FilteredTransaction`` all you need is the root throw MerkleTreeException("Rate Fix Oracle: Couldn't verify partial Merkle tree.") } +Or combine the two steps together: + +.. container:: codeset + + .. sourcecode:: kotlin + + ftx.verifyWithFunction(merkleRoot, ::check) .. note:: The way the ``FilteredTransaction`` is constructed ensures that after signing of the root hash it's impossible to add or remove leaves. However, it can happen that having transaction with multiple commands one party reveals only subset of them to the Oracle. diff --git a/docs/source/oracles.rst b/docs/source/oracles.rst index 7150d273dd..641cadb3b1 100644 --- a/docs/source/oracles.rst +++ b/docs/source/oracles.rst @@ -239,8 +239,11 @@ those for ``NodeInterestRates.Oracle``. :start-after: DOCSTART 1 :end-before: DOCEND 1 -You'll note that the ``FixSignFlow`` requires a ``FilterFuns`` instance with the appropriate filter to include only -the ``Fix`` commands. You can find a further explanation of this in :doc:`merkle-trees`. +You'll note that the ``FixSignFlow`` requires a ``FilterTransaction`` instance which includes only ``Fix`` commands. +You can find a further explanation of this in :doc:`merkle-trees`. Below you will see how to build such transaction with +hidden fields. + +.. _filtering_ref: Using an oracle --------------- @@ -260,8 +263,9 @@ As you can see, this: 2. Does some quick validation. 3. Adds the command to the transaction containing the fact to be signed for by the oracle. 4. Calls an extension point that allows clients to generate output states based on the fact from the oracle. -5. Requests the signature from the oracle using the client sub-flow for signing from above. -6. Adds the signature returned from the oracle. +5. Builds filtered transaction based on filtering function extended from ``RatesFixFlow``. +6. Requests the signature from the oracle using the client sub-flow for signing from above. +7. Adds the signature returned from the oracle. Here's an example of it in action from ``FixingFlow.Fixer``. @@ -269,3 +273,8 @@ Here's an example of it in action from ``FixingFlow.Fixer``. :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 + +.. note:: + When overriding be careful when making the sub-class an anonymous or inner class (object declarations in Kotlin), + because that kind of classes can access variables from the enclosing scope and cause serialization problems when + checkpointed. diff --git a/docs/source/resources/merkleTree.png b/docs/source/resources/merkleTree.png index 696f510450..6aa07752b0 100644 Binary files a/docs/source/resources/merkleTree.png and b/docs/source/resources/merkleTree.png differ diff --git a/docs/source/resources/partialMerkle.png b/docs/source/resources/partialMerkle.png index a2c5af7a23..c99030f38b 100644 Binary files a/docs/source/resources/partialMerkle.png and b/docs/source/resources/partialMerkle.png differ diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt index b172a9768d..b524dedf9b 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt @@ -192,31 +192,27 @@ object NodeInterestRates { if (!ftx.verify(merkleRoot)) { throw MerkleTreeException("Rate Fix Oracle: Couldn't verify partial Merkle tree.") } - - // Reject if we have something different than only commands. - val leaves = ftx.filteredLeaves - require(leaves.inputs.isEmpty() && leaves.outputs.isEmpty() && leaves.attachments.isEmpty()) - - val fixes: List = ftx.filteredLeaves.commands. - filter { identity.owningKey in it.signers && it.value is Fix }. - map { it.value as Fix } - - // Reject signing attempt if we received more commands than we should. - if (fixes.size != ftx.filteredLeaves.commands.size) - throw IllegalArgumentException() - - // Reject this signing attempt if there are no commands of the right kind. - if (fixes.isEmpty()) - throw IllegalArgumentException() - - // For each fix, verify that the data is correct. - val knownFixes = knownFixes // Snapshot - for (fix in fixes) { + // Performing validation of obtained FilteredLeaves. + fun commandValidator(elem: Command): Boolean { + if (!(identity.owningKey in elem.signers && elem.value is Fix)) + throw IllegalArgumentException("Oracle received unknown command (not in signers or not Fix).") + val fix = elem.value as Fix val known = knownFixes[fix.of] if (known == null || known != fix) throw UnknownFix(fix.of) + return true } + fun check(elem: Any): Boolean { + return when (elem) { + is Command -> commandValidator(elem) + else -> throw IllegalArgumentException("Oracle received data of different type than expected.") + } + } + val leaves = ftx.filteredLeaves + if (!leaves.checkWithFun(::check)) + throw IllegalArgumentException() + // 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 diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt index c34dbb0ac7..67245913af 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt @@ -10,7 +10,6 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.PluginServiceHub import net.corda.core.node.services.ServiceType import net.corda.core.seconds -import net.corda.core.transactions.FilterFuns import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.trace @@ -68,12 +67,8 @@ object FixingFlow { val oracle = serviceHub.networkMapCache.getNodesWithService(handshake.payload.oracleType).first() val oracleParty = oracle.serviceIdentities(handshake.payload.oracleType).first() - // TODO Could it be solved in better way, move filtering here not in RatesFixFlow? // DOCSTART 1 - fun filterCommands(c: Command) = oracleParty.owningKey in c.signers && c.value is Fix - - val filterFuns = FilterFuns(filterCommands = ::filterCommands) - val addFixing = object : RatesFixFlow(ptx, filterFuns, oracleParty, fixOf, BigDecimal.ZERO, BigDecimal.ONE) { + val addFixing = object : RatesFixFlow(ptx, oracleParty, fixOf, BigDecimal.ZERO, BigDecimal.ONE) { @Suspendable override fun beforeSigning(fix: Fix) { newDeal.generateFix(ptx, StateAndRef(txState, handshake.payload.ref), fix) @@ -82,6 +77,14 @@ object FixingFlow { // to have one. ptx.setTime(serviceHub.clock.instant(), 30.seconds) } + + @Suspendable + override fun filtering(elem: Any): Boolean { + return when (elem) { + is Command -> oracleParty.owningKey in elem.signers && elem.value is Fix + else -> false + } + } } subFlow(addFixing) // DOCEND 1 diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt index 237264cfc8..2bf9be5e1e 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt @@ -7,7 +7,6 @@ import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.Party import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic -import net.corda.core.transactions.FilterFuns import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker @@ -28,12 +27,10 @@ import java.util.* * @throws FixOutOfRange if the returned fix was further away from the expected rate by the given amount. */ open class RatesFixFlow(protected val tx: TransactionBuilder, - /** Filtering functions over transaction, used to build partial transaction presented to oracle. */ - private val filterFuns: FilterFuns, - private val oracle: Party, - private val fixOf: FixOf, - private val expectedRate: BigDecimal, - private val rateTolerance: BigDecimal, + protected val oracle: Party, + protected val fixOf: FixOf, + protected val expectedRate: BigDecimal, + protected val rateTolerance: BigDecimal, override val progressTracker: ProgressTracker = RatesFixFlow.tracker(fixOf.name)) : FlowLogic() { companion object { @@ -59,7 +56,8 @@ open class RatesFixFlow(protected val tx: TransactionBuilder, tx.addCommand(fix, oracle.owningKey) beforeSigning(fix) progressTracker.currentStep = SIGNING - val signature = subFlow(FixSignFlow(tx, oracle, filterFuns)) + val mtx = tx.toWireTransaction().buildFilteredTransaction({ filtering(it) }) + val signature = subFlow(FixSignFlow(tx, oracle, mtx)) tx.addSignatureUnchecked(signature) } // DOCEND 2 @@ -72,6 +70,15 @@ open class RatesFixFlow(protected val tx: TransactionBuilder, protected open fun beforeSigning(fix: Fix) { } + /** + * Filtering functions over transaction, used to build partial transaction with partial Merkle tree presented to oracle. + * When overriding be careful when making the sub-class an anonymous or inner class (object declarations in Kotlin), + * because that kind of classes can access variables from the enclosing scope and cause serialization problems when + * checkpointed. + */ + @Suspendable + protected open fun filtering(elem: Any): Boolean = false + private fun checkFixIsNearExpected(fix: Fix) { val delta = (fix.value - expectedRate).abs() if (delta > rateTolerance) { @@ -97,13 +104,12 @@ open class RatesFixFlow(protected val tx: TransactionBuilder, } } - class FixSignFlow(val tx: TransactionBuilder, val oracle: Party, val filterFuns: FilterFuns) : FlowLogic() { + class FixSignFlow(val tx: TransactionBuilder, val oracle: Party, + val partialMerkleTx: FilteredTransaction) : FlowLogic() { @Suspendable override fun call(): DigitalSignature.LegallyIdentifiable { val wtx = tx.toWireTransaction() - val partialMerkleTx = FilteredTransaction.buildMerkleTransaction(wtx, filterFuns) val rootHash = wtx.id - val resp = sendAndReceive(oracle, SignRequest(rootHash, partialMerkleTx)) return resp.unwrap { sig -> check(sig.signer == oracle) diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/NodeInterestRatesTest.kt index a837a22fc7..afed9222a4 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/NodeInterestRatesTest.kt @@ -11,10 +11,10 @@ import net.corda.core.crypto.Party import net.corda.core.crypto.generateKeyPair import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo -import net.corda.core.transactions.FilterFuns -import net.corda.core.transactions.FilteredTransaction +import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.LogHelper +import net.corda.core.utilities.ProgressTracker import net.corda.irs.api.NodeInterestRates import net.corda.irs.flows.RatesFixFlow import net.corda.node.utilities.configureDatabase @@ -30,6 +30,7 @@ import org.junit.Assert import org.junit.Before import org.junit.Test import java.io.Closeable +import java.math.BigDecimal import java.time.Clock import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -53,6 +54,15 @@ class NodeInterestRatesTest { lateinit var dataSource: Closeable lateinit var database: Database + fun fixCmdFilter(elem: Any): Boolean { + return when (elem) { + is Command -> oracle.identity.owningKey in elem.signers && elem.value is Fix + else -> false + } + } + + fun filterCmds(elem: Any): Boolean = elem is Command + @Before fun setUp() { val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties()) @@ -120,11 +130,17 @@ class NodeInterestRatesTest { databaseTransaction(database) { val tx = makeTX() val wtx1 = tx.toWireTransaction() - val ftx1 = FilteredTransaction.buildMerkleTransaction(wtx1, FilterFuns(filterOutputs = { true })) + fun filterAllOutputs(elem: Any): Boolean { + return when (elem) { + is TransactionState -> true + else -> false + } + } + val ftx1 = wtx1.buildFilteredTransaction(::filterAllOutputs) assertFailsWith { oracle.sign(ftx1, wtx1.id) } tx.addCommand(Cash.Commands.Move(), ALICE_PUBKEY) val wtx2 = tx.toWireTransaction() - val ftx2 = FilteredTransaction.buildMerkleTransaction(wtx2, FilterFuns(filterCommands = { true })) + val ftx2 = wtx2.buildFilteredTransaction { x -> filterCmds(x) } assertFalse(wtx1.id == wtx2.id) assertFailsWith { oracle.sign(ftx2, wtx2.id) } } @@ -138,9 +154,7 @@ class NodeInterestRatesTest { tx.addCommand(fix, oracle.identity.owningKey) // Sign successfully. val wtx = tx.toWireTransaction() - fun filterCommands(c: Command) = oracle.identity.owningKey in c.signers && c.value is Fix - val filterFuns = FilterFuns(filterCommands = ::filterCommands) - val ftx = FilteredTransaction.buildMerkleTransaction(wtx, filterFuns) + val ftx = wtx.buildFilteredTransaction { x -> fixCmdFilter(x) } val signature = oracle.sign(ftx, wtx.id) tx.checkAndAddSignature(signature) } @@ -154,9 +168,7 @@ class NodeInterestRatesTest { val badFix = Fix(fixOf, "0.6789".bd) tx.addCommand(badFix, oracle.identity.owningKey) val wtx = tx.toWireTransaction() - fun filterCommands(c: Command) = oracle.identity.owningKey in c.signers && c.value is Fix - val filterFuns = FilterFuns(filterCommands = ::filterCommands) - val ftx = FilteredTransaction.buildMerkleTransaction(wtx, filterFuns) + val ftx = wtx.buildFilteredTransaction { x -> fixCmdFilter(x) } val e1 = assertFailsWith { oracle.sign(ftx, wtx.id) } assertEquals(fixOf, e1.fix) } @@ -167,15 +179,28 @@ class NodeInterestRatesTest { databaseTransaction(database) { val tx = makeTX() val fix = oracle.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")), clock.instant()).first() + fun filtering(elem: Any): Boolean { + return when (elem) { + is Command -> oracle.identity.owningKey in elem.signers && elem.value is Fix + is TransactionState -> true + else -> false + } + } tx.addCommand(fix, oracle.identity.owningKey) val wtx = tx.toWireTransaction() - fun filterCommands(c: Command) = oracle.identity.owningKey in c.signers && c.value is Fix - val filterFuns = FilterFuns(filterCommands = ::filterCommands, filterOutputs = { true }) - val ftx = FilteredTransaction.buildMerkleTransaction(wtx, filterFuns) + val ftx = wtx.buildFilteredTransaction(::filtering) assertFailsWith { oracle.sign(ftx, wtx.id) } } } + @Test + fun `empty partial transaction to sign`() { + val tx = makeTX() + val wtx = tx.toWireTransaction() + val ftx = wtx.buildFilteredTransaction({ false }) + assertFailsWith { oracle.sign(ftx, wtx.id) } + } + @Test fun `partial tree verification exception`() { databaseTransaction(database) { @@ -183,7 +208,7 @@ class NodeInterestRatesTest { val wtx1 = tx.toWireTransaction() tx.addCommand(Cash.Commands.Move(), ALICE_PUBKEY) val wtx2 = tx.toWireTransaction() - val ftx2 = FilteredTransaction.buildMerkleTransaction(wtx2, FilterFuns(filterCommands = { true })) + val ftx2 = wtx2.buildFilteredTransaction { x -> filterCmds(x) } assertFalse(wtx1.id == wtx2.id) assertFailsWith { oracle.sign(ftx2, wtx1.id) } } @@ -200,9 +225,7 @@ class NodeInterestRatesTest { val tx = TransactionType.General.Builder(null) val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") val oracle = n2.info.serviceIdentities(NodeInterestRates.type).first() - fun filterCommands(c: Command) = oracle.owningKey in c.signers && c.value is Fix - val filterFuns = FilterFuns(filterCommands = ::filterCommands) - val flow = RatesFixFlow(tx, filterFuns, oracle, fixOf, "0.675".bd, "0.1".bd) + val flow = FilteredRatesFlow(tx, oracle, fixOf, "0.675".bd, "0.1".bd) LogHelper.setLevel("rates") net.runNetwork() val future = n1.services.startFlow(flow).resultFuture @@ -214,5 +237,19 @@ class NodeInterestRatesTest { assertEquals("0.678".bd, fix.value) } + class FilteredRatesFlow(tx: TransactionBuilder, + oracle: Party, + fixOf: FixOf, + expectedRate: BigDecimal, + rateTolerance: BigDecimal, + progressTracker: ProgressTracker = RatesFixFlow.tracker(fixOf.name)) : RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) { + override fun filtering(elem: Any): Boolean { + return when (elem) { + is Command -> oracle.owningKey in elem.signers && elem.value is Fix + else -> false + } + } + } + private fun makeTX() = TransactionType.General.Builder(DUMMY_NOTARY).withItems(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY) }