Merged irs-oracle into master

This commit is contained in:
Mike Hearn 2016-03-09 14:38:39 +01:00
commit 975d569e55
15 changed files with 692 additions and 82 deletions

View File

@ -118,4 +118,18 @@ tasks.withType(Test) {
tasks.withType(JavaExec) {
jvmArgs "-javaagent:${configurations.quasar.singleFile}"
jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation"
}
// Package up the other demo programs.
task getRateFixDemo(type: CreateStartScripts) {
mainClassName = "demos.RateFixDemoKt"
applicationName = "get-rate-fix"
defaultJvmOpts = ["-javaagent:${configurations.quasar.singleFile}"]
outputDir = new File(project.buildDir, 'scripts')
classpath = jar.outputs.files + project.configurations.runtime
}
applicationDistribution.into("bin") {
from(getRateFixDemo)
fileMode = 0755
}

View File

@ -8,7 +8,6 @@
package core
import java.math.BigDecimal
import java.security.PublicKey
import java.util.*
@ -47,63 +46,6 @@ class Requirements {
val R = Requirements()
inline fun <R> requireThat(body: Requirements.() -> R) = R.body()
//// Amounts //////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Amount represents a positive quantity of currency, measured in pennies, which are the smallest representable units.
*
* Note that "pennies" are not necessarily 1/100ths of a currency unit, but are the actual smallest amount used in
* whatever currency the amount represents.
*
* Amounts of different currencies *do not mix* and attempting to add or subtract two amounts of different currencies
* will throw [IllegalArgumentException]. Amounts may not be negative. Amounts are represented internally using a signed
* 64 bit value, therefore, the maximum expressable amount is 2^63 - 1 == Long.MAX_VALUE. Addition, subtraction and
* multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer
* overflow.
*
* TODO: It may make sense to replace this with convenience extensions over the JSR 354 MonetaryAmount interface
* TODO: Should amount be abstracted to cover things like quantities of a stock, bond, commercial paper etc? Probably.
* TODO: Think about how positive-only vs positive-or-negative amounts can be represented in the type system.
*/
data class Amount(val pennies: Long, val currency: Currency) : Comparable<Amount> {
init {
// Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain
// contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance.
// If you want to express a negative amount, for now, use a long.
require(pennies >= 0) { "Negative amounts are not allowed: $pennies" }
}
operator fun plus(other: Amount): Amount {
checkCurrency(other)
return Amount(Math.addExact(pennies, other.pennies), currency)
}
operator fun minus(other: Amount): Amount {
checkCurrency(other)
return Amount(Math.subtractExact(pennies, other.pennies), currency)
}
private fun checkCurrency(other: Amount) {
require(other.currency == currency) { "Currency mismatch: ${other.currency} vs $currency" }
}
operator fun div(other: Long): Amount = Amount(pennies / other, currency)
operator fun times(other: Long): Amount = Amount(Math.multiplyExact(pennies, other), currency)
operator fun div(other: Int): Amount = Amount(pennies / other, currency)
operator fun times(other: Int): Amount = Amount(Math.multiplyExact(pennies, other.toLong()), currency)
override fun toString(): String = currency.currencyCode + " " + (BigDecimal(pennies) / BigDecimal(100)).toPlainString()
override fun compareTo(other: Amount): Int {
checkCurrency(other)
return pennies.compareTo(other.pennies)
}
}
fun Iterable<Amount>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
fun Iterable<Amount>.sumOrThrow() = reduce { left, right -> left + right }
fun Iterable<Amount>.sumOrZero(currency: Currency) = if (iterator().hasNext()) sumOrThrow() else Amount(0, currency)
//// Authenticated commands ///////////////////////////////////////////////////////////////////////////////////////////
/** Filters the command list by type, party and public key all at once. */

View File

@ -0,0 +1,79 @@
/*
* 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
import java.math.BigDecimal
import java.time.Duration
import java.time.LocalDate
import java.util.*
/**
* Amount represents a positive quantity of currency, measured in pennies, which are the smallest representable units.
*
* Note that "pennies" are not necessarily 1/100ths of a currency unit, but are the actual smallest amount used in
* whatever currency the amount represents.
*
* Amounts of different currencies *do not mix* and attempting to add or subtract two amounts of different currencies
* will throw [IllegalArgumentException]. Amounts may not be negative. Amounts are represented internally using a signed
* 64 bit value, therefore, the maximum expressable amount is 2^63 - 1 == Long.MAX_VALUE. Addition, subtraction and
* multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer
* overflow.
*
* TODO: It may make sense to replace this with convenience extensions over the JSR 354 MonetaryAmount interface
* TODO: Should amount be abstracted to cover things like quantities of a stock, bond, commercial paper etc? Probably.
* TODO: Think about how positive-only vs positive-or-negative amounts can be represented in the type system.
*/
data class Amount(val pennies: Long, val currency: Currency) : Comparable<Amount> {
init {
// Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain
// contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance.
// If you want to express a negative amount, for now, use a long.
require(pennies >= 0) { "Negative amounts are not allowed: $pennies" }
}
operator fun plus(other: Amount): Amount {
checkCurrency(other)
return Amount(Math.addExact(pennies, other.pennies), currency)
}
operator fun minus(other: Amount): Amount {
checkCurrency(other)
return Amount(Math.subtractExact(pennies, other.pennies), currency)
}
private fun checkCurrency(other: Amount) {
require(other.currency == currency) { "Currency mismatch: ${other.currency} vs $currency" }
}
operator fun div(other: Long): Amount = Amount(pennies / other, currency)
operator fun times(other: Long): Amount = Amount(Math.multiplyExact(pennies, other), currency)
operator fun div(other: Int): Amount = Amount(pennies / other, currency)
operator fun times(other: Int): Amount = Amount(Math.multiplyExact(pennies, other.toLong()), currency)
override fun toString(): String = currency.currencyCode + " " + (BigDecimal(pennies) / BigDecimal(100)).toPlainString()
override fun compareTo(other: Amount): Int {
checkCurrency(other)
return pennies.compareTo(other.pennies)
}
}
fun Iterable<Amount>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
fun Iterable<Amount>.sumOrThrow() = reduce { left, right -> left + right }
fun Iterable<Amount>.sumOrZero(currency: Currency) = if (iterator().hasNext()) sumOrThrow() else Amount(0, currency)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Interest rate fixes
//
/** A [FixOf] identifies the question side of a fix: what day, tenor and type of fix ("LIBOR", "EURIBOR" etc) */
data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Duration)
/** 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

View File

@ -15,7 +15,7 @@ you can upload it by running this command from a UNIX terminal:
.. sourcecode:: shell
curl -F myfile=@path/to/my/file.zip http://localhost:31338/attachments/upload
curl -F myfile=@path/to/my/file.zip http://localhost:31338/upload/attachment
The attachment will be identified by the SHA-256 hash of the contents, which you can get by doing:
@ -23,8 +23,8 @@ The attachment will be identified by the SHA-256 hash of the contents, which you
shasum -a 256 file.zip
on a Mac or by using ``sha256sum`` on Linux. Alternatively, check the node logs. There is presently no way to manage
attachments from a GUI.
on a Mac or by using ``sha256sum`` on Linux. Alternatively, the hash will be returned to you when you upload the
attachment.
An attachment may be downloaded by fetching:
@ -39,3 +39,24 @@ containers, you can also fetch a specific file within the attachment by appendin
http://localhost:31338/attachments/DECD098666B9657314870E192CED0C3519C2C9D395507A238338F8D003929DE9/path/within/zip.txt
Uploading interest rate fixes
-----------------------------
If you would like to operate an interest rate fixing service (oracle), you can upload fix data by uploading data in
a simple text format to the ``/upload/interest-rates`` path on the web server.
The file looks like this::
# Some pretend noddy rate fixes, for the interest rate oracles.
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
The columns are:
* Name of the fix
* Date of the fix
* The tenor / time to maturity in days
* The interest rate itself

View File

@ -0,0 +1,6 @@
# Some pretend noddy rate fixes, for the interest rate oracles.
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

24
scripts/get-rate-fix.sh Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env bash
# This needs the buyer node to be running first.
if [ ! -e ./gradlew ]; then
echo "Run from the root directory please"
exit 1
fi
bin="build/install/r3prototyping/bin/get-rate-fix"
if [ ! -e $bin ]; then
./gradlew installDist
fi
if [ ! -e buyer/identity-public ]; then
echo "You must run scripts/trade-demo.sh buyer before running this script (and keep it running)"
exit 1
fi
# Upload the rates to the buyer node
curl -F rates=@scripts/example.rates.txt http://localhost:31338/upload/interest-rates
$bin --network-address=localhost:31300 --oracle=localhost --oracle-identity-file=buyer/identity-public

View File

@ -48,6 +48,11 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
// low-performance prototyping period.
protected open val serverThread = Executors.newSingleThreadExecutor()
// Objects in this list will be scanned by the DataUploadServlet and can be handed new data via HTTP.
// Don't mutate this after startup.
protected val _servicesThatAcceptUploads = ArrayList<AcceptsFileUpload>()
val servicesThatAcceptUploads: List<AcceptsFileUpload> = _servicesThatAcceptUploads
val services = object : ServiceHub {
override val networkService: MessagingService get() = net
override val networkMapService: NetworkMap = MockNetworkMap()
@ -85,11 +90,13 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
open fun start(): AbstractNode {
log.info("Node starting up ...")
storage = initialiseStorageService(dir)
net = makeMessagingService()
smm = StateMachineManager(services, serverThread)
wallet = NodeWalletService(services)
keyManagement = E2ETestKeyManagementService()
makeInterestRateOracleService()
// Insert a network map entry for the timestamper: this is all temp scaffolding and will go away. If we are
// given the details, the timestamping node is somewhere else. Otherwise, we do our own timestamping.
@ -111,6 +118,12 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
return this
}
protected fun makeInterestRateOracleService() {
// Constructing the service registers message handlers that ensure the service won't be garbage collected.
// TODO: Once the service has data, automatically register with the network map service (once built).
_servicesThatAcceptUploads += NodeInterestRates.Service(this)
}
protected open fun makeIdentityService(): IdentityService {
// We don't have any identity infrastructure right now, so we just throw together the only two identities we
// know about: our own, and the identity of the remote timestamper node (if any).
@ -130,6 +143,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
protected open fun initialiseStorageService(dir: Path): StorageService {
val attachments = makeAttachmentStorage(dir)
_servicesThatAcceptUploads += attachments
val (identity, keypair) = obtainKeyPair(dir)
return constructStorageService(attachments, identity, keypair)
}
@ -195,7 +209,6 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
Files.createDirectory(attachmentsDir)
} catch (e: FileAlreadyExistsException) {
}
val attachments = NodeAttachmentService(attachmentsDir)
return attachments
return NodeAttachmentService(attachmentsDir)
}
}

View File

@ -0,0 +1,30 @@
/*
* 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
import java.io.InputStream
/**
* A service that implements AcceptsFileUpload can have new binary data provided to it via an HTTP upload.
*
* TODO: In future, also accept uploads over the MQ interface too.
*/
interface AcceptsFileUpload {
/** A string that prefixes the URLs, e.g. "attachments" or "interest-rates". Should be OK for URLs. */
val dataTypePrefix: String
/** What file extensions are acceptable for the file to be handed to upload() */
val acceptableFileExtensions: List<String>
/**
* Accepts the data in the given input stream, and returns some sort of useful return message that will be sent
* back to the user in the response.
*/
fun upload(data: InputStream): String
}

View File

@ -13,7 +13,7 @@ import core.messaging.LegallyIdentifiableNode
import core.messaging.MessagingService
import core.node.services.ArtemisMessagingService
import core.node.servlets.AttachmentDownloadServlet
import core.node.servlets.AttachmentUploadServlet
import core.node.servlets.DataUploadServlet
import core.utilities.loggerFor
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.servlet.ServletContextHandler
@ -59,8 +59,8 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
val port = p2pAddr.port + 1 // TODO: Move this into the node config file.
val server = Server(port)
val handler = ServletContextHandler()
handler.setAttribute("storage", storage)
handler.addServlet(AttachmentUploadServlet::class.java, "/attachments/upload")
handler.setAttribute("node", this)
handler.addServlet(DataUploadServlet::class.java, "/upload/*")
handler.addServlet(AttachmentDownloadServlet::class.java, "/attachments/*")
server.handler = handler
server.start()

View File

@ -15,6 +15,7 @@ import com.google.common.io.CountingInputStream
import core.Attachment
import core.crypto.SecureHash
import core.extractZipFile
import core.node.AcceptsFileUpload
import core.utilities.loggerFor
import java.io.FilterInputStream
import java.io.InputStream
@ -30,7 +31,7 @@ import javax.annotation.concurrent.ThreadSafe
* Stores attachments in the specified local directory, which must exist. Doesn't allow new attachments to be uploaded.
*/
@ThreadSafe
class NodeAttachmentService(val storePath: Path) : AttachmentStorage {
class NodeAttachmentService(val storePath: Path) : AttachmentStorage, AcceptsFileUpload {
private val log = loggerFor<NodeAttachmentService>()
@VisibleForTesting
@ -140,4 +141,9 @@ class NodeAttachmentService(val storePath: Path) : AttachmentStorage {
}
}
}
// Implementations for AcceptsFileUpload
override val dataTypePrefix = "attachment"
override val acceptableFileExtensions = listOf(".jar", ".zip")
override fun upload(data: InputStream) = importAttachment(data).toString()
}

View File

@ -0,0 +1,169 @@
/*
* 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.node.AcceptsFileUpload
import core.serialization.deserialize
import protocols.RatesFixProtocol
import java.io.InputStream
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 {
/** 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) : AcceptsFileUpload {
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)
}
}
// File upload support
override val dataTypePrefix = "interest-rates"
override val acceptableFileExtensions = listOf(".rates", ".txt")
override fun upload(data: InputStream): String {
val fixes: Map<FixOf, Fix> = data.
bufferedReader().
readLines().
map { it.trim() }.
// Filter out comment and empty lines.
filterNot { it.startsWith("#") || it.isBlank() }.
map { parseOneRate(it) }.
associate { it.first to it.second }
// TODO: Save the uploaded fixes to the storage service and reload on construction.
// This assignment is thread safe because knownFixes is volatile and the oracle code always snapshots
// the pointer to the stack before working with the map.
oracle.knownFixes = fixes
return "Accepted ${fixes.size} new interest rate fixes"
}
}
/**
* 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 = emptyMap<FixOf, Fix>()
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() {
override fun toString() = "Unknown fix: $fix"
}
}

View File

@ -8,8 +8,8 @@
package core.node.servlets
import core.crypto.SecureHash
import core.node.services.StorageService
import core.node.AcceptsFileUpload
import core.node.Node
import core.utilities.loggerFor
import org.apache.commons.fileupload.servlet.ServletFileUpload
import java.util.*
@ -17,43 +17,55 @@ import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
class AttachmentUploadServlet : HttpServlet() {
private val log = loggerFor<AttachmentUploadServlet>()
/**
* Accepts binary streams, finds the right [AcceptsFileUpload] implementor and hands the stream off to it.
*/
class DataUploadServlet : HttpServlet() {
private val log = loggerFor<DataUploadServlet>()
override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) {
val node = servletContext.getAttribute("node") as Node
@Suppress("DEPRECATION") // Bogus warning due to superclass static method being deprecated.
val isMultipart = ServletFileUpload.isMultipartContent(req)
if (!isMultipart) {
log.error("Got a non-file upload request to the attachments servlet")
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "This end point is for file uploads only.")
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "This end point is for data uploads only.")
return
}
val acceptor: AcceptsFileUpload? = findAcceptor(node, req)
if (acceptor == null) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Got a file upload request for an unknown data type")
return
}
val upload = ServletFileUpload()
val iterator = upload.getItemIterator(req)
val ids = ArrayList<SecureHash>()
val messages = ArrayList<String>()
while (iterator.hasNext()) {
val item = iterator.next()
if (!item.name.endsWith(".jar")) {
log.error("Attempted upload of a non-JAR attachment: mime=${item.contentType} filename=${item.name}")
if (item.name != null && !acceptor.acceptableFileExtensions.any { item.name.endsWith(it) }) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
"${item.name}: Must be have a MIME type of application/java-archive and a filename ending in .jar")
"${item.name}: Must be have a filename ending in one of: ${acceptor.acceptableFileExtensions}")
return
}
log.info("Receiving ${item.name}")
val storage = servletContext.getAttribute("storage") as StorageService
item.openStream().use {
val id = storage.attachments.importAttachment(it)
log.info("${item.name} successfully inserted into the attachment store with id $id")
ids += id
val message = acceptor.upload(it)
log.info("${item.name} successfully accepted: $message")
messages += message
}
}
// Send back the hashes as a convenience for the user.
val writer = resp.writer
ids.forEach { writer.println(it) }
messages.forEach { writer.println(it) }
}
private fun findAcceptor(node: Node, req: HttpServletRequest): AcceptsFileUpload? {
return node.servicesThatAcceptUploads.firstOrNull { req.pathInfo.substring(1).substringBefore('/') == it.dataTypePrefix }
}
}

View File

@ -0,0 +1,89 @@
/*
* 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 demos
import contracts.Cash
import core.*
import core.messaging.LegallyIdentifiableNode
import core.node.Node
import core.node.NodeConfiguration
import core.node.services.ArtemisMessagingService
import core.node.services.NodeInterestRates
import core.serialization.deserialize
import core.utilities.ANSIProgressRenderer
import core.utilities.BriefLogFormatter
import core.utilities.Emoji
import joptsimple.OptionParser
import protocols.RatesFixProtocol
import java.math.BigDecimal
import java.nio.file.Files
import java.nio.file.Paths
import kotlin.system.exitProcess
/**
* Creates a dummy transaction that requires a rate fix within a certain range, and gets it signed by an oracle
* service.
*/
fun main(args: Array<String>) {
val parser = OptionParser()
val networkAddressArg = parser.accepts("network-address").withRequiredArg().required()
val dirArg = parser.accepts("directory").withRequiredArg().defaultsTo("rate-fix-demo-data")
val oracleAddrArg = parser.accepts("oracle").withRequiredArg().required()
val oracleIdentityArg = parser.accepts("oracle-identity-file").withRequiredArg().required()
val fixOfArg = parser.accepts("fix-of").withRequiredArg().defaultsTo("LIBOR 2016-03-16 30")
val expectedRateArg = parser.accepts("expected-rate").withRequiredArg().defaultsTo("0.67")
val rateToleranceArg = parser.accepts("rate-tolerance").withRequiredArg().defaultsTo("0.1")
val options = try {
parser.parse(*args)
} catch (e: Exception) {
println(e.message)
exitProcess(1)
}
// Suppress the Artemis MQ noise, and activate the demo logging.
BriefLogFormatter.initVerbose("+demo.ratefix", "-org.apache.activemq")
// TODO: Move this into the AbstractNode class.
val dir = Paths.get(options.valueOf(dirArg))
if (!Files.exists(dir)) {
Files.createDirectory(dir)
}
// Load oracle stuff (in lieu of having a network map service)
val oracleAddr = ArtemisMessagingService.makeRecipient(options.valueOf(oracleAddrArg))
val oracleIdentity = Files.readAllBytes(Paths.get(options.valueOf(oracleIdentityArg))).deserialize<Party>(includeClassName = true)
val oracleNode = LegallyIdentifiableNode(oracleAddr, oracleIdentity)
val fixOf: FixOf = NodeInterestRates.parseFixOf(options.valueOf(fixOfArg))
val expectedRate = BigDecimal(options.valueOf(expectedRateArg))
val rateTolerance = BigDecimal(options.valueOf(rateToleranceArg))
// Bring up node.
val myNetAddr = ArtemisMessagingService.toHostAndPort(options.valueOf(networkAddressArg))
val config = object : NodeConfiguration {
override val myLegalName: String = "Rate fix demo node"
}
val node = logElapsedTime("Node startup") { Node(dir, myNetAddr, config, null).start() }
// Make a garbage transaction that includes a rate fix.
val tx = TransactionBuilder()
tx.addOutputState(Cash.State(node.storage.myLegalIdentity.ref(1), 1500.DOLLARS, node.keyManagement.freshKey().public))
val protocol = RatesFixProtocol(tx, oracleNode, fixOf, expectedRate, rateTolerance)
ANSIProgressRenderer.progressTracker = protocol.progressTracker
node.smm.add("demo.ratefix", protocol).get()
node.stop()
// Show the user the output.
println("Got rate fix")
println()
print(Emoji.renderIfSupported(tx.toWireTransaction()))
println(tx.toSignedTransaction().sigs)
}

View File

@ -0,0 +1,103 @@
/*
* 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 core.utilities.ProgressTracker
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.
*/
open class RatesFixProtocol(protected 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 QUERYING(val name: String) : ProgressTracker.Step("Querying oracle for $name interest rate")
object WORKING : ProgressTracker.Step("Working with data returned by oracle")
object SIGNING : ProgressTracker.Step("Requesting transaction signature from interest rate oracle")
}
override val progressTracker = ProgressTracker(QUERYING(fixOf.name), WORKING, SIGNING)
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)
beforeSigning(fix)
tx.addSignatureUnchecked(sign())
}
/**
* You can override this to perform any additional work needed after the fix is added to the transaction but
* before it's sent back to the oracle for signing (for example, adding output states that depend on the fix).
*/
@Suspendable
protected open fun beforeSigning(fix: Fix) {
}
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).oracle.knownFixes = TEST_DATA
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))
}