mirror of
https://github.com/corda/corda.git
synced 2025-01-18 02:39:51 +00:00
Add an interest rates oracle.
Currently lacking the following useful things: - Progress reporting - Ability to upload new rates - Demo app[s]
This commit is contained in:
parent
f1c9b5495c
commit
a7fec047ed
149
src/main/kotlin/core/node/services/NodeInterestRates.kt
Normal file
149
src/main/kotlin/core/node/services/NodeInterestRates.kt
Normal file
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
|
||||
package core.node.services
|
||||
|
||||
import core.*
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.signWithECDSA
|
||||
import core.messaging.send
|
||||
import core.node.AbstractNode
|
||||
import core.serialization.deserialize
|
||||
import protocols.RatesFixProtocol
|
||||
import java.math.BigDecimal
|
||||
import java.security.KeyPair
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* An interest rates service is an oracle that signs transactions which contain embedded assertions about an interest
|
||||
* rate fix (e.g. LIBOR, EURIBOR ...).
|
||||
*
|
||||
* The oracle has two functions. It can be queried for a fix for the given day. And it can sign a transaction that
|
||||
* includes a fix that it finds acceptable. So to use it you would query the oracle, incorporate its answer into the
|
||||
* transaction you are building, and then (after possibly extra steps) hand the final transaction back to the oracle
|
||||
* for signing.
|
||||
*/
|
||||
object NodeInterestRates {
|
||||
val INITIAL_FIXES = parseFile("""
|
||||
LIBOR 2016-03-16 30 = 0.678
|
||||
LIBOR 2016-03-16 60 = 0.655
|
||||
EURIBOR 2016-03-15 30 = 0.123
|
||||
EURIBOR 2016-03-15 60 = 0.111
|
||||
""".trimIndent())
|
||||
|
||||
/** Parses a string of the form "LIBOR 16-March-2016 30 = 0.678" into a [FixOf] and [Fix] */
|
||||
fun parseOneRate(s: String): Pair<FixOf, Fix> {
|
||||
val (key, value) = s.split('=').map { it.trim() }
|
||||
val of = parseFixOf(key)
|
||||
val rate = BigDecimal(value)
|
||||
return of to Fix(of, rate)
|
||||
}
|
||||
|
||||
/** Parses a string of the form "LIBOR 16-March-2016 30" into a [FixOf] */
|
||||
fun parseFixOf(key: String): FixOf {
|
||||
val (name, date, days) = key.split(' ')
|
||||
return FixOf(name, LocalDate.parse(date), days.toInt().days)
|
||||
}
|
||||
|
||||
/** Parses lines containing fixes */
|
||||
fun parseFile(s: String): Map<FixOf, Fix> {
|
||||
val results = HashMap<FixOf, Fix>()
|
||||
for (line in s.lines()) {
|
||||
val (fixOf, fix) = parseOneRate(line.trim())
|
||||
results[fixOf] = fix
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* The Service that wraps [Oracle] and handles messages/network interaction/request scrubbing.
|
||||
*/
|
||||
class Service(node: AbstractNode) {
|
||||
val ss = node.services.storageService
|
||||
val oracle = Oracle(ss.myLegalIdentity, ss.myLegalIdentityKey)
|
||||
val net = node.services.networkService
|
||||
|
||||
init {
|
||||
handleQueries()
|
||||
handleSignRequests()
|
||||
}
|
||||
|
||||
private fun handleSignRequests() {
|
||||
net.addMessageHandler(RatesFixProtocol.TOPIC + ".sign.0") { message, registration ->
|
||||
val request = message.data.deserialize<RatesFixProtocol.SignRequest>()
|
||||
val sig = oracle.sign(request.tx)
|
||||
net.send("${RatesFixProtocol.TOPIC}.sign.${request.sessionID}", request.replyTo, sig)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleQueries() {
|
||||
net.addMessageHandler(RatesFixProtocol.TOPIC + ".query.0") { message, registration ->
|
||||
val request = message.data.deserialize<RatesFixProtocol.QueryRequest>()
|
||||
val answers = oracle.query(request.queries)
|
||||
net.send("${RatesFixProtocol.TOPIC}.query.${request.sessionID}", request.replyTo, answers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of an interest rate fix oracle which is given data in a simple string format.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class Oracle(val identity: Party, private val signingKey: KeyPair) {
|
||||
init {
|
||||
require(signingKey.public == identity.owningKey)
|
||||
}
|
||||
|
||||
/** The fix data being served by this oracle. */
|
||||
@Transient var knownFixes = INITIAL_FIXES
|
||||
set(value) {
|
||||
require(value.isNotEmpty())
|
||||
field = value
|
||||
}
|
||||
|
||||
fun query(queries: List<FixOf>): List<Fix> {
|
||||
require(queries.isNotEmpty())
|
||||
val knownFixes = knownFixes // Snapshot
|
||||
|
||||
val answers: List<Fix?> = queries.map { knownFixes[it] }
|
||||
val firstNull = answers.indexOf(null)
|
||||
if (firstNull != -1)
|
||||
throw UnknownFix(queries[firstNull])
|
||||
return answers.filterNotNull()
|
||||
}
|
||||
|
||||
fun sign(wtx: WireTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
// Extract the fix commands marked as being signable by us.
|
||||
val fixes: List<Fix> = wtx.commands.
|
||||
filter { identity.owningKey in it.pubkeys && it.data is Fix }.
|
||||
map { it.data as Fix }
|
||||
|
||||
// Reject this signing attempt if there are no commands of the right kind.
|
||||
if (fixes.isEmpty())
|
||||
throw IllegalArgumentException()
|
||||
|
||||
// For each fix, verify that the data is correct.
|
||||
val knownFixes = knownFixes // Snapshot
|
||||
for (fix in fixes) {
|
||||
val known = knownFixes[fix.of]
|
||||
if (known == null || known != fix)
|
||||
throw UnknownFix(fix.of)
|
||||
}
|
||||
|
||||
// It all checks out, so we can return a signature.
|
||||
//
|
||||
// Note that we will happily sign an invalid transaction: we don't bother trying to validate the whole
|
||||
// thing. This is so that later on we can start using tear-offs.
|
||||
return signingKey.signWithECDSA(wtx.serialized, identity)
|
||||
}
|
||||
}
|
||||
|
||||
class UnknownFix(val fix: FixOf) : Exception()
|
||||
}
|
87
src/main/kotlin/protocols/RatesFixProtocol.kt
Normal file
87
src/main/kotlin/protocols/RatesFixProtocol.kt
Normal file
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
|
||||
package protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import core.*
|
||||
import core.crypto.DigitalSignature
|
||||
import core.messaging.LegallyIdentifiableNode
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.protocols.ProtocolLogic
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
|
||||
// This code is unit tested in NodeInterestRates.kt
|
||||
|
||||
/**
|
||||
* This protocol queries the given oracle for an interest rate fix, and if it is within the given tolerance embeds the
|
||||
* fix in the transaction and then proceeds to get the oracle to sign it. Although the [call] method combines the query
|
||||
* and signing step, you can run the steps individually by constructing this object and then using the public methods
|
||||
* for each step.
|
||||
*
|
||||
* @throws FixOutOfRange if the returned fix was further away from the expected rate by the given amount.
|
||||
*/
|
||||
class RatesFixProtocol(private val tx: TransactionBuilder,
|
||||
private val oracle: LegallyIdentifiableNode,
|
||||
private val fixOf: FixOf,
|
||||
private val expectedRate: BigDecimal,
|
||||
private val rateTolerance: BigDecimal) : ProtocolLogic<Unit>() {
|
||||
companion object {
|
||||
val TOPIC = "platform.rates.interest.fix"
|
||||
}
|
||||
|
||||
class FixOutOfRange(val byAmount: BigDecimal) : Exception()
|
||||
|
||||
data class QueryRequest(val queries: List<FixOf>, val replyTo: SingleMessageRecipient, val sessionID: Long)
|
||||
data class SignRequest(val tx: WireTransaction, val replyTo: SingleMessageRecipient, val sessionID: Long)
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val fix = query()
|
||||
checkFixIsNearExpected(fix)
|
||||
tx.addCommand(fix, oracle.identity.owningKey)
|
||||
tx.addSignatureUnchecked(sign())
|
||||
}
|
||||
|
||||
private fun checkFixIsNearExpected(fix: Fix) {
|
||||
val delta = (fix.value - expectedRate).abs()
|
||||
if (delta > rateTolerance) {
|
||||
// TODO: Kick to a user confirmation / ui flow if it's out of bounds instead of raising an exception.
|
||||
throw FixOutOfRange(delta)
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
fun sign(): DigitalSignature.LegallyIdentifiable {
|
||||
val sessionID = random63BitValue()
|
||||
val wtx = tx.toWireTransaction()
|
||||
val req = SignRequest(wtx, serviceHub.networkService.myAddress, sessionID)
|
||||
val resp = sendAndReceive<DigitalSignature.LegallyIdentifiable>(TOPIC + ".sign", oracle.address, 0, sessionID, req)
|
||||
|
||||
return resp.validate { sig ->
|
||||
check(sig.signer == oracle.identity)
|
||||
tx.checkSignature(sig)
|
||||
sig
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
fun query(): Fix {
|
||||
val sessionID = random63BitValue()
|
||||
val req = QueryRequest(listOf(fixOf), serviceHub.networkService.myAddress, sessionID)
|
||||
val resp = sendAndReceive<ArrayList<Fix>>(TOPIC + ".query", oracle.address, 0, sessionID, req)
|
||||
|
||||
return resp.validate {
|
||||
val fix = it.first()
|
||||
// Check the returned fix is for what we asked for.
|
||||
check(fix.of == fixOf)
|
||||
fix
|
||||
}
|
||||
}
|
||||
}
|
102
src/test/kotlin/core/node/services/NodeInterestRatesTest.kt
Normal file
102
src/test/kotlin/core/node/services/NodeInterestRatesTest.kt
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
|
||||
package core.node.services
|
||||
|
||||
import contracts.Cash
|
||||
import core.DOLLARS
|
||||
import core.Fix
|
||||
import core.TransactionBuilder
|
||||
import core.d
|
||||
import core.node.MockNetwork
|
||||
import core.testutils.*
|
||||
import core.utilities.BriefLogFormatter
|
||||
import org.junit.Test
|
||||
import protocols.RatesFixProtocol
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class NodeInterestRatesTest {
|
||||
val TEST_DATA = NodeInterestRates.parseFile("""
|
||||
LIBOR 2016-03-16 30 = 0.678
|
||||
LIBOR 2016-03-16 60 = 0.655
|
||||
EURIBOR 2016-03-15 30 = 0.123
|
||||
EURIBOR 2016-03-15 60 = 0.111
|
||||
""".trimIndent())
|
||||
|
||||
val service = NodeInterestRates.Oracle(MEGA_CORP, MEGA_CORP_KEY).apply { knownFixes = TEST_DATA }
|
||||
|
||||
@Test fun `query successfully`() {
|
||||
val q = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 30")
|
||||
val res = service.query(listOf(q))
|
||||
assertEquals(1, res.size)
|
||||
assertEquals("0.678".d, res[0].value)
|
||||
assertEquals(q, res[0].of)
|
||||
}
|
||||
|
||||
@Test fun `query with one success and one missing`() {
|
||||
val q1 = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 30")
|
||||
val q2 = NodeInterestRates.parseFixOf("LIBOR 2016-03-19 30")
|
||||
val e = assertFailsWith<NodeInterestRates.UnknownFix> { service.query(listOf(q1, q2)) }
|
||||
assertEquals(e.fix, q2)
|
||||
}
|
||||
|
||||
@Test fun `empty query`() {
|
||||
assertFailsWith<IllegalArgumentException> { service.query(emptyList()) }
|
||||
}
|
||||
|
||||
@Test fun `refuse to sign with no relevant commands`() {
|
||||
val tx = makeTX()
|
||||
assertFailsWith<IllegalArgumentException> { service.sign(tx.toWireTransaction()) }
|
||||
tx.addCommand(Cash.Commands.Move(), ALICE)
|
||||
assertFailsWith<IllegalArgumentException> { service.sign(tx.toWireTransaction()) }
|
||||
}
|
||||
|
||||
@Test fun `sign successfully`() {
|
||||
val tx = makeTX()
|
||||
val fix = service.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 30"))).first()
|
||||
tx.addCommand(fix, service.identity.owningKey)
|
||||
|
||||
// Sign successfully.
|
||||
val signature = service.sign(tx.toWireTransaction())
|
||||
tx.checkAndAddSignature(signature)
|
||||
}
|
||||
|
||||
@Test fun `do not sign with unknown fix`() {
|
||||
val tx = makeTX()
|
||||
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 30")
|
||||
val badFix = Fix(fixOf, "0.6789".d)
|
||||
tx.addCommand(badFix, service.identity.owningKey)
|
||||
|
||||
val e1 = assertFailsWith<NodeInterestRates.UnknownFix> { service.sign(tx.toWireTransaction()) }
|
||||
assertEquals(fixOf, e1.fix)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun network() {
|
||||
val net = MockNetwork()
|
||||
val (n1, n2) = net.createTwoNodes()
|
||||
NodeInterestRates.Service(n2)
|
||||
|
||||
val tx = TransactionBuilder()
|
||||
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 30")
|
||||
val protocol = RatesFixProtocol(tx, n2.legallyIdentifableAddress, fixOf, "0.675".d, "0.1".d)
|
||||
BriefLogFormatter.initVerbose("rates")
|
||||
val future = n1.smm.add("rates", protocol)
|
||||
|
||||
net.runNetwork()
|
||||
future.get()
|
||||
|
||||
// We should now have a valid signature over our tx from the oracle.
|
||||
val fix = tx.toSignedTransaction(true).tx.commands.map { it.data as Fix }.first()
|
||||
assertEquals(fixOf, fix.of)
|
||||
assertEquals("0.678".d, fix.value)
|
||||
}
|
||||
|
||||
private fun makeTX() = TransactionBuilder(outputs = mutableListOf(1000.DOLLARS.CASH `owned by` ALICE))
|
||||
}
|
Loading…
Reference in New Issue
Block a user