Merge pull request #697 from corda/anthony-os-merge-20180406

OS Merge 20180406
This commit is contained in:
Anthony Keenan 2018-04-06 13:30:56 +01:00 committed by GitHub
commit ed7e9e64cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 285 additions and 107 deletions

View File

@ -145,6 +145,8 @@ which is then referenced within a custom flow:
:start-after: DOCSTART TopupIssuer
:end-before: DOCEND TopupIssuer
For examples on testing ``@CordaService`` implementations, see the oracle example :doc:`here <oracles>`
.. _database_migration_ref:
Database Migration

View File

@ -59,6 +59,8 @@ object, or by using named paramters in Kotlin:
.. sourcecode:: kotlin
val network = MockNetwork(
// A list of packages to scan. Any contracts, flows and Corda services within these
// packages will be automatically available to any nodes within the mock network
cordappPackages = listOf("my.cordapp.package", "my.other.cordapp.package"),
// If true then each node will be run in its own thread. This can result in race conditions in your
// code if not carefully written, but is more realistic and may help if you have flows in your app that
@ -77,7 +79,10 @@ object, or by using named paramters in Kotlin:
// notary implementations.
servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random())
val network2 = MockNetwork(listOf("my.cordapp.package", "my.other.cordapp.package"), MockNetworkParameters(
val network2 = MockNetwork(
// A list of packages to scan. Any contracts, flows and Corda services within these
// packages will be automatically available to any nodes within the mock network
listOf("my.cordapp.package", "my.other.cordapp.package"), MockNetworkParameters(
// If true then each node will be run in its own thread. This can result in race conditions in your
// code if not carefully written, but is more realistic and may help if you have flows in your app that
// do long blocking operations.
@ -98,7 +103,10 @@ object, or by using named paramters in Kotlin:
.. sourcecode:: java
MockNetwork network = MockNetwork(ImmutableList.of("my.cordapp.package", "my.other.cordapp.package"),
MockNetwork network = MockNetwork(
// A list of packages to scan. Any contracts, flows and Corda services within these
// packages will be automatically available to any nodes within the mock network
ImmutableList.of("my.cordapp.package", "my.other.cordapp.package"),
new MockNetworkParameters()
// If true then each node will be run in its own thread. This can result in race conditions in
// your code if not carefully written, but is more realistic and may help if you have flows in
@ -294,6 +302,7 @@ Further examples
^^^^^^^^^^^^^^^^
* See the flow testing tutorial :doc:`here <flow-testing>`
* See the oracle tutorial :doc:`here <oracles>` for information on testing ``@CordaService`` classes
* Further examples are available in the Example CorDapp in
`Java <https://github.com/corda/cordapp-example/blob/release-V3/java-source/src/test/java/com/example/flow/IOUFlowTests.java>`_ and
`Kotlin <https://github.com/corda/cordapp-example/blob/release-V3/kotlin-source/src/test/kotlin/com/example/flow/IOUFlowTests.kt>`_

View File

@ -269,7 +269,26 @@ Here's an example of it in action from ``FixingFlow.Fixer``.
Testing
-------
When unit testing, we make use of the ``MockNetwork`` which allows us to create ``MockNode`` instances. A ``MockNode``
is a simplified node suitable for tests. One feature that isn't available (and which is not suitable for unit testing
anyway) is the node's ability to scan and automatically install oracles it finds in the CorDapp jars. Instead, when
working with ``MockNode``, use the ``installCordaService`` method to manually install the oracle on the relevant node.
The ``MockNetwork`` allows the creation of ``MockNode`` instances, which are simplified nodes which can be used for
testing (see :doc:`api-testing`). When creating the ``MockNetwork`` you supply a list of packages to scan for CorDapps.
Make sure the packages you provide include your oracle service, and it automatically be installed in the test nodes.
Then you can create an oracle node on the ``MockNetwork`` and insert any initialisation logic you want to use. In this
case, our ``Oracle`` service is in the ``net.corda.irs.api`` package, so the following test setup will install
the service in each node. Then an oracle node with an oracle service which is initialised with some data is created on
the mock network:
.. literalinclude:: ../../samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt
:language: kotlin
:start-after: DOCSTART 1
:end-before: DOCEND 1
:dedent: 4
You can then write tests on your mock network to verify the nodes interact with your Oracle correctly.
.. literalinclude:: ../../samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt
:language: kotlin
:start-after: DOCSTART 2
:end-before: DOCEND 2
:dedent: 4
See `here <https://github.com/corda/corda/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt>`_ for more examples.

View File

@ -58,6 +58,61 @@ interface NetworkMapCacheBaseInternal : NetworkMapCacheBase {
interface ServiceHubInternal : ServiceHub {
companion object {
private val log = contextLogger()
fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>,
validatedTransactions: WritableTransactionStorage,
stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage,
vaultService: VaultServiceInternal) {
require(txs.any()) { "No transactions passed in for recording" }
val recordedTransactions = txs.filter { validatedTransactions.addTransaction(it) }
val stateMachineRunId = FlowStateMachineImpl.currentStateMachine()?.id
if (stateMachineRunId != null) {
recordedTransactions.forEach {
stateMachineRecordedTransactionMapping.addMapping(stateMachineRunId, it.id)
}
} else {
log.warn("Transactions recorded from outside of a state machine")
}
if (statesToRecord != StatesToRecord.NONE) {
// When the user has requested StatesToRecord.ALL we may end up recording and relationally mapping states
// that do not involve us and that we cannot sign for. This will break coin selection and thus a warning
// is present in the documentation for this feature (see the "Observer nodes" tutorial on docs.corda.net).
//
// The reason for this is three-fold:
//
// 1) We are putting in place the observer mode feature relatively quickly to meet specific customer
// launch target dates.
//
// 2) The right design for vaults which mix observations and relevant states isn't entirely clear yet.
//
// 3) If we get the design wrong it could create security problems and business confusions.
//
// Back in the bitcoinj days I did add support for "watching addresses" to the wallet code, which is the
// Bitcoin equivalent of observer nodes:
//
// https://bitcoinj.github.io/working-with-the-wallet#watching-wallets
//
// The ability to have a wallet containing both irrelevant and relevant states complicated everything quite
// dramatically, even methods as basic as the getBalance() API which required additional modes to let you
// query "balance I can spend" vs "balance I am observing". In the end it might have been better to just
// require the user to create an entirely separate wallet for observing with.
//
// In Corda we don't support a single node having multiple vaults (at the time of writing), and it's not
// clear that's the right way to go: perhaps adding an "origin" column to the VAULT_STATES table is a better
// solution. Then you could select subsets of states depending on where the report came from.
//
// The risk of doing this is that apps/developers may use 'canned SQL queries' not written by us that forget
// to add a WHERE clause for the origin column. Those queries will seem to work most of the time until
// they're run on an observer node and mix in irrelevant data. In the worst case this may result in
// erroneous data being reported to the user, which could cause security problems.
//
// Because the primary use case for recording irrelevant states is observer/regulator nodes, who are unlikely
// to make writes to the ledger very often or at all, we choose to punt this issue for the time being.
vaultService.notifyAll(statesToRecord, recordedTransactions.map { it.coreTransaction })
}
}
}
override val vaultService: VaultServiceInternal
@ -80,54 +135,7 @@ interface ServiceHubInternal : ServiceHub {
val networkMapUpdater: NetworkMapUpdater
override val cordappProvider: CordappProviderInternal
override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>) {
require(txs.any()) { "No transactions passed in for recording" }
val recordedTransactions = txs.filter { validatedTransactions.addTransaction(it) }
val stateMachineRunId = FlowStateMachineImpl.currentStateMachine()?.id
if (stateMachineRunId != null) {
recordedTransactions.forEach {
stateMachineRecordedTransactionMapping.addMapping(stateMachineRunId, it.id)
}
} else {
log.warn("Transactions recorded from outside of a state machine")
}
if (statesToRecord != StatesToRecord.NONE) {
// When the user has requested StatesToRecord.ALL we may end up recording and relationally mapping states
// that do not involve us and that we cannot sign for. This will break coin selection and thus a warning
// is present in the documentation for this feature (see the "Observer nodes" tutorial on docs.corda.net).
//
// The reason for this is three-fold:
//
// 1) We are putting in place the observer mode feature relatively quickly to meet specific customer
// launch target dates.
//
// 2) The right design for vaults which mix observations and relevant states isn't entirely clear yet.
//
// 3) If we get the design wrong it could create security problems and business confusions.
//
// Back in the bitcoinj days I did add support for "watching addresses" to the wallet code, which is the
// Bitcoin equivalent of observer nodes:
//
// https://bitcoinj.github.io/working-with-the-wallet#watching-wallets
//
// The ability to have a wallet containing both irrelevant and relevant states complicated everything quite
// dramatically, even methods as basic as the getBalance() API which required additional modes to let you
// query "balance I can spend" vs "balance I am observing". In the end it might have been better to just
// require the user to create an entirely separate wallet for observing with.
//
// In Corda we don't support a single node having multiple vaults (at the time of writing), and it's not
// clear that's the right way to go: perhaps adding an "origin" column to the VAULT_STATES table is a better
// solution. Then you could select subsets of states depending on where the report came from.
//
// The risk of doing this is that apps/developers may use 'canned SQL queries' not written by us that forget
// to add a WHERE clause for the origin column. Those queries will seem to work most of the time until
// they're run on an observer node and mix in irrelevant data. In the worst case this may result in
// erroneous data being reported to the user, which could cause security problems.
//
// Because the primary use case for recording irrelevant states is observer/regulator nodes, who are unlikely
// to make writes to the ledger very often or at all, we choose to punt this issue for the time being.
vaultService.notifyAll(statesToRecord, txs.map { it.coreTransaction })
}
recordTransactions(statesToRecord, txs, validatedTransactions, stateMachineRecordedTransactionMapping, vaultService)
}
fun getFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>?

View File

@ -140,6 +140,18 @@ class NodeVaultServiceTest {
return tryLockFungibleStatesForSpending(lockId, baseCriteria, amount, Cash.State::class.java)
}
@Test
fun `duplicate insert of transaction does not fail`() {
database.transaction {
val cash = Cash()
val howMuch = 100.DOLLARS
val issuance = TransactionBuilder(null as Party?)
cash.generateIssue(issuance, Amount(howMuch.quantity, Issued(DUMMY_CASH_ISSUER, howMuch.token)), services.myInfo.singleIdentity(), dummyNotary.party)
val transaction = issuerServices.signInitialTransaction(issuance, DUMMY_CASH_ISSUER.party.owningKey)
services.recordTransactions(transaction)
services.recordTransactions(transaction)
}
}
@Test
fun `states not local to instance`() {

View File

@ -79,6 +79,8 @@ object FixingFlow {
@Suspendable
override fun filtering(elem: Any): Boolean {
return when (elem) {
// Only expose Fix commands in which the oracle is on the list of requested signers
// to the oracle node, to avoid leaking privacy
is Command<*> -> handshake.payload.oracle.owningKey in elem.signers && elem.value is Fix
else -> false
}
@ -91,7 +93,7 @@ object FixingFlow {
}
/**
* One side of the fixing flow for an interest rate swap, but could easily be generalised furher.
* One side of the fixing flow for an interest rate swap, but could easily be generalised further.
*
* As per the [Fixer], do not infer too much from this class name in terms of business roles. This
* is just the "side" of the flow run by the party with the floating leg as a way of deciding who

View File

@ -15,22 +15,15 @@ import net.corda.core.contracts.ContractState
import net.corda.core.contracts.TransactionState
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS
import net.corda.finance.contracts.Fix
import net.corda.finance.contracts.FixOf
import net.corda.finance.contracts.asset.CASH
import net.corda.finance.contracts.asset.Cash
import net.corda.irs.flows.RatesFixFlow
import net.corda.node.internal.configureDatabase
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.core.*
import net.corda.testing.internal.withoutTestSerialization
import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.*
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
@ -220,50 +213,10 @@ class NodeInterestRatesTest {
assertFailsWith<IllegalArgumentException> { oracle.sign(ftx) } // It throws failed requirement (as it is empty there is no command to check and sign).
}
@Test
fun `network tearoff`() = withoutTestSerialization {
val mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts", "net.corda.irs"))
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
val oracleNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)).apply {
registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java)
registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java)
database.transaction {
services.cordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA
}
}
val oracle = oracleNode.services.myInfo.singleIdentity()
val tx = makePartialTX()
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1"))
LogHelper.setLevel("rates")
mockNet.runNetwork()
val future = aliceNode.startFlow(flow)
mockNet.runNetwork()
future.getOrThrow()
// We should now have a valid fix of our tx from the oracle.
val fix = tx.toWireTransaction(services).commands.map { it.value as Fix }.first()
assertEquals(fixOf, fix.of)
assertEquals(BigDecimal("0.678"), fix.value)
mockNet.stopNodes()
}
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 makePartialTX() = TransactionBuilder(DUMMY_NOTARY).withItems(
TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy ALICE, Cash.PROGRAM_ID, DUMMY_NOTARY))
private fun makeFullTx() = makePartialTX().withItems(dummyCommand())
}

View File

@ -0,0 +1,155 @@
package net.corda.irs.api
import com.google.common.collect.testing.Helpers
import com.google.common.collect.testing.Helpers.assertContains
import net.corda.core.contracts.Command
import net.corda.core.contracts.TransactionState
import net.corda.core.flows.UnexpectedFlowEndException
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS
import net.corda.finance.contracts.Fix
import net.corda.finance.contracts.FixOf
import net.corda.finance.contracts.asset.CASH
import net.corda.finance.contracts.asset.Cash
import net.corda.irs.flows.RatesFixFlow
import net.corda.testing.core.*
import net.corda.testing.internal.LogHelper
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNodeParameters
import net.corda.testing.node.StartedMockNode
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.math.BigDecimal
import kotlin.test.assertEquals
class OracleNodeTearOffTests {
private val TEST_DATA = NodeInterestRates.parseFile("""
LIBOR 2016-03-16 1M = 0.678
LIBOR 2016-03-16 2M = 0.685
LIBOR 2016-03-16 1Y = 0.890
LIBOR 2016-03-16 2Y = 0.962
EURIBOR 2016-03-15 1M = 0.123
EURIBOR 2016-03-15 2M = 0.111
""".trimIndent())
private val dummyCashIssuer = TestIdentity(CordaX500Name("Cash issuer", "London", "GB"))
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
val alice = TestIdentity(ALICE_NAME, 70)
private lateinit var mockNet: MockNetwork
private lateinit var aliceNode: StartedMockNode
private lateinit var oracleNode: StartedMockNode
private val oracle get() = oracleNode.services.myInfo.singleIdentity()
@Before
// DOCSTART 1
fun setUp() {
mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts", "net.corda.irs"))
aliceNode = mockNet.createPartyNode(ALICE_NAME)
oracleNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)).apply {
transaction {
services.cordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA
}
}
}
// DOCEND 1
@After
fun tearDown() {
mockNet.stopNodes()
}
// DOCSTART 2
@Test
fun `verify that the oracle signs the transaction if the interest rate within allowed limit`() {
// Create a partial transaction
val tx = TransactionBuilder(DUMMY_NOTARY)
.withItems(TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy alice.party, Cash.PROGRAM_ID, DUMMY_NOTARY))
// Specify the rate we wish to get verified by the oracle
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
// Create a new flow for the fix
val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1"))
// Run the mock network and wait for a result
mockNet.runNetwork()
val future = aliceNode.startFlow(flow)
mockNet.runNetwork()
future.getOrThrow()
// We should now have a valid rate on our tx from the oracle.
val fix = tx.toWireTransaction(aliceNode.services).commands.map { it }.first()
assertEquals(fixOf, (fix.value as Fix).of)
// Check that the response contains the valid rate, which is within the supplied tolerance
assertEquals(BigDecimal("0.678"), (fix.value as Fix).value)
// Check that the transaction has been signed by the oracle
assertContains(fix.signers, oracle.owningKey)
}
// DOCEND 2
@Test
fun `verify that the oracle rejects the transaction if the interest rate is outside the allowed limit`() {
val tx = makePartialTX()
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.695"), BigDecimal("0.01"))
LogHelper.setLevel("rates")
mockNet.runNetwork()
val future = aliceNode.startFlow(flow)
mockNet.runNetwork()
assertThatThrownBy{
future.getOrThrow()
}.isInstanceOf(RatesFixFlow.FixOutOfRange::class.java).hasMessage("Fix out of range by 0.017")
}
@Test
fun `verify that the oracle rejects the transaction if there is a privacy leak`() {
val tx = makePartialTX()
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
val flow = OverFilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1"))
LogHelper.setLevel("rates")
mockNet.runNetwork()
val future = aliceNode.startFlow(flow)
mockNet.runNetwork()
//The oracle
assertThatThrownBy{
future.getOrThrow()
}.isInstanceOf(UnexpectedFlowEndException::class.java)
}
// Creates a version of [RatesFixFlow] that makes the command
class FilteredRatesFlow(tx: TransactionBuilder,
oracle: Party,
fixOf: FixOf,
expectedRate: BigDecimal,
rateTolerance: BigDecimal,
progressTracker: ProgressTracker = 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
}
}
}
// Creates a version of [RatesFixFlow] that makes the command
class OverFilteredRatesFlow(tx: TransactionBuilder,
oracle: Party,
fixOf: FixOf,
expectedRate: BigDecimal,
rateTolerance: BigDecimal,
progressTracker: ProgressTracker = tracker(fixOf.name))
: RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) {
override fun filtering(elem: Any): Boolean = true
}
private fun makePartialTX() = TransactionBuilder(DUMMY_NOTARY).withItems(
TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy alice.party, Cash.PROGRAM_ID, DUMMY_NOTARY))
}

View File

@ -19,10 +19,13 @@ import net.corda.core.contracts.StateRef
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.messaging.DataFeed
import net.corda.core.messaging.FlowHandle
import net.corda.core.messaging.FlowProgressHandle
import net.corda.core.messaging.StateMachineTransactionMapping
import net.corda.core.node.*
import net.corda.core.node.services.*
import net.corda.core.serialization.SerializeAsToken
@ -37,6 +40,7 @@ import net.corda.node.services.api.WritableTransactionStorage
import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.configOf
import net.corda.node.services.config.parseToDbSchemaFriendlyName
import net.corda.node.services.api.*
import net.corda.node.services.identity.InMemoryIdentityService
import net.corda.node.services.schema.HibernateObserver
import net.corda.node.services.schema.NodeSchemaService
@ -148,9 +152,10 @@ open class MockServices private constructor(
override val vaultService: VaultService = makeVaultService(database.hibernateConfig, schemaService)
override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>) {
super.recordTransactions(statesToRecord, txs)
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
(vaultService as VaultServiceInternal).notifyAll(statesToRecord, txs.map { it.coreTransaction })
ServiceHubInternal.recordTransactions(statesToRecord, txs,
validatedTransactions as WritableTransactionStorage,
mockStateMachineRecordedTransactionMappingStorage,
vaultService as VaultServiceInternal)
}
override fun jdbcSession(): Connection = database.createSession()
@ -166,6 +171,19 @@ open class MockServices private constructor(
// compiler and then the c'tor itself.
return Throwable().stackTrace[3].className.split('.').dropLast(1).joinToString(".")
}
// Because Kotlin is dumb and makes not publicly visible objects public, thus changing the public API.
private val mockStateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage()
}
private class MockStateMachineRecordedTransactionMappingStorage : StateMachineRecordedTransactionMappingStorage {
override fun addMapping(stateMachineRunId: StateMachineRunId, transactionId: SecureHash) {
throw UnsupportedOperationException()
}
override fun track(): DataFeed<List<StateMachineTransactionMapping>, StateMachineTransactionMapping> {
throw UnsupportedOperationException()
}
}
private constructor(cordappLoader: CordappLoader, identityService: IdentityService, networkParameters: NetworkParameters,