From 45d8e0f76d04f56a13cb1ccf182a8028e94bdcc8 Mon Sep 17 00:00:00 2001 From: kasiastreich Date: Fri, 3 Feb 2017 14:02:51 +0000 Subject: [PATCH] Tearoff fixes (#78) * Move merkle building extension functions on wire tx to WireTransaction class. * Add timestamp, notary, transaction type and signers to wire transaction id calculation. * Change construction of MerkleTree from duplicating last node on a given level to padding leaves' list with zero hash to size of the nearest power of 2 - so we always have a full binary tree. The problem was that it was possible to construct 2 different transactions with the same ids. Trick worked for txs having number of leaves that were not power of 2. * Update tear-offs documentation and diagrams to reflect changes in construction of Merkle trees - padding with zero hashes and including all WireTransaction fields in id computation. * Change in filtering API of WireTransaction for partial Merkle trees calculation. Instead of many filtering functions over a transaction only one needs to be provided. Additional change to check and verification of FilteredTransaction. * IRS demo change. Make filtering function a protected method of RatesFixFlow class. Comment on situation when capturing too much scope and connected problems with checkpointing. Change oracle and tear-offs documentation. --- .../corda/core/crypto/PartialMerkleTree.kt | 51 +++-- .../net/corda/core/crypto/SecureHash.kt | 2 + .../core/transactions/MerkleTransaction.kt | 195 +++++++++--------- .../core/transactions/WireTransaction.kt | 48 ++++- .../core/crypto/PartialMerkleTreeTest.kt | 104 ++++++++-- docs/source/merkle-trees.rst | 72 ++++--- docs/source/oracles.rst | 17 +- docs/source/resources/merkleTree.png | Bin 15642 -> 22627 bytes docs/source/resources/partialMerkle.png | Bin 8741 -> 18381 bytes .../net/corda/irs/api/NodeInterestRates.kt | 36 ++-- .../kotlin/net/corda/irs/flows/FixingFlow.kt | 15 +- .../net/corda/irs/flows/RatesFixFlow.kt | 28 ++- .../irs/testing/NodeInterestRatesTest.kt | 71 +++++-- 13 files changed, 409 insertions(+), 230 deletions(-) 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 696f5104504ca964273dab39a890452d8a7c4c6f..6aa07752b0daf0f4af34d3a61788543d91a1dd66 100644 GIT binary patch literal 22627 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 literal 15642 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 SyBqw3cqIK)sz^fD>wf?-Ose|; diff --git a/docs/source/resources/partialMerkle.png b/docs/source/resources/partialMerkle.png index a2c5af7a2350d95224f4cb838e6d9e1696e304bc..c99030f38ba05717f5a7b79f067197edeeb369eb 100644 GIT binary patch literal 18381 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 literal 8741 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 -p{zW38ITE@*aRW8)m)3V$d46&T70 zK~FMS79(1K4ql+Pw{Wj*f4r)QTFJsRyYvlOO7B32*~(tRU5Un_&hK!egAq`~Unv+V zRiNK*I-C#Tg7e!h69N-ijD50*|3_VlcEx4ImdB_8iveXJ8keO4dOex0&GeWlkSZf_ z$A{}NlsaH?Sr)reFpTu{ExXf^BmDUy<^AFPnT96b9!fNfDm%k}S0^!2P8Spv!vU>2 zb3R1mpumTLa7{dfx8QZ#L?GZ0K!R3!tSB%4HgeZ*{vW^QCjJZA1+cn85zG}Oi)e7} zoDe>sh_4qMA1wJtZ|9x-j#M?PSjY$UXEw@dlv*F~_)a(Polw4&XH_R^0bp}pL98K6 zm0g;cE$M9!ir3mp#4~TdRet@zt$wB@?`!X5rjJBh^+|TwHirBplgW-@-&jj-KGtq= zI^HfgK4}3672F3i)Q5-Q;9WoYOM)9t8XL26b}o3D9xXMWLUrN5UXL+vif5$FU3xqQ z?5t>V2iVmc9c_oJ{v9P=Z;$V&_MX{NAccPknB-veec*g2WTySrInTib+u6zL%UUN* zv8tj|SC^-?*v9pz6EIF`q?(X@H)fAMR$ef*ZAl2iTy^nTMFkI{Z~yo zDy$iZHR2xMsV$YJLp-g0hA=xOx2dwnE~?$OQFEX#R9Z(HCnY&DR9$lB^LTS!+-0tw zTX9{Ky2p;I4Q;$97XLVE>|%<1;>QNH_c#wrXU7%JT=pf$#TiAqyT-UXo>R1wrGGLK zWv1OKR6Dc2sdIPRt;Nm`q6!;!bkAqBEll(K`(TKzOqLS7%&J-LVo?ZZ7(HYJt3LRTlRr`S8;uG?0x0=b;I9UWJVKe zc_Nw5=GjnO-^D5&r!Bri4cDhPrp*STwX2l#y=@3xjiXFSVP(JTxjkA9Uj zbKlo)VzY>Yf4r}h_xxYD&?nIny$9jJ`CchymhO3{HNvE7^cLNR(ngE+$Pb#weN-qa zxg9vA>J_DT>pFf4Ez?K~Rc3BWa#|%~dvypJav@VJ^X8#2LlPP@r9sfhj{kur&_#hG pQU+YYAZUyH=LPuSL;dzKC6-RqtXa-H7ks4v8R(d5ztnUg{6B!v`R4!t 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) }