Merged in plt-17-api-server-merge5 (pull request #51)

IRS related protocols and scripts
This commit is contained in:
Rick Parker 2016-04-07 15:01:25 +01:00
commit 34940c903b
28 changed files with 1594 additions and 127 deletions

View File

@ -144,6 +144,14 @@ task getRateFixDemo(type: CreateStartScripts) {
classpath = jar.outputs.files + project.configurations.runtime 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 // 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 // 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 // NOTE: need to make sure the output isn't on the classpath or every other run it generates empty results, so
@ -166,5 +174,6 @@ jar.dependsOn quasarScan
applicationDistribution.into("bin") { applicationDistribution.into("bin") {
from(getRateFixDemo) from(getRateFixDemo)
from(getIRSDemo)
fileMode = 0755 fileMode = 0755
} }

View File

@ -8,18 +8,8 @@
package contracts 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.*
import core.crypto.SecureHash import core.crypto.SecureHash
import core.node.services.DummyTimestampingAuthority
import org.apache.commons.jexl3.JexlBuilder import org.apache.commons.jexl3.JexlBuilder
import org.apache.commons.jexl3.MapContext import org.apache.commons.jexl3.MapContext
import java.math.BigDecimal import java.math.BigDecimal
@ -31,7 +21,16 @@ import java.util.*
val IRS_PROGRAM_ID = SecureHash.sha256("replace-me-later-with-bytecode-hash-of-irs-code") val IRS_PROGRAM_ID = SecureHash.sha256("replace-me-later-with-bytecode-hash-of-irs-code")
// This is a placeholder for some types that we haven't identified exactly what they are just yet for things still in discussion // 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. * Event superclass - everything happens on a date.
@ -182,7 +181,7 @@ class InterestRateSwap() : Contract {
* data that will changed from state to state (Recall that the design insists that everything is immutable, so we actually * data that will changed from state to state (Recall that the design insists that everything is immutable, so we actually
* copy / update for each transition) * copy / update for each transition)
*/ */
data class Calculation( data class Calculation (
val expression: Expression, val expression: Expression,
val floatingLegPaymentSchedule: Map<LocalDate, FloatingRatePaymentEvent>, val floatingLegPaymentSchedule: Map<LocalDate, FloatingRatePaymentEvent>,
val fixedLegPaymentSchedule: Map<LocalDate, FixedRatePaymentEvent> val fixedLegPaymentSchedule: Map<LocalDate, FixedRatePaymentEvent>
@ -240,6 +239,48 @@ class InterestRateSwap() : Contract {
"TerminationDateAdjustment=$terminationDateAdjustment,DayCountBasis=$dayCountBasisDay/$dayCountBasisYear,DayInMonth=$dayInMonth," + "TerminationDateAdjustment=$terminationDateAdjustment,DayCountBasis=$dayCountBasisDay/$dayCountBasisYear,DayInMonth=$dayInMonth," +
"PaymentRule=$paymentRule,PaymentDelay=$paymentDelay,PaymentCalendar=$paymentCalendar,InterestPeriodAdjustment=$interestPeriodAdjustment" "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( open class FixedLeg(
@ -264,6 +305,29 @@ class InterestRateSwap() : Contract {
dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment) { dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment) {
override fun toString(): String = "FixedLeg(Payer=$fixedRatePayer," + super.toString() + ",fixedRate=$fixedRate," + override fun toString(): String = "FixedLeg(Payer=$fixedRatePayer," + super.toString() + ",fixedRate=$fixedRate," +
"rollConvention=$rollConvention" "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( open class FloatingLeg(
@ -298,6 +362,44 @@ class InterestRateSwap() : Contract {
"FixingPeriond=$fixingPeriod,ResetRule=$resetRule,FixingsPerPayment=$fixingsPerPayment,FixingCalendar=$fixingCalendar," + "FixingPeriond=$fixingPeriod,ResetRule=$resetRule,FixingsPerPayment=$fixingsPerPayment,FixingCalendar=$fixingCalendar," +
"Index=$index,IndexSource=$indexSource,IndexTenor=$indexTenor" "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 +407,7 @@ class InterestRateSwap() : Contract {
*/ */
override fun verify(tx: TransactionForVerification) { override fun verify(tx: TransactionForVerification) {
val command = tx.commands.requireSingleCommand<InterestRateSwap.Commands>() 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", "Bank A")?.midpoint
if (time == null) throw IllegalArgumentException("must be timestamped") if (time == null) throw IllegalArgumentException("must be timestamped")
val irs = tx.outStates.filterIsInstance<InterestRateSwap.State>().single() val irs = tx.outStates.filterIsInstance<InterestRateSwap.State>().single()
@ -356,7 +458,8 @@ class InterestRateSwap() : Contract {
val floatingLeg: FloatingLeg, val floatingLeg: FloatingLeg,
val calculation: Calculation, val calculation: Calculation,
val common: Common val common: Common
) : LinearState { ) : FixableDealState {
override val programRef = IRS_PROGRAM_ID override val programRef = IRS_PROGRAM_ID
override val thread = SecureHash.sha256(common.tradeID) override val thread = SecureHash.sha256(common.tradeID)
override val ref = common.tradeID override val ref = common.tradeID
@ -365,6 +468,39 @@ class InterestRateSwap() : Contract {
return (fixedLeg.fixedRatePayer.owningKey in ourKeys) || (floatingLeg.floatingRatePayer.owningKey in ourKeys) 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 * For evaluating arbitrary java on the platform
*/ */

View File

@ -1,17 +1,8 @@
package contracts package contracts
import com.fasterxml.jackson.core.JsonParseException import core.*
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 java.math.BigDecimal 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. // 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 open class RatioUnit(value: BigDecimal) { // TODO: Discuss this type
val value = value 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) 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 * 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. * A very basic subclass to represent a fixed rate.

View File

@ -269,7 +269,7 @@ open class BusinessCalendar private constructor(val calendars: Array<out String>
calname.flatMap { (TEST_CALENDAR_DATA[it] ?: throw UnknownCalendar(it)).split(",") }. calname.flatMap { (TEST_CALENDAR_DATA[it] ?: throw UnknownCalendar(it)).split(",") }.
toSet(). toSet().
map{ parseDateFromString(it) }. map{ parseDateFromString(it) }.
toList() toList().sorted()
) )
/** Calculates an event schedule that moves events around to ensure they fall on working days. */ /** 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 = open fun isWorkingDay(date: LocalDate): Boolean =
when { when {
date.dayOfWeek == DayOfWeek.SATURDAY -> false date.dayOfWeek == DayOfWeek.SATURDAY -> false

View File

@ -18,6 +18,7 @@ import java.io.OutputStream
import java.security.PublicKey import java.security.PublicKey
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.time.LocalDate
import java.util.jar.JarInputStream import java.util.jar.JarInputStream
/** Implemented by anything that can be named by a secure hash value (e.g. transactions, attachments). */ /** Implemented by anything that can be named by a secure hash value (e.g. transactions, attachments). */
@ -56,10 +57,6 @@ interface LinearState: ContractState {
/** Unique thread id within the wallets of all parties */ /** Unique thread id within the wallets of all parties */
val thread: SecureHash 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) */ /** true if this should be tracked by our wallet(s) */
fun isRelevant(ourKeys: Set<PublicKey>): Boolean fun isRelevant(ourKeys: Set<PublicKey>): Boolean
} }

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

View File

@ -1,51 +1,51 @@
# Some pretend noddy rate fixes, for the interest rate oracles. # Some pretend noddy rate fixes, for the interest rate oracles.
LIBOR 2016-03-16 1M = 0.678 ICE LIBOR 2016-03-16 1M = 0.678
LIBOR 2016-03-16 2M = 0.655 ICE LIBOR 2016-03-16 2M = 0.655
EURIBOR 2016-03-15 1M = 0.123 EURIBOR 2016-03-15 1M = 0.123
EURIBOR 2016-03-15 2M = 0.111 EURIBOR 2016-03-15 2M = 0.111
LIBOR 2016-03-08 3M = 0.01 ICE LIBOR 2016-03-08 3M = 0.0063515
LIBOR 2016-06-08 3M = 0.01 ICE LIBOR 2016-06-08 3M = 0.0063520
LIBOR 2016-09-08 3M = 0.01 ICE LIBOR 2016-09-08 3M = 0.0063521
LIBOR 2016-12-08 3M = 0.01 ICE LIBOR 2016-12-08 3M = 0.0063515
LIBOR 2017-03-08 3M = 0.01 ICE LIBOR 2017-03-08 3M = 0.0063525
LIBOR 2017-06-08 3M = 0.01 ICE LIBOR 2017-06-08 3M = 0.0063530
LIBOR 2017-09-07 3M = 0.01 ICE LIBOR 2017-09-07 3M = 0.0063531
LIBOR 2017-12-07 3M = 0.01 ICE LIBOR 2017-12-07 3M = 0.0063532
LIBOR 2018-03-08 3M = 0.01 ICE LIBOR 2018-03-08 3M = 0.0063533
LIBOR 2018-06-07 3M = 0.01 ICE LIBOR 2018-06-07 3M = 0.0063534
LIBOR 2018-09-06 3M = 0.01 ICE LIBOR 2018-09-06 3M = 0.0063535
LIBOR 2018-12-06 3M = 0.01 ICE LIBOR 2018-12-06 3M = 0.0063536
LIBOR 2019-03-07 3M = 0.01 ICE LIBOR 2019-03-07 3M = 0.0063537
LIBOR 2019-06-06 3M = 0.01 ICE LIBOR 2019-06-06 3M = 0.0063538
LIBOR 2019-09-06 3M = 0.01 ICE LIBOR 2019-09-06 3M = 0.0063539
LIBOR 2019-12-06 3M = 0.01 ICE LIBOR 2019-12-06 3M = 0.0063540
LIBOR 2020-03-06 3M = 0.01 ICE LIBOR 2020-03-06 3M = 0.0063541
LIBOR 2020-06-08 3M = 0.01 ICE LIBOR 2020-06-08 3M = 0.0063542
LIBOR 2020-09-08 3M = 0.01 ICE LIBOR 2020-09-08 3M = 0.0063543
LIBOR 2020-12-08 3M = 0.01 ICE LIBOR 2020-12-08 3M = 0.0063544
LIBOR 2021-03-08 3M = 0.01 ICE LIBOR 2021-03-08 3M = 0.0063545
LIBOR 2021-06-08 3M = 0.01 ICE LIBOR 2021-06-08 3M = 0.0063546
LIBOR 2021-09-08 3M = 0.01 ICE LIBOR 2021-09-08 3M = 0.0063547
LIBOR 2021-12-08 3M = 0.01 ICE LIBOR 2021-12-08 3M = 0.0063548
LIBOR 2022-03-08 3M = 0.01 ICE LIBOR 2022-03-08 3M = 0.0063549
LIBOR 2022-06-08 3M = 0.01 ICE LIBOR 2022-06-08 3M = 0.0063550
LIBOR 2022-09-08 3M = 0.01 ICE LIBOR 2022-09-08 3M = 0.0063551
LIBOR 2022-12-08 3M = 0.01 ICE LIBOR 2022-12-08 3M = 0.0063553
LIBOR 2023-03-08 3M = 0.01 ICE LIBOR 2023-03-08 3M = 0.0063554
LIBOR 2023-06-08 3M = 0.01 ICE LIBOR 2023-06-08 3M = 0.0063555
LIBOR 2023-09-07 3M = 0.01 ICE LIBOR 2023-09-07 3M = 0.0063556
LIBOR 2023-12-07 3M = 0.01 ICE LIBOR 2023-12-07 3M = 0.0063557
LIBOR 2024-03-07 3M = 0.01 ICE LIBOR 2024-03-07 3M = 0.0063558
LIBOR 2024-06-06 3M = 0.01 ICE LIBOR 2024-06-06 3M = 0.0063559
LIBOR 2024-09-06 3M = 0.01 ICE LIBOR 2024-09-06 3M = 0.0063560
LIBOR 2024-12-06 3M = 0.01 ICE LIBOR 2024-12-06 3M = 0.0063561
LIBOR 2025-03-06 3M = 0.01 ICE LIBOR 2025-03-06 3M = 0.0063562
LIBOR 2025-06-06 3M = 0.01 ICE LIBOR 2025-06-06 3M = 0.0063563
LIBOR 2025-09-08 3M = 0.01 ICE LIBOR 2025-09-08 3M = 0.0063564
LIBOR 2025-12-08 3M = 0.01 ICE LIBOR 2025-12-08 3M = 0.0063565
LIBOR 2026-03-06 3M = 0.01 ICE LIBOR 2026-03-06 3M = 0.0063566
LIBOR 2026-06-08 3M = 0.01 ICE LIBOR 2026-06-08 3M = 0.0063567
LIBOR 2026-09-08 3M = 0.01 ICE LIBOR 2026-09-08 3M = 0.0063568
LIBOR 2026-12-08 3M = 0.01 ICE LIBOR 2026-12-08 3M = 0.0063569

65
scripts/irs-demo.sh Executable file
View 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

View File

@ -1,6 +1,7 @@
package api package api
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import contracts.DealState
import core.* import core.*
import core.crypto.DigitalSignature import core.crypto.DigitalSignature
import core.crypto.SecureHash import core.crypto.SecureHash
@ -27,7 +28,7 @@ class APIServerImpl(val node: AbstractNode): APIServer {
return states.values.map { it.ref } return states.values.map { it.ref }
} }
else if (query.criteria is StatesQuery.Criteria.Deal) { 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 it.ref == query.criteria.ref
} }
return states.values.map { it.ref } return states.values.map { it.ref }
@ -73,6 +74,7 @@ class APIServerImpl(val node: AbstractNode): APIServer {
} else if (args.containsKey(parameter.name)) { } else if (args.containsKey(parameter.name)) {
val value = args[parameter.name] val value = args[parameter.name]
if (value is Any) { 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)) { if (!(parameter.type.javaType as Class<*>).isAssignableFrom(value.javaClass)) {
// Not null and not assignable // Not null and not assignable
break@nextConstructor break@nextConstructor

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

View File

@ -9,12 +9,13 @@
package core.node package core.node
import api.Config import api.Config
import api.InterestRateSwapAPI
import api.ResponseFilter import api.ResponseFilter
import com.codahale.metrics.JmxReporter import com.codahale.metrics.JmxReporter
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import core.node.services.LegallyIdentifiableNode
import core.messaging.MessagingService import core.messaging.MessagingService
import core.node.services.ArtemisMessagingService import core.node.services.ArtemisMessagingService
import core.node.services.LegallyIdentifiableNode
import core.node.servlets.AttachmentDownloadServlet import core.node.servlets.AttachmentDownloadServlet
import core.node.servlets.DataUploadServlet import core.node.servlets.DataUploadServlet
import core.utilities.loggerFor import core.utilities.loggerFor
@ -97,6 +98,7 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
resourceConfig.register(Config(services)) resourceConfig.register(Config(services))
resourceConfig.register(ResponseFilter()) resourceConfig.register(ResponseFilter())
resourceConfig.register(api) 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 // 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", resourceConfig.addProperties(mapOf(ServerProperties.APPLICATION_NAME to "node.api",
ServerProperties.MONITORING_STATISTICS_MBEANS_ENABLED to "true")) ServerProperties.MONITORING_STATISTICS_MBEANS_ENABLED to "true"))

View File

@ -43,8 +43,8 @@ class MockNetworkMapService : NetworkMapService {
override val partyNodes = Collections.synchronizedList(ArrayList<LegallyIdentifiableNode>()) override val partyNodes = Collections.synchronizedList(ArrayList<LegallyIdentifiableNode>())
init { init {
partyNodes.add(LegallyIdentifiableNode(MockAddress("excalibur:8080"), Party("Excalibur", DummyPublicKey("Excalibur")))) partyNodes.add(LegallyIdentifiableNode(MockAddress("bankC:8080"), Party("Bank C", DummyPublicKey("Bank C"))))
partyNodes.add(LegallyIdentifiableNode(MockAddress("another:8080"), Party("ANOther", DummyPublicKey("ANOther")))) partyNodes.add(LegallyIdentifiableNode(MockAddress("bankD:8080"), Party("Bank D", DummyPublicKey("Bank D"))))
} }
} }

View File

@ -43,16 +43,21 @@ object NodeInterestRates {
/** Parses a string of the form "LIBOR 16-March-2016 1M" into a [FixOf] */ /** Parses a string of the form "LIBOR 16-March-2016 1M" into a [FixOf] */
fun parseFixOf(key: String): 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)) return FixOf(name, LocalDate.parse(date), Tenor(tenorString))
} }
/** Parses lines containing fixes */ /** Parses lines containing fixes */
fun parseFile(s: String): Map<FixOf, Fix> { fun parseFile(s: String): Map<FixOf, TreeMap<LocalDate,Fix>> {
val results = HashMap<FixOf, Fix>() val results = HashMap<FixOf, TreeMap<LocalDate,Fix>>()
for (line in s.lines()) { for (line in s.lines()) {
val (fixOf, fix) = parseOneRate(line.trim()) val (fixOf, fix) = parseOneRate(line)
results[fixOf] = fix val genericKey = FixOf(fixOf.name, LocalDate.MIN, fixOf.ofTenor)
val existingMap = results.computeIfAbsent(genericKey, { TreeMap() })
existingMap[fixOf.forDay] = fix
} }
return results return results
} }
@ -91,14 +96,13 @@ object NodeInterestRates {
override val acceptableFileExtensions = listOf(".rates", ".txt") override val acceptableFileExtensions = listOf(".rates", ".txt")
override fun upload(data: InputStream): String { override fun upload(data: InputStream): String {
val fixes: Map<FixOf, Fix> = data. val fixes: Map<FixOf, TreeMap<LocalDate,Fix>> = parseFile(data.
bufferedReader(). bufferedReader().
readLines(). readLines().
map { it.trim() }. map { it.trim() }.
// Filter out comment and empty lines. // Filter out comment and empty lines.
filterNot { it.startsWith("#") || it.isBlank() }. filterNot { it.startsWith("#") || it.isBlank() }.
map { parseOneRate(it) }. joinToString("\n"))
associate { it.first to it.second }
// TODO: Save the uploaded fixes to the storage service and reload on construction. // 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. // the pointer to the stack before working with the map.
oracle.knownFixes = fixes 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. * 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 @ThreadSafe
class Oracle(val identity: Party, private val signingKey: KeyPair) { class Oracle(val identity: Party, private val signingKey: KeyPair) {
@ -119,8 +127,13 @@ object NodeInterestRates {
require(signingKey.public == identity.owningKey) require(signingKey.public == identity.owningKey)
} }
/** The fix data being served by this oracle. */ /** The fix data being served by this oracle.
@Transient var knownFixes = emptyMap<FixOf, Fix>() *
* 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) { set(value) {
require(value.isNotEmpty()) require(value.isNotEmpty())
field = value field = value
@ -130,13 +143,24 @@ object NodeInterestRates {
require(queries.isNotEmpty()) require(queries.isNotEmpty())
val knownFixes = knownFixes // Snapshot 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) val firstNull = answers.indexOf(null)
if (firstNull != -1) if (firstNull != -1)
throw UnknownFix(queries[firstNull]) throw UnknownFix(queries[firstNull])
return answers.filterNotNull() 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 { fun sign(wtx: WireTransaction): DigitalSignature.LegallyIdentifiable {
// Extract the fix commands marked as being signable by us. // Extract the fix commands marked as being signable by us.
val fixes: List<Fix> = wtx.commands. val fixes: List<Fix> = wtx.commands.
@ -150,7 +174,7 @@ object NodeInterestRates {
// For each fix, verify that the data is correct. // For each fix, verify that the data is correct.
val knownFixes = knownFixes // Snapshot val knownFixes = knownFixes // Snapshot
for (fix in fixes) { for (fix in fixes) {
val known = knownFixes[fix.of] val known = getKnownFix(knownFixes, fix.of)
if (known == null || known != fix) if (known == null || known != fix)
throw UnknownFix(fix.of) throw UnknownFix(fix.of)
} }

View File

@ -13,7 +13,6 @@ import contracts.Cash
import core.* import core.*
import core.crypto.SecureHash import core.crypto.SecureHash
import core.messaging.MessagingService import core.messaging.MessagingService
import core.node.services.NetworkMapService
import java.io.InputStream import java.io.InputStream
import java.security.KeyPair import java.security.KeyPair
import java.security.PrivateKey import java.security.PrivateKey
@ -73,8 +72,8 @@ interface WalletService {
*/ */
val linearHeads: Map<SecureHash, StateAndRef<LinearState>> val linearHeads: Map<SecureHash, StateAndRef<LinearState>>
fun <T : LinearState> linearHeadsInstanceOf(clazz: Class<T>, predicate: (T) -> Boolean = { true } ): Map<SecureHash, StateAndRef<LinearState>> { 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) } 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?> { fun statesForRefs(refs: List<StateRef>): Map<StateRef, ContractState?> {

View File

@ -48,7 +48,10 @@ abstract class ProtocolLogic<T> {
return psm.sendAndReceive(topic, destination, sessionIDForSend, sessionIDForReceive, obj, T::class.java) return psm.sendAndReceive(topic, destination, sessionIDForSend, sessionIDForReceive, obj, T::class.java)
} }
inline fun <reified T : Any> receive(topic: String, sessionIDForReceive: Long): UntrustworthyData<T> { 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) { @Suspendable fun send(topic: String, destination: MessageRecipients, sessionID: Long, obj: Any) {
psm.send(topic, destination, sessionID, obj) psm.send(topic, destination, sessionID, obj)

View 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.LegallyIdentifiableNode
import core.node.services.MockNetworkMapService
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 {
legallyIdentifiableNode(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 {
legallyIdentifiableNode(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.networkMapService as MockNetworkMapService).partyNodes.add(node.legallyIdentifiableAddress)
// Add rates oracle to network map
(node.services.networkMapService as MockNetworkMapService).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 = legallyIdentifiableNode(hostAndPortString, identityFile)
(node.services.networkMapService as MockNetworkMapService).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 legallyIdentifiableNode(hostAndPortString: String, identityFile: String): LegallyIdentifiableNode {
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 LegallyIdentifiableNode(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())
}

View File

@ -85,8 +85,8 @@ fun main(args: Array<String>) {
val myNetAddr = HostAndPort.fromString(options.valueOf(networkAddressArg)).withDefaultPort(Node.DEFAULT_PORT) val myNetAddr = HostAndPort.fromString(options.valueOf(networkAddressArg)).withDefaultPort(Node.DEFAULT_PORT)
val listening = options.has(serviceFakeTradesArg) val listening = options.has(serviceFakeTradesArg)
if (listening && config.myLegalName != "Bank of Zurich") { if (listening && config.myLegalName != "Bank A") {
println("The buyer node must have a legal name of 'Bank of Zurich'. Please edit the config file.") println("The buyer node must have a legal name of 'Bank A'. Please edit the config file.")
exitProcess(1) exitProcess(1)
} }

View 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.networkMapService.timestampingNodes[0]
// need to pick which ever party is not us
val otherParty = notUs(*dealToBeOffered.parties).single()
val otherSide = (serviceHub.networkMapService.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
}
}
}

View 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.LegallyIdentifiableNode
import core.node.services.MockNetworkMapService
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.networkMapService.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: LegallyIdentifiableNode, message: ExitMessage) {
if(recipient.address is MockNetworkMapService.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)
}
}
}
}
}

View 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.LegallyIdentifiableNode
import core.node.services.MockNetworkMapService
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): LegallyIdentifiableNode {
val ourKeys = serviceHub.keyManagementService.keys.keys
return serviceHub.networkMapService.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: LegallyIdentifiableNode, 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: LegallyIdentifiableNode, 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: LegallyIdentifiableNode, sessionID: Long): StateAndRef<InterestRateSwap.State>? {
progressTracker.childrenFor[FIXING] = TwoPartyDealProtocol.Primary.tracker()
progressTracker.currentStep = FIXING
val participant = TwoPartyDealProtocol.Floater(party.address, sessionID, serviceHub.networkMapService.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: LegallyIdentifiableNode, sessionID: Long): StateAndRef<InterestRateSwap.State>? {
progressTracker.childrenFor[FIXING] = TwoPartyDealProtocol.Secondary.tracker()
progressTracker.currentStep = FIXING
val participant = TwoPartyDealProtocol.Fixer(party.address, serviceHub.networkMapService.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.networkMapService.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: LegallyIdentifiableNode, message: UpdateBusinessDayMessage) {
if(recipient.address is MockNetworkMapService.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)
}
}
}
}
}

View File

@ -11,8 +11,8 @@ package protocols
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import core.* import core.*
import core.crypto.DigitalSignature import core.crypto.DigitalSignature
import core.node.services.LegallyIdentifiableNode
import core.messaging.SingleMessageRecipient import core.messaging.SingleMessageRecipient
import core.node.services.LegallyIdentifiableNode
import core.protocols.ProtocolLogic import core.protocols.ProtocolLogic
import core.utilities.ProgressTracker import core.utilities.ProgressTracker
import java.math.BigDecimal import java.math.BigDecimal
@ -50,7 +50,7 @@ open class RatesFixProtocol(protected val tx: TransactionBuilder,
@Suspendable @Suspendable
override fun call() { override fun call() {
progressTracker.currentStep = progressTracker.steps[0] progressTracker.currentStep = progressTracker.steps[1]
val fix = query() val fix = query()
progressTracker.currentStep = WORKING progressTracker.currentStep = WORKING
checkFixIsNearExpected(fix) checkFixIsNearExpected(fix)

View File

@ -12,9 +12,9 @@ import co.paralleluniverse.fibers.Suspendable
import core.Party import core.Party
import core.WireTransaction import core.WireTransaction
import core.crypto.DigitalSignature import core.crypto.DigitalSignature
import core.node.services.LegallyIdentifiableNode
import core.messaging.MessageRecipients import core.messaging.MessageRecipients
import core.messaging.StateMachineManager import core.messaging.StateMachineManager
import core.node.services.LegallyIdentifiableNode
import core.node.services.NodeTimestamperService import core.node.services.NodeTimestamperService
import core.node.services.TimestamperService import core.node.services.TimestamperService
import core.protocols.ProtocolLogic import core.protocols.ProtocolLogic

View File

@ -0,0 +1,395 @@
/*
* 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.LegallyIdentifiableNode
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: LegallyIdentifiableNode,
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")
fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, TIMESTAMPING, SENDING_SIGS, RECORDING)
}
@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" }
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: LegallyIdentifiableNode,
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.networkMapService.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: LegallyIdentifiableNode,
dealToFix: StateAndRef<T>,
myKeyPair: KeyPair,
val sessionID: Long,
override val progressTracker: ProgressTracker = Primary.tracker()) : Primary<StateRef>(dealToFix.ref, otherSide, otherSessionID, myKeyPair, timestampingAuthority)
}

View File

@ -15,10 +15,9 @@ import contracts.sumCashBy
import core.* import core.*
import core.crypto.DigitalSignature import core.crypto.DigitalSignature
import core.crypto.signWithECDSA import core.crypto.signWithECDSA
import core.node.services.LegallyIdentifiableNode
import core.messaging.SingleMessageRecipient import core.messaging.SingleMessageRecipient
import core.messaging.StateMachineManager import core.messaging.StateMachineManager
import protocols.TimestampingProtocol import core.node.services.LegallyIdentifiableNode
import core.protocols.ProtocolLogic import core.protocols.ProtocolLogic
import core.utilities.ProgressTracker import core.utilities.ProgressTracker
import core.utilities.trace import core.utilities.trace
@ -26,7 +25,6 @@ import java.security.KeyPair
import java.security.PublicKey import java.security.PublicKey
import java.security.SignatureException import java.security.SignatureException
import java.time.Instant 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 * This asset trading protocol implements a "delivery vs payment" type swap. It has two parties (B and S for buyer

View File

@ -11,8 +11,6 @@ package core
import com.codahale.metrics.MetricRegistry import com.codahale.metrics.MetricRegistry
import core.crypto.* import core.crypto.*
import core.messaging.MessagingService import core.messaging.MessagingService
import core.node.services.MockNetworkMapService
import core.node.services.NetworkMapService
import core.node.services.* import core.node.services.*
import core.serialization.SerializedBytes import core.serialization.SerializedBytes
import core.serialization.deserialize import core.serialization.deserialize

View File

@ -11,7 +11,8 @@ package core.node
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import core.* import core.*
import core.crypto.SecureHash import core.crypto.SecureHash
import core.messaging.* import core.messaging.StateMachineManager
import core.messaging.TestWithInMemoryNetwork
import core.node.services.* import core.node.services.*
import core.protocols.ProtocolLogic import core.protocols.ProtocolLogic
import core.serialization.serialize import core.serialization.serialize

View File

@ -41,11 +41,19 @@ class NodeInterestRatesTest {
@Test fun `query with one success and one missing`() { @Test fun `query with one success and one missing`() {
val q1 = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") 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)) } val e = assertFailsWith<NodeInterestRates.UnknownFix> { service.query(listOf(q1, q2)) }
assertEquals(e.fix, 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`() { @Test fun `empty query`() {
assertFailsWith<IllegalArgumentException> { service.query(emptyList()) } assertFailsWith<IllegalArgumentException> { service.query(emptyList()) }
} }

View File

@ -38,8 +38,6 @@ object TestUtils {
val keypair = generateKeyPair() val keypair = generateKeyPair()
val keypair2 = generateKeyPair() val keypair2 = generateKeyPair()
val keypair3 = generateKeyPair() val keypair3 = generateKeyPair()
val keypair4 = generateKeyPair()
val keypair5 = generateKeyPair()
} }
// A dummy time at which we will be pretending test transactions are created. // 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") val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
@ -51,14 +49,6 @@ val MEGA_CORP_PUBKEY = MEGA_CORP_KEY.public
val MINI_CORP_KEY = TestUtils.keypair2 val MINI_CORP_KEY = TestUtils.keypair2
val MINI_CORP_PUBKEY = MINI_CORP_KEY.public 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_KEY = TestUtils.keypair3
val ORACLE_PUBKEY = ORACLE_KEY.public val ORACLE_PUBKEY = ORACLE_KEY.public
@ -74,17 +64,11 @@ val BOB = BOB_KEY.public
val MEGA_CORP = Party("MegaCorp", MEGA_CORP_PUBKEY) val MEGA_CORP = Party("MegaCorp", MEGA_CORP_PUBKEY)
val MINI_CORP = Party("MiniCorp", MINI_CORP_PUBKEY) val MINI_CORP = Party("MiniCorp", MINI_CORP_PUBKEY)
// TODO remove once mock API is retired val ALL_TEST_KEYS = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, DummyTimestampingAuthority.key)
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, EXCALIBUR_BANK_KEY, A_N_OTHER_BANK_KEY, DummyTimestampingAuthority.key)
val TEST_KEYS_TO_CORP_MAP: Map<PublicKey, Party> = mapOf( val TEST_KEYS_TO_CORP_MAP: Map<PublicKey, Party> = mapOf(
MEGA_CORP_PUBKEY to MEGA_CORP, MEGA_CORP_PUBKEY to MEGA_CORP,
MINI_CORP_PUBKEY to MINI_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 DUMMY_TIMESTAMPER.identity.owningKey to DUMMY_TIMESTAMPER.identity
) )