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
This commit is contained in:
Ross Nicoll 2017-09-08 17:27:01 +01:00 committed by GitHub
parent 0edaea81d2
commit 4e18937326
4 changed files with 86 additions and 72 deletions

View File

@ -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<WireTransaction>) : Callable<List<WireTransaction>> {
class Query(
val withCommandOfType: Class<out CommandData>? = null,
val followInputsOfType: Class<out ContractState>? = null
)
var query: Query = Query()
override fun call(): List<WireTransaction> {
val q = query
val alreadyVisited = HashSet<SecureHash>()
val next = ArrayList<WireTransaction>(startPoints)
val results = ArrayList<WireTransaction>()
while (next.isNotEmpty()) {
val tx = next.removeAt(next.lastIndex)
if (q.matches(tx))
results += tx
val inputsLeadingToUnvisitedTx: Iterable<StateRef> = tx.inputs.filter { it.txhash !in alreadyVisited }
val unvisitedInputTxs: Map<SecureHash, SignedTransaction> = inputsLeadingToUnvisitedTx.map { it.txhash }.toHashSet().map { transactions.getTransaction(it) }.filterNotNull().associateBy { it.id }
val unvisitedInputTxsWithInputIndex: Iterable<Pair<SignedTransaction, Int>> = 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
}
}

View File

@ -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<WireTransaction>,
private val query: Query) : Callable<List<WireTransaction>> {
/**
* 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<out CommandData>? = null,
val followInputsOfType: Class<out ContractState>? = 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<WireTransaction> {
val alreadyVisited = HashSet<SecureHash>()
val next = ArrayList<WireTransaction>(startPoints)
val results = ArrayList<WireTransaction>()
while (next.isNotEmpty()) {
val tx = next.removeAt(next.lastIndex)
if (query.matches(tx))
results += tx
val inputsLeadingToUnvisitedTx: Iterable<StateRef> = tx.inputs.filter { it.txhash !in alreadyVisited }
val unvisitedInputTxs: Map<SecureHash, SignedTransaction> = inputsLeadingToUnvisitedTx.map { it.txhash }.toHashSet().map { transactions.getTransaction(it) }.filterNotNull().associateBy { it.id }
val unvisitedInputTxsWithInputIndex: Iterable<Pair<SignedTransaction, Int>> = 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
}
}

View File

@ -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<Unit>() {
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.

View File

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