From 4e1893732669cdccc2a4ee41d09785d23c90c70c Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 8 Sep 2017 17:27:01 +0100 Subject: [PATCH] CORDA-499: Restructure TransactionGraphSearch to be Dokka-friendly (#1409) * Remove internal state of TransactionGraphSearch from being publicly visible. * Add Dokka comments for TransactionGraphSearch.Query values. * Move query into TransactionGraphSearch constructor as it should always be set except for test cases. * Move TransactionGraphSearch into trader demo --- .../core/contracts/TransactionGraphSearch.kt | 61 --------------- .../traderdemo/TransactionGraphSearch.kt | 74 +++++++++++++++++++ .../net/corda/traderdemo/flow/BuyerFlow.kt | 11 ++- .../TransactionGraphSearchTests.kt | 12 ++- 4 files changed, 86 insertions(+), 72 deletions(-) delete mode 100644 core/src/main/kotlin/net/corda/core/contracts/TransactionGraphSearch.kt create mode 100644 samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TransactionGraphSearch.kt rename {core/src/test/kotlin/net/corda/core/contracts => samples/trader-demo/src/test/kotlin/net/corda/traderdemo}/TransactionGraphSearchTests.kt (91%) diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionGraphSearch.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionGraphSearch.kt deleted file mode 100644 index 4239b5772b..0000000000 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionGraphSearch.kt +++ /dev/null @@ -1,61 +0,0 @@ -package net.corda.core.contracts - -import net.corda.core.crypto.SecureHash -import net.corda.core.node.services.TransactionStorage -import net.corda.core.transactions.SignedTransaction -import net.corda.core.transactions.WireTransaction -import java.util.* -import java.util.concurrent.Callable - -/** - * Given a map of transaction id to [SignedTransaction], performs a breadth first search of the dependency graph from - * the starting point down in order to find transactions that match the given query criteria. - * - * Currently, only one kind of query is supported: find any transaction that contains a command of the given type. - * - * In future, this should support restricting the search by time, and other types of useful query. - * - * @param transactions map of transaction id to [SignedTransaction]. - * @param startPoints transactions to use as starting points for the search. - */ -class TransactionGraphSearch(val transactions: TransactionStorage, - val startPoints: List) : Callable> { - class Query( - val withCommandOfType: Class? = null, - val followInputsOfType: Class? = null - ) - - var query: Query = Query() - - override fun call(): List { - val q = query - - val alreadyVisited = HashSet() - val next = ArrayList(startPoints) - - val results = ArrayList() - - while (next.isNotEmpty()) { - val tx = next.removeAt(next.lastIndex) - - if (q.matches(tx)) - results += tx - - val inputsLeadingToUnvisitedTx: Iterable = tx.inputs.filter { it.txhash !in alreadyVisited } - val unvisitedInputTxs: Map = inputsLeadingToUnvisitedTx.map { it.txhash }.toHashSet().map { transactions.getTransaction(it) }.filterNotNull().associateBy { it.id } - val unvisitedInputTxsWithInputIndex: Iterable> = inputsLeadingToUnvisitedTx.filter { it.txhash in unvisitedInputTxs.keys }.map { Pair(unvisitedInputTxs[it.txhash]!!, it.index) } - next += (unvisitedInputTxsWithInputIndex.filter { q.followInputsOfType == null || it.first.tx.outputs[it.second].data.javaClass == q.followInputsOfType } - .map { it.first }.filter { alreadyVisited.add(it.id) }.map { it.tx }) - } - - return results - } - - private fun Query.matches(tx: WireTransaction): Boolean { - if (withCommandOfType != null) { - if (tx.commands.any { it.value.javaClass.isAssignableFrom(withCommandOfType) }) - return true - } - return false - } -} diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TransactionGraphSearch.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TransactionGraphSearch.kt new file mode 100644 index 0000000000..97ef776656 --- /dev/null +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TransactionGraphSearch.kt @@ -0,0 +1,74 @@ +package net.corda.traderdemo + +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateRef +import net.corda.core.crypto.SecureHash +import net.corda.core.node.services.TransactionStorage +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction +import java.util.* +import java.util.concurrent.Callable + +/** + * Given a map of transaction id to [SignedTransaction], performs a breadth first search of the dependency graph from + * the starting point down in order to find transactions that match the given query criteria. + * + * Currently, only one kind of query is supported: find any transaction that contains a command of the given type. + * + * In future, this should support restricting the search by time, and other types of useful query. + * + * @property transactions map of transaction id to [SignedTransaction]. + * @property startPoints transactions to use as starting points for the search. + * @property query query to test transactions within the graph for matching. + */ +class TransactionGraphSearch(private val transactions: TransactionStorage, + private val startPoints: List, + private val query: Query) : Callable> { + /** + * Query criteria to match transactions against. + * + * @property withCommandOfType contract command class to restrict matches to, or null for no filtering by command. Matches the class or + * any subclass. + * @property followInputsOfType contract output state class to follow the corresponding inputs to. Matches this exact class only. + */ + data class Query( + val withCommandOfType: Class? = null, + val followInputsOfType: Class? = null + ) { + /** + * Test if the given transaction matches this query. Currently only supports checking if the transaction that + * contains a command of the given type. + */ + fun matches(tx: WireTransaction): Boolean { + if (withCommandOfType != null) { + if (tx.commands.any { it.value.javaClass.isAssignableFrom(withCommandOfType) }) + return true + } + return false + } + } + + override fun call(): List { + val alreadyVisited = HashSet() + val next = ArrayList(startPoints) + + val results = ArrayList() + + while (next.isNotEmpty()) { + val tx = next.removeAt(next.lastIndex) + + if (query.matches(tx)) + results += tx + + val inputsLeadingToUnvisitedTx: Iterable = tx.inputs.filter { it.txhash !in alreadyVisited } + val unvisitedInputTxs: Map = inputsLeadingToUnvisitedTx.map { it.txhash }.toHashSet().map { transactions.getTransaction(it) }.filterNotNull().associateBy { it.id } + val unvisitedInputTxsWithInputIndex: Iterable> = inputsLeadingToUnvisitedTx.filter { it.txhash in unvisitedInputTxs.keys }.map { Pair(unvisitedInputTxs[it.txhash]!!, it.index) } + next += (unvisitedInputTxsWithInputIndex.filter { (stx, idx) -> + query.followInputsOfType == null || stx.tx.outputs[idx].data.javaClass == query.followInputsOfType + }.map { it.first }.filter { stx -> alreadyVisited.add(stx.id) }.map { it.tx }) + } + + return results + } +} diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt index e166273f2e..e4a3a11802 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt @@ -2,7 +2,6 @@ package net.corda.traderdemo.flow import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Amount -import net.corda.core.contracts.TransactionGraphSearch import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatedBy import net.corda.core.identity.Party @@ -14,6 +13,7 @@ import net.corda.core.utilities.unwrap import net.corda.finance.contracts.CommercialPaper import net.corda.finance.contracts.getCashBalances import net.corda.finance.flows.TwoPartyTradeFlow +import net.corda.traderdemo.TransactionGraphSearch import java.util.* @InitiatedBy(SellerFlow::class) @@ -53,9 +53,12 @@ class BuyerFlow(val otherParty: Party) : FlowLogic() { private fun logIssuanceAttachment(tradeTX: SignedTransaction) { // Find the original CP issuance. - val search = TransactionGraphSearch(serviceHub.validatedTransactions, listOf(tradeTX.tx)) - search.query = TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java, - followInputsOfType = CommercialPaper.State::class.java) + // TODO: This is potentially very expensive, and requires transaction details we may no longer have once + // SGX is enabled. Should be replaced with including the attachment on all transactions involving + // the state. + val search = TransactionGraphSearch(serviceHub.validatedTransactions, listOf(tradeTX.tx), + TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java, + followInputsOfType = CommercialPaper.State::class.java)) val cpIssuance = search.call().single() // Buyer will fetch the attachment from the seller automatically when it resolves the transaction. diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/TransactionGraphSearchTests.kt similarity index 91% rename from core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt rename to samples/trader-demo/src/test/kotlin/net/corda/traderdemo/TransactionGraphSearchTests.kt index bfefdaae15..b39ad233a5 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt +++ b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/TransactionGraphSearchTests.kt @@ -1,5 +1,6 @@ -package net.corda.core.contracts +package net.corda.traderdemo +import net.corda.core.contracts.CommandData import net.corda.core.crypto.newSecureRandom import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder @@ -53,8 +54,7 @@ class TransactionGraphSearchTests : TestDependencyInjectionBase() { @Test fun `return empty from empty`() { val storage = buildTransactions(DummyContract.Commands.Create()) - val search = TransactionGraphSearch(storage, emptyList()) - search.query = TransactionGraphSearch.Query() + val search = TransactionGraphSearch(storage, emptyList(), TransactionGraphSearch.Query()) val expected = emptyList() val actual = search.call() @@ -64,8 +64,7 @@ class TransactionGraphSearchTests : TestDependencyInjectionBase() { @Test fun `return empty from no match`() { val storage = buildTransactions(DummyContract.Commands.Create()) - val search = TransactionGraphSearch(storage, listOf(storage.inputTx.tx)) - search.query = TransactionGraphSearch.Query() + val search = TransactionGraphSearch(storage, listOf(storage.inputTx.tx), TransactionGraphSearch.Query()) val expected = emptyList() val actual = search.call() @@ -75,8 +74,7 @@ class TransactionGraphSearchTests : TestDependencyInjectionBase() { @Test fun `return origin on match`() { val storage = buildTransactions(DummyContract.Commands.Create()) - val search = TransactionGraphSearch(storage, listOf(storage.inputTx.tx)) - search.query = TransactionGraphSearch.Query(DummyContract.Commands.Create::class.java) + val search = TransactionGraphSearch(storage, listOf(storage.inputTx.tx), TransactionGraphSearch.Query(DummyContract.Commands.Create::class.java)) val expected = listOf(storage.originTx.tx) val actual = search.call()