mirror of
https://github.com/corda/corda.git
synced 2025-02-20 17:33:15 +00:00
Merge branch 'master' into dynamic-loading
This commit is contained in:
commit
6fe8107d27
29
build.gradle
29
build.gradle
@ -148,7 +148,36 @@ task getRateFixDemo(type: CreateStartScripts) {
|
||||
classpath = jar.outputs.files + project.configurations.runtime
|
||||
}
|
||||
|
||||
task getIRSDemo(type: CreateStartScripts) {
|
||||
mainClassName = "demos.IRSDemoKt"
|
||||
applicationName = "irsdemo"
|
||||
defaultJvmOpts = ["-javaagent:${configurations.quasar.singleFile}"]
|
||||
outputDir = new File(project.buildDir, 'scripts')
|
||||
classpath = jar.outputs.files + project.configurations.runtime
|
||||
}
|
||||
|
||||
// These lines tell gradle to run the Quasar suspendables scanner to look for unannotated super methods
|
||||
// that have @Suspendable sub implementations. These tend to cause NPEs and are not caught by the verifier
|
||||
// NOTE: need to make sure the output isn't on the classpath or every other run it generates empty results, so
|
||||
// we explicitly delete to avoid that happening. We also need to turn off what seems to be a spurious warning in the IDE
|
||||
|
||||
//noinspection GroovyAssignabilityCheck
|
||||
task quasarScan(dependsOn: ['classes', 'core:classes', 'contracts:classes']) << {
|
||||
ant.taskdef(name:'scanSuspendables', classname:'co.paralleluniverse.fibers.instrument.SuspendablesScanner',
|
||||
classpath: "${sourceSets.main.output.classesDir}:${sourceSets.main.output.resourcesDir}:${configurations.runtime.asPath}")
|
||||
delete "$sourceSets.main.output.resourcesDir/META-INF/suspendables", "$sourceSets.main.output.resourcesDir/META-INF/suspendable-supers"
|
||||
ant.scanSuspendables(
|
||||
auto:false,
|
||||
suspendablesFile: "$sourceSets.main.output.resourcesDir/META-INF/suspendables",
|
||||
supersFile: "$sourceSets.main.output.resourcesDir/META-INF/suspendable-supers") {
|
||||
fileset(dir: sourceSets.main.output.classesDir)
|
||||
}
|
||||
}
|
||||
|
||||
jar.dependsOn quasarScan
|
||||
|
||||
applicationDistribution.into("bin") {
|
||||
from(getRateFixDemo)
|
||||
from(getIRSDemo)
|
||||
fileMode = 0755
|
||||
}
|
@ -83,7 +83,7 @@ class CommercialPaper : Contract {
|
||||
// Here, we match acceptable timestamp authorities by name. The list of acceptable TSAs (oracles) must be
|
||||
// hard coded into the contract because otherwise we could fail to gain consensus, if nodes disagree about
|
||||
// who or what is a trusted authority.
|
||||
val timestamp: TimestampCommand? = tx.commands.getTimestampByName("Mock Company 0", "Bank of Zurich")
|
||||
val timestamp: TimestampCommand? = tx.commands.getTimestampByName("Mock Company 0", "Bank A")
|
||||
|
||||
for (group in groups) {
|
||||
when (command.value) {
|
||||
|
@ -8,18 +8,8 @@
|
||||
|
||||
package contracts
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.JsonSerializer
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import com.fasterxml.jackson.databind.type.SimpleType
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.node.services.DummyTimestampingAuthority
|
||||
import org.apache.commons.jexl3.JexlBuilder
|
||||
import org.apache.commons.jexl3.MapContext
|
||||
import java.math.BigDecimal
|
||||
@ -31,7 +21,16 @@ import java.util.*
|
||||
val IRS_PROGRAM_ID = InterestRateSwap()
|
||||
|
||||
// This is a placeholder for some types that we haven't identified exactly what they are just yet for things still in discussion
|
||||
open class UnknownType()
|
||||
open class UnknownType() {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return (other is UnknownType)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event superclass - everything happens on a date.
|
||||
@ -53,12 +52,12 @@ abstract class PaymentEvent(date: LocalDate) : Event(date) {
|
||||
* For the floating leg, the rate refers to a reference rate which is to be "fixed" at a point in the future.
|
||||
*/
|
||||
abstract class RatePaymentEvent(date: LocalDate,
|
||||
val accrualStartDate: LocalDate,
|
||||
val accrualEndDate: LocalDate,
|
||||
val dayCountBasisDay: DayCountBasisDay,
|
||||
val dayCountBasisYear: DayCountBasisYear,
|
||||
val notional: Amount,
|
||||
val rate: Rate) : PaymentEvent(date) {
|
||||
val accrualStartDate: LocalDate,
|
||||
val accrualEndDate: LocalDate,
|
||||
val dayCountBasisDay: DayCountBasisDay,
|
||||
val dayCountBasisYear: DayCountBasisYear,
|
||||
val notional: Amount,
|
||||
val rate: Rate) : PaymentEvent(date) {
|
||||
companion object {
|
||||
val CSVHeader = "AccrualStartDate,AccrualEndDate,DayCountFactor,Days,Date,Ccy,Notional,Rate,Flow"
|
||||
}
|
||||
@ -71,7 +70,7 @@ abstract class RatePaymentEvent(date: LocalDate,
|
||||
dayCountCalculator(accrualStartDate, accrualEndDate, dayCountBasisYear, dayCountBasisDay)
|
||||
|
||||
val dayCountFactor: BigDecimal get() =
|
||||
// TODO : Fix below (use daycount convention for division)
|
||||
// TODO : Fix below (use daycount convention for division)
|
||||
(BigDecimal(days).divide(BigDecimal(360.0), 8, RoundingMode.HALF_UP)).setScale(4, RoundingMode.HALF_UP)
|
||||
|
||||
open fun asCSV(): String = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.currency},${notional},$rate,$flow"
|
||||
@ -138,12 +137,6 @@ class FloatingRatePaymentEvent(date: LocalDate,
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Don't try and use a rate that isn't ready yet.
|
||||
*/
|
||||
class DataNotReadyException : Exception()
|
||||
|
||||
|
||||
/**
|
||||
* The Interest Rate Swap class. For a quick overview of what an IRS is, see here - http://www.pimco.co.uk/EN/Education/Pages/InterestRateSwapsBasics1-08.aspx (no endorsement)
|
||||
* This contract has 4 significant data classes within it, the "Common", "Calculation", "FixedLeg" and "FloatingLeg"
|
||||
@ -240,6 +233,48 @@ class InterestRateSwap() : Contract {
|
||||
"TerminationDateAdjustment=$terminationDateAdjustment,DayCountBasis=$dayCountBasisDay/$dayCountBasisYear,DayInMonth=$dayInMonth," +
|
||||
"PaymentRule=$paymentRule,PaymentDelay=$paymentDelay,PaymentCalendar=$paymentCalendar,InterestPeriodAdjustment=$interestPeriodAdjustment"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as CommonLeg
|
||||
|
||||
if (notional != other.notional) return false
|
||||
if (paymentFrequency != other.paymentFrequency) return false
|
||||
if (effectiveDate != other.effectiveDate) return false
|
||||
if (effectiveDateAdjustment != other.effectiveDateAdjustment) return false
|
||||
if (terminationDate != other.terminationDate) return false
|
||||
if (terminationDateAdjustment != other.terminationDateAdjustment) return false
|
||||
if (dayCountBasisDay != other.dayCountBasisDay) return false
|
||||
if (dayCountBasisYear != other.dayCountBasisYear) return false
|
||||
if (dayInMonth != other.dayInMonth) return false
|
||||
if (paymentRule != other.paymentRule) return false
|
||||
if (paymentDelay != other.paymentDelay) return false
|
||||
if (paymentCalendar != other.paymentCalendar) return false
|
||||
if (interestPeriodAdjustment != other.interestPeriodAdjustment) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = notional.hashCode()
|
||||
result += 31 * result + paymentFrequency.hashCode()
|
||||
result += 31 * result + effectiveDate.hashCode()
|
||||
result += 31 * result + (effectiveDateAdjustment?.hashCode() ?: 0)
|
||||
result += 31 * result + terminationDate.hashCode()
|
||||
result += 31 * result + (terminationDateAdjustment?.hashCode() ?: 0)
|
||||
result += 31 * result + dayCountBasisDay.hashCode()
|
||||
result += 31 * result + dayCountBasisYear.hashCode()
|
||||
result += 31 * result + dayInMonth
|
||||
result += 31 * result + paymentRule.hashCode()
|
||||
result += 31 * result + paymentDelay
|
||||
result += 31 * result + paymentCalendar.hashCode()
|
||||
result += 31 * result + interestPeriodAdjustment.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
open class FixedLeg(
|
||||
@ -264,6 +299,29 @@ class InterestRateSwap() : Contract {
|
||||
dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment) {
|
||||
override fun toString(): String = "FixedLeg(Payer=$fixedRatePayer," + super.toString() + ",fixedRate=$fixedRate," +
|
||||
"rollConvention=$rollConvention"
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
other as FixedLeg
|
||||
|
||||
if (fixedRatePayer != other.fixedRatePayer) return false
|
||||
if (fixedRate != other.fixedRate) return false
|
||||
if (rollConvention != other.rollConvention) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result += 31 * result + fixedRatePayer.hashCode()
|
||||
result += 31 * result + fixedRate.hashCode()
|
||||
result += 31 * result + rollConvention.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
open class FloatingLeg(
|
||||
@ -298,6 +356,44 @@ class InterestRateSwap() : Contract {
|
||||
"FixingPeriond=$fixingPeriod,ResetRule=$resetRule,FixingsPerPayment=$fixingsPerPayment,FixingCalendar=$fixingCalendar," +
|
||||
"Index=$index,IndexSource=$indexSource,IndexTenor=$indexTenor"
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
other as FloatingLeg
|
||||
|
||||
if (floatingRatePayer != other.floatingRatePayer) return false
|
||||
if (rollConvention != other.rollConvention) return false
|
||||
if (fixingRollConvention != other.fixingRollConvention) return false
|
||||
if (resetDayInMonth != other.resetDayInMonth) return false
|
||||
if (fixingPeriod != other.fixingPeriod) return false
|
||||
if (resetRule != other.resetRule) return false
|
||||
if (fixingsPerPayment != other.fixingsPerPayment) return false
|
||||
if (fixingCalendar != other.fixingCalendar) return false
|
||||
if (index != other.index) return false
|
||||
if (indexSource != other.indexSource) return false
|
||||
if (indexTenor != other.indexTenor) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result += 31 * result + floatingRatePayer.hashCode()
|
||||
result += 31 * result + rollConvention.hashCode()
|
||||
result += 31 * result + fixingRollConvention.hashCode()
|
||||
result += 31 * result + resetDayInMonth
|
||||
result += 31 * result + fixingPeriod.hashCode()
|
||||
result += 31 * result + resetRule.hashCode()
|
||||
result += 31 * result + fixingsPerPayment.hashCode()
|
||||
result += 31 * result + fixingCalendar.hashCode()
|
||||
result += 31 * result + index.hashCode()
|
||||
result += 31 * result + indexSource.hashCode()
|
||||
result += 31 * result + indexTenor.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -305,7 +401,7 @@ class InterestRateSwap() : Contract {
|
||||
*/
|
||||
override fun verify(tx: TransactionForVerification) {
|
||||
val command = tx.commands.requireSingleCommand<InterestRateSwap.Commands>()
|
||||
val time = tx.commands.getTimestampByName("Mock Company 0", "Bank of Zurich")?.midpoint
|
||||
val time = tx.commands.getTimestampByName("Mock Company 0", "European Timestamping Service", "Bank A")?.midpoint
|
||||
if (time == null) throw IllegalArgumentException("must be timestamped")
|
||||
|
||||
val irs = tx.outStates.filterIsInstance<InterestRateSwap.State>().single()
|
||||
@ -356,7 +452,8 @@ class InterestRateSwap() : Contract {
|
||||
val floatingLeg: FloatingLeg,
|
||||
val calculation: Calculation,
|
||||
val common: Common
|
||||
) : LinearState {
|
||||
) : FixableDealState {
|
||||
|
||||
override val contract = IRS_PROGRAM_ID
|
||||
override val thread = SecureHash.sha256(common.tradeID)
|
||||
override val ref = common.tradeID
|
||||
@ -365,6 +462,39 @@ class InterestRateSwap() : Contract {
|
||||
return (fixedLeg.fixedRatePayer.owningKey in ourKeys) || (floatingLeg.floatingRatePayer.owningKey in ourKeys)
|
||||
}
|
||||
|
||||
override val parties: Array<Party>
|
||||
get() = arrayOf(fixedLeg.fixedRatePayer, floatingLeg.floatingRatePayer)
|
||||
|
||||
override fun withPublicKey(before: Party, after: PublicKey): State {
|
||||
val newParty = Party(before.name, after)
|
||||
if (before == fixedLeg.fixedRatePayer) {
|
||||
val deal = copy()
|
||||
deal.fixedLeg.fixedRatePayer = newParty
|
||||
return deal
|
||||
} else if (before == floatingLeg.floatingRatePayer) {
|
||||
val deal = copy()
|
||||
deal.floatingLeg.floatingRatePayer = newParty
|
||||
return deal
|
||||
} else {
|
||||
throw IllegalArgumentException("No such party: $before")
|
||||
}
|
||||
}
|
||||
|
||||
override fun generateAgreement(): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common)
|
||||
|
||||
override fun generateFix(ptx: TransactionBuilder, oldStateRef: StateRef, fix: Fix) {
|
||||
InterestRateSwap().generateFix(ptx, StateAndRef(this, oldStateRef), Pair(fix.of.forDay, Rate(RatioUnit(fix.value))))
|
||||
}
|
||||
|
||||
override fun nextFixingOf(): FixOf? {
|
||||
val date = calculation.nextFixingDate()
|
||||
return if (date == null) null else {
|
||||
val fixingEvent = calculation.getFixing(date)
|
||||
val oracleRate = fixingEvent.rate as ReferenceRate
|
||||
FixOf(oracleRate.name, date, oracleRate.tenor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For evaluating arbitrary java on the platform
|
||||
*/
|
||||
|
@ -1,17 +1,8 @@
|
||||
package contracts
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer
|
||||
import com.fasterxml.jackson.databind.type.SimpleType
|
||||
import core.Amount
|
||||
import core.Tenor
|
||||
import core.*
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
import java.security.PublicKey
|
||||
|
||||
|
||||
// Things in here will move to the general utils class when we've hammered out various discussions regarding amounts, dates, oracle etc.
|
||||
@ -21,6 +12,22 @@ import java.time.LocalDate
|
||||
*/
|
||||
open class RatioUnit(value: BigDecimal) { // TODO: Discuss this type
|
||||
val value = value
|
||||
|
||||
override fun equals(other: Any?): Boolean{
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as RatioUnit
|
||||
|
||||
if (value != other.value) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int{
|
||||
return value.hashCode()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -37,10 +44,78 @@ open class PercentageRatioUnit(percentageAsString: String) : RatioUnit(BigDecima
|
||||
*/
|
||||
val String.percent: PercentageRatioUnit get() = PercentageRatioUnit(this)
|
||||
|
||||
/**
|
||||
* Interface representing an agreement that exposes various attributes that are common and allow
|
||||
* implementation of general protocols that manipulate many agreement types
|
||||
*/
|
||||
interface DealState : LinearState {
|
||||
|
||||
/** Human readable well known reference (e.g. trade reference) */
|
||||
val ref: String
|
||||
|
||||
/** Exposes the Parties involved in a generic way */
|
||||
val parties: Array<Party>
|
||||
|
||||
/** Allow swapping in of potentially transaction specific public keys prior to signing */
|
||||
fun withPublicKey(before: Party, after: PublicKey): DealState
|
||||
|
||||
/**
|
||||
* Generate a partial transaction representing an agreement (command) to this deal, allowing a general
|
||||
* deal/agreement protocol to generate the necessary transaction for potential implementations
|
||||
*
|
||||
* TODO: Currently this is the "inception" transaction but in future an offer of some description might be an input state ref
|
||||
*
|
||||
* TODO: This should more likely be a method on the Contract (on a common interface) and the changes to reference a
|
||||
* Contract instance from a ContractState are imminent, at which point we can move this out of here
|
||||
*/
|
||||
fun generateAgreement(): TransactionBuilder
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface adding fixing specific methods
|
||||
*/
|
||||
interface FixableDealState : DealState {
|
||||
/**
|
||||
* When is the next fixing and what is the fixing for?
|
||||
*
|
||||
* TODO: In future we would use this to register for an event to trigger a/the fixing protocol
|
||||
*/
|
||||
fun nextFixingOf(): FixOf?
|
||||
|
||||
/**
|
||||
* Generate a fixing command for this deal and fix
|
||||
*
|
||||
* TODO: This would also likely move to methods on the Contract once the changes to reference
|
||||
* the Contract from the ContractState are in
|
||||
*/
|
||||
fun generateFix(ptx: TransactionBuilder, oldStateRef: StateRef, fix: Fix)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent of the Rate family. Used to denote fixed rates, floating rates, reference rates etc
|
||||
*/
|
||||
open class Rate(val ratioUnit: RatioUnit? = null)
|
||||
open class Rate(val ratioUnit: RatioUnit? = null) {
|
||||
|
||||
override fun equals(other: Any?): Boolean{
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as Rate
|
||||
|
||||
if (ratioUnit != other.ratioUnit) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the hash code of the ratioUnit or zero if the ratioUnit is null, as is the case for floating rate fixings
|
||||
* that have not yet happened. Yet-to-be fixed floating rates need to be equal such that schedules can be tested
|
||||
* for equality.
|
||||
*/
|
||||
override fun hashCode(): Int{
|
||||
return ratioUnit?.hashCode() ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A very basic subclass to represent a fixed rate.
|
||||
|
@ -269,7 +269,7 @@ open class BusinessCalendar private constructor(val calendars: Array<out String>
|
||||
calname.flatMap { (TEST_CALENDAR_DATA[it] ?: throw UnknownCalendar(it)).split(",") }.
|
||||
toSet().
|
||||
map{ parseDateFromString(it) }.
|
||||
toList()
|
||||
toList().sorted()
|
||||
)
|
||||
|
||||
/** Calculates an event schedule that moves events around to ensure they fall on working days. */
|
||||
@ -299,6 +299,17 @@ open class BusinessCalendar private constructor(val calendars: Array<out String>
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean = if (other is BusinessCalendar) {
|
||||
/** Note this comparison is OK as we ensure they are sorted in getInstance() */
|
||||
this.holidayDates == other.holidayDates
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return this.holidayDates.hashCode()
|
||||
}
|
||||
|
||||
open fun isWorkingDay(date: LocalDate): Boolean =
|
||||
when {
|
||||
date.dayOfWeek == DayOfWeek.SATURDAY -> false
|
||||
|
@ -18,6 +18,7 @@ import java.io.OutputStream
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.util.jar.JarInputStream
|
||||
|
||||
/** Implemented by anything that can be named by a secure hash value (e.g. transactions, attachments). */
|
||||
@ -54,10 +55,6 @@ interface LinearState: ContractState {
|
||||
/** Unique thread id within the wallets of all parties */
|
||||
val thread: SecureHash
|
||||
|
||||
/** Human readable well known reference (e.g. trade reference) */
|
||||
// TODO we will push this down out of here once we have something more sophisticated and a more powerful query API
|
||||
val ref: String
|
||||
|
||||
/** true if this should be tracked by our wallet(s) */
|
||||
fun isRelevant(ourKeys: Set<PublicKey>): Boolean
|
||||
}
|
||||
|
85
core/src/main/kotlin/core/math/Interpolators.kt
Normal file
85
core/src/main/kotlin/core/math/Interpolators.kt
Normal file
@ -0,0 +1,85 @@
|
||||
package core.math
|
||||
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Interpolates values between the given data points using a [SplineFunction].
|
||||
*
|
||||
* Implementation uses the Natural Cubic Spline algorithm as described in
|
||||
* R. L. Burden and J. D. Faires (2011), *Numerical Analysis*. 9th ed. Boston, MA: Brooks/Cole, Cengage Learning. p149-150.
|
||||
*/
|
||||
class CubicSplineInterpolator(private val xs: DoubleArray, private val ys: DoubleArray) {
|
||||
init {
|
||||
require(xs.size == ys.size) { "x and y dimensions should match: ${xs.size} != ${ys.size}" }
|
||||
require(xs.size >= 3) { "At least 3 data points are required for interpolation, received: ${xs.size}" }
|
||||
}
|
||||
|
||||
private val splineFunction by lazy { computeSplineFunction() }
|
||||
|
||||
fun interpolate(x: Double): Double {
|
||||
require(x >= xs.first() && x <= xs.last()) { "Can't interpolate below ${xs.first()} or above ${xs.last()}" }
|
||||
return splineFunction.getValue(x)
|
||||
}
|
||||
|
||||
private fun computeSplineFunction(): SplineFunction {
|
||||
val n = xs.size - 1
|
||||
|
||||
// Coefficients of polynomial
|
||||
val b = DoubleArray(n) // linear
|
||||
val c = DoubleArray(n + 1) // quadratic
|
||||
val d = DoubleArray(n) // cubic
|
||||
|
||||
// Helpers
|
||||
val h = DoubleArray(n)
|
||||
val g = DoubleArray(n)
|
||||
|
||||
for (i in 0..n - 1)
|
||||
h[i] = xs[i + 1] - xs[i]
|
||||
for (i in 1..n - 1)
|
||||
g[i] = 3 / h[i] * (ys[i + 1] - ys[i]) - 3 / h[i - 1] * (ys[i] - ys[i - 1])
|
||||
|
||||
// Solve tridiagonal linear system (using Crout Factorization)
|
||||
val m = DoubleArray(n)
|
||||
val z = DoubleArray(n)
|
||||
for (i in 1..n - 1) {
|
||||
val l = 2 * (xs[i + 1] - xs[i - 1]) - h[i - 1] * m[i - 1]
|
||||
m[i] = h[i]/l
|
||||
z[i] = (g[i] - h[i - 1] * z[i - 1]) / l
|
||||
}
|
||||
for (j in n - 1 downTo 0) {
|
||||
c[j] = z[j] - m[j] * c[j + 1]
|
||||
b[j] = (ys[j + 1] - ys[j]) / h[j] - h[j] * (c[j + 1] + 2.0 * c[j]) / 3.0
|
||||
d[j] = (c[j + 1] - c[j]) / (3.0 * h[j])
|
||||
}
|
||||
|
||||
val segmentMap = TreeMap<Double, Polynomial>()
|
||||
for (i in 0..n - 1) {
|
||||
val coefficients = doubleArrayOf(ys[i], b[i], c[i], d[i])
|
||||
segmentMap.put(xs[i], Polynomial(coefficients))
|
||||
}
|
||||
return SplineFunction(segmentMap)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a polynomial function of arbitrary degree
|
||||
* @param coefficients polynomial coefficients in the order of degree (constant first, followed by higher degree term coefficients)
|
||||
*/
|
||||
class Polynomial(private val coefficients: DoubleArray) {
|
||||
private val reversedCoefficients = coefficients.reversed()
|
||||
|
||||
fun getValue(x: Double) = reversedCoefficients.fold(0.0, { result, c -> result * x + c })
|
||||
}
|
||||
|
||||
/**
|
||||
* A *spline* is function piecewise-defined by polynomial functions.
|
||||
* Points at which polynomial pieces connect are known as *knots*.
|
||||
*
|
||||
* @param segmentMap a mapping between a knot and the polynomial that covers the subsequent interval
|
||||
*/
|
||||
class SplineFunction(private val segmentMap: TreeMap<Double, Polynomial>) {
|
||||
fun getValue(x: Double): Double {
|
||||
val (knot, polynomial) = segmentMap.floorEntry(x)
|
||||
return polynomial.getValue(x - knot)
|
||||
}
|
||||
}
|
@ -18,4 +18,5 @@ import java.security.PublicKey
|
||||
*/
|
||||
interface IdentityService {
|
||||
fun partyFromKey(key: PublicKey): Party?
|
||||
fun partyFromName(name: String): Party?
|
||||
}
|
||||
|
@ -42,13 +42,13 @@ import java.util.*
|
||||
*/
|
||||
class ProgressTracker(vararg steps: Step) {
|
||||
sealed class Change {
|
||||
class Position(val newStep: Step) : Change() {
|
||||
class Position(val tracker: ProgressTracker, val newStep: Step) : Change() {
|
||||
override fun toString() = newStep.label
|
||||
}
|
||||
class Rendering(val ofStep: Step) : Change() {
|
||||
class Rendering(val tracker: ProgressTracker, val ofStep: Step) : Change() {
|
||||
override fun toString() = ofStep.label
|
||||
}
|
||||
class Structural(val parent: Step) : Change() {
|
||||
class Structural(val tracker: ProgressTracker, val parent: Step) : Change() {
|
||||
override fun toString() = "Structural step change in child of ${parent.label}"
|
||||
}
|
||||
}
|
||||
@ -59,13 +59,13 @@ class ProgressTracker(vararg steps: Step) {
|
||||
}
|
||||
|
||||
/** This class makes it easier to relabel a step on the fly, to provide transient information. */
|
||||
open class RelabelableStep(currentLabel: String) : Step(currentLabel) {
|
||||
open inner class RelabelableStep(currentLabel: String) : Step(currentLabel) {
|
||||
override val changes = BehaviorSubject.create<Change>()
|
||||
|
||||
var currentLabel: String = currentLabel
|
||||
set(value) {
|
||||
field = value
|
||||
changes.onNext(ProgressTracker.Change.Rendering(this))
|
||||
changes.onNext(ProgressTracker.Change.Rendering(this@ProgressTracker, this@RelabelableStep))
|
||||
}
|
||||
|
||||
override val label: String get() = currentLabel
|
||||
@ -109,7 +109,7 @@ class ProgressTracker(vararg steps: Step) {
|
||||
|
||||
curChangeSubscription?.unsubscribe()
|
||||
stepIndex = index
|
||||
_changes.onNext(Change.Position(steps[index]))
|
||||
_changes.onNext(Change.Position(this, steps[index]))
|
||||
curChangeSubscription = currentStep.changes.subscribe { _changes.onNext(it) }
|
||||
|
||||
if (currentStep == DONE) _changes.onCompleted()
|
||||
@ -128,18 +128,33 @@ class ProgressTracker(vararg steps: Step) {
|
||||
override fun put(key: Step, value: ProgressTracker): ProgressTracker? {
|
||||
val r = super.put(key, value)
|
||||
childSubscriptions[value] = value.changes.subscribe({ _changes.onNext(it) }, { _changes.onError(it) })
|
||||
_changes.onNext(Change.Structural(key))
|
||||
value.parent = this@ProgressTracker
|
||||
_changes.onNext(Change.Structural(this@ProgressTracker, key))
|
||||
return r
|
||||
}
|
||||
|
||||
override fun remove(key: Step): ProgressTracker? {
|
||||
if (containsKey(key))
|
||||
childSubscriptions[this[key]]?.let { it.unsubscribe(); childSubscriptions.remove(this[key]) }
|
||||
_changes.onNext(Change.Structural(key))
|
||||
val tracker = this[key]
|
||||
if (tracker != null) {
|
||||
tracker.parent = null
|
||||
childSubscriptions[tracker]?.let { it.unsubscribe(); childSubscriptions.remove(tracker) }
|
||||
}
|
||||
_changes.onNext(Change.Structural(this@ProgressTracker, key))
|
||||
return super.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
/** The parent of this tracker: set automatically by the parent when a tracker is added as a child */
|
||||
var parent: ProgressTracker? = null
|
||||
|
||||
/** Walks up the tree to find the top level tracker. If this is the top level tracker, returns 'this' */
|
||||
val topLevelTracker: ProgressTracker
|
||||
get() {
|
||||
var cursor: ProgressTracker = this
|
||||
while (cursor.parent != null) cursor = cursor.parent!!
|
||||
return cursor
|
||||
}
|
||||
|
||||
private val childSubscriptions = HashMap<ProgressTracker, Subscription>()
|
||||
|
||||
private fun _allSteps(level: Int = 0): List<Pair<Int, Step>> {
|
||||
|
43
core/src/test/kotlin/core/math/InterpolatorsTest.kt
Normal file
43
core/src/test/kotlin/core/math/InterpolatorsTest.kt
Normal file
@ -0,0 +1,43 @@
|
||||
package core.math
|
||||
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class InterpolatorsTest {
|
||||
|
||||
@Test
|
||||
fun `throws when key to interpolate is outside the data set`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0, 4.0, 5.0)
|
||||
val interpolator = CubicSplineInterpolator(xs, ys = xs)
|
||||
assertFailsWith<IllegalArgumentException> { interpolator.interpolate(0.0) }
|
||||
assertFailsWith<IllegalArgumentException> { interpolator.interpolate(6.0) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws when data set is less than 3 points`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0)
|
||||
assertFailsWith<IllegalArgumentException> { CubicSplineInterpolator(xs, ys = xs) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns existing value when key is in data set`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0, 4.0, 5.0)
|
||||
val interpolatedValue = CubicSplineInterpolator(xs, ys = xs).interpolate(2.0)
|
||||
assertEquals(2.0, interpolatedValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `interpolates missing values correctly`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0)
|
||||
val ys = doubleArrayOf(2.0, 4.0, 5.0, 11.0, 10.0)
|
||||
val toInterpolate = doubleArrayOf(1.5, 2.5, 2.8, 3.3, 3.7, 4.3, 4.7)
|
||||
// Expected values generated using R's splinefun (package stats v3.2.4), "natural" method
|
||||
val expected = doubleArrayOf(3.28, 4.03, 4.37, 6.7, 9.46, 11.5, 10.91)
|
||||
|
||||
val interpolator = CubicSplineInterpolator(xs, ys)
|
||||
val actual = toInterpolate.map { interpolator.interpolate(it) }.toDoubleArray()
|
||||
Assert.assertArrayEquals(expected, actual, 0.01)
|
||||
}
|
||||
}
|
@ -56,7 +56,7 @@ factors are:
|
||||
* Improved contract flexibility vs Bitcoin
|
||||
* Improved scalability vs Ethereum, as well as ability to keep parts of the transaction graph private (yet still uniquely addressable)
|
||||
* No reliance on proof of work
|
||||
* Re-us of existing sandboxing virtual machines
|
||||
* Re-use of existing sandboxing virtual machines
|
||||
* Use of type safe GCd implementation languages.
|
||||
* Simplified auditing
|
||||
|
||||
|
@ -611,7 +611,7 @@ In this example we see some new features of the DSL:
|
||||
* The ``transaction`` function can also be given a time, to override the default timestamp on a transaction.
|
||||
|
||||
The ``trade`` function is not itself a unit test. Instead it builds up a trade/transaction group, with some slight
|
||||
differences depending on the parameters provided (Kotlin allows parameters to have default valus). Then it returns
|
||||
differences depending on the parameters provided (Kotlin allows parameters to have default values). Then it returns
|
||||
it, unexecuted.
|
||||
|
||||
We use it like this:
|
||||
|
104
scripts/example-irs-trade.json
Normal file
104
scripts/example-irs-trade.json
Normal file
@ -0,0 +1,104 @@
|
||||
{
|
||||
"fixedLeg": {
|
||||
"fixedRatePayer": "Bank A",
|
||||
"notional": {
|
||||
"pennies": 2500000000,
|
||||
"currency": "USD"
|
||||
},
|
||||
"paymentFrequency": "SemiAnnual",
|
||||
"effectiveDate": "2016-03-16",
|
||||
"effectiveDateAdjustment": null,
|
||||
"terminationDate": "2026-03-16",
|
||||
"terminationDateAdjustment": null,
|
||||
"fixedRate": {
|
||||
"ratioUnit": {
|
||||
"value": "0.01676"
|
||||
}
|
||||
},
|
||||
"dayCountBasisDay": "D30",
|
||||
"dayCountBasisYear": "Y360",
|
||||
"rollConvention": "ModifiedFollowing",
|
||||
"dayInMonth": 10,
|
||||
"paymentRule": "InArrears",
|
||||
"paymentDelay": 0,
|
||||
"paymentCalendar": "London",
|
||||
"interestPeriodAdjustment": "Adjusted"
|
||||
},
|
||||
"floatingLeg": {
|
||||
"floatingRatePayer": "Bank B",
|
||||
"notional": {
|
||||
"pennies": 2500000000,
|
||||
"currency": "USD"
|
||||
},
|
||||
"paymentFrequency": "Quarterly",
|
||||
"effectiveDate": "2016-03-12",
|
||||
"effectiveDateAdjustment": null,
|
||||
"terminationDate": "2026-03-12",
|
||||
"terminationDateAdjustment": null,
|
||||
"dayCountBasisDay": "D30",
|
||||
"dayCountBasisYear": "Y360",
|
||||
"rollConvention": "ModifiedFollowing",
|
||||
"fixingRollConvention": "ModifiedFollowing",
|
||||
"dayInMonth": 10,
|
||||
"resetDayInMonth": 10,
|
||||
"paymentRule": "InArrears",
|
||||
"paymentDelay": 0,
|
||||
"paymentCalendar": [ "London" ],
|
||||
"interestPeriodAdjustment": "Adjusted",
|
||||
"fixingPeriod": "TWODAYS",
|
||||
"resetRule": "InAdvance",
|
||||
"fixingsPerPayment": "Quarterly",
|
||||
"fixingCalendar": [ "NewYork" ],
|
||||
"index": "ICE LIBOR",
|
||||
"indexSource": "Rates Service Provider",
|
||||
"indexTenor": {
|
||||
"name": "3M"
|
||||
}
|
||||
},
|
||||
"calculation": {
|
||||
"expression": "( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
|
||||
"floatingLegPaymentSchedule": {
|
||||
},
|
||||
"fixedLegPaymentSchedule": {
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"baseCurrency": "EUR",
|
||||
"eligibleCurrency": "EUR",
|
||||
"eligibleCreditSupport": "Cash in an Eligible Currency",
|
||||
"independentAmounts": {
|
||||
"pennies": 0,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"threshold": {
|
||||
"pennies": 0,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"minimumTransferAmount": {
|
||||
"pennies": 25000000,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"rounding": {
|
||||
"pennies": 1000000,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"valuationDate": "Every Local Business Day",
|
||||
"notificationTime": "2:00pm London",
|
||||
"resolutionTime": "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
||||
"interestRate": {
|
||||
"oracle": "Rates Service Provider",
|
||||
"tenor": {
|
||||
"name": "6M"
|
||||
},
|
||||
"ratioUnit": null,
|
||||
"name": "EONIA"
|
||||
},
|
||||
"addressForTransfers": "",
|
||||
"exposure": {},
|
||||
"localBusinessDay": [ "London" , "NewYork" ],
|
||||
"dailyInterestAmount": "(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360",
|
||||
"tradeID": "tradeXXX",
|
||||
"hashLegalDocs": "put hash here"
|
||||
},
|
||||
"programRef": "1E6BBA305D445341F0026E51B6C7F3ACB834AFC6C2510C0EF7BC0477235EFECF"
|
||||
}
|
@ -1,51 +1,51 @@
|
||||
# Some pretend noddy rate fixes, for the interest rate oracles.
|
||||
|
||||
LIBOR 2016-03-16 1M = 0.678
|
||||
LIBOR 2016-03-16 2M = 0.655
|
||||
ICE LIBOR 2016-03-16 1M = 0.678
|
||||
ICE LIBOR 2016-03-16 2M = 0.655
|
||||
EURIBOR 2016-03-15 1M = 0.123
|
||||
EURIBOR 2016-03-15 2M = 0.111
|
||||
|
||||
LIBOR 2016-03-08 3M = 0.01
|
||||
LIBOR 2016-06-08 3M = 0.01
|
||||
LIBOR 2016-09-08 3M = 0.01
|
||||
LIBOR 2016-12-08 3M = 0.01
|
||||
LIBOR 2017-03-08 3M = 0.01
|
||||
LIBOR 2017-06-08 3M = 0.01
|
||||
LIBOR 2017-09-07 3M = 0.01
|
||||
LIBOR 2017-12-07 3M = 0.01
|
||||
LIBOR 2018-03-08 3M = 0.01
|
||||
LIBOR 2018-06-07 3M = 0.01
|
||||
LIBOR 2018-09-06 3M = 0.01
|
||||
LIBOR 2018-12-06 3M = 0.01
|
||||
LIBOR 2019-03-07 3M = 0.01
|
||||
LIBOR 2019-06-06 3M = 0.01
|
||||
LIBOR 2019-09-06 3M = 0.01
|
||||
LIBOR 2019-12-06 3M = 0.01
|
||||
LIBOR 2020-03-06 3M = 0.01
|
||||
LIBOR 2020-06-08 3M = 0.01
|
||||
LIBOR 2020-09-08 3M = 0.01
|
||||
LIBOR 2020-12-08 3M = 0.01
|
||||
LIBOR 2021-03-08 3M = 0.01
|
||||
LIBOR 2021-06-08 3M = 0.01
|
||||
LIBOR 2021-09-08 3M = 0.01
|
||||
LIBOR 2021-12-08 3M = 0.01
|
||||
LIBOR 2022-03-08 3M = 0.01
|
||||
LIBOR 2022-06-08 3M = 0.01
|
||||
LIBOR 2022-09-08 3M = 0.01
|
||||
LIBOR 2022-12-08 3M = 0.01
|
||||
LIBOR 2023-03-08 3M = 0.01
|
||||
LIBOR 2023-06-08 3M = 0.01
|
||||
LIBOR 2023-09-07 3M = 0.01
|
||||
LIBOR 2023-12-07 3M = 0.01
|
||||
LIBOR 2024-03-07 3M = 0.01
|
||||
LIBOR 2024-06-06 3M = 0.01
|
||||
LIBOR 2024-09-06 3M = 0.01
|
||||
LIBOR 2024-12-06 3M = 0.01
|
||||
LIBOR 2025-03-06 3M = 0.01
|
||||
LIBOR 2025-06-06 3M = 0.01
|
||||
LIBOR 2025-09-08 3M = 0.01
|
||||
LIBOR 2025-12-08 3M = 0.01
|
||||
LIBOR 2026-03-06 3M = 0.01
|
||||
LIBOR 2026-06-08 3M = 0.01
|
||||
LIBOR 2026-09-08 3M = 0.01
|
||||
LIBOR 2026-12-08 3M = 0.01
|
||||
ICE LIBOR 2016-03-08 3M = 0.0063515
|
||||
ICE LIBOR 2016-06-08 3M = 0.0063520
|
||||
ICE LIBOR 2016-09-08 3M = 0.0063521
|
||||
ICE LIBOR 2016-12-08 3M = 0.0063515
|
||||
ICE LIBOR 2017-03-08 3M = 0.0063525
|
||||
ICE LIBOR 2017-06-08 3M = 0.0063530
|
||||
ICE LIBOR 2017-09-07 3M = 0.0063531
|
||||
ICE LIBOR 2017-12-07 3M = 0.0063532
|
||||
ICE LIBOR 2018-03-08 3M = 0.0063533
|
||||
ICE LIBOR 2018-06-07 3M = 0.0063534
|
||||
ICE LIBOR 2018-09-06 3M = 0.0063535
|
||||
ICE LIBOR 2018-12-06 3M = 0.0063536
|
||||
ICE LIBOR 2019-03-07 3M = 0.0063537
|
||||
ICE LIBOR 2019-06-06 3M = 0.0063538
|
||||
ICE LIBOR 2019-09-06 3M = 0.0063539
|
||||
ICE LIBOR 2019-12-06 3M = 0.0063540
|
||||
ICE LIBOR 2020-03-06 3M = 0.0063541
|
||||
ICE LIBOR 2020-06-08 3M = 0.0063542
|
||||
ICE LIBOR 2020-09-08 3M = 0.0063543
|
||||
ICE LIBOR 2020-12-08 3M = 0.0063544
|
||||
ICE LIBOR 2021-03-08 3M = 0.0063545
|
||||
ICE LIBOR 2021-06-08 3M = 0.0063546
|
||||
ICE LIBOR 2021-09-08 3M = 0.0063547
|
||||
ICE LIBOR 2021-12-08 3M = 0.0063548
|
||||
ICE LIBOR 2022-03-08 3M = 0.0063549
|
||||
ICE LIBOR 2022-06-08 3M = 0.0063550
|
||||
ICE LIBOR 2022-09-08 3M = 0.0063551
|
||||
ICE LIBOR 2022-12-08 3M = 0.0063553
|
||||
ICE LIBOR 2023-03-08 3M = 0.0063554
|
||||
ICE LIBOR 2023-06-08 3M = 0.0063555
|
||||
ICE LIBOR 2023-09-07 3M = 0.0063556
|
||||
ICE LIBOR 2023-12-07 3M = 0.0063557
|
||||
ICE LIBOR 2024-03-07 3M = 0.0063558
|
||||
ICE LIBOR 2024-06-06 3M = 0.0063559
|
||||
ICE LIBOR 2024-09-06 3M = 0.0063560
|
||||
ICE LIBOR 2024-12-06 3M = 0.0063561
|
||||
ICE LIBOR 2025-03-06 3M = 0.0063562
|
||||
ICE LIBOR 2025-06-06 3M = 0.0063563
|
||||
ICE LIBOR 2025-09-08 3M = 0.0063564
|
||||
ICE LIBOR 2025-12-08 3M = 0.0063565
|
||||
ICE LIBOR 2026-03-06 3M = 0.0063566
|
||||
ICE LIBOR 2026-06-08 3M = 0.0063567
|
||||
ICE LIBOR 2026-09-08 3M = 0.0063568
|
||||
ICE LIBOR 2026-12-08 3M = 0.0063569
|
||||
|
65
scripts/irs-demo.sh
Executable file
65
scripts/irs-demo.sh
Executable file
@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
|
||||
mode=$1
|
||||
|
||||
if [ ! -e ./gradlew ]; then
|
||||
echo "Run from the root directory please"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d build/install/r3prototyping ]; then
|
||||
./gradlew installDist
|
||||
fi
|
||||
|
||||
if [[ "$mode" == "nodeA" ]]; then
|
||||
if [ ! -d nodeA ]; then
|
||||
mkdir nodeA
|
||||
echo "myLegalName = Bank A" >nodeA/config
|
||||
fi
|
||||
|
||||
RC=83
|
||||
while [ $RC -eq 83 ]
|
||||
do
|
||||
build/install/r3prototyping/bin/irsdemo --dir=nodeA --network-address=localhost --fake-trade-with-address=localhost:31340 --fake-trade-with-identity=nodeB/identity-public --timestamper-identity-file=nodeA/identity-public --timestamper-address=localhost --rates-oracle-address=localhost:31340 --rates-oracle-identity-file=nodeB/identity-public
|
||||
RC=$?
|
||||
done
|
||||
elif [[ "$mode" == "nodeB" ]]; then
|
||||
if [ ! -d nodeB ]; then
|
||||
mkdir nodeB
|
||||
echo "myLegalName = Bank B" >nodeB/config
|
||||
fi
|
||||
|
||||
# enable job control
|
||||
set -o monitor
|
||||
|
||||
RC=83
|
||||
while [ $RC -eq 83 ]
|
||||
do
|
||||
build/install/r3prototyping/bin/irsdemo --dir=nodeB --network-address=localhost:31340 --fake-trade-with-address=localhost --fake-trade-with-identity=nodeA/identity-public --timestamper-identity-file=nodeA/identity-public --timestamper-address=localhost --rates-oracle-address=localhost:31340 --rates-oracle-identity-file=nodeB/identity-public &
|
||||
while ! curl -F rates=@scripts/example.rates.txt http://localhost:31341/upload/interest-rates; do
|
||||
echo "Retry to upload interest rates to oracle after 5 seconds"
|
||||
sleep 5
|
||||
done
|
||||
fg %1
|
||||
RC=$?
|
||||
done
|
||||
elif [[ "$mode" == "trade" && "$2" != "" ]]; then
|
||||
tradeID=$2
|
||||
echo "Uploading tradeID ${tradeID}"
|
||||
sed "s/tradeXXX/${tradeID}/g" scripts/example-irs-trade.json | curl -H "Content-Type: application/json" -d @- http://localhost:31338/api/irs/deals
|
||||
elif [[ "$mode" == "date" && "$2" != "" ]]; then
|
||||
demodate=$2
|
||||
echo "Setting demo date to ${demodate}"
|
||||
echo "\"$demodate\"" | curl -H "Content-Type: application/json" -X PUT -d @- http://localhost:31338/api/irs/demodate
|
||||
else
|
||||
echo "Run like this, one in each tab:"
|
||||
echo
|
||||
echo " scripts/irs-demo.sh nodeA"
|
||||
echo " scripts/irs-demo.sh nodeB"
|
||||
echo
|
||||
echo "To upload a trade as e.g. trade10"
|
||||
echo " scripts/irs-demo.sh trade trade10"
|
||||
echo
|
||||
echo "To set the demo date, and post fixings in the interval, to e.g. 2017-01-30"
|
||||
echo " scripts/irs-demo.sh date 2017-01-30"
|
||||
fi
|
@ -12,14 +12,14 @@ fi
|
||||
if [[ "$mode" == "buyer" ]]; then
|
||||
if [ ! -d buyer ]; then
|
||||
mkdir buyer
|
||||
echo "myLegalName = Bank of Zurich" >buyer/config
|
||||
echo "myLegalName = Bank A" >buyer/config
|
||||
fi
|
||||
|
||||
build/install/r3prototyping/bin/r3prototyping --dir=buyer --service-fake-trades --network-address=localhost
|
||||
elif [[ "$mode" == "seller" ]]; then
|
||||
if [ ! -d seller ]; then
|
||||
mkdir seller
|
||||
echo "myLegalName = Bank of London" >seller/config
|
||||
echo "myLegalName = Bank B" >seller/config
|
||||
fi
|
||||
|
||||
build/install/r3prototyping/bin/r3prototyping --dir=seller --fake-trade-with=localhost --network-address=localhost:31340 --timestamper-identity-file=buyer/identity-public --timestamper-address=localhost
|
||||
|
@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import contracts.DealState
|
||||
import core.*
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.SecureHash
|
||||
@ -27,7 +28,7 @@ class APIServerImpl(val node: AbstractNode): APIServer {
|
||||
return states.values.map { it.ref }
|
||||
}
|
||||
else if (query.criteria is StatesQuery.Criteria.Deal) {
|
||||
val states = node.services.walletService.linearHeadsInstanceOf(LinearState::class.java) {
|
||||
val states = node.services.walletService.linearHeadsInstanceOf(DealState::class.java) {
|
||||
it.ref == query.criteria.ref
|
||||
}
|
||||
return states.values.map { it.ref }
|
||||
@ -73,6 +74,7 @@ class APIServerImpl(val node: AbstractNode): APIServer {
|
||||
} else if (args.containsKey(parameter.name)) {
|
||||
val value = args[parameter.name]
|
||||
if (value is Any) {
|
||||
// TODO consider supporting more complex test here to support coercing numeric/Kotlin types
|
||||
if (!(parameter.type.javaType as Class<*>).isAssignableFrom(value.javaClass)) {
|
||||
// Not null and not assignable
|
||||
break@nextConstructor
|
||||
|
@ -1,21 +1,8 @@
|
||||
package api
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
import com.fasterxml.jackson.databind.*
|
||||
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers
|
||||
import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import core.BusinessCalendar
|
||||
import core.Party
|
||||
import core.crypto.SecureHash
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import core.node.services.ServiceHub
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import core.utilities.JsonSupport
|
||||
import javax.ws.rs.ext.ContextResolver
|
||||
import javax.ws.rs.ext.Provider
|
||||
|
||||
@ -25,119 +12,6 @@ import javax.ws.rs.ext.Provider
|
||||
*/
|
||||
@Provider
|
||||
class Config(val services: ServiceHub): ContextResolver<ObjectMapper> {
|
||||
|
||||
val defaultObjectMapper = createDefaultMapper(services)
|
||||
|
||||
override fun getContext(type: java.lang.Class<*>): ObjectMapper {
|
||||
return defaultObjectMapper
|
||||
}
|
||||
|
||||
class ServiceHubObjectMapper(var serviceHub: ServiceHub): ObjectMapper() {
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun createDefaultMapper(services: ServiceHub): ObjectMapper {
|
||||
val mapper = ServiceHubObjectMapper(services)
|
||||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||
mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
|
||||
|
||||
val timeModule = SimpleModule("java.time")
|
||||
timeModule.addSerializer(LocalDate::class.java, ToStringSerializer)
|
||||
timeModule.addDeserializer(LocalDate::class.java, LocalDateDeserializer)
|
||||
timeModule.addKeyDeserializer(LocalDate::class.java, LocalDateKeyDeserializer)
|
||||
timeModule.addSerializer(LocalDateTime::class.java, ToStringSerializer)
|
||||
|
||||
val cordaModule = SimpleModule("core")
|
||||
cordaModule.addSerializer(Party::class.java, PartySerializer)
|
||||
cordaModule.addDeserializer(Party::class.java, PartyDeserializer)
|
||||
cordaModule.addSerializer(BigDecimal::class.java, ToStringSerializer)
|
||||
cordaModule.addDeserializer(BigDecimal::class.java, NumberDeserializers.BigDecimalDeserializer())
|
||||
cordaModule.addSerializer(SecureHash::class.java, SecureHashSerializer)
|
||||
// It's slightly remarkable, but apparently Jackson works out that this is the only possibility
|
||||
// for a SecureHash at the moment and tries to use SHA256 directly even though we only give it SecureHash
|
||||
cordaModule.addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
|
||||
cordaModule.addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
|
||||
|
||||
mapper.registerModule(timeModule)
|
||||
mapper.registerModule(cordaModule)
|
||||
mapper.registerModule(KotlinModule())
|
||||
return mapper
|
||||
}
|
||||
}
|
||||
|
||||
object ToStringSerializer: JsonSerializer<Any>() {
|
||||
override fun serialize(obj: Any, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(obj.toString())
|
||||
}
|
||||
}
|
||||
|
||||
object LocalDateDeserializer: JsonDeserializer<LocalDate>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): LocalDate {
|
||||
return try {
|
||||
LocalDate.parse(parser.text)
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException("Invalid LocalDate ${parser.text}: ${e.message}", parser.currentLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object LocalDateKeyDeserializer: KeyDeserializer() {
|
||||
override fun deserializeKey(text: String, p1: DeserializationContext): Any? {
|
||||
return LocalDate.parse(text)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object PartySerializer: JsonSerializer<Party>() {
|
||||
override fun serialize(obj: Party, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(obj.name)
|
||||
}
|
||||
}
|
||||
|
||||
object PartyDeserializer: JsonDeserializer<Party>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): Party {
|
||||
if(parser.currentToken == JsonToken.FIELD_NAME) {
|
||||
parser.nextToken()
|
||||
}
|
||||
val mapper = parser.codec as ServiceHubObjectMapper
|
||||
// TODO this needs to use some industry identifier(s) not just these human readable names
|
||||
val nodeForPartyName = mapper.serviceHub.networkMapService.nodeForPartyName(parser.text) ?: throw JsonParseException("Could not find a Party with name: ${parser.text}", parser.currentLocation)
|
||||
return nodeForPartyName.identity
|
||||
}
|
||||
}
|
||||
|
||||
object SecureHashSerializer: JsonSerializer<SecureHash>() {
|
||||
override fun serialize(obj: SecureHash, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(obj.toString())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implemented as a class so that we can instantiate for T
|
||||
*/
|
||||
class SecureHashDeserializer<T : SecureHash>: JsonDeserializer<T>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): T {
|
||||
if(parser.currentToken == JsonToken.FIELD_NAME) {
|
||||
parser.nextToken()
|
||||
}
|
||||
return try {
|
||||
return SecureHash.parse(parser.text) as T
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException("Invalid hash ${parser.text}: ${e.message}", parser.currentLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object CalendarDeserializer: JsonDeserializer<BusinessCalendar>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar {
|
||||
return try {
|
||||
val array = StringArrayDeserializer.instance.deserialize(parser, context)
|
||||
BusinessCalendar.getInstance(*array)
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException("Invalid calendar(s) ${parser.text}: ${e.message}", parser.currentLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
val defaultObjectMapper = JsonSupport.createDefaultMapper(services.identityService)
|
||||
override fun getContext(type: java.lang.Class<*>) = defaultObjectMapper
|
||||
}
|
109
src/main/kotlin/api/InterestRateSwapAPI.kt
Normal file
109
src/main/kotlin/api/InterestRateSwapAPI.kt
Normal file
@ -0,0 +1,109 @@
|
||||
package api
|
||||
|
||||
import contracts.InterestRateSwap
|
||||
import core.utilities.loggerFor
|
||||
import demos.protocols.AutoOfferProtocol
|
||||
import demos.protocols.ExitServerProtocol
|
||||
import demos.protocols.UpdateBusinessDayProtocol
|
||||
import java.net.URI
|
||||
import java.time.LocalDate
|
||||
import javax.ws.rs.*
|
||||
import javax.ws.rs.core.MediaType
|
||||
import javax.ws.rs.core.Response
|
||||
|
||||
/**
|
||||
* This provides a simplified API, currently for demonstration use only.
|
||||
*
|
||||
* It provides several JSON REST calls as follows:
|
||||
*
|
||||
* GET /api/irs/deals - returns an array of all deals tracked by the wallet of this node.
|
||||
* GET /api/irs/deals/{ref} - return the deal referenced by the externally provided refence that was previously uploaded.
|
||||
* POST /api/irs/deals - Payload is a JSON formatted [InterestRateSwap.State] create a new deal (includes an externally provided reference for use above).
|
||||
*
|
||||
* TODO: where we currently refer to singular external deal reference, of course this could easily be multiple identifiers e.g. CUSIP, ISIN.
|
||||
*
|
||||
* GET /api/irs/demodate - return the current date as viewed by the system in YYYY-MM-DD format.
|
||||
* PUT /api/irs/demodate - put date in format YYYY-MM-DD to advance the current date as viewed by the system and
|
||||
* simulate any associated business processing (currently fixing).
|
||||
*
|
||||
* TODO: replace simulated date advancement with business event based implementation
|
||||
*
|
||||
* PUT /api/irs/restart - (empty payload) cause the node to restart for API user emergency use in case any servers become unresponsive,
|
||||
* or if the demodate or population of deals should be reset (will only work while persistence is disabled).
|
||||
*/
|
||||
@Path("irs")
|
||||
class InterestRateSwapAPI(val api: APIServer) {
|
||||
|
||||
private val logger = loggerFor<InterestRateSwapAPI>()
|
||||
|
||||
private fun generateDealLink(deal: InterestRateSwap.State) = "/api/irs/deals/"+deal.common.tradeID
|
||||
|
||||
private fun getDealByRef(ref: String): InterestRateSwap.State? {
|
||||
val states = api.queryStates(StatesQuery.selectDeal(ref))
|
||||
return if (states.isEmpty()) null else {
|
||||
val deals = api.fetchStates(states).values.map { it as InterestRateSwap.State}.filterNotNull()
|
||||
return if(deals.isEmpty()) null else deals[0]
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAllDeals(): Array<InterestRateSwap.State> {
|
||||
val states = api.queryStates(StatesQuery.selectAllDeals())
|
||||
val swaps = api.fetchStates(states).values.map { it as InterestRateSwap.State }.filterNotNull().toTypedArray()
|
||||
return swaps
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("deals")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
fun fetchDeals(): Array<InterestRateSwap.State> = getAllDeals()
|
||||
|
||||
@POST
|
||||
@Path("deals")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
fun storeDeal(newDeal: InterestRateSwap.State): Response {
|
||||
api.invokeProtocolSync(ProtocolClassRef(AutoOfferProtocol.Requester::class.java.name!!), mapOf("dealToBeOffered" to newDeal))
|
||||
return Response.created(URI.create(generateDealLink(newDeal))).build()
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("deals/{ref}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
fun fetchDeal(@PathParam("ref") ref: String): Response {
|
||||
val deal = getDealByRef(ref)
|
||||
if (deal == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build()
|
||||
} else {
|
||||
return Response.ok().entity(deal).build()
|
||||
}
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("demodate")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
fun storeDemoDate(newDemoDate: LocalDate): Response {
|
||||
val priorDemoDate = api.serverTime().toLocalDate()
|
||||
// Can only move date forwards
|
||||
if(newDemoDate.isAfter(priorDemoDate)) {
|
||||
api.invokeProtocolSync(ProtocolClassRef(UpdateBusinessDayProtocol.Broadcast::class.java.name!!), mapOf("date" to newDemoDate))
|
||||
return Response.ok().build()
|
||||
}
|
||||
val msg = "demodate is already $priorDemoDate and can only be updated with a later date"
|
||||
logger.info("Attempt to set demodate to $newDemoDate but $msg")
|
||||
return Response.status(Response.Status.CONFLICT).entity(msg).build()
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("demodate")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
fun fetchDemoDate(): LocalDate {
|
||||
return api.serverTime().toLocalDate()
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("restart")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
fun exitServer(): Response {
|
||||
api.invokeProtocolSync(ProtocolClassRef(ExitServerProtocol.Broadcast::class.java.name!!), mapOf("exitCode" to 83))
|
||||
return Response.ok().build()
|
||||
}
|
||||
}
|
@ -80,6 +80,8 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor)
|
||||
// checkpointing when unit tests are run inside Gradle. The right fix is probably to stop Quasar's
|
||||
// bit-too-clever-for-its-own-good ThreadLocal serialisation trick. It already wasted far more time than it can
|
||||
// ever recover.
|
||||
//
|
||||
// TODO: Remove this now that TLS serialisation is fixed.
|
||||
val checkpointing: Boolean get() = !System.err.javaClass.name.contains("LinePerThreadBufferingOutputStream")
|
||||
|
||||
/** Returns a list of all state machines executing the given protocol logic at the top level (subprotocols do not count) */
|
||||
|
@ -34,6 +34,7 @@ import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.time.Clock
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
@ -42,7 +43,7 @@ import java.util.concurrent.Executors
|
||||
* A base node implementation that can be customised either for production (with real implementations that do real
|
||||
* I/O), or a mock implementation suitable for unit test environments.
|
||||
*/
|
||||
abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration, val timestamperAddress: LegallyIdentifiableNode?, val platformClock: Clock) {
|
||||
abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration, val timestamperAddress: NodeInfo?, val platformClock: Clock) {
|
||||
companion object {
|
||||
val PRIVATE_KEY_FILE_NAME = "identity-private-key"
|
||||
val PUBLIC_IDENTITY_FILE_NAME = "identity-public"
|
||||
@ -61,7 +62,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
||||
|
||||
val services = object : ServiceHub {
|
||||
override val networkService: MessagingService get() = net
|
||||
override val networkMapService: NetworkMapService = MockNetworkMapService()
|
||||
override val networkMapCache: NetworkMapCache = MockNetworkMapCache()
|
||||
override val storageService: StorageService get() = storage
|
||||
override val walletService: WalletService get() = wallet
|
||||
override val keyManagementService: KeyManagementService get() = keyManagement
|
||||
@ -70,8 +71,8 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
||||
override val clock: Clock get() = platformClock
|
||||
}
|
||||
|
||||
val legallyIdentifiableAddress: LegallyIdentifiableNode by lazy {
|
||||
LegallyIdentifiableNode(net.myAddress, storage.myLegalIdentity, findMyLocation())
|
||||
val info: NodeInfo by lazy {
|
||||
NodeInfo(net.myAddress, storage.myLegalIdentity, findMyLocation())
|
||||
}
|
||||
|
||||
protected open fun findMyLocation(): PhysicalLocation? = CityDatabase[configuration.nearestCity]
|
||||
@ -93,20 +94,9 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
||||
smm = StateMachineManager(services, serverThread)
|
||||
wallet = NodeWalletService(services)
|
||||
keyManagement = E2ETestKeyManagementService()
|
||||
makeInterestRateOracleService()
|
||||
makeInterestRatesOracleService()
|
||||
api = APIServerImpl(this)
|
||||
|
||||
// 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.
|
||||
val tsid = if (timestamperAddress != null) {
|
||||
inNodeTimestampingService = null
|
||||
timestamperAddress
|
||||
} else {
|
||||
inNodeTimestampingService = NodeTimestamperService(net, storage.myLegalIdentity, storage.myLegalIdentityKey, platformClock)
|
||||
LegallyIdentifiableNode(net.myAddress, storage.myLegalIdentity)
|
||||
}
|
||||
(services.networkMapService as MockNetworkMapService).timestampingNodes.add(tsid)
|
||||
|
||||
makeTimestampingService(timestamperAddress)
|
||||
identity = makeIdentityService()
|
||||
|
||||
// This object doesn't need to be referenced from this class because it registers handlers on the network
|
||||
@ -116,20 +106,50 @@ 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.
|
||||
private fun makeTimestampingService(timestamperAddress: NodeInfo?) {
|
||||
// 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.
|
||||
val tsid = if (timestamperAddress != null) {
|
||||
inNodeTimestampingService = null
|
||||
timestamperAddress
|
||||
} else {
|
||||
inNodeTimestampingService = NodeTimestamperService(net, storage.myLegalIdentity, storage.myLegalIdentityKey, platformClock)
|
||||
NodeInfo(net.myAddress, storage.myLegalIdentity)
|
||||
}
|
||||
(services.networkMapCache as MockNetworkMapCache).timestampingNodes.add(tsid)
|
||||
}
|
||||
|
||||
lateinit var interestRatesService: NodeInterestRates.Service
|
||||
|
||||
open protected fun makeInterestRatesOracleService() {
|
||||
// TODO: Once the service has data, automatically register with the network map service (once built).
|
||||
_servicesThatAcceptUploads += NodeInterestRates.Service(this)
|
||||
interestRatesService = NodeInterestRates.Service(this)
|
||||
_servicesThatAcceptUploads += interestRatesService
|
||||
}
|
||||
|
||||
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).
|
||||
val knownIdentities = if (timestamperAddress != null)
|
||||
// We don't have any identity infrastructure right now, so we just throw together the only identities we
|
||||
// know about: our own, the identity of the remote timestamper node (if any), plus whatever is in the
|
||||
// network map.
|
||||
//
|
||||
// TODO: All this will be replaced soon enough.
|
||||
val fixedIdentities = if (timestamperAddress != null)
|
||||
listOf(storage.myLegalIdentity, timestamperAddress.identity)
|
||||
else
|
||||
listOf(storage.myLegalIdentity)
|
||||
return FixedIdentityService(knownIdentities)
|
||||
|
||||
return object : IdentityService {
|
||||
private val identities: List<Party> get() = fixedIdentities + services.networkMapCache.partyNodes.map { it.identity }
|
||||
private val keyToParties: Map<PublicKey, Party> get() = identities.associateBy { it.owningKey }
|
||||
private val nameToParties: Map<String, Party> get() = identities.associateBy { it.name }
|
||||
|
||||
override fun partyFromKey(key: PublicKey): Party? = keyToParties[key]
|
||||
override fun partyFromName(name: String): Party? = nameToParties[name]
|
||||
|
||||
override fun toString(): String {
|
||||
return identities.joinToString { it.name }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun stop() {
|
||||
|
@ -9,12 +9,13 @@
|
||||
package core.node
|
||||
|
||||
import api.Config
|
||||
import api.InterestRateSwapAPI
|
||||
import api.ResponseFilter
|
||||
import com.codahale.metrics.JmxReporter
|
||||
import com.google.common.net.HostAndPort
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.messaging.MessagingService
|
||||
import core.node.services.ArtemisMessagingService
|
||||
import core.node.services.NodeInfo
|
||||
import core.node.servlets.AttachmentDownloadServlet
|
||||
import core.node.servlets.DataUploadServlet
|
||||
import core.utilities.loggerFor
|
||||
@ -51,7 +52,7 @@ class ConfigurationException(message: String) : Exception(message)
|
||||
* @param clock The clock used within the node and by all protocols etc
|
||||
*/
|
||||
class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration,
|
||||
timestamperAddress: LegallyIdentifiableNode?,
|
||||
timestamperAddress: NodeInfo?,
|
||||
clock: Clock = Clock.systemUTC()) : AbstractNode(dir, configuration, timestamperAddress, clock) {
|
||||
companion object {
|
||||
/** The port that is used by default if none is specified. As you know, 31337 is the most elite number. */
|
||||
@ -97,6 +98,7 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
|
||||
resourceConfig.register(Config(services))
|
||||
resourceConfig.register(ResponseFilter())
|
||||
resourceConfig.register(api)
|
||||
resourceConfig.register(InterestRateSwapAPI(api))
|
||||
// Give the app a slightly better name in JMX rather than a randomly generated one and enable JMX
|
||||
resourceConfig.addProperties(mapOf(ServerProperties.APPLICATION_NAME to "node.api",
|
||||
ServerProperties.MONITORING_STATISTICS_MBEANS_ENABLED to "true"))
|
||||
|
@ -8,14 +8,23 @@
|
||||
|
||||
package core.node.services
|
||||
|
||||
import core.node.services.IdentityService
|
||||
import core.Party
|
||||
import java.security.PublicKey
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* Scaffolding: a dummy identity service that just expects to have identities loaded off disk or found elsewhere.
|
||||
* This class allows the provided list of identities to be mutated after construction, so it takes the list lock
|
||||
* when doing lookups and recalculates the mapping each time. The ability to change the list is used by the
|
||||
* MockNetwork code.
|
||||
*/
|
||||
class FixedIdentityService(private val identities: List<Party>) : IdentityService {
|
||||
private val keyToParties: Map<PublicKey, Party> get() = identities.associateBy { it.owningKey }
|
||||
@ThreadSafe
|
||||
class FixedIdentityService(val identities: List<Party>) : IdentityService {
|
||||
private val keyToParties: Map<PublicKey, Party>
|
||||
get() = synchronized(identities) { identities.associateBy { it.owningKey } }
|
||||
private val nameToParties: Map<String, Party>
|
||||
get() = synchronized(identities) { identities.associateBy { it.name } }
|
||||
|
||||
override fun partyFromKey(key: PublicKey): Party? = keyToParties[key]
|
||||
override fun partyFromName(name: String): Party? = nameToParties[name]
|
||||
}
|
@ -13,9 +13,11 @@ import core.crypto.DummyPublicKey
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import java.util.*
|
||||
|
||||
/** Info about a network node that has operated by some sort of verified identity. */
|
||||
data class LegallyIdentifiableNode(val address: SingleMessageRecipient, val identity: Party,
|
||||
val physicalLocation: PhysicalLocation? = null)
|
||||
/**
|
||||
* Info about a network node that acts on behalf of some sort of verified identity.
|
||||
*/
|
||||
data class NodeInfo(val address: SingleMessageRecipient, val identity: Party,
|
||||
val physicalLocation: PhysicalLocation? = null)
|
||||
|
||||
/**
|
||||
* A network map contains lists of nodes on the network along with information about their identity keys, services
|
||||
@ -26,25 +28,27 @@ data class LegallyIdentifiableNode(val address: SingleMessageRecipient, val iden
|
||||
*
|
||||
* This interface assumes fast, synchronous access to an in-memory map.
|
||||
*/
|
||||
interface NetworkMapService {
|
||||
val timestampingNodes: List<LegallyIdentifiableNode>
|
||||
val ratesOracleNodes: List<LegallyIdentifiableNode>
|
||||
val partyNodes: List<LegallyIdentifiableNode>
|
||||
interface NetworkMapCache {
|
||||
val timestampingNodes: List<NodeInfo>
|
||||
val ratesOracleNodes: List<NodeInfo>
|
||||
val partyNodes: List<NodeInfo>
|
||||
val regulators: List<NodeInfo>
|
||||
|
||||
fun nodeForPartyName(name: String): LegallyIdentifiableNode? = partyNodes.singleOrNull { it.identity.name == name }
|
||||
fun nodeForPartyName(name: String): NodeInfo? = partyNodes.singleOrNull { it.identity.name == name }
|
||||
}
|
||||
|
||||
// TODO: Move this to the test tree once a real network map is implemented and this scaffolding is no longer needed.
|
||||
class MockNetworkMapService : NetworkMapService {
|
||||
class MockNetworkMapCache : NetworkMapCache {
|
||||
data class MockAddress(val id: String): SingleMessageRecipient
|
||||
|
||||
override val timestampingNodes = Collections.synchronizedList(ArrayList<LegallyIdentifiableNode>())
|
||||
override val ratesOracleNodes = Collections.synchronizedList(ArrayList<LegallyIdentifiableNode>())
|
||||
override val partyNodes = Collections.synchronizedList(ArrayList<LegallyIdentifiableNode>())
|
||||
override val timestampingNodes = Collections.synchronizedList(ArrayList<NodeInfo>())
|
||||
override val ratesOracleNodes = Collections.synchronizedList(ArrayList<NodeInfo>())
|
||||
override val partyNodes = Collections.synchronizedList(ArrayList<NodeInfo>())
|
||||
override val regulators = Collections.synchronizedList(ArrayList<NodeInfo>())
|
||||
|
||||
init {
|
||||
partyNodes.add(LegallyIdentifiableNode(MockAddress("excalibur:8080"), Party("Excalibur", DummyPublicKey("Excalibur"))))
|
||||
partyNodes.add(LegallyIdentifiableNode(MockAddress("another:8080"), Party("ANOther", DummyPublicKey("ANOther"))))
|
||||
partyNodes.add(NodeInfo(MockAddress("bankC:8080"), Party("Bank C", DummyPublicKey("Bank C"))))
|
||||
partyNodes.add(NodeInfo(MockAddress("bankD:8080"), Party("Bank D", DummyPublicKey("Bank D"))))
|
||||
}
|
||||
}
|
||||
|
@ -43,16 +43,21 @@ object NodeInterestRates {
|
||||
|
||||
/** Parses a string of the form "LIBOR 16-March-2016 1M" into a [FixOf] */
|
||||
fun parseFixOf(key: String): FixOf {
|
||||
val (name, date, tenorString) = key.split(' ')
|
||||
val words = key.split(' ')
|
||||
val tenorString = words.last()
|
||||
val date = words.dropLast(1).last()
|
||||
val name = words.dropLast(2).joinToString(" ")
|
||||
return FixOf(name, LocalDate.parse(date), Tenor(tenorString))
|
||||
}
|
||||
|
||||
/** Parses lines containing fixes */
|
||||
fun parseFile(s: String): Map<FixOf, Fix> {
|
||||
val results = HashMap<FixOf, Fix>()
|
||||
fun parseFile(s: String): Map<FixOf, TreeMap<LocalDate,Fix>> {
|
||||
val results = HashMap<FixOf, TreeMap<LocalDate,Fix>>()
|
||||
for (line in s.lines()) {
|
||||
val (fixOf, fix) = parseOneRate(line.trim())
|
||||
results[fixOf] = fix
|
||||
val (fixOf, fix) = parseOneRate(line)
|
||||
val genericKey = FixOf(fixOf.name, LocalDate.MIN, fixOf.ofTenor)
|
||||
val existingMap = results.computeIfAbsent(genericKey, { TreeMap() })
|
||||
existingMap[fixOf.forDay] = fix
|
||||
}
|
||||
return results
|
||||
}
|
||||
@ -91,14 +96,13 @@ object NodeInterestRates {
|
||||
override val acceptableFileExtensions = listOf(".rates", ".txt")
|
||||
|
||||
override fun upload(data: InputStream): String {
|
||||
val fixes: Map<FixOf, Fix> = data.
|
||||
val fixes: Map<FixOf, TreeMap<LocalDate,Fix>> = parseFile(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 }
|
||||
joinToString("\n"))
|
||||
|
||||
// TODO: Save the uploaded fixes to the storage service and reload on construction.
|
||||
|
||||
@ -106,12 +110,16 @@ object NodeInterestRates {
|
||||
// the pointer to the stack before working with the map.
|
||||
oracle.knownFixes = fixes
|
||||
|
||||
return "Accepted ${fixes.size} new interest rate fixes"
|
||||
val sumOfFixes = fixes.map { it.value.size }.sum()
|
||||
return "Accepted $sumOfFixes new interest rate fixes"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of an interest rate fix oracle which is given data in a simple string format.
|
||||
*
|
||||
* NOTE the implementation has changed such that it will find the nearest dated entry on OR BEFORE the date
|
||||
* requested so as not to need to populate every possible date in the flat file that (typically) feeds this service
|
||||
*/
|
||||
@ThreadSafe
|
||||
class Oracle(val identity: Party, private val signingKey: KeyPair) {
|
||||
@ -119,8 +127,13 @@ object NodeInterestRates {
|
||||
require(signingKey.public == identity.owningKey)
|
||||
}
|
||||
|
||||
/** The fix data being served by this oracle. */
|
||||
@Transient var knownFixes = emptyMap<FixOf, Fix>()
|
||||
/** The fix data being served by this oracle.
|
||||
*
|
||||
* This is now a map of FixOf (but with date set to LocalDate.MIN, so just index name and tenor)
|
||||
* to a sorted map of LocalDate to Fix, allowing for approximate date finding so that we do not need
|
||||
* to populate the file with a rate for every day.
|
||||
*/
|
||||
@Volatile var knownFixes = emptyMap<FixOf, TreeMap<LocalDate,Fix>>()
|
||||
set(value) {
|
||||
require(value.isNotEmpty())
|
||||
field = value
|
||||
@ -130,13 +143,24 @@ object NodeInterestRates {
|
||||
require(queries.isNotEmpty())
|
||||
val knownFixes = knownFixes // Snapshot
|
||||
|
||||
val answers: List<Fix?> = queries.map { knownFixes[it] }
|
||||
val answers: List<Fix?> = queries.map { getKnownFix(knownFixes, it) }
|
||||
val firstNull = answers.indexOf(null)
|
||||
if (firstNull != -1)
|
||||
throw UnknownFix(queries[firstNull])
|
||||
return answers.filterNotNull()
|
||||
}
|
||||
|
||||
private fun getKnownFix(knownFixes: Map<FixOf, TreeMap<LocalDate, Fix>>, fixOf: FixOf): Fix? {
|
||||
val rates = knownFixes[FixOf(fixOf.name, LocalDate.MIN, fixOf.ofTenor)]
|
||||
// Greatest key less than or equal to the date we're looking for
|
||||
val floor = rates?.floorEntry(fixOf.forDay)?.value
|
||||
return if (floor!=null) {
|
||||
Fix(fixOf, floor.value)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun sign(wtx: WireTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
// Extract the fix commands marked as being signable by us.
|
||||
val fixes: List<Fix> = wtx.commands.
|
||||
@ -150,7 +174,7 @@ object NodeInterestRates {
|
||||
// For each fix, verify that the data is correct.
|
||||
val knownFixes = knownFixes // Snapshot
|
||||
for (fix in fixes) {
|
||||
val known = knownFixes[fix.of]
|
||||
val known = getKnownFix(knownFixes, fix.of)
|
||||
if (known == null || known != fix)
|
||||
throw UnknownFix(fix.of)
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import contracts.Cash
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.messaging.MessagingService
|
||||
import core.node.services.NetworkMapService
|
||||
import java.io.InputStream
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
@ -73,8 +72,8 @@ interface WalletService {
|
||||
*/
|
||||
val linearHeads: Map<SecureHash, StateAndRef<LinearState>>
|
||||
|
||||
fun <T : LinearState> linearHeadsInstanceOf(clazz: Class<T>, predicate: (T) -> Boolean = { true } ): Map<SecureHash, StateAndRef<LinearState>> {
|
||||
return linearHeads.filterValues { clazz.isInstance(it.state) }.filterValues { predicate(it.state as T) }
|
||||
fun <T : LinearState> linearHeadsInstanceOf(clazz: Class<T>, predicate: (T) -> Boolean = { true } ): Map<SecureHash, StateAndRef<T>> {
|
||||
return linearHeads.filterValues { clazz.isInstance(it.state) }.filterValues { predicate(it.state as T) }.mapValues { StateAndRef(it.value.state as T, it.value.ref) }
|
||||
}
|
||||
|
||||
fun statesForRefs(refs: List<StateRef>): Map<StateRef, ContractState?> {
|
||||
@ -96,6 +95,18 @@ interface WalletService {
|
||||
fun notify(tx: WireTransaction): Wallet = notifyAll(listOf(tx))
|
||||
}
|
||||
|
||||
// TODO: Document this
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified T : LinearState> WalletService.linearHeadsOfType(): Map<SecureHash, StateAndRef<T>> {
|
||||
return linearHeads.mapNotNull {
|
||||
val s = it.value.state
|
||||
if (s is T)
|
||||
Pair(it.key, it.value as StateAndRef<T>)
|
||||
else
|
||||
null
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* The KMS is responsible for storing and using private keys to sign things. An implementation of this may, for example,
|
||||
* call out to a hardware security module that enforces various auditing and frequency-of-use requirements.
|
||||
@ -156,7 +167,7 @@ interface ServiceHub {
|
||||
val identityService: IdentityService
|
||||
val storageService: StorageService
|
||||
val networkService: MessagingService
|
||||
val networkMapService: NetworkMapService
|
||||
val networkMapCache: NetworkMapCache
|
||||
val monitoringService: MonitoringService
|
||||
val clock: Clock
|
||||
|
||||
|
@ -48,7 +48,10 @@ abstract class ProtocolLogic<T> {
|
||||
return psm.sendAndReceive(topic, destination, sessionIDForSend, sessionIDForReceive, obj, T::class.java)
|
||||
}
|
||||
inline fun <reified T : Any> receive(topic: String, sessionIDForReceive: Long): UntrustworthyData<T> {
|
||||
return psm.receive(topic, sessionIDForReceive, T::class.java)
|
||||
return receive(topic, sessionIDForReceive, T::class.java)
|
||||
}
|
||||
@Suspendable fun <T : Any> receive(topic: String, sessionIDForReceive: Long, clazz: Class<T>): UntrustworthyData<T> {
|
||||
return psm.receive(topic, sessionIDForReceive, clazz)
|
||||
}
|
||||
@Suspendable fun send(topic: String, destination: MessageRecipients, sessionID: Long, obj: Any) {
|
||||
psm.send(topic, destination, sessionID, obj)
|
||||
|
141
src/main/kotlin/core/testing/IRSSimulation.kt
Normal file
141
src/main/kotlin/core/testing/IRSSimulation.kt
Normal file
@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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.testing
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import contracts.InterestRateSwap
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.node.services.FixedIdentityService
|
||||
import core.node.services.linearHeadsOfType
|
||||
import core.utilities.JsonSupport
|
||||
import protocols.TwoPartyDealProtocol
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
|
||||
|
||||
/**
|
||||
* A simulation in which banks execute interest rate swaps with each other, including the fixing events.
|
||||
*/
|
||||
class IRSSimulation(runAsync: Boolean, latencyInjector: InMemoryMessagingNetwork.LatencyCalculator?) : Simulation(runAsync, latencyInjector) {
|
||||
val om = JsonSupport.createDefaultMapper(FixedIdentityService(network.identities))
|
||||
|
||||
init {
|
||||
currentDay = LocalDate.of(2016, 3, 10) // Should be 12th but the actual first fixing date gets rolled backwards.
|
||||
}
|
||||
|
||||
private val executeOnNextIteration = Collections.synchronizedList(LinkedList<() -> Unit>())
|
||||
|
||||
override fun start() {
|
||||
startIRSDealBetween(0, 1).success {
|
||||
// Next iteration is a pause.
|
||||
executeOnNextIteration.add {}
|
||||
executeOnNextIteration.add {
|
||||
// Keep fixing until there's no more left to do.
|
||||
doNextFixing(0, 1)?.addListener(object : Runnable {
|
||||
override fun run() {
|
||||
// Pause for an iteration.
|
||||
executeOnNextIteration.add {}
|
||||
executeOnNextIteration.add {
|
||||
doNextFixing(0, 1)?.addListener(this, RunOnCallerThread)
|
||||
}
|
||||
}
|
||||
}, RunOnCallerThread)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doNextFixing(i: Int, j: Int): ListenableFuture<*>? {
|
||||
println("Doing a fixing between $i and $j")
|
||||
val node1: SimulatedNode = banks[i]
|
||||
val node2: SimulatedNode = banks[j]
|
||||
|
||||
val sessionID = random63BitValue()
|
||||
val swaps: Map<SecureHash, StateAndRef<InterestRateSwap.State>> = node1.services.walletService.linearHeadsOfType<InterestRateSwap.State>()
|
||||
val theDealRef: StateAndRef<InterestRateSwap.State> = swaps.values.single()
|
||||
|
||||
// Do we have any more days left in this deal's lifetime? If not, return.
|
||||
val nextFixingDate = theDealRef.state.calculation.nextFixingDate() ?: return null
|
||||
extraNodeLabels[node1] = "Fixing event on $nextFixingDate"
|
||||
extraNodeLabels[node2] = "Fixing event on $nextFixingDate"
|
||||
|
||||
// For some reason the first fix is always before the effective date.
|
||||
if (nextFixingDate > currentDay)
|
||||
currentDay = nextFixingDate
|
||||
|
||||
val sideA = TwoPartyDealProtocol.Floater(node2.net.myAddress, sessionID, timestamper.info,
|
||||
theDealRef, node1.services.keyManagementService.freshKey(), sessionID)
|
||||
val sideB = TwoPartyDealProtocol.Fixer(node1.net.myAddress, timestamper.info.identity,
|
||||
theDealRef, sessionID)
|
||||
|
||||
linkConsensus(listOf(node1, node2, regulators[0]), sideB)
|
||||
linkProtocolProgress(node1, sideA)
|
||||
linkProtocolProgress(node2, sideB)
|
||||
|
||||
// We have to start the protocols in separate iterations, as adding to the SMM effectively 'iterates' that node
|
||||
// in the simulation, so if we don't do this then the two sides seem to act simultaneously.
|
||||
|
||||
val retFuture = SettableFuture.create<Any>()
|
||||
val futA = node1.smm.add("floater", sideA)
|
||||
executeOnNextIteration += {
|
||||
val futB = node2.smm.add("fixer", sideB)
|
||||
Futures.allAsList(futA, futB).then {
|
||||
retFuture.set(null)
|
||||
}
|
||||
}
|
||||
return retFuture
|
||||
}
|
||||
|
||||
private fun startIRSDealBetween(i: Int, j: Int): ListenableFuture<SignedTransaction> {
|
||||
val node1: SimulatedNode = banks[i]
|
||||
val node2: SimulatedNode = banks[j]
|
||||
|
||||
extraNodeLabels[node1] = "Setting up deal"
|
||||
extraNodeLabels[node2] = "Setting up deal"
|
||||
|
||||
// We load the IRS afresh each time because the leg parts of the structure aren't data classes so they don't
|
||||
// have the convenient copy() method that'd let us make small adjustments. Instead they're partly mutable.
|
||||
// TODO: We should revisit this in post-Excalibur cleanup and fix, e.g. by introducing an interface.
|
||||
val irs = om.readValue<InterestRateSwap.State>(javaClass.getResource("trade.json"))
|
||||
irs.fixedLeg.fixedRatePayer = node1.info.identity
|
||||
irs.floatingLeg.floatingRatePayer = node2.info.identity
|
||||
|
||||
if (irs.fixedLeg.effectiveDate < irs.floatingLeg.effectiveDate)
|
||||
currentDay = irs.fixedLeg.effectiveDate
|
||||
else
|
||||
currentDay = irs.floatingLeg.effectiveDate
|
||||
|
||||
val sessionID = random63BitValue()
|
||||
|
||||
val instigator = TwoPartyDealProtocol.Instigator(node2.net.myAddress, timestamper.info,
|
||||
irs, node1.services.keyManagementService.freshKey(), sessionID)
|
||||
val acceptor = TwoPartyDealProtocol.Acceptor(node1.net.myAddress, timestamper.info.identity,
|
||||
irs, sessionID)
|
||||
|
||||
// TODO: Eliminate the need for linkProtocolProgress
|
||||
linkConsensus(listOf(node1, node2, regulators[0]), acceptor)
|
||||
linkProtocolProgress(node1, instigator)
|
||||
linkProtocolProgress(node2, acceptor)
|
||||
|
||||
val instigatorFuture: ListenableFuture<SignedTransaction> = node1.smm.add("instigator", instigator)
|
||||
|
||||
return Futures.transformAsync(Futures.allAsList(instigatorFuture, node2.smm.add("acceptor", acceptor))) {
|
||||
instigatorFuture
|
||||
}
|
||||
}
|
||||
|
||||
override fun iterate() {
|
||||
if (executeOnNextIteration.isNotEmpty())
|
||||
executeOnNextIteration.removeAt(0)()
|
||||
super.iterate()
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@ import core.ThreadBox
|
||||
import core.crypto.sha256
|
||||
import core.messaging.*
|
||||
import core.node.services.DummyTimestampingAuthority
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.node.services.NodeInfo
|
||||
import core.node.services.NodeTimestamperService
|
||||
import core.utilities.loggerFor
|
||||
import rx.Observable
|
||||
@ -150,15 +150,15 @@ class InMemoryMessagingNetwork {
|
||||
override fun hashCode() = id.hashCode()
|
||||
}
|
||||
|
||||
private var timestampingAdvert: LegallyIdentifiableNode? = null
|
||||
private var timestampingAdvert: NodeInfo? = null
|
||||
|
||||
@Synchronized
|
||||
fun setupTimestampingNode(manuallyPumped: Boolean): Pair<LegallyIdentifiableNode, InMemoryMessaging> {
|
||||
fun setupTimestampingNode(manuallyPumped: Boolean): Pair<NodeInfo, InMemoryMessaging> {
|
||||
check(timestampingAdvert == null)
|
||||
val (handle, builder) = createNode(manuallyPumped)
|
||||
val node = builder.start().get()
|
||||
NodeTimestamperService(node, DummyTimestampingAuthority.identity, DummyTimestampingAuthority.key)
|
||||
timestampingAdvert = LegallyIdentifiableNode(handle, DummyTimestampingAuthority.identity)
|
||||
timestampingAdvert = NodeInfo(handle, DummyTimestampingAuthority.identity)
|
||||
return Pair(timestampingAdvert!!, node)
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ import core.messaging.SingleMessageRecipient
|
||||
import core.node.AbstractNode
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.services.FixedIdentityService
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.node.services.NodeInfo
|
||||
import core.node.services.PhysicalLocation
|
||||
import core.utilities.loggerFor
|
||||
import org.slf4j.Logger
|
||||
@ -54,18 +54,18 @@ class MockNetwork(private val threadPerNode: Boolean = false,
|
||||
/** Allows customisation of how nodes are created. */
|
||||
interface Factory {
|
||||
fun create(dir: Path, config: NodeConfiguration, network: MockNetwork,
|
||||
timestamperAddr: LegallyIdentifiableNode?): MockNode
|
||||
timestamperAddr: NodeInfo?): MockNode
|
||||
}
|
||||
|
||||
object DefaultFactory : Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork,
|
||||
timestamperAddr: LegallyIdentifiableNode?): MockNode {
|
||||
timestamperAddr: NodeInfo?): MockNode {
|
||||
return MockNode(dir, config, network, timestamperAddr)
|
||||
}
|
||||
}
|
||||
|
||||
open class MockNode(dir: Path, config: NodeConfiguration, val mockNet: MockNetwork,
|
||||
withTimestamper: LegallyIdentifiableNode?, val forcedID: Int = -1) : AbstractNode(dir, config, withTimestamper, Clock.systemUTC()) {
|
||||
withTimestamper: NodeInfo?, val forcedID: Int = -1) : AbstractNode(dir, config, withTimestamper, Clock.systemUTC()) {
|
||||
override val log: Logger = loggerFor<MockNode>()
|
||||
override val serverThread: ExecutorService =
|
||||
if (mockNet.threadPerNode)
|
||||
@ -93,10 +93,12 @@ class MockNetwork(private val threadPerNode: Boolean = false,
|
||||
mockNet.identities.add(storage.myLegalIdentity)
|
||||
return this
|
||||
}
|
||||
|
||||
val place: PhysicalLocation get() = info.physicalLocation!!
|
||||
}
|
||||
|
||||
/** Returns a started node, optionally created by the passed factory method */
|
||||
fun createNode(withTimestamper: LegallyIdentifiableNode?, forcedID: Int = -1, nodeFactory: Factory = defaultFactory): MockNode {
|
||||
fun createNode(withTimestamper: NodeInfo?, forcedID: Int = -1, nodeFactory: Factory = defaultFactory): MockNode {
|
||||
val newNode = forcedID == -1
|
||||
val id = if (newNode) counter++ else forcedID
|
||||
|
||||
@ -132,7 +134,7 @@ class MockNetwork(private val threadPerNode: Boolean = false,
|
||||
*/
|
||||
fun createTwoNodes(nodeFactory: Factory = defaultFactory): Pair<MockNode, MockNode> {
|
||||
require(nodes.isEmpty())
|
||||
return Pair(createNode(null, -1, nodeFactory), createNode(nodes[0].legallyIdentifiableAddress, -1, nodeFactory))
|
||||
return Pair(createNode(null, -1, nodeFactory), createNode(nodes[0].info, -1, nodeFactory))
|
||||
}
|
||||
|
||||
fun addressToNode(address: SingleMessageRecipient): MockNode = nodes.single { it.net.myAddress == address }
|
||||
|
213
src/main/kotlin/core/testing/Simulation.kt
Normal file
213
src/main/kotlin/core/testing/Simulation.kt
Normal file
@ -0,0 +1,213 @@
|
||||
/*
|
||||
* 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.testing
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.services.CityDatabase
|
||||
import core.node.services.MockNetworkMapCache
|
||||
import core.node.services.NodeInfo
|
||||
import core.node.services.PhysicalLocation
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.then
|
||||
import core.utilities.ProgressTracker
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.nio.file.Path
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Base class for network simulations that are based on the unit test / mock environment.
|
||||
*
|
||||
* Sets up some nodes that can run protocols between each other, and exposes their progress trackers. Provides banks
|
||||
* in a few cities around the world.
|
||||
*/
|
||||
abstract class Simulation(val runAsync: Boolean,
|
||||
val latencyInjector: InMemoryMessagingNetwork.LatencyCalculator?) {
|
||||
init {
|
||||
if (!runAsync && latencyInjector != null)
|
||||
throw IllegalArgumentException("The latency injector is only useful when using manual pumping.")
|
||||
}
|
||||
|
||||
val bankLocations = listOf("London", "Frankfurt", "Rome")
|
||||
|
||||
// This puts together a mock network of SimulatedNodes.
|
||||
|
||||
open class SimulatedNode(dir: Path, config: NodeConfiguration, mockNet: MockNetwork,
|
||||
withTimestamper: NodeInfo?) : MockNetwork.MockNode(dir, config, mockNet, withTimestamper) {
|
||||
override fun findMyLocation(): PhysicalLocation? = CityDatabase[configuration.nearestCity]
|
||||
}
|
||||
|
||||
inner class BankFactory : MockNetwork.Factory {
|
||||
var counter = 0
|
||||
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, timestamperAddr: NodeInfo?): MockNetwork.MockNode {
|
||||
val letter = 'A' + counter
|
||||
val city = bankLocations[counter++ % bankLocations.size]
|
||||
val cfg = object : NodeConfiguration {
|
||||
// TODO: Set this back to "Bank of $city" after video day.
|
||||
override val myLegalName: String = "Bank $letter"
|
||||
override val exportJMXto: String = ""
|
||||
override val nearestCity: String = city
|
||||
}
|
||||
val node = SimulatedNode(dir, cfg, network, timestamperAddr)
|
||||
// TODO: This is obviously bogus: there should be a single network map for the whole simulated network.
|
||||
(node.services.networkMapCache as MockNetworkMapCache).ratesOracleNodes += ratesOracle.info
|
||||
(node.services.networkMapCache as MockNetworkMapCache).regulators += regulators.map { it.info }
|
||||
return node
|
||||
}
|
||||
|
||||
fun createAll(): List<SimulatedNode> = bankLocations.map { network.createNode(timestamper.info, nodeFactory = this) as SimulatedNode }
|
||||
}
|
||||
|
||||
val bankFactory = BankFactory()
|
||||
|
||||
object TimestampingNodeFactory : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, timestamperAddr: NodeInfo?): MockNetwork.MockNode {
|
||||
val cfg = object : NodeConfiguration {
|
||||
override val myLegalName: String = "Timestamping Service" // A magic string recognised by the CP contract
|
||||
override val exportJMXto: String = ""
|
||||
override val nearestCity: String = "Zurich"
|
||||
}
|
||||
return SimulatedNode(dir, cfg, network, timestamperAddr)
|
||||
}
|
||||
}
|
||||
|
||||
object RatesOracleFactory : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, timestamperAddr: NodeInfo?): MockNetwork.MockNode {
|
||||
val cfg = object : NodeConfiguration {
|
||||
override val myLegalName: String = "Rates Service Provider"
|
||||
override val exportJMXto: String = ""
|
||||
override val nearestCity: String = "Madrid"
|
||||
}
|
||||
|
||||
val n = object : SimulatedNode(dir, cfg, network, timestamperAddr) {
|
||||
override fun makeInterestRatesOracleService() {
|
||||
super.makeInterestRatesOracleService()
|
||||
interestRatesService.upload(javaClass.getResourceAsStream("example.rates.txt"))
|
||||
(services.networkMapCache as MockNetworkMapCache).ratesOracleNodes += info
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
object RegulatorFactory : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, timestamperAddr: NodeInfo?): MockNetwork.MockNode {
|
||||
val cfg = object : NodeConfiguration {
|
||||
override val myLegalName: String = "Regulator A"
|
||||
override val exportJMXto: String = ""
|
||||
override val nearestCity: String = "Paris"
|
||||
}
|
||||
|
||||
val n = object : SimulatedNode(dir, cfg, network, timestamperAddr) {
|
||||
// TODO: Regulatory nodes don't actually exist properly, this is a last minute demo request.
|
||||
// So we just fire a message at a node that doesn't know how to handle it, and it'll ignore it.
|
||||
// But that's fine for visualisation purposes.
|
||||
}
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
val network = MockNetwork(false)
|
||||
|
||||
val timestamper: SimulatedNode = network.createNode(null, nodeFactory = TimestampingNodeFactory) as SimulatedNode
|
||||
val ratesOracle: SimulatedNode = network.createNode(null, nodeFactory = RatesOracleFactory) as SimulatedNode
|
||||
val serviceProviders: List<SimulatedNode> = listOf(timestamper, ratesOracle)
|
||||
val banks: List<SimulatedNode> = bankFactory.createAll()
|
||||
val regulators: List<SimulatedNode> = listOf(network.createNode(null, nodeFactory = RegulatorFactory) as SimulatedNode)
|
||||
|
||||
private val _allProtocolSteps = PublishSubject.create<Pair<SimulatedNode, ProgressTracker.Change>>()
|
||||
private val _doneSteps = PublishSubject.create<Collection<SimulatedNode>>()
|
||||
val allProtocolSteps: Observable<Pair<SimulatedNode, ProgressTracker.Change>> = _allProtocolSteps
|
||||
val doneSteps: Observable<Collection<SimulatedNode>> = _doneSteps
|
||||
|
||||
private var pumpCursor = 0
|
||||
|
||||
/**
|
||||
* The current simulated date. By default this never changes. If you want it to change, you should do so from
|
||||
* within your overridden [iterate] call. Changes in the current day surface in the [dateChanges] observable.
|
||||
*/
|
||||
var currentDay: LocalDate = LocalDate.now()
|
||||
protected set(value) {
|
||||
field = value
|
||||
_dateChanges.onNext(value)
|
||||
}
|
||||
|
||||
private val _dateChanges = PublishSubject.create<LocalDate>()
|
||||
val dateChanges: Observable<LocalDate> = _dateChanges
|
||||
|
||||
/**
|
||||
* A place for simulations to stash human meaningful text about what the node is "thinking", which might appear
|
||||
* in the UI somewhere.
|
||||
*/
|
||||
val extraNodeLabels = Collections.synchronizedMap(HashMap<SimulatedNode, String>())
|
||||
|
||||
/**
|
||||
* Iterates the simulation by one step.
|
||||
*
|
||||
* The default implementation circles around the nodes, pumping until one of them handles a message. The next call
|
||||
* will carry on from where this one stopped. In an environment where you want to take actions between anything
|
||||
* interesting happening, or control the precise speed at which things operate (beyond the latency injector), this
|
||||
* is a useful way to do things.
|
||||
*/
|
||||
open fun iterate() {
|
||||
// Keep going until one of the nodes has something to do, or we have checked every node.
|
||||
val endpoints = network.messagingNetwork.endpoints
|
||||
var countDown = endpoints.size
|
||||
while (countDown > 0) {
|
||||
val handledMessage = endpoints[pumpCursor].pump(false)
|
||||
if (handledMessage) break
|
||||
// If this node had nothing to do, advance the cursor with wraparound and try again.
|
||||
pumpCursor = (pumpCursor + 1) % endpoints.size
|
||||
countDown--
|
||||
}
|
||||
}
|
||||
|
||||
protected fun linkProtocolProgress(node: SimulatedNode, protocol: ProtocolLogic<*>) {
|
||||
val pt = protocol.progressTracker ?: return
|
||||
pt.changes.subscribe { change: ProgressTracker.Change ->
|
||||
// Runs on node thread.
|
||||
_allProtocolSteps.onNext(Pair(node, change))
|
||||
}
|
||||
// This isn't technically a "change" but it helps with UIs to send this notification of the first step.
|
||||
_allProtocolSteps.onNext(Pair(node, ProgressTracker.Change.Position(pt, pt.steps[1])))
|
||||
}
|
||||
|
||||
protected fun linkConsensus(nodes: Collection<SimulatedNode>, protocol: ProtocolLogic<*>) {
|
||||
protocol.progressTracker?.changes?.subscribe { change: ProgressTracker.Change ->
|
||||
// Runs on node thread.
|
||||
if (protocol.progressTracker!!.currentStep == ProgressTracker.DONE) {
|
||||
_doneSteps.onNext(nodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun start() {}
|
||||
|
||||
fun stop() {
|
||||
network.nodes.forEach { it.stop() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a function that returns a future, iterates that function with arguments like (0, 1), (1, 2), (2, 3) etc
|
||||
* each time the returned future completes.
|
||||
*/
|
||||
fun startTradingCircle(tradeBetween: (indexA: Int, indexB: Int) -> ListenableFuture<*>) {
|
||||
fun next(i: Int, j: Int) {
|
||||
tradeBetween(i, j).then {
|
||||
val ni = (i + 1) % banks.size
|
||||
val nj = (j + 1) % banks.size
|
||||
next(ni, nj)
|
||||
}
|
||||
}
|
||||
next(0, 1)
|
||||
}
|
||||
}
|
60
src/main/kotlin/core/testing/TradeSimulation.kt
Normal file
60
src/main/kotlin/core/testing/TradeSimulation.kt
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.testing
|
||||
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import contracts.CommercialPaper
|
||||
import core.*
|
||||
import core.node.services.NodeWalletService
|
||||
import core.utilities.BriefLogFormatter
|
||||
import protocols.TwoPartyTradeProtocol
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Simulates a never ending series of trades that go pair-wise through the banks (e.g. A and B trade with each other,
|
||||
* then B and C trade with each other, then C and A etc).
|
||||
*/
|
||||
class TradeSimulation(runAsync: Boolean, latencyInjector: InMemoryMessagingNetwork.LatencyCalculator?) : Simulation(runAsync, latencyInjector) {
|
||||
override fun start() {
|
||||
BriefLogFormatter.loggingOn("bank", "core.TransactionGroup", "recordingmap")
|
||||
startTradingCircle { i, j -> tradeBetween(i, j) }
|
||||
}
|
||||
|
||||
private fun tradeBetween(buyerBankIndex: Int, sellerBankIndex: Int): ListenableFuture<MutableList<SignedTransaction>> {
|
||||
val buyer = banks[buyerBankIndex]
|
||||
val seller = banks[sellerBankIndex]
|
||||
|
||||
(buyer.services.walletService as NodeWalletService).fillWithSomeTestCash(1500.DOLLARS)
|
||||
|
||||
val issuance = run {
|
||||
val tx = CommercialPaper().generateIssue(seller.info.identity.ref(1, 2, 3), 1100.DOLLARS, Instant.now() + 10.days)
|
||||
tx.setTime(Instant.now(), timestamper.info.identity, 30.seconds)
|
||||
tx.signWith(timestamper.storage.myLegalIdentityKey)
|
||||
tx.signWith(seller.storage.myLegalIdentityKey)
|
||||
tx.toSignedTransaction(true)
|
||||
}
|
||||
seller.services.storageService.validatedTransactions[issuance.id] = issuance
|
||||
|
||||
val sessionID = random63BitValue()
|
||||
val buyerProtocol = TwoPartyTradeProtocol.Buyer(seller.net.myAddress, timestamper.info.identity,
|
||||
1000.DOLLARS, CommercialPaper.State::class.java, sessionID)
|
||||
val sellerProtocol = TwoPartyTradeProtocol.Seller(buyer.net.myAddress, timestamper.info,
|
||||
issuance.tx.outRef(0), 1000.DOLLARS, seller.storage.myLegalIdentityKey, sessionID)
|
||||
|
||||
linkConsensus(listOf(buyer, seller, timestamper), sellerProtocol)
|
||||
linkProtocolProgress(buyer, buyerProtocol)
|
||||
linkProtocolProgress(seller, sellerProtocol)
|
||||
|
||||
val buyerFuture = buyer.smm.add("bank.$buyerBankIndex.${TwoPartyTradeProtocol.TRADE_TOPIC}.buyer", buyerProtocol)
|
||||
val sellerFuture = seller.smm.add("bank.$sellerBankIndex.${TwoPartyTradeProtocol.TRADE_TOPIC}.seller", sellerProtocol)
|
||||
|
||||
return Futures.successfulAsList(buyerFuture, sellerFuture)
|
||||
}
|
||||
}
|
137
src/main/kotlin/core/utilities/JsonSupport.kt
Normal file
137
src/main/kotlin/core/utilities/JsonSupport.kt
Normal file
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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.utilities
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
import com.fasterxml.jackson.databind.*
|
||||
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers
|
||||
import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import core.BusinessCalendar
|
||||
import core.Party
|
||||
import core.crypto.SecureHash
|
||||
import core.node.services.IdentityService
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* Utilities and serialisers for working with JSON representations of basic types. This adds Jackson support for
|
||||
* the java.time API, some core types, and Kotlin data classes.
|
||||
*/
|
||||
object JsonSupport {
|
||||
fun createDefaultMapper(identities: IdentityService): ObjectMapper {
|
||||
val mapper = ServiceHubObjectMapper(identities)
|
||||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||
mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
|
||||
|
||||
val timeModule = SimpleModule("java.time")
|
||||
timeModule.addSerializer(LocalDate::class.java, ToStringSerializer)
|
||||
timeModule.addDeserializer(LocalDate::class.java, LocalDateDeserializer)
|
||||
timeModule.addKeyDeserializer(LocalDate::class.java, LocalDateKeyDeserializer)
|
||||
timeModule.addSerializer(LocalDateTime::class.java, ToStringSerializer)
|
||||
|
||||
val cordaModule = SimpleModule("core")
|
||||
cordaModule.addSerializer(Party::class.java, PartySerializer)
|
||||
cordaModule.addDeserializer(Party::class.java, PartyDeserializer)
|
||||
cordaModule.addSerializer(BigDecimal::class.java, ToStringSerializer)
|
||||
cordaModule.addDeserializer(BigDecimal::class.java, NumberDeserializers.BigDecimalDeserializer())
|
||||
cordaModule.addSerializer(SecureHash::class.java, SecureHashSerializer)
|
||||
// It's slightly remarkable, but apparently Jackson works out that this is the only possibility
|
||||
// for a SecureHash at the moment and tries to use SHA256 directly even though we only give it SecureHash
|
||||
cordaModule.addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
|
||||
cordaModule.addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
|
||||
|
||||
mapper.registerModule(timeModule)
|
||||
mapper.registerModule(cordaModule)
|
||||
mapper.registerModule(KotlinModule())
|
||||
return mapper
|
||||
}
|
||||
|
||||
class ServiceHubObjectMapper(val identities: IdentityService): ObjectMapper()
|
||||
|
||||
object ToStringSerializer: JsonSerializer<Any>() {
|
||||
override fun serialize(obj: Any, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(obj.toString())
|
||||
}
|
||||
}
|
||||
|
||||
object LocalDateDeserializer: JsonDeserializer<LocalDate>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): LocalDate {
|
||||
return try {
|
||||
LocalDate.parse(parser.text)
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException("Invalid LocalDate ${parser.text}: ${e.message}", parser.currentLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object LocalDateKeyDeserializer: KeyDeserializer() {
|
||||
override fun deserializeKey(text: String, p1: DeserializationContext): Any? {
|
||||
return LocalDate.parse(text)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object PartySerializer: JsonSerializer<Party>() {
|
||||
override fun serialize(obj: Party, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(obj.name)
|
||||
}
|
||||
}
|
||||
|
||||
object PartyDeserializer: JsonDeserializer<Party>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): Party {
|
||||
if(parser.currentToken == JsonToken.FIELD_NAME) {
|
||||
parser.nextToken()
|
||||
}
|
||||
val mapper = parser.codec as ServiceHubObjectMapper
|
||||
// TODO this needs to use some industry identifier(s) not just these human readable names
|
||||
return mapper.identities.partyFromName(parser.text) ?: throw JsonParseException("Could not find a Party with name: ${parser.text}", parser.currentLocation)
|
||||
}
|
||||
}
|
||||
|
||||
object SecureHashSerializer: JsonSerializer<SecureHash>() {
|
||||
override fun serialize(obj: SecureHash, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(obj.toString())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implemented as a class so that we can instantiate for T
|
||||
*/
|
||||
class SecureHashDeserializer<T : SecureHash>: JsonDeserializer<T>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): T {
|
||||
if(parser.currentToken == JsonToken.FIELD_NAME) {
|
||||
parser.nextToken()
|
||||
}
|
||||
try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return SecureHash.parse(parser.text) as T
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException("Invalid hash ${parser.text}: ${e.message}", parser.currentLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object CalendarDeserializer: JsonDeserializer<BusinessCalendar>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar {
|
||||
return try {
|
||||
val array = StringArrayDeserializer.instance.deserialize(parser, context)
|
||||
BusinessCalendar.getInstance(*array)
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException("Invalid calendar(s) ${parser.text}: ${e.message}", parser.currentLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
182
src/main/kotlin/demos/IRSDemo.kt
Normal file
182
src/main/kotlin/demos/IRSDemo.kt
Normal file
@ -0,0 +1,182 @@
|
||||
/*
|
||||
* 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 com.google.common.net.HostAndPort
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import core.Party
|
||||
import core.logElapsedTime
|
||||
import core.node.Node
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.NodeConfigurationFromConfig
|
||||
import core.node.services.ArtemisMessagingService
|
||||
import core.node.services.NodeInfo
|
||||
import core.node.services.MockNetworkMapCache
|
||||
import core.serialization.deserialize
|
||||
import core.utilities.BriefLogFormatter
|
||||
import joptsimple.OptionParser
|
||||
import demos.protocols.AutoOfferProtocol
|
||||
import demos.protocols.ExitServerProtocol
|
||||
import demos.protocols.UpdateBusinessDayProtocol
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
// IRS DEMO
|
||||
//
|
||||
// TODO: Please see TBD
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val parser = OptionParser()
|
||||
val networkAddressArg = parser.accepts("network-address").withRequiredArg().required()
|
||||
val dirArg = parser.accepts("directory").withRequiredArg().defaultsTo("nodedata")
|
||||
|
||||
// Temporary flags until network map and service discovery is fleshed out. The identity file does NOT contain the
|
||||
// network address because all this stuff is meant to come from a dynamic discovery service anyway, and the identity
|
||||
// is meant to be long-term stable. It could contain a domain name, but we may end up not routing messages directly
|
||||
// to DNS-identified endpoints anyway (e.g. consider onion routing as a possibility).
|
||||
val timestamperIdentityFile = parser.accepts("timestamper-identity-file").withRequiredArg().required()
|
||||
val timestamperNetAddr = parser.accepts("timestamper-address").requiredIf(timestamperIdentityFile).withRequiredArg()
|
||||
|
||||
val rateOracleIdentityFile = parser.accepts("rates-oracle-identity-file").withRequiredArg().required()
|
||||
val rateOracleNetAddr = parser.accepts("rates-oracle-address").requiredIf(rateOracleIdentityFile).withRequiredArg()
|
||||
|
||||
// Use these to list one or more peers (again, will be superseded by discovery implementation)
|
||||
val fakeTradeWithAddr = parser.accepts("fake-trade-with-address").withRequiredArg().required()
|
||||
val fakeTradeWithIdentityFile = parser.accepts("fake-trade-with-identity-file").withRequiredArg().required()
|
||||
|
||||
val options = try {
|
||||
parser.parse(*args)
|
||||
} catch (e: Exception) {
|
||||
println(e.message)
|
||||
printHelp()
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
// Suppress the Artemis MQ noise, and activate the demo logging.
|
||||
BriefLogFormatter.initVerbose("+demo.irsdemo", "-org.apache.activemq")
|
||||
|
||||
val dir = Paths.get(options.valueOf(dirArg))
|
||||
val configFile = dir.resolve("config")
|
||||
|
||||
if (!Files.exists(dir)) {
|
||||
Files.createDirectory(dir)
|
||||
}
|
||||
|
||||
val config = loadConfigFile(configFile)
|
||||
|
||||
val myNetAddr = HostAndPort.fromString(options.valueOf(networkAddressArg)).withDefaultPort(Node.DEFAULT_PORT)
|
||||
|
||||
// The timestamping node runs in the same process as the one that passes null to Node constructor.
|
||||
val timestamperId = if(options.valueOf(timestamperNetAddr).equals(options.valueOf(networkAddressArg))) {
|
||||
null
|
||||
} else {
|
||||
try {
|
||||
nodeInfo(options.valueOf(timestamperNetAddr), options.valueOf(timestamperIdentityFile))
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// The timestamping node runs in the same process as the one that passes null to Node constructor.
|
||||
val rateOracleId = if(options.valueOf(rateOracleNetAddr).equals(options.valueOf(networkAddressArg))) {
|
||||
null
|
||||
} else {
|
||||
try {
|
||||
nodeInfo(options.valueOf(rateOracleNetAddr), options.valueOf(rateOracleIdentityFile))
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val node = logElapsedTime("Node startup") { Node(dir, myNetAddr, config, timestamperId, DemoClock()).start() }
|
||||
|
||||
// Add self to network map
|
||||
(node.services.networkMapCache as MockNetworkMapCache).partyNodes.add(node.info)
|
||||
|
||||
// Add rates oracle to network map
|
||||
(node.services.networkMapCache as MockNetworkMapCache).ratesOracleNodes.add(rateOracleId)
|
||||
|
||||
val hostAndPortStrings = options.valuesOf(fakeTradeWithAddr)
|
||||
val identityFiles = options.valuesOf(fakeTradeWithIdentityFile)
|
||||
if(hostAndPortStrings.size != identityFiles.size) {
|
||||
throw IllegalArgumentException("Different number of peer addresses (${hostAndPortStrings.size}) and identities (${identityFiles.size})")
|
||||
}
|
||||
for ((hostAndPortString,identityFile) in hostAndPortStrings.zip(identityFiles)) {
|
||||
try {
|
||||
val peerId = nodeInfo(hostAndPortString, identityFile)
|
||||
(node.services.networkMapCache as MockNetworkMapCache).partyNodes.add(peerId)
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
// Register handlers for the demo
|
||||
AutoOfferProtocol.Handler.register(node)
|
||||
UpdateBusinessDayProtocol.Handler.register(node)
|
||||
ExitServerProtocol.Handler.register(node)
|
||||
|
||||
while(true) {
|
||||
Thread.sleep(1000L)
|
||||
}
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
fun nodeInfo(hostAndPortString: String, identityFile: String): NodeInfo {
|
||||
try {
|
||||
val addr = HostAndPort.fromString(hostAndPortString).withDefaultPort(Node.DEFAULT_PORT)
|
||||
val path = Paths.get(identityFile)
|
||||
val party = Files.readAllBytes(path).deserialize<Party>(includeClassName = true)
|
||||
return NodeInfo(ArtemisMessagingService.makeRecipient(addr), party)
|
||||
} catch (e: Exception) {
|
||||
println("Could not find identify file $identityFile. If the file has just been created as part of starting the demo, please restart this node")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadConfigFile(configFile: Path): NodeConfiguration {
|
||||
fun askAdminToEditConfig(configFile: Path?) {
|
||||
println()
|
||||
println("This is the first run, so you should edit the config file in $configFile and then start the node again.")
|
||||
println()
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
val defaultLegalName = "Global MegaCorp, Ltd."
|
||||
|
||||
if (!Files.exists(configFile)) {
|
||||
createDefaultConfigFile(configFile, defaultLegalName)
|
||||
askAdminToEditConfig(configFile)
|
||||
}
|
||||
|
||||
System.setProperty("config.file", configFile.toAbsolutePath().toString())
|
||||
val config = NodeConfigurationFromConfig(ConfigFactory.load())
|
||||
|
||||
// Make sure admin did actually edit at least the legal name.
|
||||
if (config.myLegalName == defaultLegalName)
|
||||
askAdminToEditConfig(configFile)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
private fun createDefaultConfigFile(configFile: Path?, defaultLegalName: String) {
|
||||
Files.write(configFile,
|
||||
"""
|
||||
# Node configuration: give the buyer node the name 'Bank of Zurich' (no quotes)
|
||||
# The seller node can be named whatever you like.
|
||||
|
||||
myLegalName = $defaultLegalName
|
||||
""".trimIndent().toByteArray())
|
||||
}
|
||||
|
||||
private fun printHelp() {
|
||||
println("""
|
||||
Please refer to the documentation that doesn't yet exist to learn how to run the demo.
|
||||
""".trimIndent())
|
||||
}
|
@ -13,7 +13,7 @@ import core.*
|
||||
import core.node.Node
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.services.ArtemisMessagingService
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.node.services.NodeInfo
|
||||
import core.node.services.NodeInterestRates
|
||||
import core.serialization.deserialize
|
||||
import core.utilities.ANSIProgressRenderer
|
||||
@ -60,7 +60,7 @@ fun main(args: Array<String>) {
|
||||
// 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 oracleNode = NodeInfo(oracleAddr, oracleIdentity)
|
||||
|
||||
val fixOf: FixOf = NodeInterestRates.parseFixOf(options.valueOf(fixOfArg))
|
||||
val expectedRate = BigDecimal(options.valueOf(expectedRateArg))
|
||||
|
@ -20,7 +20,7 @@ import core.node.Node
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.NodeConfigurationFromConfig
|
||||
import core.node.services.ArtemisMessagingService
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.node.services.NodeInfo
|
||||
import core.node.services.NodeAttachmentService
|
||||
import core.node.services.NodeWalletService
|
||||
import core.protocols.ProtocolLogic
|
||||
@ -85,8 +85,8 @@ fun main(args: Array<String>) {
|
||||
val myNetAddr = HostAndPort.fromString(options.valueOf(networkAddressArg)).withDefaultPort(Node.DEFAULT_PORT)
|
||||
val listening = options.has(serviceFakeTradesArg)
|
||||
|
||||
if (listening && config.myLegalName != "Bank of Zurich") {
|
||||
println("The buyer node must have a legal name of 'Bank of Zurich'. Please edit the config file.")
|
||||
if (listening && config.myLegalName != "Bank A") {
|
||||
println("The buyer node must have a legal name of 'Bank A'. Please edit the config file.")
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
@ -95,7 +95,7 @@ fun main(args: Array<String>) {
|
||||
val addr = HostAndPort.fromString(options.valueOf(timestamperNetAddr)).withDefaultPort(Node.DEFAULT_PORT)
|
||||
val path = Paths.get(options.valueOf(timestamperIdentityFile))
|
||||
val party = Files.readAllBytes(path).deserialize<Party>(includeClassName = true)
|
||||
LegallyIdentifiableNode(ArtemisMessagingService.makeRecipient(addr), party)
|
||||
NodeInfo(ArtemisMessagingService.makeRecipient(addr), party)
|
||||
} else null
|
||||
|
||||
val node = logElapsedTime("Node startup") { Node(dir, myNetAddr, config, timestamperId).start() }
|
||||
@ -163,7 +163,7 @@ class TraderDemoProtocolBuyer(private val attachmentsPath: Path) : ProtocolLogic
|
||||
progressTracker.currentStep = STARTING_BUY
|
||||
send("test.junktrade", newPartnerAddr, 0, sessionID)
|
||||
|
||||
val tsa = serviceHub.networkMapService.timestampingNodes[0]
|
||||
val tsa = serviceHub.networkMapCache.timestampingNodes[0]
|
||||
val buyer = TwoPartyTradeProtocol.Buyer(newPartnerAddr, tsa.identity, 1000.DOLLARS,
|
||||
CommercialPaper.State::class.java, sessionID)
|
||||
val tradeTX: SignedTransaction = subProtocol(buyer)
|
||||
@ -222,7 +222,7 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort,
|
||||
|
||||
progressTracker.currentStep = SELF_ISSUING
|
||||
|
||||
val tsa = serviceHub.networkMapService.timestampingNodes[0]
|
||||
val tsa = serviceHub.networkMapCache.timestampingNodes[0]
|
||||
val cpOwnerKey = serviceHub.keyManagementService.freshKey()
|
||||
val commercialPaper = selfIssueSomeCommercialPaper(cpOwnerKey.public, tsa)
|
||||
|
||||
@ -236,7 +236,7 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort,
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
fun selfIssueSomeCommercialPaper(ownedBy: PublicKey, tsa: LegallyIdentifiableNode): StateAndRef<CommercialPaper.State> {
|
||||
fun selfIssueSomeCommercialPaper(ownedBy: PublicKey, tsa: NodeInfo): StateAndRef<CommercialPaper.State> {
|
||||
// Make a fake company that's issued its own paper.
|
||||
val keyPair = generateKeyPair()
|
||||
val party = Party("Bank of London", keyPair.public)
|
||||
|
120
src/main/kotlin/demos/protocols/AutoOfferProtocol.kt
Normal file
120
src/main/kotlin/demos/protocols/AutoOfferProtocol.kt
Normal file
@ -0,0 +1,120 @@
|
||||
package demos.protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.util.concurrent.FutureCallback
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import contracts.DealState
|
||||
import core.Party
|
||||
import core.SignedTransaction
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.node.Node
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.random63BitValue
|
||||
import core.serialization.deserialize
|
||||
import core.utilities.ANSIProgressRenderer
|
||||
import core.utilities.ProgressTracker
|
||||
import protocols.TwoPartyDealProtocol
|
||||
|
||||
/**
|
||||
* This whole class is really part of a demo just to initiate the agreement of a deal with a simple
|
||||
* API call from a single party without bi-directional access to the database of offers etc.
|
||||
*
|
||||
* In the "real world", we'd probably have the offers sitting in the platform prior to the agreement step
|
||||
* or the protocol would have to reach out to external systems (or users) to verify the deals
|
||||
*/
|
||||
object AutoOfferProtocol {
|
||||
val TOPIC = "autooffer.topic"
|
||||
|
||||
data class AutoOfferMessage(val otherSide: SingleMessageRecipient,
|
||||
val otherSessionID: Long, val dealBeingOffered: DealState)
|
||||
|
||||
object Handler {
|
||||
|
||||
object RECEIVED : ProgressTracker.Step("Received offer")
|
||||
object DEALING : ProgressTracker.Step("Starting the deal protocol")
|
||||
|
||||
fun tracker() = ProgressTracker(RECEIVED, DEALING).apply {
|
||||
childrenFor[DEALING] = TwoPartyDealProtocol.Primary.tracker()
|
||||
}
|
||||
|
||||
class Callback(val success: (SignedTransaction) -> Unit) : FutureCallback<SignedTransaction> {
|
||||
override fun onFailure(t: Throwable?) {
|
||||
// TODO handle exceptions
|
||||
}
|
||||
|
||||
override fun onSuccess(st: SignedTransaction?) {
|
||||
success(st!!)
|
||||
}
|
||||
}
|
||||
|
||||
fun register(node: Node) {
|
||||
node.net.addMessageHandler("${TOPIC}.0") { msg, registration ->
|
||||
val progressTracker = tracker()
|
||||
ANSIProgressRenderer.progressTracker = progressTracker
|
||||
progressTracker.currentStep = RECEIVED
|
||||
val autoOfferMessage = msg.data.deserialize<AutoOfferMessage>()
|
||||
// Put the deal onto the ledger
|
||||
progressTracker.currentStep = DEALING
|
||||
// TODO required as messaging layer does not currently queue messages that arrive before we expect them
|
||||
Thread.sleep(100)
|
||||
val seller = TwoPartyDealProtocol.Instigator(autoOfferMessage.otherSide, node.timestamperAddress!!,
|
||||
autoOfferMessage.dealBeingOffered, node.services.keyManagementService.freshKey(), autoOfferMessage.otherSessionID, progressTracker.childrenFor[DEALING]!!)
|
||||
val future = node.smm.add("${TwoPartyDealProtocol.DEAL_TOPIC}.seller", seller)
|
||||
// This is required because we are doing child progress outside of a subprotocol. In future, we should just wrap things like this in a protocol to avoid it
|
||||
Futures.addCallback(future, Callback() {
|
||||
seller.progressTracker.currentStep = ProgressTracker.DONE
|
||||
progressTracker.currentStep = ProgressTracker.DONE
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Requester<T>(val dealToBeOffered: DealState) : ProtocolLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
object RECEIVED : ProgressTracker.Step("Received API call")
|
||||
object ANNOUNCING : ProgressTracker.Step("Announcing to the peer node")
|
||||
object DEALING : ProgressTracker.Step("Starting the deal protocol")
|
||||
|
||||
// We vend a progress tracker that already knows there's going to be a TwoPartyTradingProtocol involved at some
|
||||
// point: by setting up the tracker in advance, the user can see what's coming in more detail, instead of being
|
||||
// surprised when it appears as a new set of tasks below the current one.
|
||||
fun tracker() = ProgressTracker(RECEIVED, ANNOUNCING, DEALING).apply {
|
||||
childrenFor[DEALING] = TwoPartyDealProtocol.Secondary.tracker()
|
||||
}
|
||||
}
|
||||
|
||||
override val progressTracker = tracker()
|
||||
|
||||
init {
|
||||
progressTracker.currentStep = RECEIVED
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val ourSessionID = random63BitValue()
|
||||
|
||||
val timestampingAuthority = serviceHub.networkMapCache.timestampingNodes[0]
|
||||
// need to pick which ever party is not us
|
||||
val otherParty = notUs(*dealToBeOffered.parties).single()
|
||||
val otherSide = (serviceHub.networkMapCache.nodeForPartyName(otherParty.name))!!.address
|
||||
progressTracker.currentStep = ANNOUNCING
|
||||
send(TOPIC, otherSide, 0, AutoOfferMessage(serviceHub.networkService.myAddress, ourSessionID, dealToBeOffered))
|
||||
progressTracker.currentStep = DEALING
|
||||
val stx = subProtocol(TwoPartyDealProtocol.Acceptor(otherSide, timestampingAuthority.identity, dealToBeOffered, ourSessionID, progressTracker.childrenFor[DEALING]!!))
|
||||
return stx
|
||||
}
|
||||
|
||||
fun notUs(vararg parties: Party): List<Party> {
|
||||
val notUsParties : MutableList<Party> = arrayListOf()
|
||||
for(party in parties) {
|
||||
if (serviceHub.storageService.myLegalIdentity != party) {
|
||||
notUsParties.add(party)
|
||||
}
|
||||
}
|
||||
return notUsParties
|
||||
}
|
||||
|
||||
}
|
||||
}
|
71
src/main/kotlin/demos/protocols/ExitServerProtocol.kt
Normal file
71
src/main/kotlin/demos/protocols/ExitServerProtocol.kt
Normal file
@ -0,0 +1,71 @@
|
||||
package demos.protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import core.node.Node
|
||||
import core.node.services.NodeInfo
|
||||
import core.node.services.MockNetworkMapCache
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.serialization.deserialize
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
object ExitServerProtocol {
|
||||
|
||||
val TOPIC = "exit.topic"
|
||||
|
||||
// Will only be enabled if you install the Handler
|
||||
@Volatile private var enabled = false
|
||||
|
||||
data class ExitMessage(val exitCode: Int)
|
||||
|
||||
object Handler {
|
||||
|
||||
fun register(node: Node) {
|
||||
node.net.addMessageHandler("${TOPIC}.0") { msg, registration ->
|
||||
// Just to validate we got the message
|
||||
if(enabled) {
|
||||
val message = msg.data.deserialize<ExitMessage>()
|
||||
System.exit(message.exitCode)
|
||||
}
|
||||
}
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This takes a Java Integer rather than Kotlin Int as that is what we end up with in the calling map and currently
|
||||
* we do not support coercing numeric types in the reflective search for matching constructors
|
||||
*/
|
||||
class Broadcast(val exitCode: Integer) : ProtocolLogic<Boolean>() {
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Boolean {
|
||||
if(enabled) {
|
||||
val rc = exitCode.toInt()
|
||||
val message = ExitMessage(rc)
|
||||
|
||||
for (recipient in serviceHub.networkMapCache.partyNodes) {
|
||||
doNextRecipient(recipient, message)
|
||||
}
|
||||
// Sleep a little in case any async message delivery to other nodes needs to happen
|
||||
Strand.sleep(1, TimeUnit.SECONDS)
|
||||
System.exit(rc)
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun doNextRecipient(recipient: NodeInfo, message: ExitMessage) {
|
||||
if(recipient.address is MockNetworkMapCache.MockAddress) {
|
||||
// Ignore
|
||||
} else {
|
||||
// TODO: messaging ourselves seems to trigger a bug for the time being and we continuously receive messages
|
||||
if (recipient.identity != serviceHub.storageService.myLegalIdentity) {
|
||||
send(TOPIC, recipient.address, 0, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
174
src/main/kotlin/demos/protocols/UpdateBusinessDayProtocol.kt
Normal file
174
src/main/kotlin/demos/protocols/UpdateBusinessDayProtocol.kt
Normal file
@ -0,0 +1,174 @@
|
||||
package demos.protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import contracts.DealState
|
||||
import contracts.InterestRateSwap
|
||||
import core.StateAndRef
|
||||
import core.node.Node
|
||||
import core.node.services.NodeInfo
|
||||
import core.node.services.MockNetworkMapCache
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.random63BitValue
|
||||
import core.serialization.deserialize
|
||||
import core.utilities.ANSIProgressRenderer
|
||||
import core.utilities.ProgressTracker
|
||||
import demos.DemoClock
|
||||
import protocols.TwoPartyDealProtocol
|
||||
import java.time.LocalDate
|
||||
|
||||
/**
|
||||
* This is a very temporary, demo-oriented way of initiating processing of temporal events and is not
|
||||
* intended as the way things will necessarily be done longer term
|
||||
*/
|
||||
object UpdateBusinessDayProtocol {
|
||||
|
||||
val TOPIC = "businessday.topic"
|
||||
|
||||
class Updater(val date: LocalDate, val sessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Updater.tracker()) : ProtocolLogic<Boolean>() {
|
||||
|
||||
companion object {
|
||||
object FETCHING : ProgressTracker.Step("Fetching deals")
|
||||
object ITERATING_DEALS : ProgressTracker.Step("Interating over deals")
|
||||
object ITERATING_FIXINGS : ProgressTracker.Step("Iterating over fixings")
|
||||
object FIXING : ProgressTracker.Step("Fixing deal")
|
||||
|
||||
fun tracker() = ProgressTracker(FETCHING, ITERATING_DEALS, ITERATING_FIXINGS, FIXING)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Boolean {
|
||||
// Get deals
|
||||
progressTracker.currentStep = FETCHING
|
||||
val dealStateRefs = serviceHub.walletService.linearHeadsInstanceOf(DealState::class.java)
|
||||
val otherPartyToDeals = dealStateRefs.values.groupBy { otherParty(it.state) }
|
||||
|
||||
// TODO we need to process these in parallel to stop there being an ordering problem across more than two nodes
|
||||
val sortedParties = otherPartyToDeals.keys.sortedBy { it.identity.name }
|
||||
for (party in sortedParties) {
|
||||
val sortedDeals = otherPartyToDeals[party]!!.sortedBy { it.state.ref }
|
||||
for(deal in sortedDeals) {
|
||||
progressTracker.currentStep = ITERATING_DEALS
|
||||
processDeal(party, deal, date, sessionID)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// This assumes we definitely have one key or the other
|
||||
fun otherParty(deal: DealState): NodeInfo {
|
||||
val ourKeys = serviceHub.keyManagementService.keys.keys
|
||||
return serviceHub.networkMapCache.nodeForPartyName(deal.parties.single { !ourKeys.contains(it.owningKey) }.name)!!
|
||||
}
|
||||
|
||||
// TODO we should make this more object oriented when we can ask a state for it's contract
|
||||
@Suspendable
|
||||
fun processDeal(party: NodeInfo, deal: StateAndRef<DealState>, date: LocalDate, sessionID: Long) {
|
||||
when(deal.state) {
|
||||
is InterestRateSwap.State -> processInterestRateSwap(party, StateAndRef(deal.state as InterestRateSwap.State, deal.ref), date, sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO and this would move to the InterestRateSwap and cope with permutations of Fixed/Floating and Floating/Floating etc
|
||||
@Suspendable
|
||||
fun processInterestRateSwap(party: NodeInfo, deal: StateAndRef<InterestRateSwap.State>, date: LocalDate, sessionID: Long) {
|
||||
var dealStateAndRef: StateAndRef<InterestRateSwap.State>? = deal
|
||||
var nextFixingDate = deal.state.calculation.nextFixingDate()
|
||||
while (nextFixingDate != null && !nextFixingDate.isAfter(date)) {
|
||||
progressTracker.currentStep = ITERATING_FIXINGS
|
||||
/*
|
||||
* Note that this choice of fixed versus floating leg is simply to assign roles in
|
||||
* the fixing protocol and doesn't infer roles or responsibilities in a business sense.
|
||||
* One of the parties needs to take the lead in the coordination and this is a reliable deterministic way
|
||||
* to do it.
|
||||
*/
|
||||
if (party.identity.name == deal.state.fixedLeg.fixedRatePayer.name) {
|
||||
dealStateAndRef = nextFixingFloatingLeg(dealStateAndRef!!, party, sessionID)
|
||||
} else {
|
||||
dealStateAndRef = nextFixingFixedLeg(dealStateAndRef!!, party, sessionID)
|
||||
}
|
||||
nextFixingDate = dealStateAndRef?.state?.calculation?.nextFixingDate()
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun nextFixingFloatingLeg(dealStateAndRef: StateAndRef<InterestRateSwap.State>, party: NodeInfo, sessionID: Long): StateAndRef<InterestRateSwap.State>? {
|
||||
progressTracker.childrenFor[FIXING] = TwoPartyDealProtocol.Primary.tracker()
|
||||
progressTracker.currentStep = FIXING
|
||||
|
||||
val participant = TwoPartyDealProtocol.Floater(party.address, sessionID, serviceHub.networkMapCache.timestampingNodes[0], dealStateAndRef, serviceHub.keyManagementService.freshKey(), sessionID, progressTracker.childrenFor[FIXING]!!)
|
||||
Strand.sleep(100)
|
||||
val result = subProtocol(participant)
|
||||
return result.tx.outRef(0)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun nextFixingFixedLeg(dealStateAndRef: StateAndRef<InterestRateSwap.State>, party: NodeInfo, sessionID: Long): StateAndRef<InterestRateSwap.State>? {
|
||||
progressTracker.childrenFor[FIXING] = TwoPartyDealProtocol.Secondary.tracker()
|
||||
progressTracker.currentStep = FIXING
|
||||
|
||||
val participant = TwoPartyDealProtocol.Fixer(party.address, serviceHub.networkMapCache.timestampingNodes[0].identity, dealStateAndRef, sessionID, progressTracker.childrenFor[FIXING]!!)
|
||||
val result = subProtocol(participant)
|
||||
return result.tx.outRef(0)
|
||||
}
|
||||
}
|
||||
|
||||
data class UpdateBusinessDayMessage(val date: LocalDate, val sessionID: Long)
|
||||
|
||||
object Handler {
|
||||
|
||||
fun register(node: Node) {
|
||||
node.net.addMessageHandler("${TOPIC}.0") { msg, registration ->
|
||||
// Just to validate we got the message
|
||||
val updateBusinessDayMessage = msg.data.deserialize<UpdateBusinessDayMessage>()
|
||||
if ((node.services.clock as DemoClock).updateDate(updateBusinessDayMessage.date)) {
|
||||
val participant = Updater(updateBusinessDayMessage.date, updateBusinessDayMessage.sessionID)
|
||||
ANSIProgressRenderer.progressTracker = participant.progressTracker
|
||||
node.smm.add("update.business.day", participant)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Broadcast(val date: LocalDate,
|
||||
override val progressTracker: ProgressTracker = Broadcast.tracker()) : ProtocolLogic<Boolean>() {
|
||||
|
||||
companion object {
|
||||
object NOTIFYING : ProgressTracker.Step("Notifying peer")
|
||||
object LOCAL : ProgressTracker.Step("Updating locally")
|
||||
|
||||
fun tracker() = ProgressTracker(NOTIFYING, LOCAL).apply {
|
||||
childrenFor[LOCAL] = Updater.tracker()
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Boolean {
|
||||
val message = UpdateBusinessDayMessage(date, random63BitValue())
|
||||
|
||||
for (recipient in serviceHub.networkMapCache.partyNodes) {
|
||||
progressTracker.currentStep = NOTIFYING
|
||||
doNextRecipient(recipient, message)
|
||||
}
|
||||
if ((serviceHub.clock as DemoClock).updateDate(message.date)) {
|
||||
progressTracker.currentStep = LOCAL
|
||||
subProtocol(Updater(message.date, message.sessionID, progressTracker.childrenFor[LOCAL]!!))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun doNextRecipient(recipient: NodeInfo, message: UpdateBusinessDayMessage) {
|
||||
if(recipient.address is MockNetworkMapCache.MockAddress) {
|
||||
// Ignore
|
||||
} else {
|
||||
// TODO: messaging ourselves seems to trigger a bug for the time being and we continuously receive messages
|
||||
if (recipient.identity != serviceHub.storageService.myLegalIdentity) {
|
||||
send(TOPIC, recipient.address, 0, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -11,8 +11,8 @@ package protocols
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import core.*
|
||||
import core.crypto.DigitalSignature
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.node.services.NodeInfo
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.utilities.ProgressTracker
|
||||
import java.math.BigDecimal
|
||||
@ -29,7 +29,7 @@ import java.util.*
|
||||
* @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 oracle: NodeInfo,
|
||||
private val fixOf: FixOf,
|
||||
private val expectedRate: BigDecimal,
|
||||
private val rateTolerance: BigDecimal) : ProtocolLogic<Unit>() {
|
||||
@ -50,7 +50,7 @@ open class RatesFixProtocol(protected val tx: TransactionBuilder,
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
progressTracker.currentStep = progressTracker.steps[0]
|
||||
progressTracker.currentStep = progressTracker.steps[1]
|
||||
val fix = query()
|
||||
progressTracker.currentStep = WORKING
|
||||
checkFixIsNearExpected(fix)
|
||||
|
@ -12,9 +12,9 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import core.Party
|
||||
import core.WireTransaction
|
||||
import core.crypto.DigitalSignature
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.messaging.MessageRecipients
|
||||
import core.messaging.StateMachineManager
|
||||
import core.node.services.NodeInfo
|
||||
import core.node.services.NodeTimestamperService
|
||||
import core.node.services.TimestamperService
|
||||
import core.protocols.ProtocolLogic
|
||||
@ -31,11 +31,11 @@ import core.utilities.ProgressTracker
|
||||
* the built transaction. Please be aware that this will block, meaning it should not be used on a thread that is handling
|
||||
* a network message: use it only from spare application threads that don't have to respond to anything.
|
||||
*/
|
||||
class TimestampingProtocol(private val node: LegallyIdentifiableNode,
|
||||
class TimestampingProtocol(private val node: NodeInfo,
|
||||
private val wtxBytes: SerializedBytes<WireTransaction>,
|
||||
override val progressTracker: ProgressTracker = TimestampingProtocol.tracker()) : ProtocolLogic<DigitalSignature.LegallyIdentifiable>() {
|
||||
|
||||
class Client(private val stateMachineManager: StateMachineManager, private val node: LegallyIdentifiableNode) : TimestamperService {
|
||||
class Client(private val stateMachineManager: StateMachineManager, private val node: NodeInfo) : TimestamperService {
|
||||
override val identity: Party = node.identity
|
||||
|
||||
override fun timestamp(wtxBytes: SerializedBytes<WireTransaction>): DigitalSignature.LegallyIdentifiable {
|
||||
|
407
src/main/kotlin/protocols/TwoPartyDealProtocol.kt
Normal file
407
src/main/kotlin/protocols/TwoPartyDealProtocol.kt
Normal file
@ -0,0 +1,407 @@
|
||||
/*
|
||||
* 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 contracts.DealState
|
||||
import contracts.FixableDealState
|
||||
import core.*
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.signWithECDSA
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.node.services.NodeInfo
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.utilities.ProgressTracker
|
||||
import core.utilities.UntrustworthyData
|
||||
import core.utilities.trace
|
||||
import java.math.BigDecimal
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
|
||||
/**
|
||||
* Classes for manipulating a two party deal or agreement.
|
||||
*
|
||||
* TODO: The subclasses should probably be broken out into individual protocols rather than making this an ever expanding collection of subclasses.
|
||||
*
|
||||
* TODO: Also, the term Deal is used here where we might prefer Agreement.
|
||||
*
|
||||
*/
|
||||
object TwoPartyDealProtocol {
|
||||
val DEAL_TOPIC = "platform.deal"
|
||||
|
||||
class DealMismatchException(val expectedDeal: ContractState, val actualDeal: ContractState) : Exception() {
|
||||
override fun toString() = "The submitted deal didn't match the expected: $expectedDeal vs $actualDeal"
|
||||
}
|
||||
|
||||
class DealRefMismatchException(val expectedDeal: StateRef, val actualDeal: StateRef) : Exception() {
|
||||
override fun toString() = "The submitted deal didn't match the expected: $expectedDeal vs $actualDeal"
|
||||
}
|
||||
|
||||
// This object is serialised to the network and is the first protocol message the seller sends to the buyer.
|
||||
data class Handshake<T>(
|
||||
val payload: T,
|
||||
val publicKey: PublicKey,
|
||||
val sessionID: Long
|
||||
)
|
||||
|
||||
class SignaturesFromPrimary(val timestampAuthoritySig: DigitalSignature.WithKey, val sellerSig: DigitalSignature.WithKey)
|
||||
|
||||
/**
|
||||
* Abstracted bilateral deal protocol participant that initiates communication/handshake.
|
||||
*
|
||||
* There's a good chance we can push at least some of this logic down into core protocol logic
|
||||
* and helper methods etc.
|
||||
*/
|
||||
abstract class Primary<U>(val payload: U,
|
||||
val otherSide: SingleMessageRecipient,
|
||||
val otherSessionID: Long,
|
||||
val myKeyPair: KeyPair,
|
||||
val timestampingAuthority: NodeInfo,
|
||||
override val progressTracker: ProgressTracker = Primary.tracker()) : ProtocolLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
object AWAITING_PROPOSAL : ProgressTracker.Step("Awaiting transaction proposal from other")
|
||||
object VERIFYING : ProgressTracker.Step("Verifying transaction proposal from other")
|
||||
object SIGNING : ProgressTracker.Step("Signing transaction")
|
||||
object TIMESTAMPING : ProgressTracker.Step("Timestamping transaction")
|
||||
object SENDING_SIGS : ProgressTracker.Step("Sending transaction signatures to other party")
|
||||
object RECORDING : ProgressTracker.Step("Recording completed transaction")
|
||||
object COPYING_TO_REGULATOR : ProgressTracker.Step("Copying regulator")
|
||||
|
||||
fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, TIMESTAMPING, SENDING_SIGS, RECORDING, COPYING_TO_REGULATOR).apply {
|
||||
childrenFor[TIMESTAMPING] = TimestampingProtocol.tracker()
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
fun getPartialTransaction(): UntrustworthyData<SignedTransaction> {
|
||||
progressTracker.currentStep = AWAITING_PROPOSAL
|
||||
|
||||
val sessionID = random63BitValue()
|
||||
|
||||
// Make the first message we'll send to kick off the protocol.
|
||||
val hello = Handshake<U>(payload, myKeyPair.public, sessionID)
|
||||
|
||||
val maybeSTX = sendAndReceive<SignedTransaction>(DEAL_TOPIC, otherSide, otherSessionID, sessionID, hello)
|
||||
|
||||
return maybeSTX
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData<SignedTransaction>): SignedTransaction {
|
||||
progressTracker.currentStep = VERIFYING
|
||||
|
||||
untrustedPartialTX.validate {
|
||||
progressTracker.nextStep()
|
||||
|
||||
// Check that the tx proposed by the buyer is valid.
|
||||
val missingSigs = it.verify(throwIfSignaturesAreMissing = false)
|
||||
if (missingSigs != setOf(myKeyPair.public, timestampingAuthority.identity.owningKey))
|
||||
throw SignatureException("The set of missing signatures is not as expected: $missingSigs")
|
||||
|
||||
val wtx: WireTransaction = it.tx
|
||||
logger.trace { "Received partially signed transaction: ${it.id}" }
|
||||
|
||||
checkDependencies(it)
|
||||
|
||||
// This verifies that the transaction is contract-valid, even though it is missing signatures.
|
||||
serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments))
|
||||
|
||||
// There are all sorts of funny games a malicious secondary might play here, we should fix them:
|
||||
//
|
||||
// - This tx may attempt to send some assets we aren't intending to sell to the secondary, if
|
||||
// we're reusing keys! So don't reuse keys!
|
||||
// - This tx may include output states that impose odd conditions on the movement of the cash,
|
||||
// once we implement state pairing.
|
||||
//
|
||||
// but the goal of this code is not to be fully secure (yet), but rather, just to find good ways to
|
||||
// express protocol state machines on top of the messaging layer.
|
||||
|
||||
return it
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun checkDependencies(stx: SignedTransaction) {
|
||||
// Download and check all the transactions that this transaction depends on, but do not check this
|
||||
// transaction itself.
|
||||
val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet()
|
||||
subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide))
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val partialTX: SignedTransaction = verifyPartialTransaction(getPartialTransaction())
|
||||
|
||||
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
|
||||
val ourSignature = signWithOurKey(partialTX)
|
||||
val tsaSig = timestamp(partialTX)
|
||||
|
||||
val fullySigned = sendSignatures(partialTX, ourSignature, tsaSig)
|
||||
|
||||
progressTracker.currentStep = RECORDING
|
||||
|
||||
serviceHub.recordTransactions(listOf(fullySigned))
|
||||
|
||||
logger.trace { "Deal stored" }
|
||||
|
||||
val regulators = serviceHub.networkMapCache.regulators
|
||||
if (regulators.isNotEmpty()) {
|
||||
// Copy the transaction to every regulator in the network. This is obviously completely bogus, it's
|
||||
// just for demo purposes.
|
||||
for (regulator in regulators) {
|
||||
send("regulator.all.seeing.eye", regulator.address, 0, fullySigned)
|
||||
}
|
||||
}
|
||||
|
||||
return fullySigned
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun timestamp(partialTX: SignedTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
progressTracker.childrenFor[TIMESTAMPING] = TimestampingProtocol.tracker()
|
||||
progressTracker.currentStep = TIMESTAMPING
|
||||
return subProtocol(TimestampingProtocol(timestampingAuthority, partialTX.txBits, progressTracker.childrenFor[TIMESTAMPING]!!))
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
open fun signWithOurKey(partialTX: SignedTransaction): DigitalSignature.WithKey {
|
||||
progressTracker.currentStep = SIGNING
|
||||
return myKeyPair.signWithECDSA(partialTX.txBits)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun sendSignatures(partialTX: SignedTransaction, ourSignature: DigitalSignature.WithKey,
|
||||
tsaSig: DigitalSignature.LegallyIdentifiable): SignedTransaction {
|
||||
progressTracker.currentStep = SENDING_SIGS
|
||||
val fullySigned = partialTX + tsaSig + ourSignature
|
||||
|
||||
logger.trace { "Built finished transaction, sending back to other party!" }
|
||||
|
||||
send(DEAL_TOPIC, otherSide, otherSessionID, SignaturesFromPrimary(tsaSig, ourSignature))
|
||||
return fullySigned
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Abstracted bilateral deal protocol participant that is recipient of initial communication.
|
||||
*
|
||||
* There's a good chance we can push at least some of this logic down into core protocol logic
|
||||
* and helper methods etc.
|
||||
*/
|
||||
abstract class Secondary<U>(val otherSide: SingleMessageRecipient,
|
||||
val timestampingAuthority: Party,
|
||||
val sessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Secondary.tracker()) : ProtocolLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
object RECEIVING : ProgressTracker.Step("Waiting for deal info")
|
||||
object VERIFYING : ProgressTracker.Step("Verifying deal info")
|
||||
object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal")
|
||||
object SWAPPING_SIGNATURES : ProgressTracker.Step("Swapping signatures with the other party")
|
||||
object RECORDING : ProgressTracker.Step("Recording completed transaction")
|
||||
|
||||
fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING, SWAPPING_SIGNATURES, RECORDING)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val handshake = receiveAndValidateHandshake()
|
||||
|
||||
progressTracker.currentStep = SIGNING
|
||||
val (ptx, additionalSigningPubKeys) = assembleSharedTX(handshake)
|
||||
val stx = signWithOurKeys(additionalSigningPubKeys, ptx)
|
||||
|
||||
val signatures = swapSignaturesWithPrimary(stx, handshake.sessionID)
|
||||
|
||||
logger.trace { "Got signatures from other party, verifying ... " }
|
||||
val fullySigned = stx + signatures.timestampAuthoritySig + signatures.sellerSig
|
||||
fullySigned.verify()
|
||||
|
||||
logger.trace { "Signatures received are valid. Deal transaction complete! :-)" }
|
||||
|
||||
progressTracker.currentStep = RECORDING
|
||||
serviceHub.recordTransactions(listOf(fullySigned))
|
||||
|
||||
logger.trace { "Deal transaction stored" }
|
||||
return fullySigned
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun receiveAndValidateHandshake(): Handshake<U> {
|
||||
progressTracker.currentStep = RECEIVING
|
||||
// Wait for a trade request to come in on our pre-provided session ID.
|
||||
val handshake = receive(DEAL_TOPIC, sessionID, Handshake::class.java)
|
||||
|
||||
progressTracker.currentStep = VERIFYING
|
||||
handshake.validate {
|
||||
return validateHandshake(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun swapSignaturesWithPrimary(stx: SignedTransaction, theirSessionID: Long): SignaturesFromPrimary {
|
||||
progressTracker.currentStep = SWAPPING_SIGNATURES
|
||||
logger.trace { "Sending partially signed transaction to seller" }
|
||||
|
||||
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
|
||||
|
||||
return sendAndReceive<SignaturesFromPrimary>(DEAL_TOPIC, otherSide, theirSessionID, sessionID, stx).validate { it }
|
||||
}
|
||||
|
||||
private fun signWithOurKeys(signingPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
|
||||
// Now sign the transaction with whatever keys we need to move the cash.
|
||||
for (k in signingPubKeys) {
|
||||
val priv = serviceHub.keyManagementService.toPrivate(k)
|
||||
ptx.signWith(KeyPair(k, priv))
|
||||
}
|
||||
|
||||
return ptx.toSignedTransaction(checkSufficientSignatures = false)
|
||||
}
|
||||
|
||||
@Suspendable protected abstract fun validateHandshake(handshake: Handshake<*>): Handshake<U>
|
||||
@Suspendable protected abstract fun assembleSharedTX(handshake: Handshake<U>): Pair<TransactionBuilder, List<PublicKey>>
|
||||
}
|
||||
|
||||
/**
|
||||
* One side of the protocol for inserting a pre-agreed deal.
|
||||
*/
|
||||
open class Instigator<T : DealState>(otherSide: SingleMessageRecipient,
|
||||
timestampingAuthority: NodeInfo,
|
||||
dealBeingOffered: T,
|
||||
myKeyPair: KeyPair,
|
||||
buyerSessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Primary.tracker()) : Primary<T>(dealBeingOffered, otherSide, buyerSessionID, myKeyPair, timestampingAuthority)
|
||||
|
||||
/**
|
||||
* One side of the protocol for inserting a pre-agreed deal.
|
||||
*/
|
||||
open class Acceptor<T : DealState>(otherSide: SingleMessageRecipient,
|
||||
timestampingAuthority: Party,
|
||||
val dealToBuy: T,
|
||||
sessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Secondary.tracker()) : Secondary<T>(otherSide, timestampingAuthority, sessionID) {
|
||||
|
||||
@Suspendable
|
||||
override fun validateHandshake(handshake: Handshake<*>): Handshake<T> {
|
||||
with(handshake as Handshake<T>) {
|
||||
// What is the seller trying to sell us?
|
||||
val deal = handshake.payload
|
||||
val otherKey = handshake.publicKey
|
||||
logger.trace { "Got deal request for: ${handshake.payload}" }
|
||||
|
||||
// Check the start message for acceptability.
|
||||
check(handshake.sessionID > 0)
|
||||
if (dealToBuy != deal)
|
||||
throw DealMismatchException(dealToBuy, deal)
|
||||
|
||||
// We need to substitute in the new public keys for the Parties
|
||||
val myName = serviceHub.storageService.myLegalIdentity.name
|
||||
val myOldParty = deal.parties.single { it.name == myName }
|
||||
val theirOldParty = deal.parties.single { it.name != myName }
|
||||
|
||||
val newDeal = deal.withPublicKey(myOldParty, serviceHub.keyManagementService.freshKey().public).withPublicKey(theirOldParty, otherKey) as T
|
||||
|
||||
return handshake.copy(payload = newDeal)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun assembleSharedTX(handshake: Handshake<T>): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
val ptx = handshake.payload.generateAgreement()
|
||||
|
||||
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
|
||||
// to have one.
|
||||
ptx.setTime(serviceHub.clock.instant(), timestampingAuthority, 30.seconds)
|
||||
return Pair(ptx, arrayListOf(handshake.payload.parties.single { it.name == serviceHub.storageService.myLegalIdentity.name }.owningKey))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* One side of the fixing protocol for an interest rate swap, but could easily be generalised further.
|
||||
*
|
||||
* Do not infer too much from the name of the class. This is just to indicate that it is the "side"
|
||||
* of the protocol that is run by the party with the fixed leg of swap deal, which is the basis for decided
|
||||
* who does what in the protocol.
|
||||
*/
|
||||
open class Fixer<T : FixableDealState>(otherSide: SingleMessageRecipient,
|
||||
timestampingAuthority: Party,
|
||||
val dealToFix: StateAndRef<T>,
|
||||
sessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Secondary.tracker()) : Secondary<StateRef>(otherSide, timestampingAuthority, sessionID) {
|
||||
|
||||
@Suspendable
|
||||
override fun validateHandshake(handshake: Handshake<*>): Handshake<StateRef> {
|
||||
with(handshake as Handshake<StateRef>) {
|
||||
logger.trace { "Got fixing request for: ${dealToFix.state}" }
|
||||
|
||||
// Check the start message for acceptability.
|
||||
if (dealToFix.ref != handshake.payload)
|
||||
throw DealRefMismatchException(dealToFix.ref, handshake.payload)
|
||||
|
||||
// Check the transaction that contains the state which is being resolved.
|
||||
// We only have a hash here, so if we don't know it already, we have to ask for it.
|
||||
//subProtocol(ResolveTransactionsProtocol(setOf(handshake.payload.txhash), otherSide))
|
||||
|
||||
return handshake
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun assembleSharedTX(handshake: Handshake<StateRef>): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
val fixOf = dealToFix.state.nextFixingOf()!!
|
||||
|
||||
// TODO Do we need/want to substitute in new public keys for the Parties?
|
||||
val myName = serviceHub.storageService.myLegalIdentity.name
|
||||
val deal = dealToFix.state
|
||||
val myOldParty = deal.parties.single { it.name == myName }
|
||||
val theirOldParty = deal.parties.single { it.name != myName }
|
||||
val myNewKey = serviceHub.keyManagementService.freshKey().public
|
||||
|
||||
val newDeal = deal.withPublicKey(myOldParty, myNewKey).withPublicKey(theirOldParty, handshake.publicKey) as T
|
||||
val oldRef = dealToFix.ref
|
||||
|
||||
val ptx = TransactionBuilder()
|
||||
val addFixing = object : RatesFixProtocol(ptx, serviceHub.networkMapCache.ratesOracleNodes[0], fixOf, BigDecimal.ZERO, BigDecimal.ONE) {
|
||||
|
||||
@Suspendable
|
||||
override fun beforeSigning(fix: Fix) {
|
||||
newDeal.generateFix(ptx, oldRef, fix)
|
||||
|
||||
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
|
||||
// to have one.
|
||||
ptx.setTime(serviceHub.clock.instant(), timestampingAuthority, 30.seconds)
|
||||
}
|
||||
|
||||
}
|
||||
subProtocol(addFixing)
|
||||
|
||||
return Pair(ptx, arrayListOf(myNewKey))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One side of the fixing protocol for an interest rate swap, but could easily be generalised furher
|
||||
*
|
||||
* As per the [Fixer], do not infer too much from this class name in terms of business roles. This
|
||||
* is just the "side" of the protocol run by the party with the floating leg as a way of deciding who
|
||||
* does what in the protocol.
|
||||
*/
|
||||
open class Floater<T : FixableDealState>(otherSide: SingleMessageRecipient,
|
||||
otherSessionID: Long,
|
||||
timestampingAuthority: NodeInfo,
|
||||
dealToFix: StateAndRef<T>,
|
||||
myKeyPair: KeyPair,
|
||||
val sessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Primary.tracker()) : Primary<StateRef>(dealToFix.ref, otherSide, otherSessionID, myKeyPair, timestampingAuthority)
|
||||
}
|
@ -15,10 +15,9 @@ import contracts.sumCashBy
|
||||
import core.*
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.signWithECDSA
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.messaging.StateMachineManager
|
||||
import protocols.TimestampingProtocol
|
||||
import core.node.services.NodeInfo
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.utilities.ProgressTracker
|
||||
import core.utilities.trace
|
||||
@ -26,7 +25,6 @@ import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.time.Instant
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* This asset trading protocol implements a "delivery vs payment" type swap. It has two parties (B and S for buyer
|
||||
@ -54,14 +52,14 @@ import kotlin.system.exitProcess
|
||||
object TwoPartyTradeProtocol {
|
||||
val TRADE_TOPIC = "platform.trade"
|
||||
|
||||
fun runSeller(smm: StateMachineManager, timestampingAuthority: LegallyIdentifiableNode,
|
||||
fun runSeller(smm: StateMachineManager, timestampingAuthority: NodeInfo,
|
||||
otherSide: SingleMessageRecipient, assetToSell: StateAndRef<OwnableState>, price: Amount,
|
||||
myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture<SignedTransaction> {
|
||||
val seller = Seller(otherSide, timestampingAuthority, assetToSell, price, myKeyPair, buyerSessionID)
|
||||
return smm.add("${TRADE_TOPIC}.seller", seller)
|
||||
}
|
||||
|
||||
fun runBuyer(smm: StateMachineManager, timestampingAuthority: LegallyIdentifiableNode,
|
||||
fun runBuyer(smm: StateMachineManager, timestampingAuthority: NodeInfo,
|
||||
otherSide: SingleMessageRecipient, acceptablePrice: Amount, typeToBuy: Class<out OwnableState>,
|
||||
sessionID: Long): ListenableFuture<SignedTransaction> {
|
||||
val buyer = Buyer(otherSide, timestampingAuthority.identity, acceptablePrice, typeToBuy, sessionID)
|
||||
@ -84,7 +82,7 @@ object TwoPartyTradeProtocol {
|
||||
class SignaturesFromSeller(val timestampAuthoritySig: DigitalSignature.WithKey, val sellerSig: DigitalSignature.WithKey)
|
||||
|
||||
open class Seller(val otherSide: SingleMessageRecipient,
|
||||
val timestampingAuthority: LegallyIdentifiableNode,
|
||||
val timestampingAuthority: NodeInfo,
|
||||
val assetToSell: StateAndRef<OwnableState>,
|
||||
val price: Amount,
|
||||
val myKeyPair: KeyPair,
|
||||
@ -148,7 +146,7 @@ object TwoPartyTradeProtocol {
|
||||
serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments))
|
||||
|
||||
if (wtx.outputs.sumCashBy(myKeyPair.public) != price)
|
||||
throw IllegalArgumentException("Transaction is not sending us the right amounnt of cash")
|
||||
throw IllegalArgumentException("Transaction is not sending us the right amount of cash")
|
||||
|
||||
// There are all sorts of funny games a malicious secondary might play here, we should fix them:
|
||||
//
|
||||
|
51
src/main/resources/core/testing/example.rates.txt
Normal file
51
src/main/resources/core/testing/example.rates.txt
Normal file
@ -0,0 +1,51 @@
|
||||
# Some pretend noddy rate fixes, for the interest rate oracles.
|
||||
|
||||
3M USD 2016-03-16 1M = 0.678
|
||||
3M USD 2016-03-16 2M = 0.655
|
||||
EURIBOR 2016-03-15 1M = 0.123
|
||||
EURIBOR 2016-03-15 2M = 0.111
|
||||
|
||||
3M USD 2016-03-08 3M = 0.0063515
|
||||
3M USD 2016-06-08 3M = 0.0063520
|
||||
3M USD 2016-09-08 3M = 0.0063521
|
||||
3M USD 2016-12-08 3M = 0.0063515
|
||||
3M USD 2017-03-08 3M = 0.0063525
|
||||
3M USD 2017-06-08 3M = 0.0063530
|
||||
3M USD 2017-09-07 3M = 0.0063531
|
||||
3M USD 2017-12-07 3M = 0.0063532
|
||||
3M USD 2018-03-08 3M = 0.0063533
|
||||
3M USD 2018-06-07 3M = 0.0063534
|
||||
3M USD 2018-09-06 3M = 0.0063535
|
||||
3M USD 2018-12-06 3M = 0.0063536
|
||||
3M USD 2019-03-07 3M = 0.0063537
|
||||
3M USD 2019-06-06 3M = 0.0063538
|
||||
3M USD 2019-09-06 3M = 0.0063539
|
||||
3M USD 2019-12-06 3M = 0.0063540
|
||||
3M USD 2020-03-06 3M = 0.0063541
|
||||
3M USD 2020-06-08 3M = 0.0063542
|
||||
3M USD 2020-09-08 3M = 0.0063543
|
||||
3M USD 2020-12-08 3M = 0.0063544
|
||||
3M USD 2021-03-08 3M = 0.0063545
|
||||
3M USD 2021-06-08 3M = 0.0063546
|
||||
3M USD 2021-09-08 3M = 0.0063547
|
||||
3M USD 2021-12-08 3M = 0.0063548
|
||||
3M USD 2022-03-08 3M = 0.0063549
|
||||
3M USD 2022-06-08 3M = 0.0063550
|
||||
3M USD 2022-09-08 3M = 0.0063551
|
||||
3M USD 2022-12-08 3M = 0.0063553
|
||||
3M USD 2023-03-08 3M = 0.0063554
|
||||
3M USD 2023-06-08 3M = 0.0063555
|
||||
3M USD 2023-09-07 3M = 0.0063556
|
||||
3M USD 2023-12-07 3M = 0.0063557
|
||||
3M USD 2024-03-07 3M = 0.0063558
|
||||
3M USD 2024-06-06 3M = 0.0063559
|
||||
3M USD 2024-09-06 3M = 0.0063560
|
||||
3M USD 2024-12-06 3M = 0.0063561
|
||||
3M USD 2025-03-06 3M = 0.0063562
|
||||
3M USD 2025-06-06 3M = 0.0063563
|
||||
3M USD 2025-09-08 3M = 0.0063564
|
||||
3M USD 2025-12-08 3M = 0.0063565
|
||||
3M USD 2026-03-06 3M = 0.0063566
|
||||
3M USD 2026-06-08 3M = 0.0063567
|
||||
3M USD 2026-09-08 3M = 0.0063568
|
||||
3M USD 2026-12-08 3M = 0.0063569
|
104
src/main/resources/core/testing/trade.json
Normal file
104
src/main/resources/core/testing/trade.json
Normal file
@ -0,0 +1,104 @@
|
||||
{
|
||||
"fixedLeg": {
|
||||
"fixedRatePayer": "Bank A",
|
||||
"notional": {
|
||||
"pennies": 2500000000,
|
||||
"currency": "USD"
|
||||
},
|
||||
"paymentFrequency": "SemiAnnual",
|
||||
"effectiveDate": "2016-03-16",
|
||||
"effectiveDateAdjustment": null,
|
||||
"terminationDate": "2026-03-16",
|
||||
"terminationDateAdjustment": null,
|
||||
"fixedRate": {
|
||||
"ratioUnit": {
|
||||
"value": "0.01676"
|
||||
}
|
||||
},
|
||||
"dayCountBasisDay": "D30",
|
||||
"dayCountBasisYear": "Y360",
|
||||
"rollConvention": "ModifiedFollowing",
|
||||
"dayInMonth": 10,
|
||||
"paymentRule": "InArrears",
|
||||
"paymentDelay": 0,
|
||||
"paymentCalendar": "London",
|
||||
"interestPeriodAdjustment": "Adjusted"
|
||||
},
|
||||
"floatingLeg": {
|
||||
"floatingRatePayer": "Bank B",
|
||||
"notional": {
|
||||
"pennies": 2500000000,
|
||||
"currency": "USD"
|
||||
},
|
||||
"paymentFrequency": "Quarterly",
|
||||
"effectiveDate": "2016-03-12",
|
||||
"effectiveDateAdjustment": null,
|
||||
"terminationDate": "2026-03-12",
|
||||
"terminationDateAdjustment": null,
|
||||
"dayCountBasisDay": "D30",
|
||||
"dayCountBasisYear": "Y360",
|
||||
"rollConvention": "ModifiedFollowing",
|
||||
"fixingRollConvention": "ModifiedFollowing",
|
||||
"dayInMonth": 10,
|
||||
"resetDayInMonth": 10,
|
||||
"paymentRule": "InArrears",
|
||||
"paymentDelay": 0,
|
||||
"paymentCalendar": [ "London" ],
|
||||
"interestPeriodAdjustment": "Adjusted",
|
||||
"fixingPeriod": "TWODAYS",
|
||||
"resetRule": "InAdvance",
|
||||
"fixingsPerPayment": "Quarterly",
|
||||
"fixingCalendar": [ "NewYork" ],
|
||||
"index": "3M USD",
|
||||
"indexSource": "Rates Service Provider",
|
||||
"indexTenor": {
|
||||
"name": "3M"
|
||||
}
|
||||
},
|
||||
"calculation": {
|
||||
"expression": "( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
|
||||
"floatingLegPaymentSchedule": {
|
||||
},
|
||||
"fixedLegPaymentSchedule": {
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"baseCurrency": "EUR",
|
||||
"eligibleCurrency": "EUR",
|
||||
"eligibleCreditSupport": "Cash in an Eligible Currency",
|
||||
"independentAmounts": {
|
||||
"pennies": 0,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"threshold": {
|
||||
"pennies": 0,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"minimumTransferAmount": {
|
||||
"pennies": 25000000,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"rounding": {
|
||||
"pennies": 1000000,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"valuationDate": "Every Local Business Day",
|
||||
"notificationTime": "2:00pm London",
|
||||
"resolutionTime": "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
||||
"interestRate": {
|
||||
"oracle": "Rates Service Provider",
|
||||
"tenor": {
|
||||
"name": "6M"
|
||||
},
|
||||
"ratioUnit": null,
|
||||
"name": "EONIA"
|
||||
},
|
||||
"addressForTransfers": "",
|
||||
"exposure": {},
|
||||
"localBusinessDay": [ "London" , "NewYork" ],
|
||||
"dailyInterestAmount": "(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360",
|
||||
"tradeID": "tradeXXX",
|
||||
"hashLegalDocs": "put hash here"
|
||||
},
|
||||
"programRef": "1E6BBA305D445341F0026E51B6C7F3ACB834AFC6C2510C0EF7BC0477235EFECF"
|
||||
}
|
@ -11,12 +11,10 @@ package core
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import core.crypto.*
|
||||
import core.messaging.MessagingService
|
||||
import core.node.services.MockNetworkMapService
|
||||
import core.node.services.NetworkMapService
|
||||
import core.node.services.*
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.deserialize
|
||||
import core.testutils.TEST_KEYS_TO_CORP_MAP
|
||||
import core.testutils.MockIdentityService
|
||||
import core.testutils.TEST_PROGRAM_MAP
|
||||
import core.testutils.TEST_TX_TIME
|
||||
import java.io.ByteArrayInputStream
|
||||
@ -52,10 +50,6 @@ class DummyTimestamper(var clock: Clock = Clock.fixed(TEST_TX_TIME, ZoneId.syste
|
||||
|
||||
val DUMMY_TIMESTAMPER = DummyTimestamper()
|
||||
|
||||
object MockIdentityService : IdentityService {
|
||||
override fun partyFromKey(key: PublicKey): Party? = TEST_KEYS_TO_CORP_MAP[key]
|
||||
}
|
||||
|
||||
class MockKeyManagementService(vararg initialKeys: KeyPair) : KeyManagementService {
|
||||
override val keys: MutableMap<PublicKey, PrivateKey>
|
||||
|
||||
@ -124,7 +118,7 @@ class MockServices(
|
||||
val net: MessagingService? = null,
|
||||
val identity: IdentityService? = MockIdentityService,
|
||||
val storage: StorageService? = MockStorageService(),
|
||||
val networkMap: NetworkMapService? = MockNetworkMapService(),
|
||||
val networkMap: NetworkMapCache? = MockNetworkMapCache(),
|
||||
val overrideClock: Clock? = Clock.systemUTC()
|
||||
) : ServiceHub {
|
||||
override val walletService: WalletService
|
||||
@ -135,7 +129,7 @@ class MockServices(
|
||||
get() = identity ?: throw UnsupportedOperationException()
|
||||
override val networkService: MessagingService
|
||||
get() = net ?: throw UnsupportedOperationException()
|
||||
override val networkMapService: NetworkMapService
|
||||
override val networkMapCache: NetworkMapCache
|
||||
get() = networkMap ?: throw UnsupportedOperationException()
|
||||
override val storageService: StorageService
|
||||
get() = storage ?: throw UnsupportedOperationException()
|
||||
|
@ -12,7 +12,7 @@ import core.Attachment
|
||||
import core.crypto.SecureHash
|
||||
import core.crypto.sha256
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.node.services.NodeInfo
|
||||
import core.node.services.NodeAttachmentService
|
||||
import core.serialization.OpaqueBytes
|
||||
import core.testing.MockNetwork
|
||||
@ -94,7 +94,7 @@ class AttachmentTests {
|
||||
fun maliciousResponse() {
|
||||
// Make a node that doesn't do sanity checking at load time.
|
||||
val n0 = network.createNode(null, nodeFactory = object : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, timestamperAddr: LegallyIdentifiableNode?): MockNetwork.MockNode {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, timestamperAddr: NodeInfo?): MockNetwork.MockNode {
|
||||
return object : MockNetwork.MockNode(dir, config, network, timestamperAddr) {
|
||||
override fun start(): MockNetwork.MockNode {
|
||||
super.start()
|
||||
@ -104,7 +104,7 @@ class AttachmentTests {
|
||||
}
|
||||
}
|
||||
})
|
||||
val n1 = network.createNode(n0.legallyIdentifiableAddress)
|
||||
val n1 = network.createNode(n0.info)
|
||||
|
||||
// Insert an attachment into node zero's store directly.
|
||||
val id = n0.storage.attachments.importAttachment(ByteArrayInputStream(fakeAttachment()))
|
||||
|
@ -48,7 +48,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
|
||||
@Before
|
||||
fun before() {
|
||||
net = MockNetwork(false)
|
||||
net.identities += TEST_KEYS_TO_CORP_MAP.values
|
||||
net.identities += MockIdentityService.identities
|
||||
BriefLogFormatter.loggingOn("platform.trade", "core.TransactionGroup", "recordingmap")
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
|
||||
transactionGroupFor<ContractState> {
|
||||
val (aliceNode, bobNode) = net.createTwoNodes()
|
||||
(bobNode.wallet as NodeWalletService).fillWithSomeTestCash(2000.DOLLARS)
|
||||
val alicesFakePaper = fillUpForSeller(false, aliceNode.legallyIdentifiableAddress.identity, null).second
|
||||
val alicesFakePaper = fillUpForSeller(false, aliceNode.info.identity, null).second
|
||||
|
||||
insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
|
||||
|
||||
@ -74,7 +74,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
|
||||
|
||||
val aliceResult = TwoPartyTradeProtocol.runSeller(
|
||||
aliceNode.smm,
|
||||
aliceNode.legallyIdentifiableAddress,
|
||||
aliceNode.info,
|
||||
bobNode.net.myAddress,
|
||||
lookup("alice's paper"),
|
||||
1000.DOLLARS,
|
||||
@ -83,7 +83,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
|
||||
)
|
||||
val bobResult = TwoPartyTradeProtocol.runBuyer(
|
||||
bobNode.smm,
|
||||
aliceNode.legallyIdentifiableAddress,
|
||||
aliceNode.info,
|
||||
aliceNode.net.myAddress,
|
||||
1000.DOLLARS,
|
||||
CommercialPaper.State::class.java,
|
||||
@ -105,7 +105,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
|
||||
var (aliceNode, bobNode) = net.createTwoNodes()
|
||||
val aliceAddr = aliceNode.net.myAddress
|
||||
val bobAddr = bobNode.net.myAddress as InMemoryMessagingNetwork.Handle
|
||||
val timestamperAddr = aliceNode.legallyIdentifiableAddress
|
||||
val timestamperAddr = aliceNode.info
|
||||
|
||||
(bobNode.wallet as NodeWalletService).fillWithSomeTestCash(2000.DOLLARS)
|
||||
val alicesFakePaper = fillUpForSeller(false, timestamperAddr.identity, null).second
|
||||
@ -165,7 +165,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
|
||||
// ... bring the node back up ... the act of constructing the SMM will re-register the message handlers
|
||||
// that Bob was waiting on before the reboot occurred.
|
||||
bobNode = net.createNode(timestamperAddr, bobAddr.id, object : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, timestamperAddr: LegallyIdentifiableNode?): MockNetwork.MockNode {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, timestamperAddr: NodeInfo?): MockNetwork.MockNode {
|
||||
return object : MockNetwork.MockNode(dir, config, net, timestamperAddr, bobAddr.id) {
|
||||
override fun initialiseStorageService(dir: Path): StorageService {
|
||||
val ss = super.initialiseStorageService(dir)
|
||||
@ -196,7 +196,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
|
||||
private fun makeNodeWithTracking(name: String): MockNetwork.MockNode {
|
||||
// Create a node in the mock network ...
|
||||
return net.createNode(null, nodeFactory = object : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, timestamperAddr: LegallyIdentifiableNode?): MockNetwork.MockNode {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, timestamperAddr: NodeInfo?): MockNetwork.MockNode {
|
||||
return object : MockNetwork.MockNode(dir, config, network, timestamperAddr) {
|
||||
// That constructs the storage service object in a customised way ...
|
||||
override fun constructStorageService(attachments: NodeAttachmentService, keypair: KeyPair, identity: Party): StorageServiceImpl {
|
||||
@ -212,7 +212,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
|
||||
fun checkDependenciesOfSaleAssetAreResolved() {
|
||||
transactionGroupFor<ContractState> {
|
||||
val aliceNode = makeNodeWithTracking("alice")
|
||||
val timestamperAddr = aliceNode.legallyIdentifiableAddress
|
||||
val timestamperAddr = aliceNode.info
|
||||
val bobNode = makeNodeWithTracking("bob")
|
||||
|
||||
// Insert a prospectus type attachment into the commercial paper transaction.
|
||||
@ -323,7 +323,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
|
||||
var (aliceNode, bobNode) = net.createTwoNodes()
|
||||
val aliceAddr = aliceNode.net.myAddress
|
||||
val bobAddr = bobNode.net.myAddress as InMemoryMessagingNetwork.Handle
|
||||
val timestamperAddr = aliceNode.legallyIdentifiableAddress
|
||||
val timestamperAddr = aliceNode.info
|
||||
|
||||
val bobKey = bobNode.keyManagement.freshKey()
|
||||
val bobsBadCash = fillUpForBuyer(bobError, bobKey.public).second
|
||||
|
@ -11,7 +11,8 @@ package core.node
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.messaging.*
|
||||
import core.messaging.StateMachineManager
|
||||
import core.messaging.TestWithInMemoryNetwork
|
||||
import core.node.services.*
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.serialization.serialize
|
||||
@ -57,14 +58,14 @@ class TimestamperNodeServiceTest : TestWithInMemoryNetwork() {
|
||||
mockServices = MockServices(net = serviceMessaging.second, storage = MockStorageService())
|
||||
|
||||
val timestampingNodeID = network.setupTimestampingNode(true).first
|
||||
(mockServices.networkMapService as MockNetworkMapService).timestampingNodes.add(timestampingNodeID)
|
||||
(mockServices.networkMapCache as MockNetworkMapCache).timestampingNodes.add(timestampingNodeID)
|
||||
serverKey = timestampingNodeID.identity.owningKey
|
||||
|
||||
// And a separate one to be tested directly, to make the unit tests a bit faster.
|
||||
service = NodeTimestamperService(serviceMessaging.second, Party("Unit test suite", ALICE), ALICE_KEY)
|
||||
}
|
||||
|
||||
class TestPSM(val server: LegallyIdentifiableNode, val now: Instant) : ProtocolLogic<Boolean>() {
|
||||
class TestPSM(val server: NodeInfo, val now: Instant) : ProtocolLogic<Boolean>() {
|
||||
@Suspendable
|
||||
override fun call(): Boolean {
|
||||
val ptx = TransactionBuilder().apply {
|
||||
@ -85,7 +86,7 @@ class TimestamperNodeServiceTest : TestWithInMemoryNetwork() {
|
||||
val psm = runNetwork {
|
||||
val smm = StateMachineManager(MockServices(net = myMessaging.second), RunOnCallerThread)
|
||||
val logName = NodeTimestamperService.TIMESTAMPING_PROTOCOL_TOPIC
|
||||
val psm = TestPSM(mockServices.networkMapService.timestampingNodes[0], clock.instant())
|
||||
val psm = TestPSM(mockServices.networkMapCache.timestampingNodes[0], clock.instant())
|
||||
smm.add(logName, psm)
|
||||
}
|
||||
assertTrue(psm.isDone)
|
||||
|
@ -41,11 +41,19 @@ class NodeInterestRatesTest {
|
||||
|
||||
@Test fun `query with one success and one missing`() {
|
||||
val q1 = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
val q2 = NodeInterestRates.parseFixOf("LIBOR 2016-03-19 1M")
|
||||
val q2 = NodeInterestRates.parseFixOf("LIBOR 2016-03-15 1M")
|
||||
val e = assertFailsWith<NodeInterestRates.UnknownFix> { service.query(listOf(q1, q2)) }
|
||||
assertEquals(e.fix, q2)
|
||||
}
|
||||
|
||||
@Test fun `query successfully with one date beyond`() {
|
||||
val q = NodeInterestRates.parseFixOf("LIBOR 2016-03-19 1M")
|
||||
val res = service.query(listOf(q))
|
||||
assertEquals(1, res.size)
|
||||
assertEquals("0.678".bd, res[0].value)
|
||||
assertEquals(q, res[0].of)
|
||||
}
|
||||
|
||||
@Test fun `empty query`() {
|
||||
assertFailsWith<IllegalArgumentException> { service.query(emptyList()) }
|
||||
}
|
||||
@ -85,7 +93,7 @@ class NodeInterestRatesTest {
|
||||
|
||||
val tx = TransactionBuilder()
|
||||
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
val protocol = RatesFixProtocol(tx, n2.legallyIdentifiableAddress, fixOf, "0.675".bd, "0.1".bd)
|
||||
val protocol = RatesFixProtocol(tx, n2.info, fixOf, "0.675".bd, "0.1".bd)
|
||||
BriefLogFormatter.initVerbose("rates")
|
||||
val future = n1.smm.add("rates", protocol)
|
||||
|
||||
|
@ -11,10 +11,7 @@ package core.serialization
|
||||
import contracts.Cash
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.testutils.DUMMY_PUBKEY_1
|
||||
import core.testutils.MINI_CORP
|
||||
import core.testutils.TEST_TX_TIME
|
||||
import core.testutils.TestUtils
|
||||
import core.testutils.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.security.SignatureException
|
||||
|
@ -15,6 +15,7 @@ import contracts.*
|
||||
import core.*
|
||||
import core.crypto.*
|
||||
import core.node.services.DummyTimestampingAuthority
|
||||
import core.node.services.FixedIdentityService
|
||||
import core.serialization.serialize
|
||||
import core.visualiser.GraphVisualiser
|
||||
import java.security.KeyPair
|
||||
@ -38,8 +39,6 @@ object TestUtils {
|
||||
val keypair = generateKeyPair()
|
||||
val keypair2 = generateKeyPair()
|
||||
val keypair3 = generateKeyPair()
|
||||
val keypair4 = generateKeyPair()
|
||||
val keypair5 = generateKeyPair()
|
||||
}
|
||||
// A dummy time at which we will be pretending test transactions are created.
|
||||
val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
|
||||
@ -51,14 +50,6 @@ val MEGA_CORP_PUBKEY = MEGA_CORP_KEY.public
|
||||
val MINI_CORP_KEY = TestUtils.keypair2
|
||||
val MINI_CORP_PUBKEY = MINI_CORP_KEY.public
|
||||
|
||||
// TODO remove once mock API is retired
|
||||
val EXCALIBUR_BANK_KEY = TestUtils.keypair4
|
||||
val EXCALIBUR_BANK_PUBKEY = EXCALIBUR_BANK_KEY.public
|
||||
|
||||
// TODO remove once mock API is retired
|
||||
val A_N_OTHER_BANK_KEY = TestUtils.keypair5
|
||||
val A_N_OTHER_BANK_PUBKEY = A_N_OTHER_BANK_KEY.public
|
||||
|
||||
val ORACLE_KEY = TestUtils.keypair3
|
||||
val ORACLE_PUBKEY = ORACLE_KEY.public
|
||||
|
||||
@ -74,19 +65,9 @@ val BOB = BOB_KEY.public
|
||||
val MEGA_CORP = Party("MegaCorp", MEGA_CORP_PUBKEY)
|
||||
val MINI_CORP = Party("MiniCorp", MINI_CORP_PUBKEY)
|
||||
|
||||
// TODO remove once mock API is retired
|
||||
val EXCALIBUR_BANK = Party("Excalibur", EXCALIBUR_BANK_PUBKEY)
|
||||
val A_N_OTHER_BANK = Party("ANOther",A_N_OTHER_BANK_PUBKEY)
|
||||
val ALL_TEST_KEYS = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, DummyTimestampingAuthority.key)
|
||||
|
||||
val ALL_TEST_KEYS = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, EXCALIBUR_BANK_KEY, A_N_OTHER_BANK_KEY, DummyTimestampingAuthority.key)
|
||||
|
||||
val TEST_KEYS_TO_CORP_MAP: Map<PublicKey, Party> = mapOf(
|
||||
MEGA_CORP_PUBKEY to MEGA_CORP,
|
||||
MINI_CORP_PUBKEY to MINI_CORP,
|
||||
EXCALIBUR_BANK_PUBKEY to EXCALIBUR_BANK,
|
||||
A_N_OTHER_BANK_PUBKEY to A_N_OTHER_BANK,
|
||||
DUMMY_TIMESTAMPER.identity.owningKey to DUMMY_TIMESTAMPER.identity
|
||||
)
|
||||
val MockIdentityService = FixedIdentityService(listOf(MEGA_CORP, MINI_CORP, DUMMY_TIMESTAMPER.identity))
|
||||
|
||||
// In a real system this would be a persistent map of hash to bytecode and we'd instantiate the object as needed inside
|
||||
// a sandbox. For unit tests we just have a hard-coded list.
|
||||
@ -144,7 +125,7 @@ abstract class AbstractTransactionForTest {
|
||||
open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, s()).apply { outStates.add(this) }
|
||||
|
||||
protected fun commandsToAuthenticatedObjects(): List<AuthenticatedObject<CommandData>> {
|
||||
return commands.map { AuthenticatedObject(it.pubkeys, it.pubkeys.mapNotNull { TEST_KEYS_TO_CORP_MAP[it] }, it.data) }
|
||||
return commands.map { AuthenticatedObject(it.pubkeys, it.pubkeys.mapNotNull { MockIdentityService.partyFromKey(it) }, it.data) }
|
||||
}
|
||||
|
||||
fun attachment(attachmentID: SecureHash) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user