mirror of
https://github.com/corda/corda.git
synced 2024-12-19 13:08:04 +00:00
Merge from master
This commit is contained in:
commit
1d25d23b73
1
.idea/runConfigurations/Clean___Install.xml
generated
1
.idea/runConfigurations/Clean___Install.xml
generated
@ -11,6 +11,7 @@
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="clean" />
|
||||
<option value="build" />
|
||||
<option value="installDist" />
|
||||
<option value="buildCordaJAR" />
|
||||
</list>
|
||||
|
43
build.gradle
43
build.gradle
@ -30,7 +30,6 @@ plugins {
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
apply plugin: 'project-report'
|
||||
apply plugin: QuasarPlugin
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
@ -93,14 +92,8 @@ configurations {
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
}
|
||||
|
||||
// This is required for quasar. I think.
|
||||
applicationDefaultJvmArgs = ["-javaagent:${configurations.quasar.singleFile}"]
|
||||
// Needed by the :startScripts task
|
||||
mainClassName = 'com.r3corda.demos.TraderDemoKt'
|
||||
|
||||
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
|
||||
// build/reports/project/dependencies/index.html for green highlighted parts of the tree.
|
||||
|
||||
dependencies {
|
||||
compile project(':node')
|
||||
// TODO: Demos should not depend on test code, but only use production APIs
|
||||
@ -121,41 +114,11 @@ dependencies {
|
||||
integrationTestCompile project(':test-utils')
|
||||
}
|
||||
|
||||
// Package up the demo programs.
|
||||
|
||||
task getAttachmentDemo(type: CreateStartScripts) {
|
||||
mainClassName = "com.r3corda.demos.attachment.AttachmentDemoKt"
|
||||
applicationName = "attachment-demo"
|
||||
defaultJvmOpts = ["-javaagent:${configurations.quasar.singleFile}"]
|
||||
outputDir = new File(project.buildDir, 'scripts')
|
||||
classpath = jar.outputs.files + project.configurations.runtime
|
||||
}
|
||||
|
||||
task getTraderDemo(type: CreateStartScripts) {
|
||||
mainClassName = "com.r3corda.demos.TraderDemoKt"
|
||||
applicationName = "trader-demo"
|
||||
defaultJvmOpts = ["-javaagent:${configurations.quasar.singleFile}"]
|
||||
outputDir = new File(project.buildDir, 'scripts')
|
||||
classpath = jar.outputs.files + project.configurations.runtime
|
||||
}
|
||||
|
||||
// Force windows script classpath to wildcard path to avoid the 'Command Line Is Too Long' issues
|
||||
// with generated scripts. Include Jolokia .war explicitly as this isn't picked up by wildcard
|
||||
tasks.withType(CreateStartScripts) {
|
||||
doLast {
|
||||
windowsScript.text = windowsScript
|
||||
.readLines()
|
||||
.collect { line -> line.replaceAll(~/^set CLASSPATH=.*$/, 'set CLASSPATH=%APP_HOME%/lib/*;%APP_HOME%/lib/jolokia-agent-war-'+project.ext.jolokia_version+'.war') }
|
||||
.join('\r\n')
|
||||
}
|
||||
}
|
||||
|
||||
task integrationTest(type: Test, dependsOn: [':node:integrationTest',':client:integrationTest']) {
|
||||
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
|
||||
|
||||
task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
|
||||
dependsOn = subprojects.test
|
||||
additionalSourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs)
|
||||
@ -183,12 +146,6 @@ tasks.withType(Test) {
|
||||
|
||||
quasarScan.dependsOn('classes', 'core:classes', 'contracts:classes', 'node:classes')
|
||||
|
||||
applicationDistribution.into("bin") {
|
||||
from(getAttachmentDemo)
|
||||
from(getTraderDemo)
|
||||
fileMode = 0755
|
||||
}
|
||||
|
||||
task buildCordaJAR(type: FatCapsule, dependsOn: ['quasarScan', 'buildCertSigningRequestUtilityJAR']) {
|
||||
applicationClass 'com.r3corda.node.MainKt'
|
||||
archiveName 'corda.jar'
|
||||
|
0
buildSrc/scripts/runnodes
Normal file → Executable file
0
buildSrc/scripts/runnodes
Normal file → Executable file
@ -40,7 +40,7 @@ sourceSets {
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
clients(MavenPublication) {
|
||||
client(MavenPublication) {
|
||||
from components.java
|
||||
artifactId 'client'
|
||||
|
||||
|
@ -66,7 +66,7 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||
)
|
||||
) {
|
||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Currency>>>
|
||||
= tx.groupStates<State, Issued<Currency>> { it.issuanceDef }
|
||||
= tx.groupStates<State, Issued<Currency>> { it.amount.token }
|
||||
}
|
||||
|
||||
class Issue : AbstractIssue<State, Commands, Currency>(
|
||||
@ -90,16 +90,14 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||
constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: PublicKey)
|
||||
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
|
||||
|
||||
override val deposit = amount.token.issuer
|
||||
override val exitKeys = setOf(owner, deposit.party.owningKey)
|
||||
override val exitKeys = setOf(owner, amount.token.issuer.party.owningKey)
|
||||
override val contract = CASH_PROGRAM_ID
|
||||
override val issuanceDef = amount.token
|
||||
override val participants = listOf(owner)
|
||||
|
||||
override fun move(newAmount: Amount<Issued<Currency>>, newOwner: PublicKey): FungibleAsset<Currency>
|
||||
= copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner)
|
||||
|
||||
override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})"
|
||||
override fun toString() = "${Emoji.bagOfCash}Cash($amount at ${amount.token.issuer} owned by ${owner.toStringShort()})"
|
||||
|
||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
|
||||
@ -197,8 +195,8 @@ fun Iterable<ContractState>.sumCashOrZero(currency: Issued<Currency>): Amount<Is
|
||||
}
|
||||
|
||||
fun Cash.State.ownedBy(owner: PublicKey) = copy(owner = owner)
|
||||
fun Cash.State.issuedBy(party: Party) = copy(amount = Amount(amount.quantity, issuanceDef.copy(issuer = deposit.copy(party = party))))
|
||||
fun Cash.State.issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, issuanceDef.copy(issuer = deposit)))
|
||||
fun Cash.State.issuedBy(party: Party) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = amount.token.issuer.copy(party = party))))
|
||||
fun Cash.State.issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = deposit)))
|
||||
fun Cash.State.withDeposit(deposit: PartyAndReference): Cash.State = copy(amount = amount.copy(token = amount.token.copy(issuer = deposit)))
|
||||
|
||||
infix fun Cash.State.`owned by`(owner: PublicKey) = ownedBy(owner)
|
||||
|
@ -72,7 +72,7 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.Commands, C
|
||||
* Group commodity states by issuance definition (issuer and underlying commodity).
|
||||
*/
|
||||
override fun groupStates(tx: TransactionForContract)
|
||||
= tx.groupStates<State, Issued<Commodity>> { it.issuanceDef }
|
||||
= tx.groupStates<State, Issued<Commodity>> { it.amount.token }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -101,16 +101,14 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.Commands, C
|
||||
constructor(deposit: PartyAndReference, amount: Amount<Commodity>, owner: PublicKey)
|
||||
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
|
||||
|
||||
override val deposit = amount.token.issuer
|
||||
override val contract = COMMODITY_PROGRAM_ID
|
||||
override val exitKeys = Collections.singleton(owner)
|
||||
override val issuanceDef = amount.token
|
||||
override val participants = listOf(owner)
|
||||
|
||||
override fun move(newAmount: Amount<Issued<Commodity>>, newOwner: PublicKey): FungibleAsset<Commodity>
|
||||
= copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner)
|
||||
|
||||
override fun toString() = "Commodity($amount at $deposit owned by ${owner.toStringShort()})"
|
||||
override fun toString() = "Commodity($amount at ${amount.token.issuer} owned by ${owner.toStringShort()})"
|
||||
|
||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ class Obligation<P> : Contract {
|
||||
)
|
||||
) {
|
||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<Obligation.State<P>, Issued<Terms<P>>>>
|
||||
= tx.groupStates<Obligation.State<P>, Issued<Terms<P>>> { it.issuanceDef }
|
||||
= tx.groupStates<Obligation.State<P>, Issued<Terms<P>>> { it.amount.token }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -152,7 +152,7 @@ class Obligation<P> : Contract {
|
||||
.filter { it.contract.legalContractReference in template.acceptableContracts }
|
||||
// Restrict the states to those of the correct issuance definition (this normally
|
||||
// covers issued product and obligor, but is opaque to us)
|
||||
.filter { it.issuanceDef in template.acceptableIssuedProducts }
|
||||
.filter { it.amount.token in template.acceptableIssuedProducts }
|
||||
// Catch that there's nothing useful here, so we can dump out a useful error
|
||||
requireThat {
|
||||
"there are fungible asset state outputs" by (assetStates.size > 0)
|
||||
@ -164,7 +164,7 @@ class Obligation<P> : Contract {
|
||||
// this one.
|
||||
val moveCommands = tx.commands.select<MoveCommand>()
|
||||
var totalPenniesSettled = 0L
|
||||
val requiredSigners = inputs.map { it.deposit.party.owningKey }.toSet()
|
||||
val requiredSigners = inputs.map { it.amount.token.issuer.party.owningKey }.toSet()
|
||||
|
||||
for ((beneficiary, obligations) in inputs.groupBy { it.owner }) {
|
||||
val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull<P>()
|
||||
@ -268,21 +268,12 @@ class Obligation<P> : Contract {
|
||||
/** The public key of the entity the contract pays to */
|
||||
val beneficiary: PublicKey
|
||||
) : FungibleAsset<Obligation.Terms<P>>, NettableState<State<P>, MultilateralNetState<P>> {
|
||||
override val amount: Amount<Issued<Terms<P>>>
|
||||
get() = Amount(quantity, issuanceDef)
|
||||
override val amount: Amount<Issued<Terms<P>>> = Amount(quantity, Issued(obligor.ref(0), template))
|
||||
override val contract = OBLIGATION_PROGRAM_ID
|
||||
override val deposit: PartyAndReference
|
||||
get() = amount.token.issuer
|
||||
override val exitKeys: Collection<PublicKey>
|
||||
get() = setOf(owner)
|
||||
val dueBefore: Instant
|
||||
get() = template.dueBefore
|
||||
override val issuanceDef: Issued<Terms<P>>
|
||||
get() = Issued(obligor.ref(0), template)
|
||||
override val participants: List<PublicKey>
|
||||
get() = listOf(obligor.owningKey, beneficiary)
|
||||
override val owner: PublicKey
|
||||
get() = beneficiary
|
||||
override val exitKeys: Collection<PublicKey> = setOf(beneficiary)
|
||||
val dueBefore: Instant = template.dueBefore
|
||||
override val participants: List<PublicKey> = listOf(obligor.owningKey, beneficiary)
|
||||
override val owner: PublicKey = beneficiary
|
||||
|
||||
override fun move(newAmount: Amount<Issued<Terms<P>>>, newOwner: PublicKey): State<P>
|
||||
= copy(quantity = newAmount.quantity, beneficiary = newOwner)
|
||||
@ -522,7 +513,7 @@ class Obligation<P> : Contract {
|
||||
require(states.all { it.lifecycle == existingLifecycle }) { "initial lifecycle must be $existingLifecycle for all input states" }
|
||||
|
||||
// Produce a new set of states
|
||||
val groups = statesAndRefs.groupBy { it.state.data.issuanceDef }
|
||||
val groups = statesAndRefs.groupBy { it.state.data.amount.token }
|
||||
for ((aggregateState, stateAndRefs) in groups) {
|
||||
val partiesUsed = ArrayList<PublicKey>()
|
||||
stateAndRefs.forEach { stateAndRef ->
|
||||
@ -608,7 +599,7 @@ class Obligation<P> : Contract {
|
||||
|
||||
/** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */
|
||||
private fun getIssuanceDefinitionOrThrow(states: Iterable<State<P>>): Issued<Terms<P>> =
|
||||
states.map { it.issuanceDef }.distinct().single()
|
||||
states.map { it.amount.token }.distinct().single()
|
||||
|
||||
/** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */
|
||||
private fun getTermsOrThrow(states: Iterable<State<P>>) =
|
||||
|
@ -1,12 +1,7 @@
|
||||
package com.r3corda.contracts.clause
|
||||
|
||||
import com.r3corda.core.contracts.FungibleAsset
|
||||
import com.r3corda.core.contracts.InsufficientBalanceException
|
||||
import com.r3corda.core.contracts.sumFungibleOrNull
|
||||
import com.r3corda.core.contracts.sumFungibleOrZero
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.Clause
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
@ -68,7 +63,7 @@ abstract class AbstractConserveAmount<S : FungibleAsset<T>, C : CommandData, T :
|
||||
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, Amount(amount.quantity, currency))
|
||||
val takeChangeFrom = gathered.lastOrNull()
|
||||
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
|
||||
Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
|
||||
Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.amount.token)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ import java.util.*
|
||||
import kotlin.test.*
|
||||
|
||||
class CashTests {
|
||||
val defaultRef = OpaqueBytes(ByteArray(1, {1}))
|
||||
val defaultRef = OpaqueBytes(ByteArray(1, { 1 }))
|
||||
val defaultIssuer = MEGA_CORP.ref(defaultRef)
|
||||
val inState = Cash.State(
|
||||
amount = 1000.DOLLARS `issued by` defaultIssuer,
|
||||
@ -43,7 +43,7 @@ class CashTests {
|
||||
val outState = issuerInState.copy(owner = DUMMY_PUBKEY_2)
|
||||
|
||||
fun Cash.State.editDepositRef(ref: Byte) = copy(
|
||||
amount = Amount(amount.quantity, token = amount.token.copy(deposit.copy(reference = OpaqueBytes.of(ref))))
|
||||
amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref))))
|
||||
)
|
||||
|
||||
lateinit var services: MockServices
|
||||
@ -85,12 +85,6 @@ class CashTests {
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
LogHelper.reset(NodeVaultService::class)
|
||||
dataSource.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun trivial() {
|
||||
transaction {
|
||||
@ -173,7 +167,7 @@ class CashTests {
|
||||
assertTrue(tx.inputs.isEmpty())
|
||||
val s = tx.outputs[0].data as Cash.State
|
||||
assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount)
|
||||
assertEquals(MINI_CORP, s.deposit.party)
|
||||
assertEquals(MINI_CORP, s.amount.token.issuer.party)
|
||||
assertEquals(DUMMY_PUBKEY_1, s.owner)
|
||||
assertTrue(tx.commands[0].value is Cash.Commands.Issue)
|
||||
assertEquals(MINI_CORP_PUBKEY, tx.commands[0].signers[0])
|
||||
@ -492,7 +486,7 @@ class CashTests {
|
||||
}
|
||||
|
||||
fun makeSpend(amount: Amount<Currency>, dest: PublicKey): WireTransaction {
|
||||
var tx = TransactionType.General.Builder(DUMMY_NOTARY)
|
||||
val tx = TransactionType.General.Builder(DUMMY_NOTARY)
|
||||
databaseTransaction(database) {
|
||||
vault.generateSpend(tx, amount, dest)
|
||||
}
|
||||
@ -618,8 +612,8 @@ class CashTests {
|
||||
assertEquals(vaultState0.ref, wtx.inputs[0])
|
||||
assertEquals(vaultState1.ref, wtx.inputs[1])
|
||||
assertEquals(vaultState2.ref, wtx.inputs[2])
|
||||
assertEquals(vaultState0.state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
|
||||
assertEquals(vaultState2.state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1].data)
|
||||
assertEquals(vaultState0.state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data)
|
||||
assertEquals(vaultState2.state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0].data)
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
}
|
||||
@ -650,22 +644,22 @@ class CashTests {
|
||||
val oneThousandDollarsFromMini = Cash.State(1000.DOLLARS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY)
|
||||
|
||||
// Obviously it must be possible to aggregate states with themselves
|
||||
assertEquals(fiveThousandDollarsFromMega.issuanceDef, fiveThousandDollarsFromMega.issuanceDef)
|
||||
assertEquals(fiveThousandDollarsFromMega.amount.token, fiveThousandDollarsFromMega.amount.token)
|
||||
|
||||
// Owner is not considered when calculating whether it is possible to aggregate states
|
||||
assertEquals(fiveThousandDollarsFromMega.issuanceDef, twoThousandDollarsFromMega.issuanceDef)
|
||||
assertEquals(fiveThousandDollarsFromMega.amount.token, twoThousandDollarsFromMega.amount.token)
|
||||
|
||||
// States cannot be aggregated if the deposit differs
|
||||
assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, oneThousandDollarsFromMini.issuanceDef)
|
||||
assertNotEquals(twoThousandDollarsFromMega.issuanceDef, oneThousandDollarsFromMini.issuanceDef)
|
||||
assertNotEquals(fiveThousandDollarsFromMega.amount.token, oneThousandDollarsFromMini.amount.token)
|
||||
assertNotEquals(twoThousandDollarsFromMega.amount.token, oneThousandDollarsFromMini.amount.token)
|
||||
|
||||
// States cannot be aggregated if the currency differs
|
||||
assertNotEquals(oneThousandDollarsFromMini.issuanceDef,
|
||||
Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY).issuanceDef)
|
||||
assertNotEquals(oneThousandDollarsFromMini.amount.token,
|
||||
Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY).amount.token)
|
||||
|
||||
// States cannot be aggregated if the reference differs
|
||||
assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, (fiveThousandDollarsFromMega `with deposit` defaultIssuer).issuanceDef)
|
||||
assertNotEquals((fiveThousandDollarsFromMega `with deposit` defaultIssuer).issuanceDef, fiveThousandDollarsFromMega.issuanceDef)
|
||||
assertNotEquals(fiveThousandDollarsFromMega.amount.token, (fiveThousandDollarsFromMega `with deposit` defaultIssuer).amount.token)
|
||||
assertNotEquals((fiveThousandDollarsFromMega `with deposit` defaultIssuer).amount.token, fiveThousandDollarsFromMega.amount.token)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -245,7 +245,7 @@ class ObligationTests {
|
||||
val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)
|
||||
val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE_PUBKEY)
|
||||
val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
||||
Obligation<Currency>().generatePaymentNetting(this, obligationAliceToBob.issuanceDef, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
|
||||
Obligation<Currency>().generatePaymentNetting(this, obligationAliceToBob.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
|
||||
signWith(ALICE_KEY)
|
||||
signWith(BOB_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
@ -259,7 +259,7 @@ class ObligationTests {
|
||||
val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)
|
||||
val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(BOB, ALICE_PUBKEY)
|
||||
val tx = TransactionType.General.Builder(null).apply {
|
||||
Obligation<Currency>().generatePaymentNetting(this, obligationAliceToBob.issuanceDef, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
|
||||
Obligation<Currency>().generatePaymentNetting(this, obligationAliceToBob.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
|
||||
signWith(ALICE_KEY)
|
||||
signWith(BOB_KEY)
|
||||
}.toSignedTransaction().tx
|
||||
@ -453,7 +453,7 @@ class ObligationTests {
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Alice's $1,000,000")
|
||||
output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.issuanceDef)) }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
|
||||
this.verifies()
|
||||
}
|
||||
@ -467,7 +467,7 @@ class ObligationTests {
|
||||
input(500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY)
|
||||
output("Alice's $500,000 obligation to Bob") { halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) }
|
||||
output("Bob's $500,000") { 500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.issuanceDef)) }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
|
||||
this.verifies()
|
||||
}
|
||||
@ -480,7 +480,7 @@ class ObligationTests {
|
||||
input(defaultedObligation) // Alice's defaulted $1,000,000 obligation to Bob
|
||||
input(1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY)
|
||||
output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.issuanceDef)) }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
|
||||
this `fails with` "all inputs are in the normal state"
|
||||
}
|
||||
@ -493,7 +493,7 @@ class ObligationTests {
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Alice's $1,000,000")
|
||||
output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.issuanceDef)) }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
|
||||
this `fails with` "amount in settle command"
|
||||
}
|
||||
@ -517,7 +517,7 @@ class ObligationTests {
|
||||
input("Alice's 1 FCOJ obligation to Bob")
|
||||
input("Alice's 1 FCOJ")
|
||||
output("Bob's 1 FCOJ") { CommodityContract.State(oneUnitFcoj, BOB_PUBKEY) }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneUnitFcoj.quantity, oneUnitFcojObligation.issuanceDef)) }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneUnitFcoj.quantity, oneUnitFcojObligation.amount.token)) }
|
||||
command(ALICE_PUBKEY) { CommodityContract.Commands.Move(Obligation<Commodity>().legalContractReference) }
|
||||
verifies()
|
||||
}
|
||||
@ -648,13 +648,13 @@ class ObligationTests {
|
||||
output { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) }
|
||||
|
||||
tweak {
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(100.DOLLARS.quantity, inState.issuanceDef)) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(100.DOLLARS.quantity, inState.amount.token)) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() }
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
|
||||
tweak {
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.issuanceDef)) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token)) }
|
||||
this `fails with` "required com.r3corda.core.contracts.FungibleAsset.Commands.Move command"
|
||||
|
||||
tweak {
|
||||
@ -679,10 +679,10 @@ class ObligationTests {
|
||||
|
||||
this `fails with` "for reference [00] at issuer MegaCorp the amounts balance"
|
||||
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.issuanceDef.copy(product = megaCorpDollarSettlement))) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token.copy(product = megaCorpDollarSettlement))) }
|
||||
this `fails with` "for reference [00] at issuer MegaCorp the amounts balance"
|
||||
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.POUNDS.quantity, inState.issuanceDef.copy(product = megaCorpPoundSettlement))) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.POUNDS.quantity, inState.amount.token.copy(product = megaCorpPoundSettlement))) }
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
|
@ -85,6 +85,9 @@ dependencies {
|
||||
|
||||
// JPA 2.1 annotations.
|
||||
compile "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final"
|
||||
|
||||
// RS API: Response type and codes for ApiUtils.
|
||||
compile "javax.ws.rs:javax.ws.rs-api:2.0"
|
||||
}
|
||||
|
||||
publishing {
|
||||
|
@ -21,12 +21,6 @@ class InsufficientBalanceException(val amountMissing: Amount<*>) : Exception() {
|
||||
* (GBP, USD, oil, shares in company <X>, etc.) and any additional metadata (issuer, grade, class, etc.).
|
||||
*/
|
||||
interface FungibleAsset<T> : OwnableState {
|
||||
/**
|
||||
* Where the underlying asset backing this ledger entry can be found. The reference
|
||||
* is only intended for use by the issuer, and is not intended to be meaningful to others.
|
||||
*/
|
||||
val deposit: PartyAndReference
|
||||
val issuanceDef: Issued<T>
|
||||
val amount: Amount<Issued<T>>
|
||||
/**
|
||||
* There must be an ExitCommand signed by these keys to destroy the amount. While all states require their
|
||||
@ -55,7 +49,6 @@ interface FungibleAsset<T> : OwnableState {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Small DSL extensions.
|
||||
|
||||
/** Sums the asset states in the list, returning null if there are none. */
|
||||
|
@ -30,8 +30,8 @@ abstract class CordaPluginRegistry {
|
||||
|
||||
/**
|
||||
* List of additional long lived services to be hosted within the node.
|
||||
* They are expected to have a single parameter constructor that takes a ServiceHubInternal as input.
|
||||
* The ServiceHubInternal will be fully constructed before the plugin service is created and will
|
||||
* They are expected to have a single parameter constructor that takes a [PluginServiceHub] as input.
|
||||
* The [PluginServiceHub] will be fully constructed before the plugin service is created and will
|
||||
* allow access to the protocol factory and protocol initiation entry points there.
|
||||
*/
|
||||
open val servicePlugins: List<Class<*>> = emptyList()
|
||||
|
@ -0,0 +1,30 @@
|
||||
package com.r3corda.core.node
|
||||
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* A service hub to be used by the [CordaPluginRegistry]
|
||||
*/
|
||||
interface PluginServiceHub : ServiceHub {
|
||||
/**
|
||||
* Register the protocol factory we wish to use when a initiating party attempts to communicate with us. The
|
||||
* registration is done against a marker [KClass] which is sent in the session handshake by the other party. If this
|
||||
* marker class has been registered then the corresponding factory will be used to create the protocol which will
|
||||
* communicate with the other side. If there is no mapping then the session attempt is rejected.
|
||||
* @param markerClass The marker [KClass] present in a session initiation attempt, which is a 1:1 mapping to a [Class]
|
||||
* using the <pre>::class</pre> construct. Conventionally this is a [ProtocolLogic] subclass, however any class can
|
||||
* be used, with the default being the class of the initiating protocol. This enables the registration to be of the
|
||||
* form: registerProtocolInitiator(InitiatorProtocol::class, ::InitiatedProtocol)
|
||||
* @param protocolFactory The protocol factory generating the initiated protocol.
|
||||
*/
|
||||
|
||||
// TODO: remove dependency on Kotlin relfection (Kotlin KClass -> Java Class).
|
||||
fun registerProtocolInitiator(markerClass: KClass<*>, protocolFactory: (Party) -> ProtocolLogic<*>)
|
||||
|
||||
/**
|
||||
* Return the protocol factory that has been registered with [markerClass], or null if no factory is found.
|
||||
*/
|
||||
fun getProtocolFactory(markerClass: Class<*>): ((Party) -> ProtocolLogic<*>)?
|
||||
}
|
@ -177,7 +177,9 @@ interface VaultService {
|
||||
fun getTransactionNotes(txnId: SecureHash): Iterable<String>
|
||||
|
||||
/**
|
||||
* Fungible Asset operations
|
||||
* [InsufficientBalanceException] is thrown when a Cash Spending transaction fails because
|
||||
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
|
||||
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
|
||||
**/
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
fun generateSpend(tx: TransactionBuilder,
|
||||
|
27
core/src/main/kotlin/com/r3corda/core/utilities/ApiUtils.kt
Normal file
27
core/src/main/kotlin/com/r3corda/core/utilities/ApiUtils.kt
Normal file
@ -0,0 +1,27 @@
|
||||
package com.r3corda.core.utilities
|
||||
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.parsePublicKeyBase58
|
||||
import com.r3corda.core.node.ServiceHub
|
||||
import javax.ws.rs.core.Response
|
||||
|
||||
/**
|
||||
* Utility functions to reduce boilerplate when developing HTTP APIs
|
||||
*/
|
||||
class ApiUtils(val services: ServiceHub) {
|
||||
private val defaultNotFound = { msg: String -> Response.status(Response.Status.NOT_FOUND).entity(msg).build() }
|
||||
|
||||
/**
|
||||
* Get a party and then execute the passed function with the party public key as a parameter.
|
||||
* Usage: withParty(key) { doSomethingWith(it) }
|
||||
*/
|
||||
fun withParty(partyKeyStr: String, notFound: (String) -> Response = defaultNotFound, found: (Party) -> Response): Response {
|
||||
return try {
|
||||
val partyKey = parsePublicKeyBase58(partyKeyStr)
|
||||
val party = services.identityService.partyFromKey(partyKey)
|
||||
if(party == null) notFound("Unknown party") else found(party)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
notFound("Invalid base58 key passed for party key")
|
||||
}
|
||||
}
|
||||
}
|
@ -230,6 +230,10 @@ class X509UtilitiesTest {
|
||||
clientParams.endpointIdentificationAlgorithm = "HTTPS" // enable hostname checking
|
||||
clientSocket.sslParameters = clientParams
|
||||
clientSocket.useClientMode = true
|
||||
// We need to specify this explicitly because by default the client binds to 'localhost' and we want it to bind
|
||||
// to whatever <hostname> resolves to(as that's what the server binds to). In particular on Debian <hostname>
|
||||
// resolves to 127.0.1.1 instead of the external address of the interface, so the ssl handshake fails.
|
||||
clientSocket.bind(InetSocketAddress(InetAddress.getLocalHost(), 0))
|
||||
|
||||
val lock = Object()
|
||||
var done = false
|
||||
|
@ -88,6 +88,13 @@
|
||||
year = 2013
|
||||
}
|
||||
|
||||
@misc{BIP32,
|
||||
title = "Hierarchical deterministic wallets",
|
||||
author = "{{Pieter Wiulle}}",
|
||||
howpublished = "{\url{https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki}}",
|
||||
year = 2013
|
||||
}
|
||||
|
||||
@misc{HBBFT,
|
||||
author = {Andrew Miller and Yu Xia and Kyle Croman and Elaine Shi and Dawn Song},
|
||||
title = "{{The Honey Badger of BFT Protocols}}",
|
||||
@ -137,3 +144,16 @@
|
||||
address = {New York, NY, USA},
|
||||
keywords = {Large-Scale Distributed Storage},
|
||||
}
|
||||
|
||||
@misc{JavaTimeScale,
|
||||
title = "{{java.time.Instant documentation}}",
|
||||
howpublished = "{\url{https://docs.oracle.com/javase/8/docs/api/java/time/Instant.html}}",
|
||||
year = 2014
|
||||
}
|
||||
|
||||
@misc{ZipFormat,
|
||||
title = {Zip file format},
|
||||
howpublished = {\url{https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT}},
|
||||
year = 1989,
|
||||
author = {PKWARE}
|
||||
}
|
@ -16,6 +16,8 @@
|
||||
\usepackage[nottoc]{tocbibind}
|
||||
\usepackage[parfill]{parskip}
|
||||
\usepackage{textcomp}
|
||||
\usepackage{scrextend}
|
||||
\addtokomafont{labelinglabel}{\sffamily}
|
||||
%\usepackage[natbibapa]{apacite}
|
||||
\renewcommand{\thefootnote}{\alph{footnote}}
|
||||
|
||||
@ -159,8 +161,8 @@ A Corda network consists of the following components:
|
||||
\item A network map service that publishes information about nodes on the network.
|
||||
\item One or more notary services. A notary may itself be distributed over multiple nodes.
|
||||
\item Zero or more oracle services. An oracle is a well known service that signs transactions if they state a fact
|
||||
and that fact is considered to be true. This is how the ledger can be connected to the real world, despite being
|
||||
fully deterministic.
|
||||
and that fact is considered to be true. They may also optionally also provide the facts. This is how the ledger can be
|
||||
connected to the real world, despite being fully deterministic.
|
||||
\end{itemize}
|
||||
|
||||
A purely in-memory implementation of the messaging subsystem is provided which can inject simulated latency between
|
||||
@ -181,11 +183,11 @@ identities it signs are globally unique. Thus an entirely anonymous Corda networ
|
||||
IP obfuscation system like Tor is also used.
|
||||
|
||||
Whilst simple string identities are likely sufficient for some networks, the financial industry typically requires some
|
||||
level of \emph{know your customer} checking, and differentiation between different legal entities that may share
|
||||
the same brand name. Corda reuses the standard PKIX infrastructure for connecting public keys to identities and thus
|
||||
names are actually X.500 names. When a single string is sufficient the \emph{common name} field can be used alone,
|
||||
similar to the web PKI. In more complex deployments the additional structure X.500 provides may be useful to
|
||||
differentiate between entities with the same name. For example there are at least five different companies called
|
||||
level of \emph{know your customer} checking, and differentiation between different legal entities, branches and desks
|
||||
that may share the same brand name. Corda reuses the standard PKIX infrastructure for connecting public keys to
|
||||
identities and thus names are actually X.500 names. When a single string is sufficient the \emph{common name} field can
|
||||
be used alone, similar to the web PKI. In more complex deployments the additional structure X.500 provides may be useful
|
||||
to differentiate between entities with the same name. For example there are at least five different companies called
|
||||
\emph{American Savings Bank} and in the past there may have been more than 40 independent banks with that name.
|
||||
|
||||
More complex notions of identity that may attest to many time-varying attributes are not handled at this layer of the
|
||||
@ -194,10 +196,10 @@ themselves may still contain anonymous public keys.
|
||||
|
||||
\subsection{The network map}
|
||||
|
||||
Every network require a network map service, which may itself be composed of multiple cooperating nodes. This is
|
||||
Every network requires a network map service, which may itself be composed of multiple cooperating nodes. This is
|
||||
similar to Tor's concept of \emph{directory authorities}. The network map publishes the IP addresses through which
|
||||
every node on the network can be reached, along with the identity certificates of those nodes and the services they
|
||||
provide. On receiving a connection nodes check that the connecting node is in the network map.
|
||||
provide. On receiving a connection, nodes check that the connecting node is in the network map.
|
||||
|
||||
The network map abstracts the underlying IP addresses of the nodes from more useful business concepts like identities
|
||||
and services. Each participant on the network, called a \emph{party}, publishes one or more IP addresses in the
|
||||
@ -312,20 +314,295 @@ with a solution. The ability to request manual solutions is useful for cases whe
|
||||
are contacting them, for example, the specified reason for sending a payment is not recognised, or when the asset used for
|
||||
a payment is not considered acceptable.
|
||||
|
||||
Flows are named using reverse DNS notation and several are defined by the base protocol. Note that the framework is
|
||||
not required to implement the wire protocols, it is just a development aid.
|
||||
|
||||
\subsection{Data visibility and dependency resolution}
|
||||
|
||||
When a transaction is presented to a node as part of a flow it may need to be checked. Simply sending you
|
||||
a message saying that I am paying you \pounds1000 is only useful if youa are sure I own the money I'm using to pay me.
|
||||
Checking transaction validity is the responsibility of the \texttt{ResolveTransactions} flow. This flow performs
|
||||
a breadth-first search over the transaction graph, downloading any missing transactions into local storage and
|
||||
validating them. The search bottoms out at the issuance transactions. A transaction is not considered valid if
|
||||
any of its transitive dependencies are invalid.
|
||||
|
||||
It is required that a node be able to present the entire dependency graph for a transaction it is asking another
|
||||
node to accept. Thus there is never any confusion about where to find transaction data. Because transactions are
|
||||
always communicated inside a flow, and flows embed the resolution flow, the necessary dependencies are fetched
|
||||
and checked automatically from the correct peer. Transactions propagate around the network lazily and there is
|
||||
no need for distributed hash tables.
|
||||
|
||||
This approach has several consequences. One is that transactions that move highly liquid assets like cash may
|
||||
end up becoming a part of a very long chain of transactions. The act of resolving the tip of such a graph can
|
||||
involve many round-trips and thus take some time to fully complete. How quickly a Corda network can send payments
|
||||
is thus difficult to characterise: it depends heavily on usage and distance between nodes. Whilst nodes could
|
||||
pre-push transactions in anticipation of them being fetched anyway, such optimisations are left for future work.
|
||||
|
||||
A more important consequence is that in the absence of additional privacy measures it is difficult to reason
|
||||
about who may get to see transaction data. We can say it's definitely better than a system that uses global
|
||||
broadcast, but how much better is hard to characterise. This uncertainty is mitigated by several factors.
|
||||
|
||||
\paragraph{Small-subgraph transactions.}Some uses of the ledger do not involve widely circulated asset states.
|
||||
For example, two institutions that wish to keep their view of a particular deal synchronised but who are making
|
||||
related payments off-ledger may use transactions that never go outside the involved parties. A discussion of
|
||||
on-ledger vs off-ledger cash can be found in a later section.
|
||||
|
||||
\paragraph{Transaction privacy techniques.}Corda supports a variety of transaction data hiding techniques. For
|
||||
example, public keys can be randomised to make it difficult to link transactions to an identity. ``Tear-offs''
|
||||
allow some parts of a transaction to be presented without the others. In future versions of the system secure hardware
|
||||
and/or zero knowledge proofs could be used to convince a party of the validity of a transaction without revealing the
|
||||
underlying data.
|
||||
|
||||
\paragraph{State re-issuance.}In cases where a state represents an asset that is backed by a particular issuer,
|
||||
and the issuer is trusted to behave atomically even when the ledger isn't forcing atomicity, the state can
|
||||
simply be `exited' from the ledger and then re-issued. Because there are no links between the exit and reissue
|
||||
transactions this shortens the chain. In practice most issuers of highly liquid assets are already trusted with
|
||||
far more sensitive tasks than reliably issuing pairs of signed data structures, so this approach is unlikely to
|
||||
be an issue.
|
||||
|
||||
\section{Data model}
|
||||
\subsection{Commands}
|
||||
|
||||
Transactions consist of the following components:
|
||||
|
||||
\begin{labeling}{Input references}
|
||||
\item [Input references] These are \texttt{(hash, output index)} pairs that point to the states a
|
||||
transaction is consuming.
|
||||
\item [Output states] Each state specifies the notary for the new state, the contract(s) that define its allowed
|
||||
transition functions and finally the data itself.
|
||||
\item [Attachments] Transactions specify an ordered list of zip file hashes. Each zip file may contain
|
||||
code, data, certificates or supporting documentation for the transaction. Contract code has access to the contents
|
||||
of the attachments when checking the transaction for validity.
|
||||
\item [Commands] There may be multiple allowed output states from any given input state. For instance
|
||||
an asset can be moved to a new owner on the ledger, or issued, or exited from the ledger if the asset has been
|
||||
redeemed by the owner and no longer needs to be tracked. A command is essentially a parameter to the contract
|
||||
that specifies more information than is obtainable from examination of the states by themselves (e.g. data from an oracle
|
||||
service). Each command has an associated list of public keys. Like states, commands are object graphs.
|
||||
\item [Signatures] The set of required signatures is equal to the union of the commands' public keys.
|
||||
\item [Type] Transactions can either be normal or notary-changing. The validation rules for each are
|
||||
different.
|
||||
\item [Timestamp] When present, a timestamp defines a time range in which the transaction is considered to
|
||||
have occurrred. This is discussed in more detail below.
|
||||
\end{labeling}
|
||||
|
||||
% TODO: Update this one transaction types are separated.
|
||||
% TODO: This description ignores the participants field in states, because it probably needs a rethink.
|
||||
% TODO: Specify the curve used here once we decide how much we care about BIP32 public derivation.
|
||||
|
||||
Signatures are appended to the end of a transaction and transactions are identified by the hash used for signing, so
|
||||
signature malleability is not a problem. There is never a need to identify a transaction including its accompanying
|
||||
signatures by hash. Signatures can be both checked and generated in parallel, and they are not directly exposed to
|
||||
contract code. Instead contracts check that the set of public keys specified by a command is appropriate, knowing that
|
||||
the transaction will not be valid unless every key listed in every command has a matching signature. Public key
|
||||
structures are themselves opaque. In this way algorithmic agility is retained: new signature algorithms can be deployed
|
||||
without adjusting the code of the smart contracts themselves.
|
||||
|
||||
\subsection{Compound keys}
|
||||
|
||||
The term ``public key'' in the description above actually refers to a \emph{compound key}. Compound keys are trees in
|
||||
which leafs are regular cryptographic public keys with an accompanying algorithm identifiers. Nodes in the tree specify
|
||||
both the weights of each child and a threshold weight that must be met. The validty of a set of signatures can be
|
||||
determined by walking the tree bottom-up, summing the weights of the keys that have a valid signature and comparing
|
||||
against the threshold. By using weights and thresholds a variety of conditions can be encoded, including boolean
|
||||
formulas with AND and OR.
|
||||
|
||||
Compound keys are useful in multiple scenarios. For example, assets can be placed under the control of a 2-of-2
|
||||
compound key where one leaf key is owned by a user, and the other by an independent risk analysis system. The
|
||||
risk analysis system refuses to sign if the transaction seems suspicious, like if too much value has been
|
||||
transferred in too short a time window. Another example involves encoding corporate structures into the key,
|
||||
allowing a CFO to sign a large transaction alone but his subordinates are required to work together. Compound keys
|
||||
are also useful for notaries. Each participant in a distributed notary is represented by a leaf, and the threshold
|
||||
is set such that some participants can be offline or refusing to sign yet the signature of the group is still valid.
|
||||
|
||||
Whilst there are threshold signature schemes in the literature that allow compound keys and signatures to be produced
|
||||
mathematically, we choose the less space efficient explicit form in order to allow a mixture of keys using different
|
||||
algorithms. In this way old algorithms can be phased out and new algorithms phased in without requiring all
|
||||
participants in a group to upgrade simultaneously.
|
||||
|
||||
\subsection{Timestamps}
|
||||
|
||||
Transaction timestamps specify a \texttt{[start, end]} time window within which the transaction is asserted to have
|
||||
occurred. Timestamps are expressed as windows because in a distributed system there is no true time, only a large number
|
||||
of desynchronised clocks. This is not only implied by the laws of physics but also by the nature of shared transactions
|
||||
- especially if the signing of a transaction requires multiple human authorisations, the process of constructing
|
||||
a joint transaction could take hours or even days.
|
||||
|
||||
It is important to note that the purpose of a transaction timestamp is to communicate the transaction's position
|
||||
on the timeline to the smart contract code for the enforcement of contractual logic. Whilst such timestamps may
|
||||
also be used for other purposes, such as regulatory reporting or ordering of events in a user interface, there is
|
||||
no requirement to use them like that and locally observed timestamps may sometimes be preferable even if they will
|
||||
not exactly match the time observed by other parties. Alternatively if a precise point on the timeline is required
|
||||
and it must also be agreed by multiple parties, the midpoint of the time window may be used by convention. Even
|
||||
though this may not precisely align to any particular action (like a keystroke or verbal agreement) it is often
|
||||
useful nonetheless.
|
||||
|
||||
Timestamp windows may be open ended in order to communicate that the transaction occurred before a certain
|
||||
time or after a certain time, but how much before or after is unimportant. This can be used in a similar
|
||||
way to Bitcoin's \texttt{nLockTime} transaction field, which specifies a \emph{happens-after} constraint.
|
||||
|
||||
Timestamps are checked and enforced by notary services. As the participants in a notary service will themselves
|
||||
not have precisely aligned clocks, whether a transaction is considered valid or not at the moment it is submitted
|
||||
to a notary may be unpredictable if submission occurs right on a boundary of the given window. However, from the
|
||||
perspective of all other observers the notaries signature is decisive: if the signature is present, the transaction
|
||||
is assumed to have occurred within that time.
|
||||
|
||||
\paragraph{Reference clocks.}In order to allow for relatively tight time windows to be used when transactions are fully
|
||||
under the control of a single party, notaries are expected to be synchronised to the atomic clocks at the US Naval
|
||||
Observatory. Accurate feeds of this clock can be obtained from GPS satellites. Note that Corda uses the Java
|
||||
timeline\cite{JavaTimeScale} which is UTC with leap seconds spread over the last 1000 seconds of the day, thus each day
|
||||
always has exactly 86400 seconds. Care should be taken to ensure that changes in the GPS leap second counter are
|
||||
correctly smeared in order to stay synchronised with Java time. When setting a transaction time window care must be
|
||||
taken to account for network propagation delays between the user and the notary service, and messaging within the notary
|
||||
service.
|
||||
|
||||
\subsection{Attachments and contract bytecodes}
|
||||
|
||||
Transactions may have a number of \emph{attachments}, identified by the hash of the file. Attachments are stored
|
||||
and transmitted separately to transaction data and are fetched by the standard resolution flow only when the
|
||||
attachment has not previously been seen before.
|
||||
|
||||
Attachments are always zip files\cite{ZipFormat} and cannot be referred to individually by contract code. The files
|
||||
within the zips are collapsed together into a single logical file system, with overlapping files being resolved in
|
||||
favour of the first mentioned. Not coincidentally, this is the mechanism used by Java classpaths.
|
||||
|
||||
Smart contracts in Corda are defined using JVM bytecode as specified in \emph{``The Java Virtual Machine Specification SE 8 Edition''}\cite{JVM},
|
||||
with some small differences that are described in a later section. A contract is simply a class that implements
|
||||
the \texttt{Contract} interface, which in turn exposes a single function called \texttt{verify}. The verify
|
||||
function is passed a transaction and either throws an exception if the transaction is considered to be invalid,
|
||||
or returns with no result if the transaction is valid. Embedding the JVM specification in the Corda specification
|
||||
enables developers to write code in a variety of languages, use well developed toolchains, and to reuse code
|
||||
already authored in Java or other JVM compatible languages.
|
||||
|
||||
The Java standards also specify a comprehensive type system for expressing common business data. Time and calendar
|
||||
handling is provided by an implementation of the JSR 310 specification, decimal calculations can be performed either
|
||||
using portable (`\texttt{strictfp}') floating point arithmetic or the provided bignum library, and so on. These
|
||||
libraries have been carefully engineered by the business Java community over a period of many years and it makes
|
||||
sense to build on this investment.
|
||||
|
||||
Contract bytecode also defines the states themselves, which may be arbitrary object graphs. Because JVM classes
|
||||
are not a convenient form to work with from non-JVM platforms the allowed types are restricted and a standardised
|
||||
binary encoding scheme is provided. States may label their properties with a small set of standardised annotations.
|
||||
These can be useful for controlling how states are serialised to JSON and XML (using JSR 367 and JSR 222 respectively),
|
||||
for expressing static validation constraints (JSR 349) and for controlling how states are inserted into relational
|
||||
databases (JSR 338). This feature is discussed later.
|
||||
|
||||
Attachments may also contain data files that support the contract code. These may be in the same zip as the
|
||||
bytecode files, or in a different zip that must be provided for the transaction to be valid. Examples of such
|
||||
data files might include currency definitions, timezone data and public holiday calendars. Any public data may
|
||||
be referenced in this way. Attachments are intended for data on the ledger that many parties may wish to reuse
|
||||
over and over again. Data files are accessed by contract code using the same APIs as any file on the classpath
|
||||
would be accessed. The platform imposes some restrictions on what kinds of data can be included in attachments
|
||||
along with size limits, to avoid people placing inappropriate files on the global ledger (videos, PowerPoints etc).
|
||||
|
||||
Note that the creator of a transaction gets to choose which files are attached. Therefore, it is typical that
|
||||
states place constraints on the data they're willing to accept. Attachments \emph{provide} data but do not
|
||||
\emph{authenticate} it, so if there's a risk of someone providing bad data to gain an economic advantage
|
||||
there must be a constraints mechanism to prevent that from happening. This is rooted at the contract constraints
|
||||
encoded in the states themselves: a state can not only name a class that implements the \texttt{Contract}
|
||||
interface but also place constraints on the zip/jar file that provides it. That constraint can in turn be used to
|
||||
ensure that the contract checks the authenticity of the data - either by checking the hash of the data directly,
|
||||
or by requiring the data to be signed by some trusted third party.
|
||||
|
||||
% TODO: The code doesn't match this description yet.
|
||||
|
||||
\subsection{Hard forks, specifications and dispute resolution}
|
||||
|
||||
Decentralised ledger systems often differ in their underlying political ideology as well as their technical
|
||||
choices. The Ethereum project originally promised ``unstoppable apps'' which would implement ``code as law''. After
|
||||
a prominent smart contract was hacked, an argument took place over whether what had occurred could be described
|
||||
as a hack at all given the lack of any non-code specification of what the program was meant to do. The disagreement
|
||||
eventually led to a split in the community.
|
||||
|
||||
As Corda contracts are simply zip files, it is easy to include a PDF or other documents describing what a contract
|
||||
is meant to actually do. There is no requirement to use this mechanism, and there is no requirement that these
|
||||
documents have any legal weight. However in financial use cases it's expected that they would be legal contracts that
|
||||
take precedence over the software implementations in case of disagreement.
|
||||
|
||||
It is technically possible to write a contract that cannot be upgraded. If such a contract governed an asset that
|
||||
existed only on the ledger, like a cryptocurrency, then that would provide an approximation of ``code as law''. We
|
||||
leave discussion of this wisdom of this concept to political scientists and reddit.
|
||||
|
||||
\paragraph{Platform logging}There is no direct equivalent in Corda of a block chain ``hard fork'', so the only solution
|
||||
to discarding buggy or fraudulent transaction chains would be to mutually agree out of band to discard an entire
|
||||
transaction subgraph. As there is no global visibility either this mutual agreement would not need to encompass all
|
||||
network participants: only those who may have received and processed such transactions. The flip side of lacking global
|
||||
visibility is that there is no single point that records who exactly has seen which transactions. Determining the set
|
||||
of entities that'd have to agree to discard a subgraph means correlating node activity logs. Corda nodes log sufficient
|
||||
information to ensure this correlation can take place. The platform defines a flow to assist with this, which can be
|
||||
used by anyone. A tool is provided that generates an ``investigation request'' and sends it to a seed node. The flow
|
||||
signals to the node administrator that a decision is required, and sufficient information is transmitted to the node to
|
||||
try and convince the administrator to take part (e.g. a signed court order). If the administrator accepts the request
|
||||
through the node explorer interface, the next hops in the transaction chain are returned. In this way the tool can
|
||||
semi-automatically crawl the network to find all parties that would be affected by a proposed rollback. The platform
|
||||
does not take a position on what types of transaction rollback are justified and provides only minimal support for
|
||||
implementing rollbacks beyond locating the parties that would have to agree.
|
||||
|
||||
% TODO: DB logging of tx transmits is COR-544.
|
||||
|
||||
Once involved parties are identified there are at least two strategies for editing the ledger. One is to extend
|
||||
the transaction chain with new transactions that simply correct the database to match the intended reality. For
|
||||
this to be possible the smart contract must have been written to allow arbitrary changes outside its normal
|
||||
business logic when a sufficient threshold of signatures is present. This strategy is simple and makes the most
|
||||
sense when the number of parties involved in a state is small and parties have no incentive to leave bad information
|
||||
in the ledger. For asset states that are the result of theft or fraud the only party involved in a state may
|
||||
resist attempts to patch things up in this way, as they may be able to benefit in the real world from the time
|
||||
lag between the ledger becoming inaccurate and it catching up with reality. In this case a more complex approach
|
||||
can be used in which the involved parties minus the uncooperative party agree to mark the relevant states as
|
||||
no longer consumed/spent. This is essentially a limited form of database rollback.
|
||||
|
||||
\subsection{Identity lookups}
|
||||
\subsection{Attachments, legal prose and bytecode}
|
||||
|
||||
In all block chain inspired systems there exists a tension between wanting to know who you are dealing with and
|
||||
not wanting others to know. A standard technique is to use randomised public keys in the shared data, and keep
|
||||
the knowledge of the identity that key maps to private. For instance, it is considered good practice to generate
|
||||
a fresh key for every received payment. This technique exploits the fact that verifying the integrity of the ledger
|
||||
does not require knowing exactly who took part in the transactions, only that they followed the agreed upon
|
||||
rules of the system.
|
||||
|
||||
Platforms such as Bitcoin and Ethereum have relatively ad-hoc mechanisms for linking identities and keys. Typically
|
||||
it is the user's responsibility to manually label public keys in their wallet software using knowledge gleaned from
|
||||
websites, shop signs and so on. Because these mechanisms are ad hoc and tedious many users don't bother, which
|
||||
can make it hard to figure out where money went later. It also complicates the deployment of secure signing devices
|
||||
and risk analysis engines. Bitcoin has BIP 70\cite{BIP70} which specifies a way of signed a ``payment
|
||||
request'' using X.509 certificates linked to the web PKI, giving a cryptographically secured and standardised way
|
||||
of knowing who you are dealing with. Identities in this system are the same as used in the web PKI: a domain name,
|
||||
email address or EV (extended validation) organisation name.
|
||||
|
||||
Corda takes this concept further. States may define fields of type \texttt{Party}, which encapsulates an identity
|
||||
and a public key. When a state is deserialised from a transaction in its raw form, the identity field of the
|
||||
\texttt{Party} object is null and only the public (compound) key is present. If a transaction is deserialised
|
||||
in conjunction with X.509 certificate chains linking the transient public keys to long term identity keys the
|
||||
identity field is set. In this way a single data representation can be used for both the anonymised case, such
|
||||
as when validating dependencies of a transaction, and the identified case, such as when trading directly with
|
||||
a counterparty. Trading flows incorporate sub-flows to transmit certificates for the keys used, which are then
|
||||
stored in the local database. However the transaction resolution flow does not transmit such data, keeping the
|
||||
transactions in the chain of custody pseudonymous.
|
||||
|
||||
\paragraph{Deterministic key derivation} Corda allows for but does not mandate the use of determinstic key
|
||||
derivation schemes such as BIP 32\cite{BIP32}. The infrastructure does not assume any mathematical relationship
|
||||
between public keys because some cryptographic schemes are not compatible with such systems. Thus we take the
|
||||
efficiency hit of always linking transient public keys to longer term keys with X.509 certificates.
|
||||
|
||||
% TODO: Discuss the crypto suites used in Corda.
|
||||
|
||||
\subsection{Merkle-structured transactions}
|
||||
\subsection{Encumbrances}
|
||||
\subsection{Contract constraints}
|
||||
|
||||
% TODO: Contract constraints aren't designed yet.
|
||||
|
||||
\section{Cash and Obligations}
|
||||
\section{Non-asset instruments}
|
||||
\section{Integration with existing infrastructure}
|
||||
\section{Deterministic JVM}
|
||||
\section{Notaries}
|
||||
\section{Clauses}
|
||||
\section{Secure signing devices}
|
||||
\section{Client RPC and reactive collections}
|
||||
\section{Event scheduling}
|
||||
\section{Future work}
|
||||
|
||||
\paragraph Secure hardware
|
||||
\paragraph Zero knowledge proofs
|
||||
|
||||
\section{Conclusion}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.r3corda.plugins
|
||||
|
||||
import org.apache.tools.ant.filters.FixCrLfFilter
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import java.nio.file.Path
|
||||
@ -53,7 +54,7 @@ class Cordform extends DefaultTask {
|
||||
*/
|
||||
protected Node getNodeByName(String name) {
|
||||
for(Node node : nodes) {
|
||||
if(node.name.equals(networkMapNodeName)) {
|
||||
if(node.name == networkMapNodeName) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
@ -69,7 +70,7 @@ class Cordform extends DefaultTask {
|
||||
from Cordformation.getPluginFile(project, "com/r3corda/plugins/runnodes")
|
||||
filter { String line -> line.replace("JAR_NAME", Node.JAR_NAME) }
|
||||
// Replaces end of line with lf to avoid issues with the bash interpreter and Windows style line endings.
|
||||
filter(org.apache.tools.ant.filters.FixCrLfFilter.class, eol: org.apache.tools.ant.filters.FixCrLfFilter.CrLf.newInstance("lf"))
|
||||
filter(FixCrLfFilter.class, eol: FixCrLfFilter.CrLf.newInstance("lf"))
|
||||
into "${directory}/"
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package com.r3corda.plugins
|
||||
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.artifacts.Configuration
|
||||
|
||||
/**
|
||||
* The Cordformation plugin deploys nodes to a directory in a state ready to be used by a developer for experimentation,
|
||||
@ -9,7 +10,9 @@ import org.gradle.api.Project
|
||||
*/
|
||||
class Cordformation implements Plugin<Project> {
|
||||
void apply(Project project) {
|
||||
|
||||
Configuration cordappConf = project.configurations.create("cordapp")
|
||||
cordappConf.transitive = false
|
||||
project.configurations.compile.extendsFrom cordappConf
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -25,7 +25,8 @@ class Node {
|
||||
*/
|
||||
protected List<String> advertisedServices = []
|
||||
/**
|
||||
* Set thThe list of cordapps to install to the plugins directory.
|
||||
* Set the list of CorDapps to install to the plugins directory. Each cordapp is a fully qualified Maven
|
||||
* dependency name, eg: com.example:product-name:0.1
|
||||
*
|
||||
* @note Your app will be installed by default and does not need to be included here.
|
||||
*/
|
||||
@ -35,7 +36,7 @@ class Node {
|
||||
private Config config = ConfigFactory.empty()
|
||||
//private Map<String, Object> config = new HashMap<String, Object>()
|
||||
private File nodeDir
|
||||
private def project
|
||||
private Project project
|
||||
|
||||
/**
|
||||
* Set the name of the node.
|
||||
@ -150,7 +151,7 @@ class Node {
|
||||
* Installs this project's cordapp to this directory.
|
||||
*/
|
||||
private void installBuiltPlugin() {
|
||||
def pluginsDir = getAndCreateDirectory(nodeDir, "plugins")
|
||||
def pluginsDir = new File(nodeDir, "plugins")
|
||||
project.copy {
|
||||
from project.jar
|
||||
into pluginsDir
|
||||
@ -161,7 +162,7 @@ class Node {
|
||||
* Installs other cordapps to this node's plugins directory.
|
||||
*/
|
||||
private void installCordapps() {
|
||||
def pluginsDir = getAndCreateDirectory(nodeDir, "plugins")
|
||||
def pluginsDir = new File(nodeDir, "plugins")
|
||||
def cordapps = getCordappList()
|
||||
project.copy {
|
||||
from cordapps
|
||||
@ -174,9 +175,9 @@ class Node {
|
||||
*/
|
||||
private void installDependencies() {
|
||||
def cordaJar = verifyAndGetCordaJar()
|
||||
def cordappList = getCordappList()
|
||||
def depsDir = getAndCreateDirectory(nodeDir, "dependencies")
|
||||
def appDeps = project.configurations.runtime.filter { it != cordaJar && !cordappList.contains(it) }
|
||||
def cordappDeps = getCordappList()
|
||||
def depsDir = new File(nodeDir, "dependencies")
|
||||
def appDeps = project.configurations.runtime.filter { it != cordaJar && !cordappDeps.contains(it) }
|
||||
project.copy {
|
||||
from appDeps
|
||||
into depsDir
|
||||
@ -190,9 +191,17 @@ class Node {
|
||||
// Adding required default values
|
||||
config = config.withValue('extraAdvertisedServiceIds',
|
||||
ConfigValueFactory.fromAnyRef(advertisedServices.join(',')))
|
||||
|
||||
def configFileText = config.root().render(new ConfigRenderOptions(false, false, true, false)).split("\n").toList()
|
||||
Files.write(new File(nodeDir, 'node.conf').toPath(), configFileText, StandardCharsets.UTF_8)
|
||||
|
||||
// Need to write a temporary file first to use the project.copy, which resolves directories correctly.
|
||||
def tmpDir = new File(project.buildDir, "tmp")
|
||||
def tmpConfFile = new File(tmpDir, 'node.conf')
|
||||
Files.write(tmpConfFile.toPath(), configFileText, StandardCharsets.UTF_8)
|
||||
|
||||
project.copy {
|
||||
from tmpConfFile
|
||||
into nodeDir
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -216,25 +225,9 @@ class Node {
|
||||
*
|
||||
* @return List of this node's cordapps.
|
||||
*/
|
||||
private AbstractFileCollection getCordappList() {
|
||||
def cordaJar = verifyAndGetCordaJar()
|
||||
return project.configurations.runtime.filter {
|
||||
def jarName = it.name.split('-').first()
|
||||
return (it != cordaJar) && cordapps.contains(jarName)
|
||||
private Collection<File> getCordappList() {
|
||||
return project.configurations.cordapp.files {
|
||||
cordapps.contains("${it.group}:${it.name}:${it.version}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a directory if it doesn't exist and return the file representation of it.
|
||||
*
|
||||
* @param baseDir The base directory to create the directory at.
|
||||
* @param subDirName A valid name of the subdirectory to get and create if not exists.
|
||||
* @return A file representing the subdirectory.
|
||||
*/
|
||||
private static File getAndCreateDirectory(File baseDir, String subDirName) {
|
||||
File dir = new File(baseDir, subDirName)
|
||||
assert(!dir.exists() || dir.isDirectory())
|
||||
dir.mkdirs()
|
||||
return dir
|
||||
}
|
||||
}
|
||||
|
0
gradle-plugins/cordformation/src/main/resources/com/r3corda/plugins/runnodes
Normal file → Executable file
0
gradle-plugins/cordformation/src/main/resources/com/r3corda/plugins/runnodes
Normal file → Executable file
@ -122,7 +122,7 @@ dependencies {
|
||||
testCompile 'com.pholser:junit-quickcheck-core:0.6'
|
||||
|
||||
// For H2 database support in persistence
|
||||
compile "com.h2database:h2:1.3.176"
|
||||
compile "com.h2database:h2:1.4.192"
|
||||
|
||||
// Exposed: Kotlin SQL library - under evaluation
|
||||
compile "org.jetbrains.exposed:exposed:0.5.0"
|
||||
|
@ -24,6 +24,10 @@ class JDBCHashMapTestSuite {
|
||||
lateinit var dataSource: Closeable
|
||||
lateinit var transaction: Transaction
|
||||
lateinit var database: Database
|
||||
lateinit var loadOnInitFalseMap: JDBCHashMap<String, String>
|
||||
lateinit var loadOnInitTrueMap: JDBCHashMap<String, String>
|
||||
lateinit var loadOnInitFalseSet: JDBCHashSet<String>
|
||||
lateinit var loadOnInitTrueSet: JDBCHashSet<String>
|
||||
|
||||
@JvmStatic
|
||||
@BeforeClass
|
||||
@ -32,6 +36,10 @@ class JDBCHashMapTestSuite {
|
||||
dataSource = dataSourceAndDatabase.first
|
||||
database = dataSourceAndDatabase.second
|
||||
setUpDatabaseTx()
|
||||
loadOnInitFalseMap = JDBCHashMap<String, String>("test_map_false", loadOnInit = false)
|
||||
loadOnInitTrueMap = JDBCHashMap<String, String>("test_map_true", loadOnInit = true)
|
||||
loadOnInitFalseSet = JDBCHashSet<String>("test_set_false", loadOnInit = false)
|
||||
loadOnInitTrueSet = JDBCHashSet<String>("test_set_true", loadOnInit = true)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@ -112,7 +120,8 @@ class JDBCHashMapTestSuite {
|
||||
*/
|
||||
class JDBCHashMapTestGenerator(val loadOnInit: Boolean) : com.google.common.collect.testing.TestStringMapGenerator() {
|
||||
override fun create(elements: Array<Map.Entry<String, String>>): Map<String, String> {
|
||||
val map = JDBCHashMap<String, String>("test_map_${System.nanoTime()}", loadOnInit = loadOnInit)
|
||||
val map = if (loadOnInit) loadOnInitTrueMap else loadOnInitFalseMap
|
||||
map.clear()
|
||||
map.putAll(elements.associate { Pair(it.key, it.value) })
|
||||
return map
|
||||
}
|
||||
@ -143,7 +152,8 @@ class JDBCHashMapTestSuite {
|
||||
*/
|
||||
class JDBCHashSetTestGenerator(val loadOnInit: Boolean) : com.google.common.collect.testing.TestStringSetGenerator() {
|
||||
override fun create(elements: Array<String>): Set<String> {
|
||||
val set = JDBCHashSet<String>("test_set_${System.nanoTime()}", loadOnInit = loadOnInit)
|
||||
val set = if (loadOnInit) loadOnInitTrueSet else loadOnInitFalseSet
|
||||
set.clear()
|
||||
set.addAll(elements)
|
||||
return set
|
||||
}
|
||||
|
@ -276,6 +276,7 @@ open class DriverDSL(
|
||||
val conn = url.openConnection() as HttpURLConnection
|
||||
conn.requestMethod = "GET"
|
||||
if (conn.responseCode != 200) {
|
||||
log.error("Received response code ${conn.responseCode} from $url during startup.")
|
||||
return null
|
||||
}
|
||||
// For now the NodeInfo is tunneled in its Kryo format over the Node's Web interface.
|
||||
@ -285,6 +286,7 @@ open class DriverDSL(
|
||||
om.registerModule(module)
|
||||
return om.readValue(conn.inputStream, NodeInfo::class.java)
|
||||
} catch(e: Exception) {
|
||||
log.error("Could not query node info at $url due to an exception.", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -307,7 +307,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
val pluginServices = pluginRegistries.flatMap { x -> x.servicePlugins }
|
||||
val serviceList = mutableListOf<Any>()
|
||||
for (serviceClass in pluginServices) {
|
||||
val service = serviceClass.getConstructor(ServiceHubInternal::class.java).newInstance(services)
|
||||
val service = serviceClass.getConstructor(PluginServiceHub::class.java).newInstance(services)
|
||||
serviceList.add(service)
|
||||
tokenizableServices.add(service)
|
||||
if (service is AcceptsFileUpload) {
|
||||
|
@ -33,6 +33,7 @@ import org.glassfish.jersey.servlet.ServletContainer
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import java.io.RandomAccessFile
|
||||
import java.lang.management.ManagementFactory
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.nio.channels.FileLock
|
||||
import java.time.Clock
|
||||
import java.util.*
|
||||
@ -143,13 +144,18 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
|
||||
|
||||
// Export JMX monitoring statistics and data over REST/JSON.
|
||||
if (configuration.exportJMXto.split(',').contains("http")) {
|
||||
val classpath = System.getProperty("java.class.path").split(System.getProperty("path.separator"))
|
||||
val warpath = classpath.firstOrNull() { it.contains("jolokia-agent-war-2") && it.endsWith(".war") }
|
||||
if (warpath != null) {
|
||||
handlerCollection.addHandler(WebAppContext().apply {
|
||||
// Find the jolokia WAR file on the classpath.
|
||||
contextPath = "/monitoring/json"
|
||||
setInitParameter("mimeType", "application/json")
|
||||
val classpath = System.getProperty("java.class.path").split(System.getProperty("path.separator"))
|
||||
war = classpath.first { it.contains("jolokia-agent-war-2") && it.endsWith(".war") }
|
||||
war = warpath
|
||||
})
|
||||
} else {
|
||||
log.warn("Unable to locate Jolokia WAR on classpath")
|
||||
}
|
||||
}
|
||||
|
||||
// API, data upload and download to services (attachments, rates oracles etc)
|
||||
@ -157,7 +163,7 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
|
||||
|
||||
val server = Server()
|
||||
|
||||
if (configuration.useHTTPS) {
|
||||
val connector = if (configuration.useHTTPS) {
|
||||
val httpsConfiguration = HttpConfiguration()
|
||||
httpsConfiguration.outputBufferSize = 32768
|
||||
httpsConfiguration.addCustomizer(SecureRequestCustomizer())
|
||||
@ -173,14 +179,16 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
|
||||
sslContextFactory.setIncludeCipherSuites(".*AES.*GCM.*")
|
||||
val sslConnector = ServerConnector(server, SslConnectionFactory(sslContextFactory, "http/1.1"), HttpConnectionFactory(httpsConfiguration))
|
||||
sslConnector.port = configuration.webAddress.port
|
||||
server.connectors = arrayOf<Connector>(sslConnector)
|
||||
sslConnector
|
||||
} else {
|
||||
val httpConfiguration = HttpConfiguration()
|
||||
httpConfiguration.outputBufferSize = 32768
|
||||
val httpConnector = ServerConnector(server, HttpConnectionFactory(httpConfiguration))
|
||||
httpConnector.port = configuration.webAddress.port
|
||||
server.connectors = arrayOf<Connector>(httpConnector)
|
||||
httpConnector
|
||||
}
|
||||
server.connectors = arrayOf<Connector>(connector)
|
||||
log.info("Starting web API server on port ${connector.port}")
|
||||
|
||||
server.handler = handlerCollection
|
||||
runOnStop += Runnable { server.stop() }
|
||||
@ -203,8 +211,19 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
|
||||
|
||||
val webAPIsOnClasspath = pluginRegistries.flatMap { x -> x.webApis }
|
||||
for (webapi in webAPIsOnClasspath) {
|
||||
log.info("Add Plugin web API from attachment ${webapi.name}")
|
||||
val customAPI = webapi.getConstructor(ServiceHub::class.java).newInstance(services)
|
||||
log.info("Add plugin web API from attachment ${webapi.name}")
|
||||
val constructor = try {
|
||||
webapi.getConstructor(ServiceHub::class.java)
|
||||
} catch (ex: NoSuchMethodException) {
|
||||
log.error("Missing constructor ${webapi.name}(ServiceHub)")
|
||||
continue
|
||||
}
|
||||
val customAPI = try {
|
||||
constructor.newInstance(services)
|
||||
} catch (ex: InvocationTargetException) {
|
||||
log.error("Constructor ${webapi.name}(ServiceHub) threw an error: ", ex.targetException)
|
||||
continue
|
||||
}
|
||||
resourceConfig.register(customAPI)
|
||||
}
|
||||
|
||||
@ -270,7 +289,13 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
|
||||
super.start()
|
||||
// Only start the service API requests once the network map registration is complete
|
||||
networkMapRegistrationFuture.then {
|
||||
try {
|
||||
webServer = initWebServer()
|
||||
} catch(ex: Exception) {
|
||||
// TODO: We need to decide if this is a fatal error, given the API is unavailable, or whether the API
|
||||
// is not critical and we continue anyway.
|
||||
log.error("Web server startup failed", ex)
|
||||
}
|
||||
// Begin exporting our own metrics via JMX.
|
||||
JmxReporter.
|
||||
forRegistry(services.monitoringService.metrics).
|
||||
|
@ -92,7 +92,8 @@ class ServerRPCOps(
|
||||
val builder: TransactionBuilder = TransactionType.General.Builder(null)
|
||||
// TODO: Have some way of restricting this to states the caller controls
|
||||
try {
|
||||
val (spendTX, keysForSigning) = services.vaultService.generateSpend(builder, req.amount.withoutIssuer(), req.recipient.owningKey)
|
||||
val (spendTX, keysForSigning) = services.vaultService.generateSpend(builder,
|
||||
req.amount.withoutIssuer(), req.recipient.owningKey, setOf(req.amount.token.issuer.party))
|
||||
|
||||
keysForSigning.forEach {
|
||||
val key = services.keyManagementService.keys[it] ?: throw IllegalStateException("Could not find signing key for ${it.toStringShort()}")
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.r3corda.node.services
|
||||
|
||||
import com.r3corda.core.node.CordaPluginRegistry
|
||||
import com.r3corda.core.node.PluginServiceHub
|
||||
import com.r3corda.core.serialization.SingletonSerializeAsToken
|
||||
import com.r3corda.node.services.api.ServiceHubInternal
|
||||
import com.r3corda.protocols.NotaryChangeProtocol
|
||||
@ -14,7 +15,7 @@ object NotaryChange {
|
||||
* A service that monitors the network for requests for changing the notary of a state,
|
||||
* and immediately runs the [NotaryChangeProtocol] if the auto-accept criteria are met.
|
||||
*/
|
||||
class Service(services: ServiceHubInternal) : SingletonSerializeAsToken() {
|
||||
class Service(services: PluginServiceHub) : SingletonSerializeAsToken() {
|
||||
init {
|
||||
services.registerProtocolInitiator(NotaryChangeProtocol.Instigator::class) { NotaryChangeProtocol.Acceptor(it) }
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package com.r3corda.node.services.api
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.messaging.MessagingService
|
||||
import com.r3corda.core.node.PluginServiceHub
|
||||
import com.r3corda.core.node.ServiceHub
|
||||
import com.r3corda.core.node.services.TxWritableStorageService
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
@ -37,7 +38,7 @@ interface MessagingServiceBuilder<out T : MessagingServiceInternal> {
|
||||
|
||||
private val log = LoggerFactory.getLogger(ServiceHubInternal::class.java)
|
||||
|
||||
abstract class ServiceHubInternal : ServiceHub {
|
||||
abstract class ServiceHubInternal : PluginServiceHub {
|
||||
abstract val monitoringService: MonitoringService
|
||||
abstract val protocolLogicRefFactory: ProtocolLogicRefFactory
|
||||
abstract val schemaService: SchemaService
|
||||
@ -71,24 +72,6 @@ abstract class ServiceHubInternal : ServiceHub {
|
||||
*/
|
||||
abstract fun <T> startProtocol(logic: ProtocolLogic<T>): ListenableFuture<T>
|
||||
|
||||
/**
|
||||
* Register the protocol factory we wish to use when a initiating party attempts to communicate with us. The
|
||||
* registration is done against a marker [KClass] which is sent in the session handshake by the other party. If this
|
||||
* marker class has been registered then the corresponding factory will be used to create the protocol which will
|
||||
* communicate with the other side. If there is no mapping then the session attempt is rejected.
|
||||
* @param markerClass The marker [KClass] present in a session initiation attempt, which is a 1:1 mapping to a [Class]
|
||||
* using the <pre>::class</pre> construct. Conventionally this is a [ProtocolLogic] subclass, however any class can
|
||||
* be used, with the default being the class of the initiating protocol. This enables the registration to be of the
|
||||
* form: registerProtocolInitiator(InitiatorProtocol::class, ::InitiatedProtocol)
|
||||
* @param protocolFactory The protocol factory generating the initiated protocol.
|
||||
*/
|
||||
abstract fun registerProtocolInitiator(markerClass: KClass<*>, protocolFactory: (Party) -> ProtocolLogic<*>)
|
||||
|
||||
/**
|
||||
* Return the protocol factory that has been registered with [markerClass], or null if no factory is found.
|
||||
*/
|
||||
abstract fun getProtocolFactory(markerClass: Class<*>): ((Party) -> ProtocolLogic<*>)?
|
||||
|
||||
override fun <T : Any> invokeProtocolAsync(logicType: Class<out ProtocolLogic<T>>, vararg args: Any?): ListenableFuture<T> {
|
||||
val logicRef = protocolLogicRefFactory.create(logicType, *args)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
@ -6,6 +6,7 @@ import com.google.common.util.concurrent.SettableFuture
|
||||
import com.r3corda.core.bufferUntilSubscribed
|
||||
import com.r3corda.core.contracts.Contract
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.toStringShort
|
||||
import com.r3corda.core.map
|
||||
import com.r3corda.core.messaging.MessagingService
|
||||
import com.r3corda.core.messaging.SingleMessageRecipient
|
||||
@ -72,10 +73,17 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
|
||||
override fun get(serviceType: ServiceType) = registeredNodes.filterValues { it.advertisedServices.any { it.info.type.isSubTypeOf(serviceType) } }.map { it.value }
|
||||
override fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo? = get(type).firstOrNull()
|
||||
override fun getNodeByLegalName(name: String) = get().singleOrNull { it.legalIdentity.name == name }
|
||||
override fun getNodeByPublicKey(publicKey: PublicKey) = get().singleOrNull {
|
||||
override fun getNodeByPublicKey(publicKey: PublicKey): NodeInfo? {
|
||||
// Although we should never have more than one match, it is theoretically possible. Report an error if it happens.
|
||||
val candidates = get().filter {
|
||||
(it.legalIdentity.owningKey == publicKey)
|
||||
|| it.advertisedServices.any { it.identity.owningKey == publicKey }
|
||||
}
|
||||
if (candidates.size > 1) {
|
||||
throw IllegalStateException("Found more than one match for key ${publicKey.toStringShort()}")
|
||||
}
|
||||
return candidates.singleOrNull()
|
||||
}
|
||||
|
||||
override fun addMapService(net: MessagingService, networkMapAddress: SingleMessageRecipient, subscribe: Boolean,
|
||||
ifChangedSinceVer: Int?): ListenableFuture<Unit> {
|
||||
|
@ -3,6 +3,7 @@ package com.r3corda.node.services.persistence
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.node.CordaPluginRegistry
|
||||
import com.r3corda.core.node.PluginServiceHub
|
||||
import com.r3corda.core.node.recordTransactions
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
import com.r3corda.core.serialization.SingletonSerializeAsToken
|
||||
@ -31,10 +32,7 @@ object DataVending {
|
||||
* Additionally, because nodes do not store invalid transactions, requesting such a transaction will always yield null.
|
||||
*/
|
||||
@ThreadSafe
|
||||
// TODO: I don't like that this needs ServiceHubInternal, but passing in a state machine breaks MockServices because
|
||||
// the state machine isn't set when this is constructed. [NodeSchedulerService] has the same problem, and both
|
||||
// should be fixed at the same time.
|
||||
class Service(services: ServiceHubInternal) : SingletonSerializeAsToken() {
|
||||
class Service(services: PluginServiceHub) : SingletonSerializeAsToken() {
|
||||
|
||||
companion object {
|
||||
val logger = loggerFor<DataVending.Service>()
|
||||
|
@ -183,7 +183,7 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
|
||||
var acceptableCoins = run {
|
||||
val ofCurrency = assetsStates.filter { it.state.data.amount.token.product == currency }
|
||||
if (onlyFromParties != null)
|
||||
ofCurrency.filter { it.state.data.deposit.party in onlyFromParties }
|
||||
ofCurrency.filter { it.state.data.amount.token.issuer.party in onlyFromParties }
|
||||
else
|
||||
ofCurrency
|
||||
}
|
||||
@ -196,27 +196,32 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
|
||||
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount)
|
||||
val takeChangeFrom = gathered.firstOrNull()
|
||||
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
|
||||
Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
|
||||
Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.amount.token)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val keysUsed = gathered.map { it.state.data.owner }.toSet()
|
||||
|
||||
val states = gathered.groupBy { it.state.data.deposit }.map {
|
||||
val states = gathered.groupBy { it.state.data.amount.token.issuer }.map {
|
||||
val coins = it.value
|
||||
val totalAmount = coins.map { it.state.data.amount }.sumOrThrow()
|
||||
deriveState(coins.first().state, totalAmount, to)
|
||||
}
|
||||
}.sortedBy { it.data.amount.quantity }
|
||||
|
||||
val outputs = if (change != null) {
|
||||
// Just copy a key across as the change key. In real life of course, this works but leaks private data.
|
||||
// In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow
|
||||
// value flows through the transaction graph.
|
||||
val changeKey = gathered.first().state.data.owner
|
||||
val existingOwner = gathered.first().state.data.owner
|
||||
// Add a change output and adjust the last output downwards.
|
||||
states.subList(0, states.lastIndex) +
|
||||
states.last().let { deriveState(it, it.data.amount - change, it.data.owner) } +
|
||||
deriveState(gathered.last().state, change, changeKey)
|
||||
states.last().let {
|
||||
val spent = it.data.amount.withoutIssuer() - change.withoutIssuer()
|
||||
deriveState(it, Amount(spent.quantity, it.data.amount.token), it.data.owner)
|
||||
} +
|
||||
states.last().let {
|
||||
deriveState(it, Amount(change.quantity, it.data.amount.token), existingOwner)
|
||||
}
|
||||
} else states
|
||||
|
||||
for (state in gathered) tx.addInputState(state)
|
||||
|
@ -6,7 +6,7 @@ keyStorePassword = "cordacadevpass"
|
||||
trustStorePassword = "trustpass"
|
||||
dataSourceProperties = {
|
||||
dataSourceClassName = org.h2.jdbcx.JdbcDataSource
|
||||
"dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;MVCC=true;MV_STORE=true;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
|
||||
"dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
|
||||
"dataSource.user" = sa
|
||||
"dataSource.password" = ""
|
||||
}
|
||||
|
@ -1,8 +1,14 @@
|
||||
package com.r3corda.node.services
|
||||
|
||||
import com.r3corda.core.crypto.generateKeyPair
|
||||
import com.r3corda.core.node.services.ServiceInfo
|
||||
import com.r3corda.node.services.network.NetworkMapService
|
||||
import com.r3corda.node.services.transactions.SimpleNotaryService
|
||||
import com.r3corda.testing.expect
|
||||
import com.r3corda.testing.node.MockNetwork
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class InMemoryNetworkMapCacheTest {
|
||||
lateinit var network: MockNetwork
|
||||
@ -20,4 +26,20 @@ class InMemoryNetworkMapCacheTest {
|
||||
network.runNetwork()
|
||||
future.get()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `key collision`() {
|
||||
val keyPair = generateKeyPair()
|
||||
val nodeA = network.createNode(null, -1, MockNetwork.DefaultFactory, true, "Node A", keyPair, ServiceInfo(NetworkMapService.type))
|
||||
val nodeB = network.createNode(null, -1, MockNetwork.DefaultFactory, true, "Node B", keyPair, ServiceInfo(NetworkMapService.type))
|
||||
|
||||
// Node A currently knows only about itself, so this returns node A
|
||||
assertEquals(nodeA.netMapCache.getNodeByPublicKey(keyPair.public), nodeA.info)
|
||||
|
||||
nodeA.netMapCache.addNode(nodeB.info)
|
||||
// Now both nodes match, so it throws an error
|
||||
expect<IllegalStateException> {
|
||||
nodeA.netMapCache.getNodeByPublicKey(keyPair.public)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,35 +1,32 @@
|
||||
package com.r3corda.node.services
|
||||
|
||||
import com.google.common.jimfs.Configuration
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.days
|
||||
import com.r3corda.core.node.ServiceHub
|
||||
import com.r3corda.core.node.recordTransactions
|
||||
import com.r3corda.core.node.services.VaultService
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
import com.r3corda.core.protocols.ProtocolLogicRef
|
||||
import com.r3corda.core.protocols.ProtocolLogicRefFactory
|
||||
import com.r3corda.core.serialization.SingletonSerializeAsToken
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.utilities.DUMMY_NOTARY
|
||||
import com.r3corda.node.services.events.NodeSchedulerService
|
||||
import com.r3corda.node.services.persistence.DBCheckpointStorage
|
||||
import com.r3corda.node.services.statemachine.StateMachineManager
|
||||
import com.r3corda.node.services.vault.NodeVaultService
|
||||
import com.r3corda.node.utilities.AddOrRemove
|
||||
import com.r3corda.node.utilities.AffinityExecutor
|
||||
import com.r3corda.node.utilities.configureDatabase
|
||||
import com.r3corda.node.utilities.databaseTransaction
|
||||
import com.r3corda.testing.ALICE_KEY
|
||||
import com.r3corda.testing.node.*
|
||||
import com.r3corda.testing.node.InMemoryMessagingNetwork
|
||||
import com.r3corda.testing.node.MockKeyManagementService
|
||||
import com.r3corda.testing.node.TestClock
|
||||
import com.r3corda.testing.node.makeTestDataSourceProperties
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.Closeable
|
||||
import java.nio.file.FileSystem
|
||||
import java.security.PublicKey
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
@ -39,8 +36,6 @@ import java.util.concurrent.TimeUnit
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
// Use an in memory file system for testing attachment storage.
|
||||
val fs: FileSystem = Jimfs.newFileSystem(Configuration.unix())
|
||||
|
||||
val realClock: Clock = Clock.systemUTC()
|
||||
val stoppedClock = Clock.fixed(realClock.instant(), realClock.zone)
|
||||
@ -82,19 +77,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
dataSource = dataSourceAndDatabase.first
|
||||
database = dataSourceAndDatabase.second
|
||||
|
||||
// Switched from InMemoryVault usage to NodeVault
|
||||
databaseTransaction(database) {
|
||||
val services1 = object : MockServices() {
|
||||
override val vaultService: VaultService = NodeVaultService(this)
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
storageService.validatedTransactions.addTransaction(stx)
|
||||
vaultService.notify(stx.tx)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
val kms = MockKeyManagementService(ALICE_KEY)
|
||||
val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging(false, InMemoryMessagingNetwork.Handle(0, "None"), AffinityExecutor.ServiceAffinityExecutor("test", 1), database)
|
||||
services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, net = mockMessagingService), TestReference {
|
||||
|
@ -1,227 +0,0 @@
|
||||
# Some pretend noddy rate fixes, for the interest rate oracles.
|
||||
|
||||
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
|
||||
|
||||
ICE LIBOR 2016-03-06 3M = 0.0063515
|
||||
ICE LIBOR 2016-03-07 3M = 0.0063516
|
||||
ICE LIBOR 2016-03-08 3M = 0.0063517
|
||||
ICE LIBOR 2016-03-09 3M = 0.0063518
|
||||
ICE LIBOR 2016-03-10 3M = 0.0063519
|
||||
ICE LIBOR 2016-06-06 3M = 0.0063520
|
||||
ICE LIBOR 2016-06-07 3M = 0.0063521
|
||||
ICE LIBOR 2016-06-08 3M = 0.0063522
|
||||
ICE LIBOR 2016-06-09 3M = 0.0063523
|
||||
ICE LIBOR 2016-06-10 3M = 0.0063524
|
||||
ICE LIBOR 2016-09-06 3M = 0.0063525
|
||||
ICE LIBOR 2016-09-07 3M = 0.0063526
|
||||
ICE LIBOR 2016-09-08 3M = 0.0063527
|
||||
ICE LIBOR 2016-09-09 3M = 0.0063528
|
||||
ICE LIBOR 2016-09-10 3M = 0.0063529
|
||||
ICE LIBOR 2016-12-06 3M = 0.0063530
|
||||
ICE LIBOR 2016-12-07 3M = 0.0063531
|
||||
ICE LIBOR 2016-12-08 3M = 0.0063532
|
||||
ICE LIBOR 2016-12-09 3M = 0.0063533
|
||||
ICE LIBOR 2016-12-10 3M = 0.0063534
|
||||
ICE LIBOR 2017-03-06 3M = 0.0063535
|
||||
ICE LIBOR 2017-03-07 3M = 0.0063536
|
||||
ICE LIBOR 2017-03-08 3M = 0.0063537
|
||||
ICE LIBOR 2017-03-09 3M = 0.0063538
|
||||
ICE LIBOR 2017-03-10 3M = 0.0063539
|
||||
ICE LIBOR 2017-06-06 3M = 0.0063540
|
||||
ICE LIBOR 2017-06-07 3M = 0.0063541
|
||||
ICE LIBOR 2017-06-08 3M = 0.0063542
|
||||
ICE LIBOR 2017-06-09 3M = 0.0063543
|
||||
ICE LIBOR 2017-06-10 3M = 0.0063544
|
||||
ICE LIBOR 2017-09-06 3M = 0.0063545
|
||||
ICE LIBOR 2017-09-07 3M = 0.0063546
|
||||
ICE LIBOR 2017-09-08 3M = 0.0063547
|
||||
ICE LIBOR 2017-09-09 3M = 0.0063548
|
||||
ICE LIBOR 2017-09-10 3M = 0.0063549
|
||||
ICE LIBOR 2017-12-06 3M = 0.0063550
|
||||
ICE LIBOR 2017-12-07 3M = 0.0063551
|
||||
ICE LIBOR 2017-12-08 3M = 0.0063552
|
||||
ICE LIBOR 2017-12-09 3M = 0.0063553
|
||||
ICE LIBOR 2017-12-10 3M = 0.0063554
|
||||
ICE LIBOR 2018-03-06 3M = 0.0063555
|
||||
ICE LIBOR 2018-03-07 3M = 0.0063556
|
||||
ICE LIBOR 2018-03-08 3M = 0.0063557
|
||||
ICE LIBOR 2018-03-09 3M = 0.0063558
|
||||
ICE LIBOR 2018-03-10 3M = 0.0063559
|
||||
ICE LIBOR 2018-06-06 3M = 0.0063560
|
||||
ICE LIBOR 2018-06-07 3M = 0.0063561
|
||||
ICE LIBOR 2018-06-08 3M = 0.0063562
|
||||
ICE LIBOR 2018-06-09 3M = 0.0063563
|
||||
ICE LIBOR 2018-06-10 3M = 0.0063564
|
||||
ICE LIBOR 2018-09-06 3M = 0.0063565
|
||||
ICE LIBOR 2018-09-07 3M = 0.0063566
|
||||
ICE LIBOR 2018-09-08 3M = 0.0063567
|
||||
ICE LIBOR 2018-09-09 3M = 0.0063568
|
||||
ICE LIBOR 2018-09-10 3M = 0.0063569
|
||||
ICE LIBOR 2018-12-06 3M = 0.0063570
|
||||
ICE LIBOR 2018-12-07 3M = 0.0063571
|
||||
ICE LIBOR 2018-12-08 3M = 0.0063572
|
||||
ICE LIBOR 2018-12-09 3M = 0.0063573
|
||||
ICE LIBOR 2018-12-10 3M = 0.0063574
|
||||
ICE LIBOR 2019-03-06 3M = 0.0063575
|
||||
ICE LIBOR 2019-03-07 3M = 0.0063576
|
||||
ICE LIBOR 2019-03-08 3M = 0.0063577
|
||||
ICE LIBOR 2019-03-09 3M = 0.0063578
|
||||
ICE LIBOR 2019-03-10 3M = 0.0063579
|
||||
ICE LIBOR 2019-06-06 3M = 0.0063580
|
||||
ICE LIBOR 2019-06-07 3M = 0.0063581
|
||||
ICE LIBOR 2019-06-08 3M = 0.0063582
|
||||
ICE LIBOR 2019-06-09 3M = 0.0063583
|
||||
ICE LIBOR 2019-06-10 3M = 0.0063584
|
||||
ICE LIBOR 2019-09-06 3M = 0.0063585
|
||||
ICE LIBOR 2019-09-07 3M = 0.0063586
|
||||
ICE LIBOR 2019-09-08 3M = 0.0063587
|
||||
ICE LIBOR 2019-09-09 3M = 0.0063588
|
||||
ICE LIBOR 2019-09-10 3M = 0.0063589
|
||||
ICE LIBOR 2019-12-06 3M = 0.0063590
|
||||
ICE LIBOR 2019-12-07 3M = 0.0063591
|
||||
ICE LIBOR 2019-12-08 3M = 0.0063592
|
||||
ICE LIBOR 2019-12-09 3M = 0.0063593
|
||||
ICE LIBOR 2019-12-10 3M = 0.0063594
|
||||
ICE LIBOR 2020-03-06 3M = 0.0063595
|
||||
ICE LIBOR 2020-03-07 3M = 0.0063596
|
||||
ICE LIBOR 2020-03-08 3M = 0.0063597
|
||||
ICE LIBOR 2020-03-09 3M = 0.0063598
|
||||
ICE LIBOR 2020-03-10 3M = 0.0063599
|
||||
ICE LIBOR 2020-06-06 3M = 0.0063600
|
||||
ICE LIBOR 2020-06-07 3M = 0.0063601
|
||||
ICE LIBOR 2020-06-08 3M = 0.0063602
|
||||
ICE LIBOR 2020-06-09 3M = 0.0063603
|
||||
ICE LIBOR 2020-06-10 3M = 0.0063604
|
||||
ICE LIBOR 2020-09-06 3M = 0.0063605
|
||||
ICE LIBOR 2020-09-07 3M = 0.0063606
|
||||
ICE LIBOR 2020-09-08 3M = 0.0063607
|
||||
ICE LIBOR 2020-09-09 3M = 0.0063608
|
||||
ICE LIBOR 2020-09-10 3M = 0.0063609
|
||||
ICE LIBOR 2020-12-06 3M = 0.0063610
|
||||
ICE LIBOR 2020-12-07 3M = 0.0063611
|
||||
ICE LIBOR 2020-12-08 3M = 0.0063612
|
||||
ICE LIBOR 2020-12-09 3M = 0.0063613
|
||||
ICE LIBOR 2020-12-10 3M = 0.0063614
|
||||
ICE LIBOR 2021-03-06 3M = 0.0063615
|
||||
ICE LIBOR 2021-03-07 3M = 0.0063616
|
||||
ICE LIBOR 2021-03-08 3M = 0.0063617
|
||||
ICE LIBOR 2021-03-09 3M = 0.0063618
|
||||
ICE LIBOR 2021-03-10 3M = 0.0063619
|
||||
ICE LIBOR 2021-06-06 3M = 0.0063620
|
||||
ICE LIBOR 2021-06-07 3M = 0.0063621
|
||||
ICE LIBOR 2021-06-08 3M = 0.0063622
|
||||
ICE LIBOR 2021-06-09 3M = 0.0063623
|
||||
ICE LIBOR 2021-06-10 3M = 0.0063624
|
||||
ICE LIBOR 2021-09-06 3M = 0.0063625
|
||||
ICE LIBOR 2021-09-07 3M = 0.0063626
|
||||
ICE LIBOR 2021-09-08 3M = 0.0063627
|
||||
ICE LIBOR 2021-09-09 3M = 0.0063628
|
||||
ICE LIBOR 2021-09-10 3M = 0.0063629
|
||||
ICE LIBOR 2021-12-06 3M = 0.0063630
|
||||
ICE LIBOR 2021-12-07 3M = 0.0063631
|
||||
ICE LIBOR 2021-12-08 3M = 0.0063632
|
||||
ICE LIBOR 2021-12-09 3M = 0.0063633
|
||||
ICE LIBOR 2021-12-10 3M = 0.0063634
|
||||
ICE LIBOR 2022-03-06 3M = 0.0063635
|
||||
ICE LIBOR 2022-03-07 3M = 0.0063636
|
||||
ICE LIBOR 2022-03-08 3M = 0.0063637
|
||||
ICE LIBOR 2022-03-09 3M = 0.0063638
|
||||
ICE LIBOR 2022-03-10 3M = 0.0063639
|
||||
ICE LIBOR 2022-06-06 3M = 0.0063640
|
||||
ICE LIBOR 2022-06-07 3M = 0.0063641
|
||||
ICE LIBOR 2022-06-08 3M = 0.0063642
|
||||
ICE LIBOR 2022-06-09 3M = 0.0063643
|
||||
ICE LIBOR 2022-06-10 3M = 0.0063644
|
||||
ICE LIBOR 2022-09-06 3M = 0.0063645
|
||||
ICE LIBOR 2022-09-07 3M = 0.0063646
|
||||
ICE LIBOR 2022-09-08 3M = 0.0063647
|
||||
ICE LIBOR 2022-09-09 3M = 0.0063648
|
||||
ICE LIBOR 2022-09-10 3M = 0.0063649
|
||||
ICE LIBOR 2022-12-06 3M = 0.0063650
|
||||
ICE LIBOR 2022-12-07 3M = 0.0063651
|
||||
ICE LIBOR 2022-12-08 3M = 0.0063652
|
||||
ICE LIBOR 2022-12-09 3M = 0.0063653
|
||||
ICE LIBOR 2022-12-10 3M = 0.0063654
|
||||
ICE LIBOR 2023-03-06 3M = 0.0063655
|
||||
ICE LIBOR 2023-03-07 3M = 0.0063656
|
||||
ICE LIBOR 2023-03-08 3M = 0.0063657
|
||||
ICE LIBOR 2023-03-09 3M = 0.0063658
|
||||
ICE LIBOR 2023-03-10 3M = 0.0063659
|
||||
ICE LIBOR 2023-06-06 3M = 0.0063660
|
||||
ICE LIBOR 2023-06-07 3M = 0.0063661
|
||||
ICE LIBOR 2023-06-08 3M = 0.0063662
|
||||
ICE LIBOR 2023-06-09 3M = 0.0063663
|
||||
ICE LIBOR 2023-06-10 3M = 0.0063664
|
||||
ICE LIBOR 2023-09-06 3M = 0.0063665
|
||||
ICE LIBOR 2023-09-07 3M = 0.0063666
|
||||
ICE LIBOR 2023-09-08 3M = 0.0063667
|
||||
ICE LIBOR 2023-09-09 3M = 0.0063668
|
||||
ICE LIBOR 2023-09-10 3M = 0.0063669
|
||||
ICE LIBOR 2023-12-06 3M = 0.0063670
|
||||
ICE LIBOR 2023-12-07 3M = 0.0063671
|
||||
ICE LIBOR 2023-12-08 3M = 0.0063672
|
||||
ICE LIBOR 2023-12-09 3M = 0.0063673
|
||||
ICE LIBOR 2023-12-10 3M = 0.0063674
|
||||
ICE LIBOR 2024-03-06 3M = 0.0063675
|
||||
ICE LIBOR 2024-03-07 3M = 0.0063676
|
||||
ICE LIBOR 2024-03-08 3M = 0.0063677
|
||||
ICE LIBOR 2024-03-09 3M = 0.0063678
|
||||
ICE LIBOR 2024-03-10 3M = 0.0063679
|
||||
ICE LIBOR 2024-06-06 3M = 0.0063680
|
||||
ICE LIBOR 2024-06-07 3M = 0.0063681
|
||||
ICE LIBOR 2024-06-08 3M = 0.0063682
|
||||
ICE LIBOR 2024-06-09 3M = 0.0063683
|
||||
ICE LIBOR 2024-06-10 3M = 0.0063684
|
||||
ICE LIBOR 2024-09-06 3M = 0.0063685
|
||||
ICE LIBOR 2024-09-07 3M = 0.0063686
|
||||
ICE LIBOR 2024-09-08 3M = 0.0063687
|
||||
ICE LIBOR 2024-09-09 3M = 0.0063688
|
||||
ICE LIBOR 2024-09-10 3M = 0.0063689
|
||||
ICE LIBOR 2024-12-06 3M = 0.0063690
|
||||
ICE LIBOR 2024-12-07 3M = 0.0063691
|
||||
ICE LIBOR 2024-12-08 3M = 0.0063692
|
||||
ICE LIBOR 2024-12-09 3M = 0.0063693
|
||||
ICE LIBOR 2024-12-10 3M = 0.0063694
|
||||
ICE LIBOR 2025-03-06 3M = 0.0063695
|
||||
ICE LIBOR 2025-03-07 3M = 0.0063696
|
||||
ICE LIBOR 2025-03-08 3M = 0.0063697
|
||||
ICE LIBOR 2025-03-09 3M = 0.0063698
|
||||
ICE LIBOR 2025-03-10 3M = 0.0063699
|
||||
ICE LIBOR 2025-06-06 3M = 0.0063700
|
||||
ICE LIBOR 2025-06-07 3M = 0.0063701
|
||||
ICE LIBOR 2025-06-08 3M = 0.0063702
|
||||
ICE LIBOR 2025-06-09 3M = 0.0063703
|
||||
ICE LIBOR 2025-06-10 3M = 0.0063704
|
||||
ICE LIBOR 2025-09-06 3M = 0.0063705
|
||||
ICE LIBOR 2025-09-07 3M = 0.0063706
|
||||
ICE LIBOR 2025-09-08 3M = 0.0063707
|
||||
ICE LIBOR 2025-09-09 3M = 0.0063708
|
||||
ICE LIBOR 2025-09-10 3M = 0.0063709
|
||||
ICE LIBOR 2025-12-06 3M = 0.0063710
|
||||
ICE LIBOR 2025-12-07 3M = 0.0063711
|
||||
ICE LIBOR 2025-12-08 3M = 0.0063712
|
||||
ICE LIBOR 2025-12-09 3M = 0.0063713
|
||||
ICE LIBOR 2025-12-10 3M = 0.0063714
|
||||
ICE LIBOR 2026-03-06 3M = 0.0063715
|
||||
ICE LIBOR 2026-03-07 3M = 0.0063716
|
||||
ICE LIBOR 2026-03-08 3M = 0.0063717
|
||||
ICE LIBOR 2026-03-09 3M = 0.0063718
|
||||
ICE LIBOR 2026-03-10 3M = 0.0063719
|
||||
ICE LIBOR 2026-06-06 3M = 0.0063720
|
||||
ICE LIBOR 2026-06-07 3M = 0.0063721
|
||||
ICE LIBOR 2026-06-08 3M = 0.0063722
|
||||
ICE LIBOR 2026-06-09 3M = 0.0063723
|
||||
ICE LIBOR 2026-06-10 3M = 0.0063724
|
||||
ICE LIBOR 2026-09-06 3M = 0.0063725
|
||||
ICE LIBOR 2026-09-07 3M = 0.0063726
|
||||
ICE LIBOR 2026-09-08 3M = 0.0063727
|
||||
ICE LIBOR 2026-09-09 3M = 0.0063728
|
||||
ICE LIBOR 2026-09-10 3M = 0.0063729
|
||||
ICE LIBOR 2026-12-06 3M = 0.0063730
|
||||
ICE LIBOR 2026-12-07 3M = 0.0063731
|
||||
ICE LIBOR 2026-12-08 3M = 0.0063732
|
||||
ICE LIBOR 2026-12-09 3M = 0.0063733
|
||||
ICE LIBOR 2026-12-10 3M = 0.0063734
|
@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This needs the buyer node to be running first.
|
||||
|
||||
if [ ! -e ./gradlew ]; then
|
||||
echo "Run from the root directory please"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bin="build/install/r3prototyping/bin/get-rate-fix"
|
||||
|
||||
if [ ! -e $bin ]; then
|
||||
./gradlew installDist
|
||||
fi
|
||||
|
||||
if [ ! -e build/trader-demo/buyer/identity-public ]; then
|
||||
echo "You must run scripts/trade-demo.sh buyer before running this script (and keep it running)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Upload the rates to the buyer node
|
||||
curl -F rates=@scripts/example.rates.txt http://localhost:31338/upload/interest-rates
|
||||
|
||||
$bin --network-address=localhost:31300 --directory=build/trader-demo/rates-fix --network-map=localhost:31337 --network-map-identity-file=build/trader-demo/buyer/identity-public
|
@ -1,55 +0,0 @@
|
||||
package com.r3corda.core.testing
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.r3corda.testing.*
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class TraderDemoTest {
|
||||
@Test fun `runs trader demo`() {
|
||||
val buyerAddr = freeLocalHostAndPort()
|
||||
val buyerApiAddr = freeLocalHostAndPort()
|
||||
val directory = "./build/integration-test/${TestTimestamp.timestamp}/trader-demo"
|
||||
var nodeProc: Process? = null
|
||||
try {
|
||||
nodeProc = runBuyer(directory, buyerAddr, buyerApiAddr)
|
||||
runSeller(directory, buyerAddr)
|
||||
} finally {
|
||||
nodeProc?.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun runBuyer(baseDirectory: String, buyerAddr: HostAndPort, buyerApiAddr: HostAndPort): Process {
|
||||
println("Running Buyer")
|
||||
val args = listOf(
|
||||
"--role", "BUYER",
|
||||
"--network-address", buyerAddr.toString(),
|
||||
"--api-address", buyerApiAddr.toString(),
|
||||
"--base-directory", baseDirectory,
|
||||
"--h2-port", "0"
|
||||
)
|
||||
val proc = spawn("com.r3corda.demos.TraderDemoKt", args, "TradeDemoBuyer")
|
||||
NodeApi.ensureNodeStartsOrKill(proc, buyerApiAddr)
|
||||
return proc
|
||||
}
|
||||
|
||||
private fun runSeller(baseDirectory: String, buyerAddr: HostAndPort) {
|
||||
println("Running Seller")
|
||||
val sellerAddr = freeLocalHostAndPort()
|
||||
val sellerApiAddr = freeLocalHostAndPort()
|
||||
val args = listOf(
|
||||
"--role", "SELLER",
|
||||
"--network-address", sellerAddr.toString(),
|
||||
"--api-address", sellerApiAddr.toString(),
|
||||
"--other-network-address", buyerAddr.toString(),
|
||||
"--base-directory", baseDirectory,
|
||||
"--h2-port", "0"
|
||||
)
|
||||
val proc = spawn("com.r3corda.demos.TraderDemoKt", args, "TradeDemoSeller")
|
||||
assertExitOrKill(proc)
|
||||
assertEquals(proc.exitValue(), 0)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,381 +0,0 @@
|
||||
package com.r3corda.demos
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.r3corda.contracts.CommercialPaper
|
||||
import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER
|
||||
import com.r3corda.contracts.testing.fillWithSomeTestCash
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.generateKeyPair
|
||||
import com.r3corda.core.days
|
||||
import com.r3corda.core.logElapsedTime
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.services.ServiceInfo
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
import com.r3corda.core.seconds
|
||||
import com.r3corda.core.success
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.utilities.Emoji
|
||||
import com.r3corda.core.utilities.LogHelper
|
||||
import com.r3corda.core.utilities.ProgressTracker
|
||||
import com.r3corda.node.internal.Node
|
||||
import com.r3corda.node.services.config.ConfigHelper
|
||||
import com.r3corda.node.services.config.FullNodeConfiguration
|
||||
import com.r3corda.node.services.messaging.NodeMessagingClient
|
||||
import com.r3corda.node.services.network.NetworkMapService
|
||||
import com.r3corda.node.services.persistence.NodeAttachmentService
|
||||
import com.r3corda.node.services.transactions.ValidatingNotaryService
|
||||
import com.r3corda.node.utilities.databaseTransaction
|
||||
import com.r3corda.protocols.NotaryProtocol
|
||||
import com.r3corda.protocols.TwoPartyTradeProtocol
|
||||
import joptsimple.OptionParser
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.system.exitProcess
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
// TRADING DEMO
|
||||
//
|
||||
// Please see docs/build/html/running-the-demos.html
|
||||
//
|
||||
// This program is a simple driver for exercising the two party trading protocol. Until Corda has a unified node server
|
||||
// programs like this are required to wire up the pieces and run a demo scenario end to end.
|
||||
//
|
||||
// If you are creating a new scenario, you can use this program as a template for creating your own driver. Make sure to
|
||||
// copy/paste the right parts of the build.gradle file to make sure it gets a script to run it deposited in
|
||||
// build/install/r3prototyping/bin
|
||||
//
|
||||
// In this scenario, a buyer wants to purchase some commercial paper by swapping his cash for the CP. The seller learns
|
||||
// that the buyer exists, and sends them a message to kick off the trade. The seller, having obtained his CP, then quits
|
||||
// and the buyer goes back to waiting. The buyer will sell as much CP as he can!
|
||||
//
|
||||
// The different roles in the scenario this program can adopt are:
|
||||
|
||||
enum class Role {
|
||||
BUYER,
|
||||
SELLER
|
||||
}
|
||||
|
||||
// And this is the directory under the current working directory where each node will create its own server directory,
|
||||
// which holds things like checkpoints, keys, databases, message logs etc.
|
||||
val DEFAULT_BASE_DIRECTORY = "./build/trader-demo"
|
||||
|
||||
private val log: Logger = LoggerFactory.getLogger("TraderDemo")
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val parser = OptionParser()
|
||||
|
||||
val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required()
|
||||
val myNetworkAddress = parser.accepts("network-address").withRequiredArg().defaultsTo("localhost")
|
||||
val theirNetworkAddress = parser.accepts("other-network-address").withRequiredArg().defaultsTo("localhost")
|
||||
val apiNetworkAddress = parser.accepts("api-address").withRequiredArg().defaultsTo("localhost")
|
||||
val baseDirectoryArg = parser.accepts("base-directory").withRequiredArg().defaultsTo(DEFAULT_BASE_DIRECTORY)
|
||||
val h2PortArg = parser.accepts("h2-port").withRequiredArg().ofType(Int::class.java).defaultsTo(-1)
|
||||
val options = try {
|
||||
parser.parse(*args)
|
||||
} catch (e: Exception) {
|
||||
log.error(e.message)
|
||||
printHelp(parser)
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
val role = options.valueOf(roleArg)!!
|
||||
|
||||
val myNetAddr = HostAndPort.fromString(options.valueOf(myNetworkAddress)).withDefaultPort(
|
||||
when (role) {
|
||||
Role.BUYER -> 31337
|
||||
Role.SELLER -> 31340
|
||||
}
|
||||
)
|
||||
val theirNetAddr = HostAndPort.fromString(options.valueOf(theirNetworkAddress)).withDefaultPort(
|
||||
when (role) {
|
||||
Role.BUYER -> 31340
|
||||
Role.SELLER -> 31337
|
||||
}
|
||||
)
|
||||
val apiNetAddr = HostAndPort.fromString(options.valueOf(apiNetworkAddress)).withDefaultPort(myNetAddr.port + 1)
|
||||
val h2Port = if (options.valueOf(h2PortArg) < 0) {
|
||||
myNetAddr.port + 2
|
||||
} else options.valueOf(h2PortArg)
|
||||
|
||||
val baseDirectory = options.valueOf(baseDirectoryArg)!!
|
||||
|
||||
// Suppress the Artemis MQ noise, and activate the demo logging.
|
||||
//
|
||||
// The first two strings correspond to the first argument to StateMachineManager.add() but the way we handle logging
|
||||
// for protocols will change in future.
|
||||
LogHelper.setLevel("+demo.buyer", "+demo.seller", "-org.apache.activemq")
|
||||
|
||||
val directory = Paths.get(baseDirectory, role.name.toLowerCase())
|
||||
log.info("Using base demo directory $directory")
|
||||
|
||||
// Override the default config file (which you can find in the file "reference.conf") to give each node a name.
|
||||
val config = run {
|
||||
val myLegalName = when (role) {
|
||||
Role.BUYER -> "Bank A"
|
||||
Role.SELLER -> "Bank B"
|
||||
}
|
||||
val configOverrides = mapOf(
|
||||
"myLegalName" to myLegalName,
|
||||
"artemisAddress" to myNetAddr.toString(),
|
||||
"webAddress" to apiNetAddr.toString(),
|
||||
"h2port" to h2Port.toString()
|
||||
)
|
||||
FullNodeConfiguration(ConfigHelper.loadConfig(directory, allowMissingConfig = true, configOverrides = configOverrides))
|
||||
}
|
||||
|
||||
// Which services will this instance of the node provide to the network?
|
||||
val advertisedServices: Set<ServiceInfo>
|
||||
|
||||
// One of the two servers needs to run the network map and notary services. In such a trivial two-node network
|
||||
// the map is not very helpful, but we need one anyway. So just make the buyer side run the network map as it's
|
||||
// the side that sticks around waiting for the seller.
|
||||
val networkMapId = if (role == Role.BUYER) {
|
||||
advertisedServices = setOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type))
|
||||
null
|
||||
} else {
|
||||
advertisedServices = emptySet()
|
||||
NodeMessagingClient.makeNetworkMapAddress(theirNetAddr)
|
||||
}
|
||||
|
||||
// And now construct then start the node object. It takes a little while.
|
||||
val node = logElapsedTime("Node startup", log) {
|
||||
Node(config, networkMapId, advertisedServices).setup().start()
|
||||
}
|
||||
|
||||
// What happens next depends on the role. The buyer sits around waiting for a trade to start. The seller role
|
||||
// will contact the buyer and actually make something happen.
|
||||
val amount = 1000.DOLLARS
|
||||
if (role == Role.BUYER) {
|
||||
runBuyer(node, amount)
|
||||
} else {
|
||||
node.networkMapRegistrationFuture.success {
|
||||
val party = node.netMapCache.getNodeByLegalName("Bank A")?.legalIdentity ?: throw IllegalStateException("Cannot find other node?!")
|
||||
runSeller(node, amount, party)
|
||||
}
|
||||
}
|
||||
|
||||
node.run()
|
||||
}
|
||||
|
||||
private fun runSeller(node: Node, amount: Amount<Currency>, otherSide: Party) {
|
||||
// The seller will sell some commercial paper to the buyer, who will pay with (self issued) cash.
|
||||
//
|
||||
// The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an
|
||||
// attachment. Make sure we have the transaction prospectus attachment loaded into our store.
|
||||
//
|
||||
// This can also be done via an HTTP upload, but here we short-circuit and do it from code.
|
||||
if (node.storage.attachments.openAttachment(TraderDemoProtocolSeller.PROSPECTUS_HASH) == null) {
|
||||
TraderDemoProtocolSeller::class.java.getResourceAsStream("bank-of-london-cp.jar").use {
|
||||
val id = node.storage.attachments.importAttachment(it)
|
||||
assertEquals(TraderDemoProtocolSeller.PROSPECTUS_HASH, id)
|
||||
}
|
||||
}
|
||||
|
||||
val tradeTX: ListenableFuture<SignedTransaction>
|
||||
if (node.isPreviousCheckpointsPresent) {
|
||||
tradeTX = node.smm.findStateMachines(TraderDemoProtocolSeller::class.java).single().second
|
||||
} else {
|
||||
val seller = TraderDemoProtocolSeller(otherSide, amount)
|
||||
tradeTX = node.services.startProtocol(seller)
|
||||
}
|
||||
|
||||
tradeTX.success {
|
||||
log.info("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(it.tx)}")
|
||||
thread {
|
||||
node.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runBuyer(node: Node, amount: Amount<Currency>) {
|
||||
// Buyer will fetch the attachment from the seller automatically when it resolves the transaction.
|
||||
// For demo purposes just extract attachment jars when saved to disk, so the user can explore them.
|
||||
val attachmentsPath = (node.storage.attachments as NodeAttachmentService).let {
|
||||
it.automaticallyExtractAttachments = true
|
||||
it.storePath
|
||||
}
|
||||
|
||||
// Self issue some cash.
|
||||
//
|
||||
// TODO: At some point this demo should be extended to have a central bank node.
|
||||
databaseTransaction(node.database) {
|
||||
node.services.fillWithSomeTestCash(300000.DOLLARS,
|
||||
outputNotary = node.info.notaryIdentity, // In this demo, the buyer and notary are on the same node, but need to use right key.
|
||||
ownedBy = node.info.legalIdentity.owningKey)
|
||||
}
|
||||
|
||||
// Wait around until a node asks to start a trade with us. In a real system, this part would happen out of band
|
||||
// via some other system like an exchange or maybe even a manual messaging system like Bloomberg. But for the
|
||||
// next stage in our building site, we will just auto-generate fake trades to give our nodes something to do.
|
||||
//
|
||||
// As the seller initiates the two-party trade protocol, here, we will be the buyer.
|
||||
node.services.registerProtocolInitiator(TraderDemoProtocolSeller::class) { otherParty ->
|
||||
TraderDemoProtocolBuyer(otherParty, attachmentsPath, amount)
|
||||
}
|
||||
}
|
||||
|
||||
// We create a couple of ad-hoc test protocols that wrap the two party trade protocol, to give us the demo logic.
|
||||
|
||||
private class TraderDemoProtocolBuyer(val otherSide: Party,
|
||||
private val attachmentsPath: Path,
|
||||
val amount: Amount<Currency>,
|
||||
override val progressTracker: ProgressTracker = ProgressTracker(STARTING_BUY)) : ProtocolLogic<Unit>() {
|
||||
|
||||
object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset")
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
progressTracker.currentStep = STARTING_BUY
|
||||
|
||||
val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
|
||||
val buyer = TwoPartyTradeProtocol.Buyer(
|
||||
otherSide,
|
||||
notary.notaryIdentity,
|
||||
amount,
|
||||
CommercialPaper.State::class.java)
|
||||
|
||||
// This invokes the trading protocol and out pops our finished transaction.
|
||||
val tradeTX: SignedTransaction = subProtocol(buyer, shareParentSessions = true)
|
||||
// TODO: This should be moved into the protocol itself.
|
||||
serviceHub.recordTransactions(listOf(tradeTX))
|
||||
|
||||
log.info("Purchase complete - we are a happy customer! Final transaction is: " +
|
||||
"\n\n${Emoji.renderIfSupported(tradeTX.tx)}")
|
||||
|
||||
logIssuanceAttachment(tradeTX)
|
||||
logBalance()
|
||||
}
|
||||
|
||||
private fun logBalance() {
|
||||
val balances = serviceHub.vaultService.cashBalances.entries.map { "${it.key.currencyCode} ${it.value}" }
|
||||
logger.info("Remaining balance: ${balances.joinToString()}")
|
||||
}
|
||||
|
||||
private fun logIssuanceAttachment(tradeTX: SignedTransaction) {
|
||||
// Find the original CP issuance.
|
||||
val search = TransactionGraphSearch(serviceHub.storageService.validatedTransactions, listOf(tradeTX.tx))
|
||||
search.query = TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java,
|
||||
followInputsOfType = CommercialPaper.State::class.java)
|
||||
val cpIssuance = search.call().single()
|
||||
|
||||
cpIssuance.attachments.first().let {
|
||||
val p = attachmentsPath.toAbsolutePath().resolve("$it.jar")
|
||||
log.info("""
|
||||
|
||||
The issuance of the commercial paper came with an attachment. You can find it expanded in this directory:
|
||||
$p
|
||||
|
||||
${Emoji.renderIfSupported(cpIssuance)}""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TraderDemoProtocolSeller(val otherSide: Party,
|
||||
val amount: Amount<Currency>,
|
||||
override val progressTracker: ProgressTracker = TraderDemoProtocolSeller.tracker()) : ProtocolLogic<SignedTransaction>() {
|
||||
companion object {
|
||||
val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9")
|
||||
|
||||
object SELF_ISSUING : ProgressTracker.Step("Got session ID back, issuing and timestamping some commercial paper")
|
||||
|
||||
object TRADING : ProgressTracker.Step("Starting the trade protocol") {
|
||||
override fun childProgressTracker(): ProgressTracker = TwoPartyTradeProtocol.Seller.tracker()
|
||||
}
|
||||
|
||||
// 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(SELF_ISSUING, TRADING)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
progressTracker.currentStep = SELF_ISSUING
|
||||
|
||||
val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
|
||||
val cpOwnerKey = serviceHub.legalIdentityKey
|
||||
val commercialPaper = selfIssueSomeCommercialPaper(cpOwnerKey.public, notary)
|
||||
|
||||
progressTracker.currentStep = TRADING
|
||||
|
||||
val seller = TwoPartyTradeProtocol.Seller(
|
||||
otherSide,
|
||||
notary,
|
||||
commercialPaper,
|
||||
amount,
|
||||
cpOwnerKey,
|
||||
progressTracker.getChildProgressTracker(TRADING)!!)
|
||||
val tradeTX: SignedTransaction = subProtocol(seller, shareParentSessions = true)
|
||||
serviceHub.recordTransactions(listOf(tradeTX))
|
||||
|
||||
return tradeTX
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
fun selfIssueSomeCommercialPaper(ownedBy: PublicKey, notaryNode: NodeInfo): StateAndRef<CommercialPaper.State> {
|
||||
// Make a fake company that's issued its own paper.
|
||||
val keyPair = generateKeyPair()
|
||||
val party = Party("Bank of London", keyPair.public)
|
||||
|
||||
val issuance: SignedTransaction = run {
|
||||
val tx = CommercialPaper().generateIssue(party.ref(1, 2, 3), 1100.DOLLARS `issued by` DUMMY_CASH_ISSUER,
|
||||
Instant.now() + 10.days, notaryNode.notaryIdentity)
|
||||
|
||||
// TODO: Consider moving these two steps below into generateIssue.
|
||||
|
||||
// Attach the prospectus.
|
||||
tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!.id)
|
||||
|
||||
// Requesting timestamping, all CP must be timestamped.
|
||||
tx.setTime(Instant.now(), 30.seconds)
|
||||
|
||||
// Sign it as ourselves.
|
||||
tx.signWith(keyPair)
|
||||
|
||||
// Get the notary to sign the timestamp
|
||||
val notarySig = subProtocol(NotaryProtocol.Client(tx.toSignedTransaction(false)))
|
||||
tx.addSignatureUnchecked(notarySig)
|
||||
|
||||
// Commit it to local storage.
|
||||
val stx = tx.toSignedTransaction(true)
|
||||
serviceHub.recordTransactions(listOf(stx))
|
||||
|
||||
stx
|
||||
}
|
||||
|
||||
// Now make a dummy transaction that moves it to a new key, just to show that resolving dependencies works.
|
||||
val move: SignedTransaction = run {
|
||||
val builder = TransactionType.General.Builder(notaryNode.notaryIdentity)
|
||||
CommercialPaper().generateMove(builder, issuance.tx.outRef(0), ownedBy)
|
||||
builder.signWith(keyPair)
|
||||
val notarySignature = subProtocol(NotaryProtocol.Client(builder.toSignedTransaction(false)))
|
||||
builder.addSignatureUnchecked(notarySignature)
|
||||
val tx = builder.toSignedTransaction(true)
|
||||
serviceHub.recordTransactions(listOf(tx))
|
||||
tx
|
||||
}
|
||||
|
||||
return move.tx.outRef(0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun printHelp(parser: OptionParser) {
|
||||
println("""
|
||||
Usage: trader-demo --role [BUYER|SELLER] [options]
|
||||
Please refer to the documentation in docs/build/index.html for more info.
|
||||
|
||||
""".trimIndent())
|
||||
parser.printHelpOn(System.out)
|
||||
}
|
||||
|
@ -1,200 +0,0 @@
|
||||
package com.r3corda.demos.attachment
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.r3corda.core.contracts.TransactionType
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.failure
|
||||
import com.r3corda.core.logElapsedTime
|
||||
import com.r3corda.core.node.services.ServiceInfo
|
||||
import com.r3corda.core.success
|
||||
import com.r3corda.core.utilities.Emoji
|
||||
import com.r3corda.core.utilities.LogHelper
|
||||
import com.r3corda.node.internal.Node
|
||||
import com.r3corda.node.services.config.ConfigHelper
|
||||
import com.r3corda.node.services.config.FullNodeConfiguration
|
||||
import com.r3corda.node.services.messaging.NodeMessagingClient
|
||||
import com.r3corda.node.services.network.NetworkMapService
|
||||
import com.r3corda.node.services.transactions.SimpleNotaryService
|
||||
import com.r3corda.protocols.FinalityProtocol
|
||||
import com.r3corda.testing.ALICE_KEY
|
||||
import joptsimple.OptionParser
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Paths
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.system.exitProcess
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
// ATTACHMENT DEMO
|
||||
//
|
||||
// Please see docs/build/html/running-the-demos.html and docs/build/html/tutorial-attachments.html
|
||||
//
|
||||
// This program is a simple demonstration of sending a transaction with an attachment from one node to another, and
|
||||
// then accessing the attachment on the remote node.
|
||||
//
|
||||
// The different roles in the scenario this program can adopt are:
|
||||
|
||||
enum class Role(val legalName: String, val port: Int) {
|
||||
SENDER("Bank A", 31337),
|
||||
RECIPIENT("Bank B", 31340);
|
||||
|
||||
val other: Role
|
||||
get() = when (this) {
|
||||
SENDER -> RECIPIENT
|
||||
RECIPIENT -> SENDER
|
||||
}
|
||||
}
|
||||
|
||||
// And this is the directory under the current working directory where each node will create its own server directory,
|
||||
// which holds things like checkpoints, keys, databases, message logs etc.
|
||||
val DEFAULT_BASE_DIRECTORY = "./build/attachment-demo"
|
||||
|
||||
val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9")
|
||||
|
||||
private val log: Logger = LoggerFactory.getLogger("AttachmentDemo")
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val parser = OptionParser()
|
||||
|
||||
val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required()
|
||||
val myNetworkAddress = parser.accepts("network-address").withRequiredArg().defaultsTo("localhost")
|
||||
val theirNetworkAddress = parser.accepts("other-network-address").withRequiredArg().defaultsTo("localhost")
|
||||
val apiNetworkAddress = parser.accepts("api-address").withRequiredArg().defaultsTo("localhost")
|
||||
val baseDirectoryArg = parser.accepts("base-directory").withRequiredArg().defaultsTo(DEFAULT_BASE_DIRECTORY)
|
||||
|
||||
val options = try {
|
||||
parser.parse(*args)
|
||||
} catch (e: Exception) {
|
||||
log.error(e.message)
|
||||
printHelp(parser)
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
val role = options.valueOf(roleArg)!!
|
||||
|
||||
val myNetAddr = HostAndPort.fromString(options.valueOf(myNetworkAddress)).withDefaultPort(role.port)
|
||||
val theirNetAddr = HostAndPort.fromString(options.valueOf(theirNetworkAddress)).withDefaultPort(role.other.port)
|
||||
val apiNetAddr = HostAndPort.fromString(options.valueOf(apiNetworkAddress)).withDefaultPort(myNetAddr.port + 1)
|
||||
|
||||
val baseDirectory = options.valueOf(baseDirectoryArg)!!
|
||||
|
||||
// Suppress the Artemis MQ noise, and activate the demo logging.
|
||||
//
|
||||
// The first two strings correspond to the first argument to StateMachineManager.add() but the way we handle logging
|
||||
// for protocols will change in future.
|
||||
LogHelper.setLevel("-org.apache.activemq")
|
||||
|
||||
val directory = Paths.get(baseDirectory, role.name.toLowerCase())
|
||||
log.info("Using base demo directory $directory")
|
||||
|
||||
|
||||
|
||||
// Override the default config file (which you can find in the file "reference.conf") to give each node a name.
|
||||
val config = run {
|
||||
val myLegalName = role.legalName
|
||||
val configOverrides = mapOf(
|
||||
"myLegalName" to myLegalName,
|
||||
"artemisAddress" to myNetAddr.toString(),
|
||||
"webAddress" to apiNetAddr.toString()
|
||||
)
|
||||
FullNodeConfiguration(ConfigHelper.loadConfig(directory, allowMissingConfig = true, configOverrides = configOverrides))
|
||||
}
|
||||
|
||||
// Which services will this instance of the node provide to the network?
|
||||
val advertisedServices: Set<ServiceInfo>
|
||||
|
||||
// One of the two servers needs to run the network map and notary services. In such a trivial two-node network
|
||||
// the map is not very helpful, but we need one anyway. So just make the recipient side run the network map as it's
|
||||
// the side that sticks around waiting for the sender.
|
||||
val networkMapId = if (role == Role.SENDER) {
|
||||
advertisedServices = setOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))
|
||||
null
|
||||
} else {
|
||||
advertisedServices = emptySet()
|
||||
NodeMessagingClient.makeNetworkMapAddress(theirNetAddr)
|
||||
}
|
||||
|
||||
// And now construct then start the node object. It takes a little while.
|
||||
val node = logElapsedTime("Node startup", log) {
|
||||
Node(config, networkMapId, advertisedServices).setup().start()
|
||||
}
|
||||
|
||||
// What happens next depends on the role. The recipient sits around waiting for a transaction. The sender role
|
||||
// will contact the recipient and actually make something happen.
|
||||
when (role) {
|
||||
Role.RECIPIENT -> runRecipient(node)
|
||||
Role.SENDER -> {
|
||||
node.networkMapRegistrationFuture.success {
|
||||
// Pause a moment to give the network map time to update
|
||||
Thread.sleep(100L)
|
||||
val party = node.netMapCache.getNodeByLegalName(Role.RECIPIENT.legalName)?.legalIdentity ?: throw IllegalStateException("Cannot find other node?!")
|
||||
runSender(node, party)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node.run()
|
||||
}
|
||||
|
||||
private fun runRecipient(node: Node) {
|
||||
val serviceHub = node.services
|
||||
|
||||
// Normally we would receive the transaction from a more specific protocol, but in this case we let [FinalityProtocol]
|
||||
// handle receiving it for us.
|
||||
serviceHub.storageService.validatedTransactions.updates.subscribe { event ->
|
||||
// When the transaction is received, it's passed through [ResolveTransactionsProtocol], which first fetches any
|
||||
// attachments for us, then verifies the transaction. As such, by the time it hits the validated transaction store,
|
||||
// we have a copy of the attachment.
|
||||
val tx = event.tx
|
||||
if (tx.attachments.isNotEmpty()) {
|
||||
val attachment = serviceHub.storageService.attachments.openAttachment(tx.attachments.first())
|
||||
assertEquals(PROSPECTUS_HASH, attachment?.id)
|
||||
|
||||
println("File received - we're happy!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(event.tx)}")
|
||||
thread {
|
||||
node.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runSender(node: Node, otherSide: Party) {
|
||||
val serviceHub = node.services
|
||||
// Make sure we have the file in storage
|
||||
// TODO: We should have our own demo file, not share the trader demo file
|
||||
if (serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH) == null) {
|
||||
com.r3corda.demos.Role::class.java.getResourceAsStream("bank-of-london-cp.jar").use {
|
||||
val id = node.storage.attachments.importAttachment(it)
|
||||
assertEquals(PROSPECTUS_HASH, id)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a trivial transaction that just passes across the attachment - in normal cases there would be
|
||||
// inputs, outputs and commands that refer to this attachment.
|
||||
val ptx = TransactionType.General.Builder(notary = null)
|
||||
ptx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!.id)
|
||||
|
||||
// Despite not having any states, we have to have at least one signature on the transaction
|
||||
ptx.signWith(ALICE_KEY)
|
||||
|
||||
// Send the transaction to the other recipient
|
||||
val tx = ptx.toSignedTransaction()
|
||||
serviceHub.startProtocol(FinalityProtocol(tx, emptySet(), setOf(otherSide))).success {
|
||||
thread {
|
||||
Thread.sleep(1000L) // Give the other side time to request the attachment
|
||||
node.stop()
|
||||
}
|
||||
}.failure {
|
||||
println("Failed to relay message ")
|
||||
}
|
||||
}
|
||||
|
||||
private fun printHelp(parser: OptionParser) {
|
||||
println("""
|
||||
Usage: attachment-demo --role [RECIPIENT|SENDER] [options]
|
||||
Please refer to the documentation in docs/build/index.html for more info.
|
||||
|
||||
""".trimIndent())
|
||||
parser.printHelpOn(System.out)
|
||||
}
|
Binary file not shown.
@ -44,6 +44,9 @@ dependencies {
|
||||
|
||||
// Guava: Google test library (collections test suite)
|
||||
compile "com.google.guava:guava-testlib:19.0"
|
||||
|
||||
// OkHTTP: Simple HTTP library.
|
||||
compile 'com.squareup.okhttp3:okhttp:3.3.1'
|
||||
}
|
||||
|
||||
quasarScan.dependsOn('classes', ':core:classes', ':contracts:classes')
|
||||
|
@ -21,6 +21,7 @@ import com.r3corda.node.services.statemachine.StateMachineManager.Change
|
||||
import com.r3corda.node.utilities.AddOrRemove.ADD
|
||||
import com.r3corda.testing.node.MockIdentityService
|
||||
import com.r3corda.testing.node.MockServices
|
||||
import com.typesafe.config.Config
|
||||
import rx.Subscriber
|
||||
import java.net.ServerSocket
|
||||
import java.security.KeyPair
|
||||
@ -164,3 +165,5 @@ inline fun <reified P : ProtocolLogic<*>> AbstractNode.initiateSingleShotProtoco
|
||||
|
||||
return future
|
||||
}
|
||||
|
||||
fun Config.getHostAndPort(name: String) = HostAndPort.fromString(getString(name))
|
@ -0,0 +1,17 @@
|
||||
package com.r3corda.testing.http
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.google.common.net.HostAndPort
|
||||
import java.net.URL
|
||||
|
||||
class HttpApi(val root: URL) {
|
||||
fun putJson(path: String, data: Any = Unit) = HttpUtils.putJson(URL(root, path), toJson(data))
|
||||
fun postJson(path: String, data: Any = Unit) = HttpUtils.postJson(URL(root, path), toJson(data))
|
||||
|
||||
private fun toJson(any: Any) = ObjectMapper().writeValueAsString(any)
|
||||
|
||||
companion object {
|
||||
fun fromHostAndPort(hostAndPort: HostAndPort, base: String, protocol: String = "http"): HttpApi
|
||||
= HttpApi(URL("$protocol://$hostAndPort/$base/"))
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package com.r3corda.testing.http
|
||||
|
||||
import com.r3corda.core.utilities.loggerFor
|
||||
import okhttp3.*
|
||||
import java.net.URL
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* A small set of utilities for making HttpCalls, aimed at demos and tests.
|
||||
*/
|
||||
object HttpUtils {
|
||||
private val logger = loggerFor<HttpUtils>()
|
||||
private val client by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(5, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS).build()
|
||||
}
|
||||
|
||||
fun putJson(url: URL, data: String) : Boolean {
|
||||
val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), data)
|
||||
return makeRequest(Request.Builder().url(url).put(body).build())
|
||||
}
|
||||
|
||||
fun postJson(url: URL, data: String) : Boolean {
|
||||
val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), data)
|
||||
return makeRequest(Request.Builder().url(url).post(body).build())
|
||||
}
|
||||
|
||||
private fun makeRequest(request: Request): Boolean {
|
||||
val response = client.newCall(request).execute()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
logger.error("Could not fulfill HTTP request of type ${request.method()} to ${request.url()}. Status Code: ${response.code()}. Message: ${response.body().string()}")
|
||||
}
|
||||
return response.isSuccessful
|
||||
}
|
||||
}
|
@ -164,7 +164,7 @@ class MockStorageService(override val attachments: AttachmentStorage = MockAttac
|
||||
fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties {
|
||||
val props = Properties()
|
||||
props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource")
|
||||
props.setProperty("dataSource.url", "jdbc:h2:mem:${nodeName}_persistence;MVCC=TRUE;DB_CLOSE_ON_EXIT=FALSE")
|
||||
props.setProperty("dataSource.url", "jdbc:h2:mem:${nodeName}_persistence;DB_CLOSE_ON_EXIT=FALSE")
|
||||
props.setProperty("dataSource.user", "sa")
|
||||
props.setProperty("dataSource.password", "")
|
||||
return props
|
||||
|
Loading…
Reference in New Issue
Block a user