Merge from master

This commit is contained in:
Jose Coll 2016-11-02 14:20:51 +00:00
commit 1d25d23b73
49 changed files with 626 additions and 1128 deletions

View File

@ -11,6 +11,7 @@
<option name="taskNames"> <option name="taskNames">
<list> <list>
<option value="clean" /> <option value="clean" />
<option value="build" />
<option value="installDist" /> <option value="installDist" />
<option value="buildCordaJAR" /> <option value="buildCordaJAR" />
</list> </list>

View File

@ -30,7 +30,6 @@ plugins {
} }
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: 'application'
apply plugin: 'project-report' apply plugin: 'project-report'
apply plugin: QuasarPlugin apply plugin: QuasarPlugin
apply plugin: 'com.github.ben-manes.versions' apply plugin: 'com.github.ben-manes.versions'
@ -93,14 +92,8 @@ configurations {
integrationTestRuntime.extendsFrom testRuntime 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 // 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. // build/reports/project/dependencies/index.html for green highlighted parts of the tree.
dependencies { dependencies {
compile project(':node') compile project(':node')
// TODO: Demos should not depend on test code, but only use production APIs // TODO: Demos should not depend on test code, but only use production APIs
@ -121,41 +114,11 @@ dependencies {
integrationTestCompile project(':test-utils') 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']) { task integrationTest(type: Test, dependsOn: [':node:integrationTest',':client:integrationTest']) {
testClassesDir = sourceSets.integrationTest.output.classesDir testClassesDir = sourceSets.integrationTest.output.classesDir
classpath = sourceSets.integrationTest.runtimeClasspath classpath = sourceSets.integrationTest.runtimeClasspath
} }
task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) { task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
dependsOn = subprojects.test dependsOn = subprojects.test
additionalSourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs) additionalSourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs)
@ -183,12 +146,6 @@ tasks.withType(Test) {
quasarScan.dependsOn('classes', 'core:classes', 'contracts:classes', 'node:classes') 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']) { task buildCordaJAR(type: FatCapsule, dependsOn: ['quasarScan', 'buildCertSigningRequestUtilityJAR']) {
applicationClass 'com.r3corda.node.MainKt' applicationClass 'com.r3corda.node.MainKt'
archiveName 'corda.jar' archiveName 'corda.jar'

0
buildSrc/scripts/runnodes Normal file → Executable file
View File

View File

@ -40,7 +40,7 @@ sourceSets {
publishing { publishing {
publications { publications {
clients(MavenPublication) { client(MavenPublication) {
from components.java from components.java
artifactId 'client' artifactId 'client'

View File

@ -66,7 +66,7 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
) )
) { ) {
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Currency>>> 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>( 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) constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: PublicKey)
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
override val deposit = amount.token.issuer override val exitKeys = setOf(owner, amount.token.issuer.party.owningKey)
override val exitKeys = setOf(owner, deposit.party.owningKey)
override val contract = CASH_PROGRAM_ID override val contract = CASH_PROGRAM_ID
override val issuanceDef = amount.token
override val participants = listOf(owner) override val participants = listOf(owner)
override fun move(newAmount: Amount<Issued<Currency>>, newOwner: PublicKey): FungibleAsset<Currency> override fun move(newAmount: Amount<Issued<Currency>>, newOwner: PublicKey): FungibleAsset<Currency>
= copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner) = 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)) 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.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(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, issuanceDef.copy(issuer = deposit))) 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))) 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) infix fun Cash.State.`owned by`(owner: PublicKey) = ownedBy(owner)

View File

@ -72,7 +72,7 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.Commands, C
* Group commodity states by issuance definition (issuer and underlying commodity). * Group commodity states by issuance definition (issuer and underlying commodity).
*/ */
override fun groupStates(tx: TransactionForContract) 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) constructor(deposit: PartyAndReference, amount: Amount<Commodity>, owner: PublicKey)
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
override val deposit = amount.token.issuer
override val contract = COMMODITY_PROGRAM_ID override val contract = COMMODITY_PROGRAM_ID
override val exitKeys = Collections.singleton(owner) override val exitKeys = Collections.singleton(owner)
override val issuanceDef = amount.token
override val participants = listOf(owner) override val participants = listOf(owner)
override fun move(newAmount: Amount<Issued<Commodity>>, newOwner: PublicKey): FungibleAsset<Commodity> override fun move(newAmount: Amount<Issued<Commodity>>, newOwner: PublicKey): FungibleAsset<Commodity>
= copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner) = 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)) override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
} }

View File

@ -60,7 +60,7 @@ class Obligation<P> : Contract {
) )
) { ) {
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<Obligation.State<P>, Issued<Terms<P>>>> 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 } .filter { it.contract.legalContractReference in template.acceptableContracts }
// Restrict the states to those of the correct issuance definition (this normally // Restrict the states to those of the correct issuance definition (this normally
// covers issued product and obligor, but is opaque to us) // 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 // Catch that there's nothing useful here, so we can dump out a useful error
requireThat { requireThat {
"there are fungible asset state outputs" by (assetStates.size > 0) "there are fungible asset state outputs" by (assetStates.size > 0)
@ -164,7 +164,7 @@ class Obligation<P> : Contract {
// this one. // this one.
val moveCommands = tx.commands.select<MoveCommand>() val moveCommands = tx.commands.select<MoveCommand>()
var totalPenniesSettled = 0L 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 }) { for ((beneficiary, obligations) in inputs.groupBy { it.owner }) {
val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull<P>() val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull<P>()
@ -268,21 +268,12 @@ class Obligation<P> : Contract {
/** The public key of the entity the contract pays to */ /** The public key of the entity the contract pays to */
val beneficiary: PublicKey val beneficiary: PublicKey
) : FungibleAsset<Obligation.Terms<P>>, NettableState<State<P>, MultilateralNetState<P>> { ) : FungibleAsset<Obligation.Terms<P>>, NettableState<State<P>, MultilateralNetState<P>> {
override val amount: Amount<Issued<Terms<P>>> override val amount: Amount<Issued<Terms<P>>> = Amount(quantity, Issued(obligor.ref(0), template))
get() = Amount(quantity, issuanceDef)
override val contract = OBLIGATION_PROGRAM_ID override val contract = OBLIGATION_PROGRAM_ID
override val deposit: PartyAndReference override val exitKeys: Collection<PublicKey> = setOf(beneficiary)
get() = amount.token.issuer val dueBefore: Instant = template.dueBefore
override val exitKeys: Collection<PublicKey> override val participants: List<PublicKey> = listOf(obligor.owningKey, beneficiary)
get() = setOf(owner) override val owner: PublicKey = beneficiary
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 fun move(newAmount: Amount<Issued<Terms<P>>>, newOwner: PublicKey): State<P> override fun move(newAmount: Amount<Issued<Terms<P>>>, newOwner: PublicKey): State<P>
= copy(quantity = newAmount.quantity, beneficiary = newOwner) = 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" } require(states.all { it.lifecycle == existingLifecycle }) { "initial lifecycle must be $existingLifecycle for all input states" }
// Produce a new set of 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) { for ((aggregateState, stateAndRefs) in groups) {
val partiesUsed = ArrayList<PublicKey>() val partiesUsed = ArrayList<PublicKey>()
stateAndRefs.forEach { stateAndRef -> 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. */ /** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */
private fun getIssuanceDefinitionOrThrow(states: Iterable<State<P>>): Issued<Terms<P>> = 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. */ /** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */
private fun getTermsOrThrow(states: Iterable<State<P>>) = private fun getTermsOrThrow(states: Iterable<State<P>>) =

View File

@ -1,12 +1,7 @@
package com.r3corda.contracts.clause 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.*
import com.r3corda.core.contracts.clauses.Clause import com.r3corda.core.contracts.clauses.Clause
import com.r3corda.core.crypto.Party
import com.r3corda.core.transactions.TransactionBuilder import com.r3corda.core.transactions.TransactionBuilder
import java.security.PublicKey import java.security.PublicKey
import java.util.* 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 (gathered, gatheredAmount) = gatherCoins(acceptableCoins, Amount(amount.quantity, currency))
val takeChangeFrom = gathered.lastOrNull() val takeChangeFrom = gathered.lastOrNull()
val change = if (takeChangeFrom != null && gatheredAmount > amount) { 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 { } else {
null null
} }

View File

@ -32,7 +32,7 @@ import java.util.*
import kotlin.test.* import kotlin.test.*
class CashTests { class CashTests {
val defaultRef = OpaqueBytes(ByteArray(1, {1})) val defaultRef = OpaqueBytes(ByteArray(1, { 1 }))
val defaultIssuer = MEGA_CORP.ref(defaultRef) val defaultIssuer = MEGA_CORP.ref(defaultRef)
val inState = Cash.State( val inState = Cash.State(
amount = 1000.DOLLARS `issued by` defaultIssuer, amount = 1000.DOLLARS `issued by` defaultIssuer,
@ -43,7 +43,7 @@ class CashTests {
val outState = issuerInState.copy(owner = DUMMY_PUBKEY_2) val outState = issuerInState.copy(owner = DUMMY_PUBKEY_2)
fun Cash.State.editDepositRef(ref: Byte) = copy( 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 lateinit var services: MockServices
@ -85,12 +85,6 @@ class CashTests {
} }
} }
@After
fun tearDown() {
LogHelper.reset(NodeVaultService::class)
dataSource.close()
}
@Test @Test
fun trivial() { fun trivial() {
transaction { transaction {
@ -173,7 +167,7 @@ class CashTests {
assertTrue(tx.inputs.isEmpty()) assertTrue(tx.inputs.isEmpty())
val s = tx.outputs[0].data as Cash.State val s = tx.outputs[0].data as Cash.State
assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount) 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) assertEquals(DUMMY_PUBKEY_1, s.owner)
assertTrue(tx.commands[0].value is Cash.Commands.Issue) assertTrue(tx.commands[0].value is Cash.Commands.Issue)
assertEquals(MINI_CORP_PUBKEY, tx.commands[0].signers[0]) assertEquals(MINI_CORP_PUBKEY, tx.commands[0].signers[0])
@ -264,7 +258,7 @@ class CashTests {
// Include the previously issued cash in a new issuance command // Include the previously issued cash in a new issuance command
ptx = TransactionType.General.Builder(DUMMY_NOTARY) ptx = TransactionType.General.Builder(DUMMY_NOTARY)
ptx.addInputState(tx.tx.outRef<Cash.State>(0)) ptx.addInputState(tx.tx.outRef<Cash.State>(0))
Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY)
} }
@Test @Test
@ -492,7 +486,7 @@ class CashTests {
} }
fun makeSpend(amount: Amount<Currency>, dest: PublicKey): WireTransaction { fun makeSpend(amount: Amount<Currency>, dest: PublicKey): WireTransaction {
var tx = TransactionType.General.Builder(DUMMY_NOTARY) val tx = TransactionType.General.Builder(DUMMY_NOTARY)
databaseTransaction(database) { databaseTransaction(database) {
vault.generateSpend(tx, amount, dest) vault.generateSpend(tx, amount, dest)
} }
@ -618,8 +612,8 @@ class CashTests {
assertEquals(vaultState0.ref, wtx.inputs[0]) assertEquals(vaultState0.ref, wtx.inputs[0])
assertEquals(vaultState1.ref, wtx.inputs[1]) assertEquals(vaultState1.ref, wtx.inputs[1])
assertEquals(vaultState2.ref, wtx.inputs[2]) 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(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[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]) 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) 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 // 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 // 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 // States cannot be aggregated if the deposit differs
assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, oneThousandDollarsFromMini.issuanceDef) assertNotEquals(fiveThousandDollarsFromMega.amount.token, oneThousandDollarsFromMini.amount.token)
assertNotEquals(twoThousandDollarsFromMega.issuanceDef, oneThousandDollarsFromMini.issuanceDef) assertNotEquals(twoThousandDollarsFromMega.amount.token, oneThousandDollarsFromMini.amount.token)
// States cannot be aggregated if the currency differs // States cannot be aggregated if the currency differs
assertNotEquals(oneThousandDollarsFromMini.issuanceDef, assertNotEquals(oneThousandDollarsFromMini.amount.token,
Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY).issuanceDef) Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY).amount.token)
// States cannot be aggregated if the reference differs // States cannot be aggregated if the reference differs
assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, (fiveThousandDollarsFromMega `with deposit` defaultIssuer).issuanceDef) assertNotEquals(fiveThousandDollarsFromMega.amount.token, (fiveThousandDollarsFromMega `with deposit` defaultIssuer).amount.token)
assertNotEquals((fiveThousandDollarsFromMega `with deposit` defaultIssuer).issuanceDef, fiveThousandDollarsFromMega.issuanceDef) assertNotEquals((fiveThousandDollarsFromMega `with deposit` defaultIssuer).amount.token, fiveThousandDollarsFromMega.amount.token)
} }
@Test @Test

View File

@ -245,7 +245,7 @@ class ObligationTests {
val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)
val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE_PUBKEY) val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE_PUBKEY)
val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { 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(ALICE_KEY)
signWith(BOB_KEY) signWith(BOB_KEY)
signWith(DUMMY_NOTARY_KEY) signWith(DUMMY_NOTARY_KEY)
@ -259,7 +259,7 @@ class ObligationTests {
val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)
val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(BOB, ALICE_PUBKEY) val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(BOB, ALICE_PUBKEY)
val tx = TransactionType.General.Builder(null).apply { 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(ALICE_KEY)
signWith(BOB_KEY) signWith(BOB_KEY)
}.toSignedTransaction().tx }.toSignedTransaction().tx
@ -453,7 +453,7 @@ class ObligationTests {
input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000 obligation to Bob")
input("Alice's $1,000,000") input("Alice's $1,000,000")
output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_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) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
this.verifies() this.verifies()
} }
@ -467,7 +467,7 @@ class ObligationTests {
input(500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY) 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("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 } 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) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
this.verifies() this.verifies()
} }
@ -480,7 +480,7 @@ class ObligationTests {
input(defaultedObligation) // Alice's defaulted $1,000,000 obligation to Bob input(defaultedObligation) // Alice's defaulted $1,000,000 obligation to Bob
input(1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY) 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 } 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) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
this `fails with` "all inputs are in the normal state" 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 obligation to Bob")
input("Alice's $1,000,000") input("Alice's $1,000,000")
output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_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 / 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) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
this `fails with` "amount in settle command" 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 obligation to Bob")
input("Alice's 1 FCOJ") input("Alice's 1 FCOJ")
output("Bob's 1 FCOJ") { CommodityContract.State(oneUnitFcoj, BOB_PUBKEY) } 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) } command(ALICE_PUBKEY) { CommodityContract.Commands.Move(Obligation<Commodity>().legalContractReference) }
verifies() verifies()
} }
@ -648,13 +648,13 @@ class ObligationTests {
output { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) } output { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) }
tweak { 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() } command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() }
this `fails with` "the amounts balance" this `fails with` "the amounts balance"
} }
tweak { 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" this `fails with` "required com.r3corda.core.contracts.FungibleAsset.Commands.Move command"
tweak { tweak {
@ -679,10 +679,10 @@ class ObligationTests {
this `fails with` "for reference [00] at issuer MegaCorp the amounts balance" 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" 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() this.verifies()
} }
} }

View File

@ -85,6 +85,9 @@ dependencies {
// JPA 2.1 annotations. // JPA 2.1 annotations.
compile "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final" 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 { publishing {

View File

@ -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.). * (GBP, USD, oil, shares in company <X>, etc.) and any additional metadata (issuer, grade, class, etc.).
*/ */
interface FungibleAsset<T> : OwnableState { 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>> val amount: Amount<Issued<T>>
/** /**
* There must be an ExitCommand signed by these keys to destroy the amount. While all states require their * 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. // Small DSL extensions.
/** Sums the asset states in the list, returning null if there are none. */ /** Sums the asset states in the list, returning null if there are none. */

View File

@ -30,8 +30,8 @@ abstract class CordaPluginRegistry {
/** /**
* List of additional long lived services to be hosted within the node. * 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. * They are expected to have a single parameter constructor that takes a [PluginServiceHub] as input.
* The ServiceHubInternal will be fully constructed before the plugin service is created and will * 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. * allow access to the protocol factory and protocol initiation entry points there.
*/ */
open val servicePlugins: List<Class<*>> = emptyList() open val servicePlugins: List<Class<*>> = emptyList()

View File

@ -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<*>)?
}

View File

@ -177,7 +177,9 @@ interface VaultService {
fun getTransactionNotes(txnId: SecureHash): Iterable<String> 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) @Throws(InsufficientBalanceException::class)
fun generateSpend(tx: TransactionBuilder, fun generateSpend(tx: TransactionBuilder,

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

View File

@ -230,6 +230,10 @@ class X509UtilitiesTest {
clientParams.endpointIdentificationAlgorithm = "HTTPS" // enable hostname checking clientParams.endpointIdentificationAlgorithm = "HTTPS" // enable hostname checking
clientSocket.sslParameters = clientParams clientSocket.sslParameters = clientParams
clientSocket.useClientMode = true 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() val lock = Object()
var done = false var done = false
@ -281,4 +285,4 @@ class X509UtilitiesTest {
serverSocket.close() serverSocket.close()
assertTrue(done) assertTrue(done)
} }
} }

View File

@ -88,6 +88,13 @@
year = 2013 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, @misc{HBBFT,
author = {Andrew Miller and Yu Xia and Kyle Croman and Elaine Shi and Dawn Song}, author = {Andrew Miller and Yu Xia and Kyle Croman and Elaine Shi and Dawn Song},
title = "{{The Honey Badger of BFT Protocols}}", title = "{{The Honey Badger of BFT Protocols}}",
@ -136,4 +143,17 @@
publisher = {ACM}, publisher = {ACM},
address = {New York, NY, USA}, address = {New York, NY, USA},
keywords = {Large-Scale Distributed Storage}, 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}
} }

View File

@ -16,6 +16,8 @@
\usepackage[nottoc]{tocbibind} \usepackage[nottoc]{tocbibind}
\usepackage[parfill]{parskip} \usepackage[parfill]{parskip}
\usepackage{textcomp} \usepackage{textcomp}
\usepackage{scrextend}
\addtokomafont{labelinglabel}{\sffamily}
%\usepackage[natbibapa]{apacite} %\usepackage[natbibapa]{apacite}
\renewcommand{\thefootnote}{\alph{footnote}} \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 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 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 \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 and that fact is considered to be true. They may also optionally also provide the facts. This is how the ledger can be
fully deterministic. connected to the real world, despite being fully deterministic.
\end{itemize} \end{itemize}
A purely in-memory implementation of the messaging subsystem is provided which can inject simulated latency between 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. IP obfuscation system like Tor is also used.
Whilst simple string identities are likely sufficient for some networks, the financial industry typically requires some 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 level of \emph{know your customer} checking, and differentiation between different legal entities, branches and desks
the same brand name. Corda reuses the standard PKIX infrastructure for connecting public keys to identities and thus that may share the same brand name. Corda reuses the standard PKIX infrastructure for connecting public keys to
names are actually X.500 names. When a single string is sufficient the \emph{common name} field can be used alone, identities and thus names are actually X.500 names. When a single string is sufficient the \emph{common name} field can
similar to the web PKI. In more complex deployments the additional structure X.500 provides may be useful to be used alone, similar to the web PKI. In more complex deployments the additional structure X.500 provides may be useful
differentiate between entities with the same name. For example there are at least five different companies called 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. \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 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} \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 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 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 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 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 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. 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} \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{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{Merkle-structured transactions}
\subsection{Encumbrances} \subsection{Encumbrances}
\subsection{Contract constraints} \subsection{Contract constraints}
% TODO: Contract constraints aren't designed yet.
\section{Cash and Obligations} \section{Cash and Obligations}
\section{Non-asset instruments}
\section{Integration with existing infrastructure} \section{Integration with existing infrastructure}
\section{Deterministic JVM} \section{Deterministic JVM}
\section{Notaries} \section{Notaries}
\section{Clauses}
\section{Secure signing devices} \section{Secure signing devices}
\section{Client RPC and reactive collections} \section{Client RPC and reactive collections}
\section{Event scheduling} \section{Event scheduling}
\section{Future work}
\paragraph Secure hardware
\paragraph Zero knowledge proofs
\section{Conclusion} \section{Conclusion}

View File

@ -1,5 +1,6 @@
package com.r3corda.plugins package com.r3corda.plugins
import org.apache.tools.ant.filters.FixCrLfFilter
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskAction
import java.nio.file.Path import java.nio.file.Path
@ -53,7 +54,7 @@ class Cordform extends DefaultTask {
*/ */
protected Node getNodeByName(String name) { protected Node getNodeByName(String name) {
for(Node node : nodes) { for(Node node : nodes) {
if(node.name.equals(networkMapNodeName)) { if(node.name == networkMapNodeName) {
return node return node
} }
} }
@ -69,7 +70,7 @@ class Cordform extends DefaultTask {
from Cordformation.getPluginFile(project, "com/r3corda/plugins/runnodes") from Cordformation.getPluginFile(project, "com/r3corda/plugins/runnodes")
filter { String line -> line.replace("JAR_NAME", Node.JAR_NAME) } 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. // 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}/" into "${directory}/"
} }
} }

View File

@ -2,6 +2,7 @@ package com.r3corda.plugins
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project 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, * 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> { class Cordformation implements Plugin<Project> {
void apply(Project project) { void apply(Project project) {
Configuration cordappConf = project.configurations.create("cordapp")
cordappConf.transitive = false
project.configurations.compile.extendsFrom cordappConf
} }
/** /**

View File

@ -25,7 +25,8 @@ class Node {
*/ */
protected List<String> advertisedServices = [] 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. * @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 Config config = ConfigFactory.empty()
//private Map<String, Object> config = new HashMap<String, Object>() //private Map<String, Object> config = new HashMap<String, Object>()
private File nodeDir private File nodeDir
private def project private Project project
/** /**
* Set the name of the node. * Set the name of the node.
@ -150,7 +151,7 @@ class Node {
* Installs this project's cordapp to this directory. * Installs this project's cordapp to this directory.
*/ */
private void installBuiltPlugin() { private void installBuiltPlugin() {
def pluginsDir = getAndCreateDirectory(nodeDir, "plugins") def pluginsDir = new File(nodeDir, "plugins")
project.copy { project.copy {
from project.jar from project.jar
into pluginsDir into pluginsDir
@ -161,7 +162,7 @@ class Node {
* Installs other cordapps to this node's plugins directory. * Installs other cordapps to this node's plugins directory.
*/ */
private void installCordapps() { private void installCordapps() {
def pluginsDir = getAndCreateDirectory(nodeDir, "plugins") def pluginsDir = new File(nodeDir, "plugins")
def cordapps = getCordappList() def cordapps = getCordappList()
project.copy { project.copy {
from cordapps from cordapps
@ -174,9 +175,9 @@ class Node {
*/ */
private void installDependencies() { private void installDependencies() {
def cordaJar = verifyAndGetCordaJar() def cordaJar = verifyAndGetCordaJar()
def cordappList = getCordappList() def cordappDeps = getCordappList()
def depsDir = getAndCreateDirectory(nodeDir, "dependencies") def depsDir = new File(nodeDir, "dependencies")
def appDeps = project.configurations.runtime.filter { it != cordaJar && !cordappList.contains(it) } def appDeps = project.configurations.runtime.filter { it != cordaJar && !cordappDeps.contains(it) }
project.copy { project.copy {
from appDeps from appDeps
into depsDir into depsDir
@ -190,9 +191,17 @@ class Node {
// Adding required default values // Adding required default values
config = config.withValue('extraAdvertisedServiceIds', config = config.withValue('extraAdvertisedServiceIds',
ConfigValueFactory.fromAnyRef(advertisedServices.join(','))) ConfigValueFactory.fromAnyRef(advertisedServices.join(',')))
def configFileText = config.root().render(new ConfigRenderOptions(false, false, true, false)).split("\n").toList() 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. * @return List of this node's cordapps.
*/ */
private AbstractFileCollection getCordappList() { private Collection<File> getCordappList() {
def cordaJar = verifyAndGetCordaJar() return project.configurations.cordapp.files {
return project.configurations.runtime.filter { cordapps.contains("${it.group}:${it.name}:${it.version}")
def jarName = it.name.split('-').first()
return (it != cordaJar) && cordapps.contains(jarName)
} }
} }
/**
* 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
}
} }

View File

@ -122,7 +122,7 @@ dependencies {
testCompile 'com.pholser:junit-quickcheck-core:0.6' testCompile 'com.pholser:junit-quickcheck-core:0.6'
// For H2 database support in persistence // 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 // Exposed: Kotlin SQL library - under evaluation
compile "org.jetbrains.exposed:exposed:0.5.0" compile "org.jetbrains.exposed:exposed:0.5.0"

View File

@ -24,6 +24,10 @@ class JDBCHashMapTestSuite {
lateinit var dataSource: Closeable lateinit var dataSource: Closeable
lateinit var transaction: Transaction lateinit var transaction: Transaction
lateinit var database: Database 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 @JvmStatic
@BeforeClass @BeforeClass
@ -32,6 +36,10 @@ class JDBCHashMapTestSuite {
dataSource = dataSourceAndDatabase.first dataSource = dataSourceAndDatabase.first
database = dataSourceAndDatabase.second database = dataSourceAndDatabase.second
setUpDatabaseTx() 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 @JvmStatic
@ -112,7 +120,8 @@ class JDBCHashMapTestSuite {
*/ */
class JDBCHashMapTestGenerator(val loadOnInit: Boolean) : com.google.common.collect.testing.TestStringMapGenerator() { class JDBCHashMapTestGenerator(val loadOnInit: Boolean) : com.google.common.collect.testing.TestStringMapGenerator() {
override fun create(elements: Array<Map.Entry<String, String>>): Map<String, String> { 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) }) map.putAll(elements.associate { Pair(it.key, it.value) })
return map return map
} }
@ -143,7 +152,8 @@ class JDBCHashMapTestSuite {
*/ */
class JDBCHashSetTestGenerator(val loadOnInit: Boolean) : com.google.common.collect.testing.TestStringSetGenerator() { class JDBCHashSetTestGenerator(val loadOnInit: Boolean) : com.google.common.collect.testing.TestStringSetGenerator() {
override fun create(elements: Array<String>): Set<String> { 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) set.addAll(elements)
return set return set
} }

View File

@ -276,6 +276,7 @@ open class DriverDSL(
val conn = url.openConnection() as HttpURLConnection val conn = url.openConnection() as HttpURLConnection
conn.requestMethod = "GET" conn.requestMethod = "GET"
if (conn.responseCode != 200) { if (conn.responseCode != 200) {
log.error("Received response code ${conn.responseCode} from $url during startup.")
return null return null
} }
// For now the NodeInfo is tunneled in its Kryo format over the Node's Web interface. // 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) om.registerModule(module)
return om.readValue(conn.inputStream, NodeInfo::class.java) return om.readValue(conn.inputStream, NodeInfo::class.java)
} catch(e: Exception) { } catch(e: Exception) {
log.error("Could not query node info at $url due to an exception.", e)
return null return null
} }
} }

View File

@ -307,7 +307,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
val pluginServices = pluginRegistries.flatMap { x -> x.servicePlugins } val pluginServices = pluginRegistries.flatMap { x -> x.servicePlugins }
val serviceList = mutableListOf<Any>() val serviceList = mutableListOf<Any>()
for (serviceClass in pluginServices) { 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) serviceList.add(service)
tokenizableServices.add(service) tokenizableServices.add(service)
if (service is AcceptsFileUpload) { if (service is AcceptsFileUpload) {

View File

@ -33,6 +33,7 @@ import org.glassfish.jersey.servlet.ServletContainer
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import java.io.RandomAccessFile import java.io.RandomAccessFile
import java.lang.management.ManagementFactory import java.lang.management.ManagementFactory
import java.lang.reflect.InvocationTargetException
import java.nio.channels.FileLock import java.nio.channels.FileLock
import java.time.Clock import java.time.Clock
import java.util.* import java.util.*
@ -143,13 +144,18 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
// Export JMX monitoring statistics and data over REST/JSON. // Export JMX monitoring statistics and data over REST/JSON.
if (configuration.exportJMXto.split(',').contains("http")) { if (configuration.exportJMXto.split(',').contains("http")) {
handlerCollection.addHandler(WebAppContext().apply { val classpath = System.getProperty("java.class.path").split(System.getProperty("path.separator"))
// Find the jolokia WAR file on the classpath. val warpath = classpath.firstOrNull() { it.contains("jolokia-agent-war-2") && it.endsWith(".war") }
contextPath = "/monitoring/json" if (warpath != null) {
setInitParameter("mimeType", "application/json") handlerCollection.addHandler(WebAppContext().apply {
val classpath = System.getProperty("java.class.path").split(System.getProperty("path.separator")) // Find the jolokia WAR file on the classpath.
war = classpath.first { it.contains("jolokia-agent-war-2") && it.endsWith(".war") } contextPath = "/monitoring/json"
}) setInitParameter("mimeType", "application/json")
war = warpath
})
} else {
log.warn("Unable to locate Jolokia WAR on classpath")
}
} }
// API, data upload and download to services (attachments, rates oracles etc) // 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() val server = Server()
if (configuration.useHTTPS) { val connector = if (configuration.useHTTPS) {
val httpsConfiguration = HttpConfiguration() val httpsConfiguration = HttpConfiguration()
httpsConfiguration.outputBufferSize = 32768 httpsConfiguration.outputBufferSize = 32768
httpsConfiguration.addCustomizer(SecureRequestCustomizer()) httpsConfiguration.addCustomizer(SecureRequestCustomizer())
@ -173,14 +179,16 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
sslContextFactory.setIncludeCipherSuites(".*AES.*GCM.*") sslContextFactory.setIncludeCipherSuites(".*AES.*GCM.*")
val sslConnector = ServerConnector(server, SslConnectionFactory(sslContextFactory, "http/1.1"), HttpConnectionFactory(httpsConfiguration)) val sslConnector = ServerConnector(server, SslConnectionFactory(sslContextFactory, "http/1.1"), HttpConnectionFactory(httpsConfiguration))
sslConnector.port = configuration.webAddress.port sslConnector.port = configuration.webAddress.port
server.connectors = arrayOf<Connector>(sslConnector) sslConnector
} else { } else {
val httpConfiguration = HttpConfiguration() val httpConfiguration = HttpConfiguration()
httpConfiguration.outputBufferSize = 32768 httpConfiguration.outputBufferSize = 32768
val httpConnector = ServerConnector(server, HttpConnectionFactory(httpConfiguration)) val httpConnector = ServerConnector(server, HttpConnectionFactory(httpConfiguration))
httpConnector.port = configuration.webAddress.port 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 server.handler = handlerCollection
runOnStop += Runnable { server.stop() } runOnStop += Runnable { server.stop() }
@ -203,8 +211,19 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
val webAPIsOnClasspath = pluginRegistries.flatMap { x -> x.webApis } val webAPIsOnClasspath = pluginRegistries.flatMap { x -> x.webApis }
for (webapi in webAPIsOnClasspath) { for (webapi in webAPIsOnClasspath) {
log.info("Add Plugin web API from attachment ${webapi.name}") log.info("Add plugin web API from attachment ${webapi.name}")
val customAPI = webapi.getConstructor(ServiceHub::class.java).newInstance(services) 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) resourceConfig.register(customAPI)
} }
@ -270,7 +289,13 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
super.start() super.start()
// Only start the service API requests once the network map registration is complete // Only start the service API requests once the network map registration is complete
networkMapRegistrationFuture.then { networkMapRegistrationFuture.then {
webServer = initWebServer() 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. // Begin exporting our own metrics via JMX.
JmxReporter. JmxReporter.
forRegistry(services.monitoringService.metrics). forRegistry(services.monitoringService.metrics).

View File

@ -92,7 +92,8 @@ class ServerRPCOps(
val builder: TransactionBuilder = TransactionType.General.Builder(null) val builder: TransactionBuilder = TransactionType.General.Builder(null)
// TODO: Have some way of restricting this to states the caller controls // TODO: Have some way of restricting this to states the caller controls
try { 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 { keysForSigning.forEach {
val key = services.keyManagementService.keys[it] ?: throw IllegalStateException("Could not find signing key for ${it.toStringShort()}") val key = services.keyManagementService.keys[it] ?: throw IllegalStateException("Could not find signing key for ${it.toStringShort()}")

View File

@ -1,6 +1,7 @@
package com.r3corda.node.services package com.r3corda.node.services
import com.r3corda.core.node.CordaPluginRegistry import com.r3corda.core.node.CordaPluginRegistry
import com.r3corda.core.node.PluginServiceHub
import com.r3corda.core.serialization.SingletonSerializeAsToken import com.r3corda.core.serialization.SingletonSerializeAsToken
import com.r3corda.node.services.api.ServiceHubInternal import com.r3corda.node.services.api.ServiceHubInternal
import com.r3corda.protocols.NotaryChangeProtocol 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, * 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. * and immediately runs the [NotaryChangeProtocol] if the auto-accept criteria are met.
*/ */
class Service(services: ServiceHubInternal) : SingletonSerializeAsToken() { class Service(services: PluginServiceHub) : SingletonSerializeAsToken() {
init { init {
services.registerProtocolInitiator(NotaryChangeProtocol.Instigator::class) { NotaryChangeProtocol.Acceptor(it) } services.registerProtocolInitiator(NotaryChangeProtocol.Instigator::class) { NotaryChangeProtocol.Acceptor(it) }
} }

View File

@ -3,6 +3,7 @@ package com.r3corda.node.services.api
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.messaging.MessagingService import com.r3corda.core.messaging.MessagingService
import com.r3corda.core.node.PluginServiceHub
import com.r3corda.core.node.ServiceHub import com.r3corda.core.node.ServiceHub
import com.r3corda.core.node.services.TxWritableStorageService import com.r3corda.core.node.services.TxWritableStorageService
import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogic
@ -37,7 +38,7 @@ interface MessagingServiceBuilder<out T : MessagingServiceInternal> {
private val log = LoggerFactory.getLogger(ServiceHubInternal::class.java) private val log = LoggerFactory.getLogger(ServiceHubInternal::class.java)
abstract class ServiceHubInternal : ServiceHub { abstract class ServiceHubInternal : PluginServiceHub {
abstract val monitoringService: MonitoringService abstract val monitoringService: MonitoringService
abstract val protocolLogicRefFactory: ProtocolLogicRefFactory abstract val protocolLogicRefFactory: ProtocolLogicRefFactory
abstract val schemaService: SchemaService abstract val schemaService: SchemaService
@ -71,24 +72,6 @@ abstract class ServiceHubInternal : ServiceHub {
*/ */
abstract fun <T> startProtocol(logic: ProtocolLogic<T>): ListenableFuture<T> 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> { override fun <T : Any> invokeProtocolAsync(logicType: Class<out ProtocolLogic<T>>, vararg args: Any?): ListenableFuture<T> {
val logicRef = protocolLogicRefFactory.create(logicType, *args) val logicRef = protocolLogicRefFactory.create(logicType, *args)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")

View File

@ -6,6 +6,7 @@ import com.google.common.util.concurrent.SettableFuture
import com.r3corda.core.bufferUntilSubscribed import com.r3corda.core.bufferUntilSubscribed
import com.r3corda.core.contracts.Contract import com.r3corda.core.contracts.Contract
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.map import com.r3corda.core.map
import com.r3corda.core.messaging.MessagingService import com.r3corda.core.messaging.MessagingService
import com.r3corda.core.messaging.SingleMessageRecipient import com.r3corda.core.messaging.SingleMessageRecipient
@ -72,9 +73,16 @@ 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 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 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 getNodeByLegalName(name: String) = get().singleOrNull { it.legalIdentity.name == name }
override fun getNodeByPublicKey(publicKey: PublicKey) = get().singleOrNull { override fun getNodeByPublicKey(publicKey: PublicKey): NodeInfo? {
(it.legalIdentity.owningKey == publicKey) // Although we should never have more than one match, it is theoretically possible. Report an error if it happens.
|| it.advertisedServices.any { it.identity.owningKey == publicKey } 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, override fun addMapService(net: MessagingService, networkMapAddress: SingleMessageRecipient, subscribe: Boolean,

View File

@ -3,6 +3,7 @@ package com.r3corda.node.services.persistence
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.node.CordaPluginRegistry import com.r3corda.core.node.CordaPluginRegistry
import com.r3corda.core.node.PluginServiceHub
import com.r3corda.core.node.recordTransactions import com.r3corda.core.node.recordTransactions
import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.serialization.SingletonSerializeAsToken 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. * Additionally, because nodes do not store invalid transactions, requesting such a transaction will always yield null.
*/ */
@ThreadSafe @ThreadSafe
// TODO: I don't like that this needs ServiceHubInternal, but passing in a state machine breaks MockServices because class Service(services: PluginServiceHub) : SingletonSerializeAsToken() {
// 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() {
companion object { companion object {
val logger = loggerFor<DataVending.Service>() val logger = loggerFor<DataVending.Service>()

View File

@ -183,7 +183,7 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
var acceptableCoins = run { var acceptableCoins = run {
val ofCurrency = assetsStates.filter { it.state.data.amount.token.product == currency } val ofCurrency = assetsStates.filter { it.state.data.amount.token.product == currency }
if (onlyFromParties != null) if (onlyFromParties != null)
ofCurrency.filter { it.state.data.deposit.party in onlyFromParties } ofCurrency.filter { it.state.data.amount.token.issuer.party in onlyFromParties }
else else
ofCurrency ofCurrency
} }
@ -196,27 +196,32 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount) val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount)
val takeChangeFrom = gathered.firstOrNull() val takeChangeFrom = gathered.firstOrNull()
val change = if (takeChangeFrom != null && gatheredAmount > amount) { 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 { } else {
null null
} }
val keysUsed = gathered.map { it.state.data.owner }.toSet() 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 coins = it.value
val totalAmount = coins.map { it.state.data.amount }.sumOrThrow() val totalAmount = coins.map { it.state.data.amount }.sumOrThrow()
deriveState(coins.first().state, totalAmount, to) deriveState(coins.first().state, totalAmount, to)
} }.sortedBy { it.data.amount.quantity }
val outputs = if (change != null) { 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. // 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 // 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. // 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. // Add a change output and adjust the last output downwards.
states.subList(0, states.lastIndex) + states.subList(0, states.lastIndex) +
states.last().let { deriveState(it, it.data.amount - change, it.data.owner) } + states.last().let {
deriveState(gathered.last().state, change, changeKey) 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 } else states
for (state in gathered) tx.addInputState(state) for (state in gathered) tx.addInputState(state)

View File

@ -6,7 +6,7 @@ keyStorePassword = "cordacadevpass"
trustStorePassword = "trustpass" trustStorePassword = "trustpass"
dataSourceProperties = { dataSourceProperties = {
dataSourceClassName = org.h2.jdbcx.JdbcDataSource 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.user" = sa
"dataSource.password" = "" "dataSource.password" = ""
} }

View File

@ -1,8 +1,14 @@
package com.r3corda.node.services 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 com.r3corda.testing.node.MockNetwork
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals
class InMemoryNetworkMapCacheTest { class InMemoryNetworkMapCacheTest {
lateinit var network: MockNetwork lateinit var network: MockNetwork
@ -20,4 +26,20 @@ class InMemoryNetworkMapCacheTest {
network.runNetwork() network.runNetwork()
future.get() 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)
}
}
} }

View File

@ -1,35 +1,32 @@
package com.r3corda.node.services 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.contracts.*
import com.r3corda.core.days import com.r3corda.core.days
import com.r3corda.core.node.ServiceHub import com.r3corda.core.node.ServiceHub
import com.r3corda.core.node.recordTransactions import com.r3corda.core.node.recordTransactions
import com.r3corda.core.node.services.VaultService
import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.protocols.ProtocolLogicRef import com.r3corda.core.protocols.ProtocolLogicRef
import com.r3corda.core.protocols.ProtocolLogicRefFactory import com.r3corda.core.protocols.ProtocolLogicRefFactory
import com.r3corda.core.serialization.SingletonSerializeAsToken import com.r3corda.core.serialization.SingletonSerializeAsToken
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.utilities.DUMMY_NOTARY import com.r3corda.core.utilities.DUMMY_NOTARY
import com.r3corda.node.services.events.NodeSchedulerService import com.r3corda.node.services.events.NodeSchedulerService
import com.r3corda.node.services.persistence.DBCheckpointStorage import com.r3corda.node.services.persistence.DBCheckpointStorage
import com.r3corda.node.services.statemachine.StateMachineManager 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.AddOrRemove
import com.r3corda.node.utilities.AffinityExecutor import com.r3corda.node.utilities.AffinityExecutor
import com.r3corda.node.utilities.configureDatabase import com.r3corda.node.utilities.configureDatabase
import com.r3corda.node.utilities.databaseTransaction import com.r3corda.node.utilities.databaseTransaction
import com.r3corda.testing.ALICE_KEY 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.assertj.core.api.Assertions.assertThat
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.io.Closeable import java.io.Closeable
import java.nio.file.FileSystem
import java.security.PublicKey import java.security.PublicKey
import java.time.Clock import java.time.Clock
import java.time.Instant import java.time.Instant
@ -39,8 +36,6 @@ import java.util.concurrent.TimeUnit
import kotlin.test.assertTrue import kotlin.test.assertTrue
class NodeSchedulerServiceTest : SingletonSerializeAsToken() { 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 realClock: Clock = Clock.systemUTC()
val stoppedClock = Clock.fixed(realClock.instant(), realClock.zone) val stoppedClock = Clock.fixed(realClock.instant(), realClock.zone)
@ -82,19 +77,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
dataSource = dataSourceAndDatabase.first dataSource = dataSourceAndDatabase.first
database = dataSourceAndDatabase.second database = dataSourceAndDatabase.second
// Switched from InMemoryVault usage to NodeVault
databaseTransaction(database) { 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 kms = MockKeyManagementService(ALICE_KEY)
val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging(false, InMemoryMessagingNetwork.Handle(0, "None"), AffinityExecutor.ServiceAffinityExecutor("test", 1), database) 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 { services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, net = mockMessagingService), TestReference {

View File

@ -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

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -44,6 +44,9 @@ dependencies {
// Guava: Google test library (collections test suite) // Guava: Google test library (collections test suite)
compile "com.google.guava:guava-testlib:19.0" 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') quasarScan.dependsOn('classes', ':core:classes', ':contracts:classes')

View File

@ -21,6 +21,7 @@ import com.r3corda.node.services.statemachine.StateMachineManager.Change
import com.r3corda.node.utilities.AddOrRemove.ADD import com.r3corda.node.utilities.AddOrRemove.ADD
import com.r3corda.testing.node.MockIdentityService import com.r3corda.testing.node.MockIdentityService
import com.r3corda.testing.node.MockServices import com.r3corda.testing.node.MockServices
import com.typesafe.config.Config
import rx.Subscriber import rx.Subscriber
import java.net.ServerSocket import java.net.ServerSocket
import java.security.KeyPair import java.security.KeyPair
@ -163,4 +164,6 @@ inline fun <reified P : ProtocolLogic<*>> AbstractNode.initiateSingleShotProtoco
smm.changes.subscribe(subscriber) smm.changes.subscribe(subscriber)
return future return future
} }
fun Config.getHostAndPort(name: String) = HostAndPort.fromString(getString(name))

View File

@ -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/"))
}
}

View File

@ -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
}
}

View File

@ -164,7 +164,7 @@ class MockStorageService(override val attachments: AttachmentStorage = MockAttac
fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties { fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties {
val props = Properties() val props = Properties()
props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource") 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.user", "sa")
props.setProperty("dataSource.password", "") props.setProperty("dataSource.password", "")
return props return props