diff --git a/build.gradle b/build.gradle index 5f41ae66ee..178192bed2 100644 --- a/build.gradle +++ b/build.gradle @@ -144,6 +144,14 @@ 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 @@ -166,5 +174,6 @@ jar.dependsOn quasarScan applicationDistribution.into("bin") { from(getRateFixDemo) + from(getIRSDemo) fileMode = 0755 } \ No newline at end of file diff --git a/contracts/src/main/kotlin/contracts/IRS.kt b/contracts/src/main/kotlin/contracts/IRS.kt index f1291a3d0d..a1d173558a 100644 --- a/contracts/src/main/kotlin/contracts/IRS.kt +++ b/contracts/src/main/kotlin/contracts/IRS.kt @@ -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 = 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 -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. @@ -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 * copy / update for each transition) */ - data class Calculation( + data class Calculation ( val expression: Expression, val floatingLegPaymentSchedule: Map, val fixedLegPaymentSchedule: Map @@ -240,6 +239,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 +305,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 +362,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 +407,7 @@ class InterestRateSwap() : Contract { */ override fun verify(tx: TransactionForVerification) { val command = tx.commands.requireSingleCommand() - 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") val irs = tx.outStates.filterIsInstance().single() @@ -356,7 +458,8 @@ class InterestRateSwap() : Contract { val floatingLeg: FloatingLeg, val calculation: Calculation, val common: Common - ) : LinearState { + ) : FixableDealState { + override val programRef = IRS_PROGRAM_ID override val thread = SecureHash.sha256(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) } + override val parties: Array + 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 */ diff --git a/contracts/src/main/kotlin/contracts/IRSUtils.kt b/contracts/src/main/kotlin/contracts/IRSUtils.kt index f189d6a2cb..0bc634ba95 100644 --- a/contracts/src/main/kotlin/contracts/IRSUtils.kt +++ b/contracts/src/main/kotlin/contracts/IRSUtils.kt @@ -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 + + /** 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. diff --git a/core/src/main/kotlin/core/FinanceTypes.kt b/core/src/main/kotlin/core/FinanceTypes.kt index 817ad3abcf..2b2cef3f3b 100644 --- a/core/src/main/kotlin/core/FinanceTypes.kt +++ b/core/src/main/kotlin/core/FinanceTypes.kt @@ -269,7 +269,7 @@ open class BusinessCalendar private constructor(val calendars: Array 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 } } + 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 diff --git a/core/src/main/kotlin/core/Structures.kt b/core/src/main/kotlin/core/Structures.kt index dbfaa196b3..a29cd728ca 100644 --- a/core/src/main/kotlin/core/Structures.kt +++ b/core/src/main/kotlin/core/Structures.kt @@ -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). */ @@ -56,10 +57,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): Boolean } diff --git a/scripts/example-irs-trade.json b/scripts/example-irs-trade.json new file mode 100644 index 0000000000..40d8aa2170 --- /dev/null +++ b/scripts/example-irs-trade.json @@ -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" +} \ No newline at end of file diff --git a/scripts/example.rates.txt b/scripts/example.rates.txt index 54170698f6..3129a18b21 100644 --- a/scripts/example.rates.txt +++ b/scripts/example.rates.txt @@ -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 diff --git a/scripts/irs-demo.sh b/scripts/irs-demo.sh new file mode 100755 index 0000000000..47fcec9584 --- /dev/null +++ b/scripts/irs-demo.sh @@ -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 diff --git a/src/main/kotlin/api/APIServerImpl.kt b/src/main/kotlin/api/APIServerImpl.kt index 4e5aa68fca..ee9013b315 100644 --- a/src/main/kotlin/api/APIServerImpl.kt +++ b/src/main/kotlin/api/APIServerImpl.kt @@ -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 diff --git a/src/main/kotlin/api/InterestRateSwapAPI.kt b/src/main/kotlin/api/InterestRateSwapAPI.kt new file mode 100644 index 0000000000..992ea1502f --- /dev/null +++ b/src/main/kotlin/api/InterestRateSwapAPI.kt @@ -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() + + 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 { + 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 = 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() + } +} diff --git a/src/main/kotlin/core/node/Node.kt b/src/main/kotlin/core/node/Node.kt index dbc7fa7275..192ed9f29d 100644 --- a/src/main/kotlin/core/node/Node.kt +++ b/src/main/kotlin/core/node/Node.kt @@ -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.LegallyIdentifiableNode import core.node.servlets.AttachmentDownloadServlet import core.node.servlets.DataUploadServlet import core.utilities.loggerFor @@ -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")) diff --git a/src/main/kotlin/core/node/services/NetworkMapService.kt b/src/main/kotlin/core/node/services/NetworkMapService.kt index dd39bccc70..d68cf0e3d0 100644 --- a/src/main/kotlin/core/node/services/NetworkMapService.kt +++ b/src/main/kotlin/core/node/services/NetworkMapService.kt @@ -43,8 +43,8 @@ class MockNetworkMapService : NetworkMapService { override val partyNodes = Collections.synchronizedList(ArrayList()) init { - partyNodes.add(LegallyIdentifiableNode(MockAddress("excalibur:8080"), Party("Excalibur", DummyPublicKey("Excalibur")))) - partyNodes.add(LegallyIdentifiableNode(MockAddress("another:8080"), Party("ANOther", DummyPublicKey("ANOther")))) + partyNodes.add(LegallyIdentifiableNode(MockAddress("bankC:8080"), Party("Bank C", DummyPublicKey("Bank C")))) + partyNodes.add(LegallyIdentifiableNode(MockAddress("bankD:8080"), Party("Bank D", DummyPublicKey("Bank D")))) } } diff --git a/src/main/kotlin/core/node/services/NodeInterestRates.kt b/src/main/kotlin/core/node/services/NodeInterestRates.kt index 422a99bba2..dd4d160c26 100644 --- a/src/main/kotlin/core/node/services/NodeInterestRates.kt +++ b/src/main/kotlin/core/node/services/NodeInterestRates.kt @@ -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 { - val results = HashMap() + fun parseFile(s: String): Map> { + val results = HashMap>() 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 = data. + val fixes: Map> = 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() + /** 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>() set(value) { require(value.isNotEmpty()) field = value @@ -130,13 +143,24 @@ object NodeInterestRates { require(queries.isNotEmpty()) val knownFixes = knownFixes // Snapshot - val answers: List = queries.map { knownFixes[it] } + val answers: List = 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: 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 = 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) } diff --git a/src/main/kotlin/core/node/services/Services.kt b/src/main/kotlin/core/node/services/Services.kt index 4fa81d2b45..628ef96a0f 100644 --- a/src/main/kotlin/core/node/services/Services.kt +++ b/src/main/kotlin/core/node/services/Services.kt @@ -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> - fun linearHeadsInstanceOf(clazz: Class, predicate: (T) -> Boolean = { true } ): Map> { - return linearHeads.filterValues { clazz.isInstance(it.state) }.filterValues { predicate(it.state as T) } + fun linearHeadsInstanceOf(clazz: Class, predicate: (T) -> Boolean = { true } ): Map> { + 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): Map { diff --git a/src/main/kotlin/core/protocols/ProtocolLogic.kt b/src/main/kotlin/core/protocols/ProtocolLogic.kt index d26263309d..6a8b5f6398 100644 --- a/src/main/kotlin/core/protocols/ProtocolLogic.kt +++ b/src/main/kotlin/core/protocols/ProtocolLogic.kt @@ -48,7 +48,10 @@ abstract class ProtocolLogic { return psm.sendAndReceive(topic, destination, sessionIDForSend, sessionIDForReceive, obj, T::class.java) } inline fun receive(topic: String, sessionIDForReceive: Long): UntrustworthyData { - return psm.receive(topic, sessionIDForReceive, T::class.java) + return receive(topic, sessionIDForReceive, T::class.java) + } + @Suspendable fun receive(topic: String, sessionIDForReceive: Long, clazz: Class): UntrustworthyData { + return psm.receive(topic, sessionIDForReceive, clazz) } @Suspendable fun send(topic: String, destination: MessageRecipients, sessionID: Long, obj: Any) { psm.send(topic, destination, sessionID, obj) diff --git a/src/main/kotlin/demos/IRSDemo.kt b/src/main/kotlin/demos/IRSDemo.kt new file mode 100644 index 0000000000..2ed7181923 --- /dev/null +++ b/src/main/kotlin/demos/IRSDemo.kt @@ -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) { + 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(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()) +} diff --git a/src/main/kotlin/demos/TraderDemo.kt b/src/main/kotlin/demos/TraderDemo.kt index 8705b213a6..4ab6cc8632 100644 --- a/src/main/kotlin/demos/TraderDemo.kt +++ b/src/main/kotlin/demos/TraderDemo.kt @@ -85,8 +85,8 @@ fun main(args: Array) { 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) } diff --git a/src/main/kotlin/demos/protocols/AutoOfferProtocol.kt b/src/main/kotlin/demos/protocols/AutoOfferProtocol.kt new file mode 100644 index 0000000000..c91e429c25 --- /dev/null +++ b/src/main/kotlin/demos/protocols/AutoOfferProtocol.kt @@ -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 { + 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() + // 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(val dealToBeOffered: DealState) : ProtocolLogic() { + + 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 { + val notUsParties : MutableList = arrayListOf() + for(party in parties) { + if (serviceHub.storageService.myLegalIdentity != party) { + notUsParties.add(party) + } + } + return notUsParties + } + + } +} diff --git a/src/main/kotlin/demos/protocols/ExitServerProtocol.kt b/src/main/kotlin/demos/protocols/ExitServerProtocol.kt new file mode 100644 index 0000000000..e0b9cdb47f --- /dev/null +++ b/src/main/kotlin/demos/protocols/ExitServerProtocol.kt @@ -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() + 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() { + + @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) + } + } + } + } + +} diff --git a/src/main/kotlin/demos/protocols/UpdateBusinessDayProtocol.kt b/src/main/kotlin/demos/protocols/UpdateBusinessDayProtocol.kt new file mode 100644 index 0000000000..50b4c8117e --- /dev/null +++ b/src/main/kotlin/demos/protocols/UpdateBusinessDayProtocol.kt @@ -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() { + + 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, 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, date: LocalDate, sessionID: Long) { + var dealStateAndRef: StateAndRef? = 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, party: LegallyIdentifiableNode, sessionID: Long): StateAndRef? { + 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, party: LegallyIdentifiableNode, sessionID: Long): StateAndRef? { + 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() + 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() { + + 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) + } + } + } + } + +} diff --git a/src/main/kotlin/protocols/RatesFixProtocol.kt b/src/main/kotlin/protocols/RatesFixProtocol.kt index 7e4a7a2e79..ae644f51ba 100644 --- a/src/main/kotlin/protocols/RatesFixProtocol.kt +++ b/src/main/kotlin/protocols/RatesFixProtocol.kt @@ -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.LegallyIdentifiableNode import core.protocols.ProtocolLogic import core.utilities.ProgressTracker import java.math.BigDecimal @@ -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) diff --git a/src/main/kotlin/protocols/TimestampingProtocol.kt b/src/main/kotlin/protocols/TimestampingProtocol.kt index 1e561de280..796f5e10de 100644 --- a/src/main/kotlin/protocols/TimestampingProtocol.kt +++ b/src/main/kotlin/protocols/TimestampingProtocol.kt @@ -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.LegallyIdentifiableNode import core.node.services.NodeTimestamperService import core.node.services.TimestamperService import core.protocols.ProtocolLogic diff --git a/src/main/kotlin/protocols/TwoPartyDealProtocol.kt b/src/main/kotlin/protocols/TwoPartyDealProtocol.kt new file mode 100644 index 0000000000..5196f7ea6c --- /dev/null +++ b/src/main/kotlin/protocols/TwoPartyDealProtocol.kt @@ -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( + 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(val payload: U, + val otherSide: SingleMessageRecipient, + val otherSessionID: Long, + val myKeyPair: KeyPair, + val timestampingAuthority: LegallyIdentifiableNode, + override val progressTracker: ProgressTracker = Primary.tracker()) : ProtocolLogic() { + + 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 { + progressTracker.currentStep = AWAITING_PROPOSAL + + val sessionID = random63BitValue() + + // Make the first message we'll send to kick off the protocol. + val hello = Handshake(payload, myKeyPair.public, sessionID) + + val maybeSTX = sendAndReceive(DEAL_TOPIC, otherSide, otherSessionID, sessionID, hello) + + return maybeSTX + } + + @Suspendable + fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData): 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(val otherSide: SingleMessageRecipient, + val timestampingAuthority: Party, + val sessionID: Long, + override val progressTracker: ProgressTracker = Secondary.tracker()) : ProtocolLogic() { + + 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 { + 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(DEAL_TOPIC, otherSide, theirSessionID, sessionID, stx).validate { it } + } + + private fun signWithOurKeys(signingPubKeys: List, 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 + @Suspendable protected abstract fun assembleSharedTX(handshake: Handshake): Pair> + } + + /** + * One side of the protocol for inserting a pre-agreed deal. + */ + open class Instigator(otherSide: SingleMessageRecipient, + timestampingAuthority: LegallyIdentifiableNode, + dealBeingOffered: T, + myKeyPair: KeyPair, + buyerSessionID: Long, + override val progressTracker: ProgressTracker = Primary.tracker()) : Primary(dealBeingOffered, otherSide, buyerSessionID, myKeyPair, timestampingAuthority) + + /** + * One side of the protocol for inserting a pre-agreed deal. + */ + open class Acceptor(otherSide: SingleMessageRecipient, + timestampingAuthority: Party, + val dealToBuy: T, + sessionID: Long, + override val progressTracker: ProgressTracker = Secondary.tracker()) : Secondary(otherSide, timestampingAuthority, sessionID) { + + @Suspendable + override fun validateHandshake(handshake: Handshake<*>): Handshake { + with(handshake as Handshake) { + // 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): Pair> { + 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(otherSide: SingleMessageRecipient, + timestampingAuthority: Party, + val dealToFix: StateAndRef, + sessionID: Long, + override val progressTracker: ProgressTracker = Secondary.tracker()) : Secondary(otherSide, timestampingAuthority, sessionID) { + + @Suspendable + override fun validateHandshake(handshake: Handshake<*>): Handshake { + with(handshake as Handshake) { + 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): Pair> { + 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(otherSide: SingleMessageRecipient, + otherSessionID: Long, + timestampingAuthority: LegallyIdentifiableNode, + dealToFix: StateAndRef, + myKeyPair: KeyPair, + val sessionID: Long, + override val progressTracker: ProgressTracker = Primary.tracker()) : Primary(dealToFix.ref, otherSide, otherSessionID, myKeyPair, timestampingAuthority) +} \ No newline at end of file diff --git a/src/main/kotlin/protocols/TwoPartyTradeProtocol.kt b/src/main/kotlin/protocols/TwoPartyTradeProtocol.kt index c192274c39..dc2c06da52 100644 --- a/src/main/kotlin/protocols/TwoPartyTradeProtocol.kt +++ b/src/main/kotlin/protocols/TwoPartyTradeProtocol.kt @@ -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.LegallyIdentifiableNode 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 diff --git a/src/test/kotlin/core/MockServices.kt b/src/test/kotlin/core/MockServices.kt index c57f830112..2a306900be 100644 --- a/src/test/kotlin/core/MockServices.kt +++ b/src/test/kotlin/core/MockServices.kt @@ -11,8 +11,6 @@ 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 diff --git a/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt b/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt index 91b9fa3d1d..734afc4f13 100644 --- a/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt +++ b/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt @@ -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 diff --git a/src/test/kotlin/core/node/services/NodeInterestRatesTest.kt b/src/test/kotlin/core/node/services/NodeInterestRatesTest.kt index 69cfcad321..d75aef9427 100644 --- a/src/test/kotlin/core/node/services/NodeInterestRatesTest.kt +++ b/src/test/kotlin/core/node/services/NodeInterestRatesTest.kt @@ -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 { 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 { service.query(emptyList()) } } diff --git a/src/test/kotlin/core/testutils/TestUtils.kt b/src/test/kotlin/core/testutils/TestUtils.kt index f5499cab3a..68aac26d83 100644 --- a/src/test/kotlin/core/testutils/TestUtils.kt +++ b/src/test/kotlin/core/testutils/TestUtils.kt @@ -38,8 +38,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 +49,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,17 +64,11 @@ 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, EXCALIBUR_BANK_KEY, A_N_OTHER_BANK_KEY, DummyTimestampingAuthority.key) +val ALL_TEST_KEYS = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, DummyTimestampingAuthority.key) val TEST_KEYS_TO_CORP_MAP: Map = 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 )