mirror of
https://github.com/corda/corda.git
synced 2025-03-17 17:45:17 +00:00
Fix missing snippets (#1154)
This commit is contained in:
parent
532a03adf8
commit
dbd07a8e6c
@ -67,11 +67,15 @@ Let's take an example of the interest rate swap fixings for our scheduled events
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/contract/IRS.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
:dedent: 8
|
||||
.. code-block:: kotlin
|
||||
|
||||
override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? {
|
||||
val nextFixingOf = nextFixingOf() ?: return null
|
||||
|
||||
// This is perhaps not how we should determine the time point in the business day, but instead expect the schedule to detail some of these aspects
|
||||
val instant = suggestInterestRateAnnouncementTimeWindow(index = nextFixingOf.name, source = floatingLeg.indexSource, date = nextFixingOf.forDay).fromTime!!
|
||||
return ScheduledActivity(flowLogicRefFactory.create("net.corda.irs.flows.FixingFlow\$FixingRoleDecider", thisStateRef), instant)
|
||||
}
|
||||
|
||||
The first thing this does is establish if there are any remaining fixings. If there are none, then it returns ``null``
|
||||
to indicate that there is no activity to schedule. Otherwise it calculates the ``Instant`` at which the interest rate
|
||||
|
@ -101,15 +101,15 @@ class that binds it to the network layer.
|
||||
|
||||
Here is an extract from the ``NodeInterestRates.Oracle`` class and supporting types:
|
||||
|
||||
.. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
.. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 2
|
||||
:end-before: DOCEND 2
|
||||
@CordaSerializable
|
||||
data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Tenor)
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
/** A [Fix] represents a named interest rate, on a given day, for a given duration. It can be embedded in a tx. */
|
||||
data class Fix(val of: FixOf, val value: BigDecimal) : CommandData
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
@ -166,11 +166,40 @@ parameter and ``CommandData`` classes.
|
||||
|
||||
Let's see how the ``sign`` method for ``NodeInterestRates.Oracle`` is written:
|
||||
|
||||
.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
:dedent: 8
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
fun sign(ftx: FilteredTransaction): TransactionSignature {
|
||||
ftx.verify()
|
||||
// Performing validation of obtained filtered components.
|
||||
fun commandValidator(elem: Command<*>): Boolean {
|
||||
require(services.myInfo.legalIdentities.first().owningKey in elem.signers && elem.value is Fix) {
|
||||
"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.")
|
||||
}
|
||||
}
|
||||
|
||||
require(ftx.checkWithFun(::check))
|
||||
ftx.checkCommandVisibility(services.myInfo.legalIdentities.first().owningKey)
|
||||
// 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
|
||||
// version so we can't resolve or check it ourselves. However, that doesn't matter much, as if we sign
|
||||
// an invalid transaction the signature is worthless.
|
||||
return services.createSignature(ftx, services.myInfo.legalIdentities.first().owningKey)
|
||||
}
|
||||
|
||||
Here we can see that there are several steps:
|
||||
|
||||
@ -192,20 +221,55 @@ Binding to the network
|
||||
The first step is to create the oracle as a service by annotating its class with ``@CordaService``. Let's see how that's
|
||||
done:
|
||||
|
||||
.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 3
|
||||
:end-before: DOCEND 3
|
||||
:dedent: 4
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
@CordaService
|
||||
class Oracle(private val services: AppServiceHub) : SingletonSerializeAsToken() {
|
||||
private val mutex = ThreadBox(InnerState())
|
||||
|
||||
init {
|
||||
// Set some default fixes to the Oracle, so we can smoothly run the IRS Demo without uploading fixes.
|
||||
// This is required to avoid a situation where the runnodes version of the demo isn't in a good state
|
||||
// upon startup.
|
||||
addDefaultFixes()
|
||||
}
|
||||
|
||||
The Corda node scans for any class with this annotation and initialises them. The only requirement is that the class provide
|
||||
a constructor with a single parameter of type ``ServiceHub``.
|
||||
|
||||
.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 2
|
||||
:end-before: DOCEND 2
|
||||
:dedent: 4
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
@InitiatedBy(RatesFixFlow.FixSignFlow::class)
|
||||
class FixSignHandler(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val request = otherPartySession.receive<RatesFixFlow.SignRequest>().unwrap { it }
|
||||
val oracle = serviceHub.cordaService(Oracle::class.java)
|
||||
otherPartySession.send(oracle.sign(request.ftx))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(RatesFixFlow.FixQueryFlow::class)
|
||||
class FixQueryHandler(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
object RECEIVED : ProgressTracker.Step("Received fix request")
|
||||
object SENDING : ProgressTracker.Step("Sending fix response")
|
||||
|
||||
override val progressTracker = ProgressTracker(RECEIVED, SENDING)
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val request = otherPartySession.receive<RatesFixFlow.QueryRequest>().unwrap { it }
|
||||
progressTracker.currentStep = RECEIVED
|
||||
val oracle = serviceHub.cordaService(Oracle::class.java)
|
||||
val answers = oracle.query(request.queries)
|
||||
progressTracker.currentStep = SENDING
|
||||
otherPartySession.send(answers)
|
||||
}
|
||||
}
|
||||
|
||||
These two flows leverage the oracle to provide the querying and signing operations. They get reference to the oracle,
|
||||
which will have already been initialised by the node, using ``ServiceHub.cordaService``. Both flows are annotated with
|
||||
@ -219,11 +283,41 @@ We mentioned the client sub-flow briefly above. They are the mechanism that cli
|
||||
use to interact with your oracle. Typically there will be one for querying and one for signing. Let's take a look at
|
||||
those for ``NodeInterestRates.Oracle``.
|
||||
|
||||
.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
:dedent: 4
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
@InitiatingFlow
|
||||
class FixQueryFlow(val fixOf: FixOf, val oracle: Party) : FlowLogic<Fix>() {
|
||||
@Suspendable
|
||||
override fun call(): Fix {
|
||||
val oracleSession = initiateFlow(oracle)
|
||||
// TODO: add deadline to receive
|
||||
val resp = oracleSession.sendAndReceive<List<Fix>>(QueryRequest(listOf(fixOf)))
|
||||
|
||||
return resp.unwrap {
|
||||
val fix = it.first()
|
||||
// Check the returned fix is for what we asked for.
|
||||
check(fix.of == fixOf)
|
||||
fix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
class FixSignFlow(val tx: TransactionBuilder, val oracle: Party,
|
||||
val partialMerkleTx: FilteredTransaction) : FlowLogic<TransactionSignature>() {
|
||||
@Suspendable
|
||||
override fun call(): TransactionSignature {
|
||||
val oracleSession = initiateFlow(oracle)
|
||||
val resp = oracleSession.sendAndReceive<TransactionSignature>(SignRequest(partialMerkleTx))
|
||||
return resp.unwrap { sig ->
|
||||
check(oracleSession.counterparty.owningKey.isFulfilledBy(listOf(sig.by)))
|
||||
tx.toWireTransaction(serviceHub).checkSignature(sig)
|
||||
sig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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:`key-concepts-oracles`. Below you will see how to build such a
|
||||
@ -238,11 +332,22 @@ The oracle is invoked through sub-flows to query for values, add them to the tra
|
||||
the transaction signed by the oracle. Following on from the above examples, this is all encapsulated in a sub-flow
|
||||
called ``RatesFixFlow``. Here's the ``call`` method of that flow.
|
||||
|
||||
.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 2
|
||||
:end-before: DOCEND 2
|
||||
:dedent: 4
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
@Suspendable
|
||||
override fun call(): TransactionSignature {
|
||||
progressTracker.currentStep = progressTracker.steps[1]
|
||||
val fix = subFlow(FixQueryFlow(fixOf, oracle))
|
||||
progressTracker.currentStep = WORKING
|
||||
checkFixIsNearExpected(fix)
|
||||
tx.addCommand(fix, oracle.owningKey)
|
||||
beforeSigning(fix)
|
||||
progressTracker.currentStep = SIGNING
|
||||
val mtx = tx.toWireTransaction(serviceHub).buildFilteredTransaction(Predicate { filtering(it) })
|
||||
return subFlow(FixSignFlow(tx, oracle, mtx))
|
||||
}
|
||||
|
||||
As you can see, this:
|
||||
|
||||
@ -255,11 +360,31 @@ As you can see, this:
|
||||
|
||||
Here's an example of it in action from ``FixingFlow.Fixer``.
|
||||
|
||||
.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
:dedent: 4
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
val addFixing = object : RatesFixFlow(ptx, handshake.payload.oracle, fixOf, BigDecimal.ZERO, BigDecimal.ONE) {
|
||||
@Suspendable
|
||||
override fun beforeSigning(fix: Fix) {
|
||||
newDeal.generateFix(ptx, StateAndRef(txState, handshake.payload.ref), fix)
|
||||
|
||||
// We set the transaction's time-window: it may be that none of the contracts need this!
|
||||
// But it can't hurt to have one.
|
||||
ptx.setTimeWindow(serviceHub.clock.instant(), 30.seconds)
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
}
|
||||
val sig = subFlow(addFixing)
|
||||
|
||||
.. note::
|
||||
When overriding be careful when making the sub-class an anonymous or inner class (object declarations in Kotlin),
|
||||
@ -271,24 +396,55 @@ Testing
|
||||
|
||||
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.
|
||||
Make sure the packages you provide include your oracle service, and it will 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
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
See `here <https://github.com/corda/corda/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt>`_ for more examples.
|
||||
|
@ -1,4 +1,8 @@
|
||||
.. highlight:: kotlin
|
||||
.. raw:: html
|
||||
|
||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||
|
||||
Using attachments
|
||||
=================
|
||||
@ -57,13 +61,15 @@ Attachments metadata can be used to query, in the similar manner as :doc:`api-va
|
||||
* Nullability (IS_NULL, NOT_NULL)
|
||||
* Collection based (IN, NOT_IN)
|
||||
|
||||
``And`` and ``or`` operators can be used to build queries of arbitrary complexity. Example of such query:
|
||||
``And`` and ``or`` operators can be used to build queries of arbitrary complexity. For example:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART AttachmentQueryExample1
|
||||
:end-before: DOCEND AttachmentQueryExample1
|
||||
:dedent: 12
|
||||
:dedent: 8
|
||||
|
||||
Protocol
|
||||
--------
|
||||
@ -99,20 +105,85 @@ and if so, printed out.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: ../../samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
.. code-block:: kotlin
|
||||
|
||||
fun recipient(rpc: CordaRPCOps, webPort: Int) {
|
||||
println("Waiting to receive transaction ...")
|
||||
val stx = rpc.internalVerifiedTransactionsFeed().updates.toBlocking().first()
|
||||
val wtx = stx.tx
|
||||
if (wtx.attachments.isNotEmpty()) {
|
||||
if (wtx.outputs.isNotEmpty()) {
|
||||
val state = wtx.outputsOfType<AttachmentContract.State>().single()
|
||||
require(rpc.attachmentExists(state.hash))
|
||||
|
||||
// Download the attachment via the Web endpoint.
|
||||
val connection = URL("http://localhost:$webPort/attachments/${state.hash}").openConnection() as HttpURLConnection
|
||||
try {
|
||||
require(connection.responseCode == SC_OK) { "HTTP status code was ${connection.responseCode}" }
|
||||
require(connection.contentType == APPLICATION_OCTET_STREAM) { "Content-Type header was ${connection.contentType}" }
|
||||
require(connection.getHeaderField(CONTENT_DISPOSITION) == "attachment; filename=\"${state.hash}.zip\"") {
|
||||
"Content-Disposition header was ${connection.getHeaderField(CONTENT_DISPOSITION)}"
|
||||
}
|
||||
|
||||
// Write out the entries inside this jar.
|
||||
println("Attachment JAR contains these entries:")
|
||||
JarInputStream(connection.inputStream).use { it ->
|
||||
while (true) {
|
||||
val e = it.nextJarEntry ?: break
|
||||
println("Entry> ${e.name}")
|
||||
it.closeEntry()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
println("File received - we're happy!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(wtx)}")
|
||||
} else {
|
||||
println("Error: no output state found in ${wtx.id}")
|
||||
}
|
||||
} else {
|
||||
println("Error: no attachments found in ${wtx.id}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
The sender correspondingly builds a transaction with the attachment, then calls ``FinalityFlow`` to complete the
|
||||
transaction and send it to the recipient node:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: ../../samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 2
|
||||
:end-before: DOCEND 2
|
||||
.. code-block:: kotlin
|
||||
|
||||
fun sender(rpc: CordaRPCOps, numOfClearBytes: Int = 1024) { // default size 1K.
|
||||
val (inputStream, hash) = InputStreamAndHash.createInMemoryTestZip(numOfClearBytes, 0)
|
||||
val executor = Executors.newScheduledThreadPool(2)
|
||||
try {
|
||||
sender(rpc, inputStream, hash, executor)
|
||||
} finally {
|
||||
executor.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash.SHA256, executor: ScheduledExecutorService) {
|
||||
|
||||
// Get the identity key of the other side (the recipient).
|
||||
val notaryFuture: CordaFuture<Party> = poll(executor, DUMMY_NOTARY_NAME.toString()) { rpc.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME) }
|
||||
val otherSideFuture: CordaFuture<Party> = poll(executor, DUMMY_BANK_B_NAME.toString()) { rpc.wellKnownPartyFromX500Name(DUMMY_BANK_B_NAME) }
|
||||
// Make sure we have the file in storage
|
||||
if (!rpc.attachmentExists(hash)) {
|
||||
inputStream.use {
|
||||
val avail = inputStream.available()
|
||||
val id = rpc.uploadAttachment(it)
|
||||
require(hash == id) { "Id was '$id' instead of '$hash'" }
|
||||
}
|
||||
require(rpc.attachmentExists(hash))
|
||||
}
|
||||
|
||||
val flowHandle = rpc.startTrackedFlow(::AttachmentDemoFlow, otherSideFuture.get(), notaryFuture.get(), hash)
|
||||
flowHandle.progress.subscribe(::println)
|
||||
val stx = flowHandle.returnValue.getOrThrow()
|
||||
println("Sent ${stx.id}")
|
||||
}
|
||||
|
||||
This side is a bit more complex. Firstly it looks up its counterparty by name in the network map. Then, if the node
|
||||
doesn't already have the attachment in its storage, we upload it from a JAR resource and check the hash was what
|
||||
|
@ -1,4 +1,8 @@
|
||||
.. highlight:: kotlin
|
||||
.. raw:: html
|
||||
|
||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||
|
||||
Writing a custom notary service (experimental)
|
||||
==============================================
|
||||
@ -12,19 +16,77 @@ Similarly to writing an oracle service, the first step is to create a service cl
|
||||
with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The custom notary
|
||||
service class should provide a constructor with two parameters of types ``AppServiceHub`` and ``PublicKey``.
|
||||
|
||||
.. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt
|
||||
:language: kotlin
|
||||
:start-after: START 1
|
||||
:end-before: END 1
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
@CordaService
|
||||
class MyCustomValidatingNotaryService(override val services: AppServiceHub, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() {
|
||||
override val uniquenessProvider = PersistentUniquenessProvider(services.clock)
|
||||
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = MyValidatingNotaryFlow(otherPartySession, this)
|
||||
|
||||
override fun start() {}
|
||||
override fun stop() {}
|
||||
}
|
||||
|
||||
The next step is to write a notary service flow. You are free to copy and modify the existing built-in flows such
|
||||
as ``ValidatingNotaryFlow``, ``NonValidatingNotaryFlow``, or implement your own from scratch (following the
|
||||
``NotaryFlow.Service`` template). Below is an example of a custom flow for a *validating* notary service:
|
||||
|
||||
.. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt
|
||||
:language: kotlin
|
||||
:start-after: START 2
|
||||
:end-before: END 2
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidatingNotaryService) : NotaryServiceFlow(otherSide, service) {
|
||||
/**
|
||||
* The received transaction is checked for contract-validity, for which the caller also has to to reveal the whole
|
||||
* transaction dependency chain.
|
||||
*/
|
||||
@Suspendable
|
||||
override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts {
|
||||
try {
|
||||
val stx = requestPayload.signedTransaction
|
||||
validateRequestSignature(NotarisationRequest(stx.inputs, stx.id), requestPayload.requestSignature)
|
||||
val notary = stx.notary
|
||||
checkNotary(notary)
|
||||
verifySignatures(stx)
|
||||
resolveAndContractVerify(stx)
|
||||
val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null
|
||||
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!)
|
||||
} catch (e: Exception) {
|
||||
throw when (e) {
|
||||
is TransactionVerificationException,
|
||||
is SignatureException -> NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||
else -> e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun resolveAndContractVerify(stx: SignedTransaction) {
|
||||
subFlow(ResolveTransactionsFlow(stx, otherSideSession))
|
||||
stx.verify(serviceHub, false)
|
||||
customVerify(stx)
|
||||
}
|
||||
|
||||
private fun verifySignatures(stx: SignedTransaction) {
|
||||
val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub)
|
||||
checkSignatures(transactionWithSignatures)
|
||||
}
|
||||
|
||||
private fun checkSignatures(tx: TransactionWithSignatures) {
|
||||
try {
|
||||
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
||||
} catch (e: SignatureException) {
|
||||
throw NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||
}
|
||||
}
|
||||
|
||||
private fun customVerify(stx: SignedTransaction) {
|
||||
// Add custom verification logic
|
||||
}
|
||||
}
|
||||
|
||||
To enable the service, add the following to the node configuration:
|
||||
|
||||
|
@ -17,10 +17,42 @@ Just define a new flow that wraps the SendTransactionFlow/ReceiveTransactionFlow
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
.. code-block:: kotlin
|
||||
|
||||
@InitiatedBy(Requester::class)
|
||||
class AutoOfferAcceptor(otherSideSession: FlowSession) : Acceptor(otherSideSession) {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val finalTx = super.call()
|
||||
// Our transaction is now committed to the ledger, so report it to our regulator. We use a custom flow
|
||||
// that wraps SendTransactionFlow to allow the receiver to customise how ReceiveTransactionFlow is run,
|
||||
// and because in a real life app you'd probably have more complex logic here e.g. describing why the report
|
||||
// was filed, checking that the reportee is a regulated entity and not some random node from the wrong
|
||||
// country and so on.
|
||||
val regulator = serviceHub.identityService.partiesFromName("Regulator", true).single()
|
||||
subFlow(ReportToRegulatorFlow(regulator, finalTx))
|
||||
return finalTx
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
class ReportToRegulatorFlow(private val regulator: Party, private val finalTx: SignedTransaction) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val session = initiateFlow(regulator)
|
||||
subFlow(SendTransactionFlow(session, finalTx))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(ReportToRegulatorFlow::class)
|
||||
class ReceiveRegulatoryReportFlow(private val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// Start the matching side of SendTransactionFlow above, but tell it to record all visible states even
|
||||
// though they (as far as the node can tell) are nothing to do with us.
|
||||
subFlow(ReceiveTransactionFlow(otherSideSession, true, StatesToRecord.ALL_VISIBLE))
|
||||
}
|
||||
}
|
||||
|
||||
In this example, the ``AutoOfferFlow`` is the business logic, and we define two very short and simple flows to send
|
||||
the transaction to the regulator. There are two important aspects to note here:
|
||||
|
@ -1,3 +1,9 @@
|
||||
.. highlight:: kotlin
|
||||
.. raw:: html
|
||||
|
||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||
|
||||
Transaction tear-offs
|
||||
=====================
|
||||
|
||||
@ -41,13 +47,42 @@ transaction components is exactly the same. Note that unlike ``WireTransaction``
|
||||
:end-before: DOCEND 3
|
||||
:dedent: 4
|
||||
|
||||
The following code snippet is taken from ``NodeInterestRates.kt`` and implements a signing part of an Oracle.
|
||||
The following code snippet is taken from the IRS Demo and implements a signing part of an Oracle.
|
||||
|
||||
.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
:dedent: 8
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
fun sign(ftx: FilteredTransaction): TransactionSignature {
|
||||
ftx.verify()
|
||||
// Performing validation of obtained filtered components.
|
||||
fun commandValidator(elem: Command<*>): Boolean {
|
||||
require(services.myInfo.legalIdentities.first().owningKey in elem.signers && elem.value is Fix) {
|
||||
"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.")
|
||||
}
|
||||
}
|
||||
|
||||
require(ftx.checkWithFun(::check))
|
||||
ftx.checkCommandVisibility(services.myInfo.legalIdentities.first().owningKey)
|
||||
// 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
|
||||
// version so we can't resolve or check it ourselves. However, that doesn't matter much, as if we sign
|
||||
// an invalid transaction the signature is worthless.
|
||||
return services.createSignature(ftx, services.myInfo.legalIdentities.first().owningKey)
|
||||
}
|
||||
|
||||
.. note:: The way the ``FilteredTransaction`` is constructed ensures that after signing of the root hash it's impossible to add or remove
|
||||
components (leaves). However, it can happen that having transaction with multiple commands one party reveals only subset of them to the Oracle.
|
||||
|
Loading…
x
Reference in New Issue
Block a user