mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Merged irs-oracle into master
This commit is contained in:
commit
975d569e55
14
build.gradle
14
build.gradle
@ -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
|
||||
}
|
@ -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. */
|
||||
|
79
core/src/main/kotlin/core/FinanceTypes.kt
Normal file
79
core/src/main/kotlin/core/FinanceTypes.kt
Normal 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
|
@ -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
|
6
scripts/example.rates.txt
Normal file
6
scripts/example.rates.txt
Normal 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
24
scripts/get-rate-fix.sh
Executable 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
|
@ -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)
|
||||
}
|
||||
}
|
30
src/main/kotlin/core/node/AcceptsFileUpload.kt
Normal file
30
src/main/kotlin/core/node/AcceptsFileUpload.kt
Normal 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
|
||||
}
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
|
169
src/main/kotlin/core/node/services/NodeInterestRates.kt
Normal file
169
src/main/kotlin/core/node/services/NodeInterestRates.kt
Normal 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"
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
89
src/main/kotlin/demos/RateFixDemo.kt
Normal file
89
src/main/kotlin/demos/RateFixDemo.kt
Normal 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)
|
||||
}
|
103
src/main/kotlin/protocols/RatesFixProtocol.kt
Normal file
103
src/main/kotlin/protocols/RatesFixProtocol.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
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).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))
|
||||
}
|
Loading…
Reference in New Issue
Block a user