Writing oracle services
This tutorial covers oracles: network services that link the ledger to the outside world by providing facts that affect the validity of transactions.
The IRS trading demo app includes an example oracle that provides an interest rate fixing service.
Introduction
Oracles are a key concept in the block chain/decentralised ledger space. They can be essential for many kinds of application, because we often wish to condition the validity of a transaction on some fact being true or false, but the ledger itself has a design that is essentially functional: all transactions are pure and immutable. Phrased another way, a contract cannot perform any input/output or depend on any state outside of the transaction itself. For example, there is no way to download a web page or interact with the user from within a contract. It must be this way because everyone must be able to independently check a transaction and arrive at an identical conclusion regarding its validity for the ledger to maintain its integrity: if a transaction could evaluate to “valid” on one computer and then “invalid” a few minutes later on a different computer, the entire shared ledger concept wouldn’t work.
But transaction validity does often depend on data from the outside world - verifying that an interest rate swap is paying out correctly may require data on interest rates, verifying that a loan has reached maturity requires knowledge about the current time, knowing which side of a bet receives the payment may require arbitrary facts about the real world (for example, the bankruptcy or solvency of a company or country), and so on.
We can solve this problem by introducing services that create digitally signed data structures which assert facts. These structures can then be used as an input to a transaction and distributed with the transaction data itself. Because the statements are themselves immutable and signed, it is impossible for an oracle to change its mind later and invalidate transactions that were previously found to be valid. In contrast, consider what would happen if a contract could do an HTTP request: it’s possible that an answer would change after being downloaded, resulting in loss of consensus.
Approaches to implementing oracles
The Corda architecture provides two ways of implementing oracles with different tradeoffs:
- Using commands
- Using attachments
When a fact is encoded in a command, it is embedded in the transaction itself. The oracle then acts as a co-signer to the entire transaction. The oracle’s signature is valid only for that transaction, and thus even if a fact (like a stock price) does not change, every transaction that incorporates that fact must go back to the oracle for signing.
When a fact is encoded as an attachment, it is a separate object to the transaction and is referred to by hash. Nodes download attachments from peers at the same time as they download transactions, unless of course the node has already seen that attachment, in which case it won’t fetch it again. Contracts have access to the contents of attachments when they run.
As you can see, both approaches share a few things: they both allow arbitrary binary data to be provided to transactions (and thus contracts). The primary difference is whether the data is a freely reusable, standalone object or whether it’s integrated with a transaction.
Here’s a quick way to decide which approach makes more sense for your data source:
- Is your data continuously changing, like a stock price, the current time, etc? If yes, use a command.
- Is your data commercially valuable, like a feed which you are not allowed to resell unless it’s incorporated into a business deal? If yes, use a command, so you can charge money for signing the same fact in each unique business context.
- Is your data very small, like a single number? If yes, use a command.
- Is your data large, static and commercially worthless, for instance, a holiday calendar? If yes, use an attachment.
- Is your data intended for human consumption, like a PDF of legal prose, or an Excel spreadsheet? If yes, use an attachment.
Asserting continuously varying data
Let’s look at the interest rates oracle that can be found in the NodeInterestRates
file. This is an example of
an oracle that uses a command because the current interest rate fix is a constantly changing fact.
The obvious way to implement such a service is this:
- The creator of the transaction that depends on the interest rate sends it to the oracle.
- The oracle inserts a command with the rate and signs the transaction.
- The oracle sends it back.
But this has a problem - it would mean that the oracle has to be the first entity to sign the transaction, which might impose ordering constraints we don’t want to deal with (being able to get all parties to sign in parallel is a very nice thing). So the way we actually implement it is like this:
- The creator of the transaction that depends on the interest rate asks for the current rate. They can abort at this point if they want to.
- They insert a command with that rate and the time it was obtained into the transaction.
- They then send it to the oracle for signing, along with everyone else, potentially in parallel. The oracle checks that the command has the correct data for the asserted time, and signs if so.
This same technique can be adapted to other types of oracle.
The oracle consists of a core class that implements the query/sign operations (for easy unit testing), and then a separate class that binds it to the network layer.
Here is an extract from the NodeInterestRates.Oracle
class and supporting types:
/** A [FixOf] identifies the question side of a fix: what day, tenor and type of fix ("LIBOR", "EURIBOR" etc) */
@CordaSerializable
data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Tenor)
/** 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
class Oracle {
fun query(queries: List<FixOf>): List<Fix>
fun sign(ftx: FilteredTransaction): TransactionSignature
}
The fix contains a timestamp (the forDay
field) that identifies the version of the data being requested. Since
there can be an arbitrary delay between a fix being requested via query
and the signature being requested via
sign
, this timestamp allows the Oracle to know which, potentially historical, value it is being asked to sign for. This is an
important technique for continuously varying data.
Hiding transaction data from the oracle
Because the transaction is sent to the oracle for signing, ordinarily the oracle would be able to see the entire contents
of that transaction including the inputs, output contract states and all the commands, not just the one (in this case)
relevant command. This is an obvious privacy leak for the other participants. We currently solve this using a
FilteredTransaction
, which implements a Merkle Tree. These reveal only the necessary parts of the transaction to the
oracle, but still allow the oracle to sign the transaction by providing the Merkle hashes for the remaining parts. See Oracles
for more details.
Implementing pay-per-play oracles
Because the signature covers the transaction, and transactions may end up being forwarded anywhere, the fact itself
is independently checkable. However, this approach can still be useful when the data itself costs money, because the act
of issuing the signature in the first place can be charged for (for example, by requiring the submission of a fresh
Cash.State
that has been re-assigned to a key owned by the oracle service). Because the signature covers the
transaction and not only the fact, this allows for a kind of weak pseudo-DRM over data feeds. Whilst a
contract could in theory include a transaction parsing and signature checking library, writing a contract in this way
would be conclusive evidence of intent to disobey the rules of the service (res ipsa loquitur). In an environment
where parties are legally identifiable, usage of such a contract would by itself be sufficient to trigger some sort of
punishment.
Implementing an oracle with continuously varying data
Implementing an oracle with continuously varying data requires the following steps:
Implementing the core classes
The key is to implement your oracle in a similar way to the NodeInterestRates.Oracle
outline we gave above with
both a query
and a sign
method. Typically, you would want one class that encapsulates the parameters to the query
method (FixOf
, above), and a CommandData
implementation (Fix
, above) that encapsulates both an instance of
that parameter class and an instance of whatever the result of the query
is (BigDecimal
above).
The NodeInterestRates.Oracle
allows querying for multiple Fix
objects, but that is not necessary and is
provided for the convenience of callers who need multiple fixes and want to be able to do it all in one query request.
Assuming you have a data source and can query it, it should be very easy to implement your query
method and the
parameter and CommandData
classes.
Let’s see how the sign
method for NodeInterestRates.Oracle
is written:
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 you can see that there are several steps:
- Ensure that the transaction we have been sent is indeed valid and passes verification, even though we cannot see all of it.
- Check that we only received commands as expected, and each of those commands expects us to sign for them and is of
the expected type (
Fix
here). - Iterate over each of the commands we identified in the last step and check that the data they represent matches exactly our data source. The final step, assuming we have got this far, is to generate a signature for the transaction and return it.
Binding to the network
@CordaService
. Let’s see how that’s
done:@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
.
@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
@InitiatedBy
. This tells the node which initiating flow (which are discussed in the next section) they are meant to
be executed with.
Providing sub-flows for querying and signing
We mentioned the client sub-flow briefly above. This is the mechanism that clients, in the form of other flows, will
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
.
@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 Oracles. Below you will see how to build such a
transaction with hidden fields.
Using an oracle
The oracle is invoked through sub-flows to query for values, add them to the transaction as commands and then get
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.
@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:
- Queries the oracle for the fact using the client sub-flow for querying defined above
- Does some quick validation
- Adds the command to the transaction containing the fact to be signed for by the oracle
- Calls an extension point that allows clients to generate output states based on the fact from the oracle
- Builds filtered transaction based on filtering function extended from
RatesFixFlow
- Requests the signature from the oracle using the client sub-flow for signing from above
Here’s an example of it in action from FixingFlow.Fixer
.
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)
Testing your oracle service
The MockNetwork
allows the creation of MockNode
instances, which are simplified nodes which can be used for testing (see API: Testing).
When creating the MockNetwork
, you supply a list of TestCordapp
objects which point to CorDapps on
the classpath. These CorDapps will be installed on each node on the network. Make sure the packages you provide reference to the CorDapp
containing your oracle service.
You can then write tests on your mock network to verify the nodes interact with your oracle correctly.
@Test(timeout=300_000)
`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)
}
For more examples, see OracleNodeTearOffTests.kt.
Was this page helpful?
Thanks for your feedback!
Chat with us
Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.
Propose documentation improvements directly
Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.
We're sorry this page wasn't helpful. Let us know how we can make it better!
Chat with us
Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.
Create an issue
Create a new GitHub issue in this repository - submit technical feedback, draw attention to a potential documentation bug, or share ideas for improvement and general feedback.
Propose documentation improvements directly
Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.