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:
Mike Hearn 2016-03-08 15:19:18 +01:00
parent f1c9b5495c
commit a7fec047ed
3 changed files with 338 additions and 0 deletions

View 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()
}

View 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
}
}
}

View 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))
}