mirror of
https://github.com/corda/corda.git
synced 2024-12-19 13:08:04 +00:00
Merge from master
This commit is contained in:
commit
1d25d23b73
1
.idea/runConfigurations/Clean___Install.xml
generated
1
.idea/runConfigurations/Clean___Install.xml
generated
@ -11,6 +11,7 @@
|
|||||||
<option name="taskNames">
|
<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>
|
||||||
|
43
build.gradle
43
build.gradle
@ -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
0
buildSrc/scripts/runnodes
Normal file → Executable file
@ -40,7 +40,7 @@ sourceSets {
|
|||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
publications {
|
publications {
|
||||||
clients(MavenPublication) {
|
client(MavenPublication) {
|
||||||
from components.java
|
from components.java
|
||||||
artifactId 'client'
|
artifactId 'client'
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Currency>>>
|
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)
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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>>) =
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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. */
|
||||||
|
@ -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()
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
package com.r3corda.core.node
|
||||||
|
|
||||||
|
import com.r3corda.core.crypto.Party
|
||||||
|
import com.r3corda.core.protocols.ProtocolLogic
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service hub to be used by the [CordaPluginRegistry]
|
||||||
|
*/
|
||||||
|
interface PluginServiceHub : ServiceHub {
|
||||||
|
/**
|
||||||
|
* Register the protocol factory we wish to use when a initiating party attempts to communicate with us. The
|
||||||
|
* registration is done against a marker [KClass] which is sent in the session handshake by the other party. If this
|
||||||
|
* marker class has been registered then the corresponding factory will be used to create the protocol which will
|
||||||
|
* communicate with the other side. If there is no mapping then the session attempt is rejected.
|
||||||
|
* @param markerClass The marker [KClass] present in a session initiation attempt, which is a 1:1 mapping to a [Class]
|
||||||
|
* using the <pre>::class</pre> construct. Conventionally this is a [ProtocolLogic] subclass, however any class can
|
||||||
|
* be used, with the default being the class of the initiating protocol. This enables the registration to be of the
|
||||||
|
* form: registerProtocolInitiator(InitiatorProtocol::class, ::InitiatedProtocol)
|
||||||
|
* @param protocolFactory The protocol factory generating the initiated protocol.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: remove dependency on Kotlin relfection (Kotlin KClass -> Java Class).
|
||||||
|
fun registerProtocolInitiator(markerClass: KClass<*>, protocolFactory: (Party) -> ProtocolLogic<*>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the protocol factory that has been registered with [markerClass], or null if no factory is found.
|
||||||
|
*/
|
||||||
|
fun getProtocolFactory(markerClass: Class<*>): ((Party) -> ProtocolLogic<*>)?
|
||||||
|
}
|
@ -177,7 +177,9 @@ interface VaultService {
|
|||||||
fun getTransactionNotes(txnId: SecureHash): Iterable<String>
|
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,
|
||||||
|
27
core/src/main/kotlin/com/r3corda/core/utilities/ApiUtils.kt
Normal file
27
core/src/main/kotlin/com/r3corda/core/utilities/ApiUtils.kt
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package com.r3corda.core.utilities
|
||||||
|
|
||||||
|
import com.r3corda.core.crypto.Party
|
||||||
|
import com.r3corda.core.crypto.parsePublicKeyBase58
|
||||||
|
import com.r3corda.core.node.ServiceHub
|
||||||
|
import javax.ws.rs.core.Response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility functions to reduce boilerplate when developing HTTP APIs
|
||||||
|
*/
|
||||||
|
class ApiUtils(val services: ServiceHub) {
|
||||||
|
private val defaultNotFound = { msg: String -> Response.status(Response.Status.NOT_FOUND).entity(msg).build() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a party and then execute the passed function with the party public key as a parameter.
|
||||||
|
* Usage: withParty(key) { doSomethingWith(it) }
|
||||||
|
*/
|
||||||
|
fun withParty(partyKeyStr: String, notFound: (String) -> Response = defaultNotFound, found: (Party) -> Response): Response {
|
||||||
|
return try {
|
||||||
|
val partyKey = parsePublicKeyBase58(partyKeyStr)
|
||||||
|
val party = services.identityService.partyFromKey(partyKey)
|
||||||
|
if(party == null) notFound("Unknown party") else found(party)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
notFound("Invalid base58 key passed for party key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -230,6 +230,10 @@ class X509UtilitiesTest {
|
|||||||
clientParams.endpointIdentificationAlgorithm = "HTTPS" // enable hostname checking
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
}
|
}
|
@ -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}
|
||||||
|
|
||||||
|
@ -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}/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
0
gradle-plugins/cordformation/src/main/resources/com/r3corda/plugins/runnodes
Normal file → Executable file
0
gradle-plugins/cordformation/src/main/resources/com/r3corda/plugins/runnodes
Normal file → Executable file
@ -122,7 +122,7 @@ dependencies {
|
|||||||
testCompile 'com.pholser:junit-quickcheck-core:0.6'
|
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"
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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).
|
||||||
|
@ -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()}")
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
@ -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,
|
||||||
|
@ -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>()
|
||||||
|
@ -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)
|
||||||
|
@ -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" = ""
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -1,227 +0,0 @@
|
|||||||
# Some pretend noddy rate fixes, for the interest rate oracles.
|
|
||||||
|
|
||||||
ICE LIBOR 2016-03-16 1M = 0.678
|
|
||||||
ICE LIBOR 2016-03-16 2M = 0.655
|
|
||||||
EURIBOR 2016-03-15 1M = 0.123
|
|
||||||
EURIBOR 2016-03-15 2M = 0.111
|
|
||||||
|
|
||||||
ICE LIBOR 2016-03-06 3M = 0.0063515
|
|
||||||
ICE LIBOR 2016-03-07 3M = 0.0063516
|
|
||||||
ICE LIBOR 2016-03-08 3M = 0.0063517
|
|
||||||
ICE LIBOR 2016-03-09 3M = 0.0063518
|
|
||||||
ICE LIBOR 2016-03-10 3M = 0.0063519
|
|
||||||
ICE LIBOR 2016-06-06 3M = 0.0063520
|
|
||||||
ICE LIBOR 2016-06-07 3M = 0.0063521
|
|
||||||
ICE LIBOR 2016-06-08 3M = 0.0063522
|
|
||||||
ICE LIBOR 2016-06-09 3M = 0.0063523
|
|
||||||
ICE LIBOR 2016-06-10 3M = 0.0063524
|
|
||||||
ICE LIBOR 2016-09-06 3M = 0.0063525
|
|
||||||
ICE LIBOR 2016-09-07 3M = 0.0063526
|
|
||||||
ICE LIBOR 2016-09-08 3M = 0.0063527
|
|
||||||
ICE LIBOR 2016-09-09 3M = 0.0063528
|
|
||||||
ICE LIBOR 2016-09-10 3M = 0.0063529
|
|
||||||
ICE LIBOR 2016-12-06 3M = 0.0063530
|
|
||||||
ICE LIBOR 2016-12-07 3M = 0.0063531
|
|
||||||
ICE LIBOR 2016-12-08 3M = 0.0063532
|
|
||||||
ICE LIBOR 2016-12-09 3M = 0.0063533
|
|
||||||
ICE LIBOR 2016-12-10 3M = 0.0063534
|
|
||||||
ICE LIBOR 2017-03-06 3M = 0.0063535
|
|
||||||
ICE LIBOR 2017-03-07 3M = 0.0063536
|
|
||||||
ICE LIBOR 2017-03-08 3M = 0.0063537
|
|
||||||
ICE LIBOR 2017-03-09 3M = 0.0063538
|
|
||||||
ICE LIBOR 2017-03-10 3M = 0.0063539
|
|
||||||
ICE LIBOR 2017-06-06 3M = 0.0063540
|
|
||||||
ICE LIBOR 2017-06-07 3M = 0.0063541
|
|
||||||
ICE LIBOR 2017-06-08 3M = 0.0063542
|
|
||||||
ICE LIBOR 2017-06-09 3M = 0.0063543
|
|
||||||
ICE LIBOR 2017-06-10 3M = 0.0063544
|
|
||||||
ICE LIBOR 2017-09-06 3M = 0.0063545
|
|
||||||
ICE LIBOR 2017-09-07 3M = 0.0063546
|
|
||||||
ICE LIBOR 2017-09-08 3M = 0.0063547
|
|
||||||
ICE LIBOR 2017-09-09 3M = 0.0063548
|
|
||||||
ICE LIBOR 2017-09-10 3M = 0.0063549
|
|
||||||
ICE LIBOR 2017-12-06 3M = 0.0063550
|
|
||||||
ICE LIBOR 2017-12-07 3M = 0.0063551
|
|
||||||
ICE LIBOR 2017-12-08 3M = 0.0063552
|
|
||||||
ICE LIBOR 2017-12-09 3M = 0.0063553
|
|
||||||
ICE LIBOR 2017-12-10 3M = 0.0063554
|
|
||||||
ICE LIBOR 2018-03-06 3M = 0.0063555
|
|
||||||
ICE LIBOR 2018-03-07 3M = 0.0063556
|
|
||||||
ICE LIBOR 2018-03-08 3M = 0.0063557
|
|
||||||
ICE LIBOR 2018-03-09 3M = 0.0063558
|
|
||||||
ICE LIBOR 2018-03-10 3M = 0.0063559
|
|
||||||
ICE LIBOR 2018-06-06 3M = 0.0063560
|
|
||||||
ICE LIBOR 2018-06-07 3M = 0.0063561
|
|
||||||
ICE LIBOR 2018-06-08 3M = 0.0063562
|
|
||||||
ICE LIBOR 2018-06-09 3M = 0.0063563
|
|
||||||
ICE LIBOR 2018-06-10 3M = 0.0063564
|
|
||||||
ICE LIBOR 2018-09-06 3M = 0.0063565
|
|
||||||
ICE LIBOR 2018-09-07 3M = 0.0063566
|
|
||||||
ICE LIBOR 2018-09-08 3M = 0.0063567
|
|
||||||
ICE LIBOR 2018-09-09 3M = 0.0063568
|
|
||||||
ICE LIBOR 2018-09-10 3M = 0.0063569
|
|
||||||
ICE LIBOR 2018-12-06 3M = 0.0063570
|
|
||||||
ICE LIBOR 2018-12-07 3M = 0.0063571
|
|
||||||
ICE LIBOR 2018-12-08 3M = 0.0063572
|
|
||||||
ICE LIBOR 2018-12-09 3M = 0.0063573
|
|
||||||
ICE LIBOR 2018-12-10 3M = 0.0063574
|
|
||||||
ICE LIBOR 2019-03-06 3M = 0.0063575
|
|
||||||
ICE LIBOR 2019-03-07 3M = 0.0063576
|
|
||||||
ICE LIBOR 2019-03-08 3M = 0.0063577
|
|
||||||
ICE LIBOR 2019-03-09 3M = 0.0063578
|
|
||||||
ICE LIBOR 2019-03-10 3M = 0.0063579
|
|
||||||
ICE LIBOR 2019-06-06 3M = 0.0063580
|
|
||||||
ICE LIBOR 2019-06-07 3M = 0.0063581
|
|
||||||
ICE LIBOR 2019-06-08 3M = 0.0063582
|
|
||||||
ICE LIBOR 2019-06-09 3M = 0.0063583
|
|
||||||
ICE LIBOR 2019-06-10 3M = 0.0063584
|
|
||||||
ICE LIBOR 2019-09-06 3M = 0.0063585
|
|
||||||
ICE LIBOR 2019-09-07 3M = 0.0063586
|
|
||||||
ICE LIBOR 2019-09-08 3M = 0.0063587
|
|
||||||
ICE LIBOR 2019-09-09 3M = 0.0063588
|
|
||||||
ICE LIBOR 2019-09-10 3M = 0.0063589
|
|
||||||
ICE LIBOR 2019-12-06 3M = 0.0063590
|
|
||||||
ICE LIBOR 2019-12-07 3M = 0.0063591
|
|
||||||
ICE LIBOR 2019-12-08 3M = 0.0063592
|
|
||||||
ICE LIBOR 2019-12-09 3M = 0.0063593
|
|
||||||
ICE LIBOR 2019-12-10 3M = 0.0063594
|
|
||||||
ICE LIBOR 2020-03-06 3M = 0.0063595
|
|
||||||
ICE LIBOR 2020-03-07 3M = 0.0063596
|
|
||||||
ICE LIBOR 2020-03-08 3M = 0.0063597
|
|
||||||
ICE LIBOR 2020-03-09 3M = 0.0063598
|
|
||||||
ICE LIBOR 2020-03-10 3M = 0.0063599
|
|
||||||
ICE LIBOR 2020-06-06 3M = 0.0063600
|
|
||||||
ICE LIBOR 2020-06-07 3M = 0.0063601
|
|
||||||
ICE LIBOR 2020-06-08 3M = 0.0063602
|
|
||||||
ICE LIBOR 2020-06-09 3M = 0.0063603
|
|
||||||
ICE LIBOR 2020-06-10 3M = 0.0063604
|
|
||||||
ICE LIBOR 2020-09-06 3M = 0.0063605
|
|
||||||
ICE LIBOR 2020-09-07 3M = 0.0063606
|
|
||||||
ICE LIBOR 2020-09-08 3M = 0.0063607
|
|
||||||
ICE LIBOR 2020-09-09 3M = 0.0063608
|
|
||||||
ICE LIBOR 2020-09-10 3M = 0.0063609
|
|
||||||
ICE LIBOR 2020-12-06 3M = 0.0063610
|
|
||||||
ICE LIBOR 2020-12-07 3M = 0.0063611
|
|
||||||
ICE LIBOR 2020-12-08 3M = 0.0063612
|
|
||||||
ICE LIBOR 2020-12-09 3M = 0.0063613
|
|
||||||
ICE LIBOR 2020-12-10 3M = 0.0063614
|
|
||||||
ICE LIBOR 2021-03-06 3M = 0.0063615
|
|
||||||
ICE LIBOR 2021-03-07 3M = 0.0063616
|
|
||||||
ICE LIBOR 2021-03-08 3M = 0.0063617
|
|
||||||
ICE LIBOR 2021-03-09 3M = 0.0063618
|
|
||||||
ICE LIBOR 2021-03-10 3M = 0.0063619
|
|
||||||
ICE LIBOR 2021-06-06 3M = 0.0063620
|
|
||||||
ICE LIBOR 2021-06-07 3M = 0.0063621
|
|
||||||
ICE LIBOR 2021-06-08 3M = 0.0063622
|
|
||||||
ICE LIBOR 2021-06-09 3M = 0.0063623
|
|
||||||
ICE LIBOR 2021-06-10 3M = 0.0063624
|
|
||||||
ICE LIBOR 2021-09-06 3M = 0.0063625
|
|
||||||
ICE LIBOR 2021-09-07 3M = 0.0063626
|
|
||||||
ICE LIBOR 2021-09-08 3M = 0.0063627
|
|
||||||
ICE LIBOR 2021-09-09 3M = 0.0063628
|
|
||||||
ICE LIBOR 2021-09-10 3M = 0.0063629
|
|
||||||
ICE LIBOR 2021-12-06 3M = 0.0063630
|
|
||||||
ICE LIBOR 2021-12-07 3M = 0.0063631
|
|
||||||
ICE LIBOR 2021-12-08 3M = 0.0063632
|
|
||||||
ICE LIBOR 2021-12-09 3M = 0.0063633
|
|
||||||
ICE LIBOR 2021-12-10 3M = 0.0063634
|
|
||||||
ICE LIBOR 2022-03-06 3M = 0.0063635
|
|
||||||
ICE LIBOR 2022-03-07 3M = 0.0063636
|
|
||||||
ICE LIBOR 2022-03-08 3M = 0.0063637
|
|
||||||
ICE LIBOR 2022-03-09 3M = 0.0063638
|
|
||||||
ICE LIBOR 2022-03-10 3M = 0.0063639
|
|
||||||
ICE LIBOR 2022-06-06 3M = 0.0063640
|
|
||||||
ICE LIBOR 2022-06-07 3M = 0.0063641
|
|
||||||
ICE LIBOR 2022-06-08 3M = 0.0063642
|
|
||||||
ICE LIBOR 2022-06-09 3M = 0.0063643
|
|
||||||
ICE LIBOR 2022-06-10 3M = 0.0063644
|
|
||||||
ICE LIBOR 2022-09-06 3M = 0.0063645
|
|
||||||
ICE LIBOR 2022-09-07 3M = 0.0063646
|
|
||||||
ICE LIBOR 2022-09-08 3M = 0.0063647
|
|
||||||
ICE LIBOR 2022-09-09 3M = 0.0063648
|
|
||||||
ICE LIBOR 2022-09-10 3M = 0.0063649
|
|
||||||
ICE LIBOR 2022-12-06 3M = 0.0063650
|
|
||||||
ICE LIBOR 2022-12-07 3M = 0.0063651
|
|
||||||
ICE LIBOR 2022-12-08 3M = 0.0063652
|
|
||||||
ICE LIBOR 2022-12-09 3M = 0.0063653
|
|
||||||
ICE LIBOR 2022-12-10 3M = 0.0063654
|
|
||||||
ICE LIBOR 2023-03-06 3M = 0.0063655
|
|
||||||
ICE LIBOR 2023-03-07 3M = 0.0063656
|
|
||||||
ICE LIBOR 2023-03-08 3M = 0.0063657
|
|
||||||
ICE LIBOR 2023-03-09 3M = 0.0063658
|
|
||||||
ICE LIBOR 2023-03-10 3M = 0.0063659
|
|
||||||
ICE LIBOR 2023-06-06 3M = 0.0063660
|
|
||||||
ICE LIBOR 2023-06-07 3M = 0.0063661
|
|
||||||
ICE LIBOR 2023-06-08 3M = 0.0063662
|
|
||||||
ICE LIBOR 2023-06-09 3M = 0.0063663
|
|
||||||
ICE LIBOR 2023-06-10 3M = 0.0063664
|
|
||||||
ICE LIBOR 2023-09-06 3M = 0.0063665
|
|
||||||
ICE LIBOR 2023-09-07 3M = 0.0063666
|
|
||||||
ICE LIBOR 2023-09-08 3M = 0.0063667
|
|
||||||
ICE LIBOR 2023-09-09 3M = 0.0063668
|
|
||||||
ICE LIBOR 2023-09-10 3M = 0.0063669
|
|
||||||
ICE LIBOR 2023-12-06 3M = 0.0063670
|
|
||||||
ICE LIBOR 2023-12-07 3M = 0.0063671
|
|
||||||
ICE LIBOR 2023-12-08 3M = 0.0063672
|
|
||||||
ICE LIBOR 2023-12-09 3M = 0.0063673
|
|
||||||
ICE LIBOR 2023-12-10 3M = 0.0063674
|
|
||||||
ICE LIBOR 2024-03-06 3M = 0.0063675
|
|
||||||
ICE LIBOR 2024-03-07 3M = 0.0063676
|
|
||||||
ICE LIBOR 2024-03-08 3M = 0.0063677
|
|
||||||
ICE LIBOR 2024-03-09 3M = 0.0063678
|
|
||||||
ICE LIBOR 2024-03-10 3M = 0.0063679
|
|
||||||
ICE LIBOR 2024-06-06 3M = 0.0063680
|
|
||||||
ICE LIBOR 2024-06-07 3M = 0.0063681
|
|
||||||
ICE LIBOR 2024-06-08 3M = 0.0063682
|
|
||||||
ICE LIBOR 2024-06-09 3M = 0.0063683
|
|
||||||
ICE LIBOR 2024-06-10 3M = 0.0063684
|
|
||||||
ICE LIBOR 2024-09-06 3M = 0.0063685
|
|
||||||
ICE LIBOR 2024-09-07 3M = 0.0063686
|
|
||||||
ICE LIBOR 2024-09-08 3M = 0.0063687
|
|
||||||
ICE LIBOR 2024-09-09 3M = 0.0063688
|
|
||||||
ICE LIBOR 2024-09-10 3M = 0.0063689
|
|
||||||
ICE LIBOR 2024-12-06 3M = 0.0063690
|
|
||||||
ICE LIBOR 2024-12-07 3M = 0.0063691
|
|
||||||
ICE LIBOR 2024-12-08 3M = 0.0063692
|
|
||||||
ICE LIBOR 2024-12-09 3M = 0.0063693
|
|
||||||
ICE LIBOR 2024-12-10 3M = 0.0063694
|
|
||||||
ICE LIBOR 2025-03-06 3M = 0.0063695
|
|
||||||
ICE LIBOR 2025-03-07 3M = 0.0063696
|
|
||||||
ICE LIBOR 2025-03-08 3M = 0.0063697
|
|
||||||
ICE LIBOR 2025-03-09 3M = 0.0063698
|
|
||||||
ICE LIBOR 2025-03-10 3M = 0.0063699
|
|
||||||
ICE LIBOR 2025-06-06 3M = 0.0063700
|
|
||||||
ICE LIBOR 2025-06-07 3M = 0.0063701
|
|
||||||
ICE LIBOR 2025-06-08 3M = 0.0063702
|
|
||||||
ICE LIBOR 2025-06-09 3M = 0.0063703
|
|
||||||
ICE LIBOR 2025-06-10 3M = 0.0063704
|
|
||||||
ICE LIBOR 2025-09-06 3M = 0.0063705
|
|
||||||
ICE LIBOR 2025-09-07 3M = 0.0063706
|
|
||||||
ICE LIBOR 2025-09-08 3M = 0.0063707
|
|
||||||
ICE LIBOR 2025-09-09 3M = 0.0063708
|
|
||||||
ICE LIBOR 2025-09-10 3M = 0.0063709
|
|
||||||
ICE LIBOR 2025-12-06 3M = 0.0063710
|
|
||||||
ICE LIBOR 2025-12-07 3M = 0.0063711
|
|
||||||
ICE LIBOR 2025-12-08 3M = 0.0063712
|
|
||||||
ICE LIBOR 2025-12-09 3M = 0.0063713
|
|
||||||
ICE LIBOR 2025-12-10 3M = 0.0063714
|
|
||||||
ICE LIBOR 2026-03-06 3M = 0.0063715
|
|
||||||
ICE LIBOR 2026-03-07 3M = 0.0063716
|
|
||||||
ICE LIBOR 2026-03-08 3M = 0.0063717
|
|
||||||
ICE LIBOR 2026-03-09 3M = 0.0063718
|
|
||||||
ICE LIBOR 2026-03-10 3M = 0.0063719
|
|
||||||
ICE LIBOR 2026-06-06 3M = 0.0063720
|
|
||||||
ICE LIBOR 2026-06-07 3M = 0.0063721
|
|
||||||
ICE LIBOR 2026-06-08 3M = 0.0063722
|
|
||||||
ICE LIBOR 2026-06-09 3M = 0.0063723
|
|
||||||
ICE LIBOR 2026-06-10 3M = 0.0063724
|
|
||||||
ICE LIBOR 2026-09-06 3M = 0.0063725
|
|
||||||
ICE LIBOR 2026-09-07 3M = 0.0063726
|
|
||||||
ICE LIBOR 2026-09-08 3M = 0.0063727
|
|
||||||
ICE LIBOR 2026-09-09 3M = 0.0063728
|
|
||||||
ICE LIBOR 2026-09-10 3M = 0.0063729
|
|
||||||
ICE LIBOR 2026-12-06 3M = 0.0063730
|
|
||||||
ICE LIBOR 2026-12-07 3M = 0.0063731
|
|
||||||
ICE LIBOR 2026-12-08 3M = 0.0063732
|
|
||||||
ICE LIBOR 2026-12-09 3M = 0.0063733
|
|
||||||
ICE LIBOR 2026-12-10 3M = 0.0063734
|
|
@ -1,24 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# This needs the buyer node to be running first.
|
|
||||||
|
|
||||||
if [ ! -e ./gradlew ]; then
|
|
||||||
echo "Run from the root directory please"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
bin="build/install/r3prototyping/bin/get-rate-fix"
|
|
||||||
|
|
||||||
if [ ! -e $bin ]; then
|
|
||||||
./gradlew installDist
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -e build/trader-demo/buyer/identity-public ]; then
|
|
||||||
echo "You must run scripts/trade-demo.sh buyer before running this script (and keep it running)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Upload the rates to the buyer node
|
|
||||||
curl -F rates=@scripts/example.rates.txt http://localhost:31338/upload/interest-rates
|
|
||||||
|
|
||||||
$bin --network-address=localhost:31300 --directory=build/trader-demo/rates-fix --network-map=localhost:31337 --network-map-identity-file=build/trader-demo/buyer/identity-public
|
|
@ -1,55 +0,0 @@
|
|||||||
package com.r3corda.core.testing
|
|
||||||
|
|
||||||
import com.google.common.net.HostAndPort
|
|
||||||
import com.r3corda.testing.*
|
|
||||||
import org.junit.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
class TraderDemoTest {
|
|
||||||
@Test fun `runs trader demo`() {
|
|
||||||
val buyerAddr = freeLocalHostAndPort()
|
|
||||||
val buyerApiAddr = freeLocalHostAndPort()
|
|
||||||
val directory = "./build/integration-test/${TestTimestamp.timestamp}/trader-demo"
|
|
||||||
var nodeProc: Process? = null
|
|
||||||
try {
|
|
||||||
nodeProc = runBuyer(directory, buyerAddr, buyerApiAddr)
|
|
||||||
runSeller(directory, buyerAddr)
|
|
||||||
} finally {
|
|
||||||
nodeProc?.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private fun runBuyer(baseDirectory: String, buyerAddr: HostAndPort, buyerApiAddr: HostAndPort): Process {
|
|
||||||
println("Running Buyer")
|
|
||||||
val args = listOf(
|
|
||||||
"--role", "BUYER",
|
|
||||||
"--network-address", buyerAddr.toString(),
|
|
||||||
"--api-address", buyerApiAddr.toString(),
|
|
||||||
"--base-directory", baseDirectory,
|
|
||||||
"--h2-port", "0"
|
|
||||||
)
|
|
||||||
val proc = spawn("com.r3corda.demos.TraderDemoKt", args, "TradeDemoBuyer")
|
|
||||||
NodeApi.ensureNodeStartsOrKill(proc, buyerApiAddr)
|
|
||||||
return proc
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runSeller(baseDirectory: String, buyerAddr: HostAndPort) {
|
|
||||||
println("Running Seller")
|
|
||||||
val sellerAddr = freeLocalHostAndPort()
|
|
||||||
val sellerApiAddr = freeLocalHostAndPort()
|
|
||||||
val args = listOf(
|
|
||||||
"--role", "SELLER",
|
|
||||||
"--network-address", sellerAddr.toString(),
|
|
||||||
"--api-address", sellerApiAddr.toString(),
|
|
||||||
"--other-network-address", buyerAddr.toString(),
|
|
||||||
"--base-directory", baseDirectory,
|
|
||||||
"--h2-port", "0"
|
|
||||||
)
|
|
||||||
val proc = spawn("com.r3corda.demos.TraderDemoKt", args, "TradeDemoSeller")
|
|
||||||
assertExitOrKill(proc)
|
|
||||||
assertEquals(proc.exitValue(), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,381 +0,0 @@
|
|||||||
package com.r3corda.demos
|
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
|
||||||
import com.google.common.net.HostAndPort
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import com.r3corda.contracts.CommercialPaper
|
|
||||||
import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER
|
|
||||||
import com.r3corda.contracts.testing.fillWithSomeTestCash
|
|
||||||
import com.r3corda.core.contracts.*
|
|
||||||
import com.r3corda.core.crypto.Party
|
|
||||||
import com.r3corda.core.crypto.SecureHash
|
|
||||||
import com.r3corda.core.crypto.generateKeyPair
|
|
||||||
import com.r3corda.core.days
|
|
||||||
import com.r3corda.core.logElapsedTime
|
|
||||||
import com.r3corda.core.node.NodeInfo
|
|
||||||
import com.r3corda.core.node.services.ServiceInfo
|
|
||||||
import com.r3corda.core.protocols.ProtocolLogic
|
|
||||||
import com.r3corda.core.seconds
|
|
||||||
import com.r3corda.core.success
|
|
||||||
import com.r3corda.core.transactions.SignedTransaction
|
|
||||||
import com.r3corda.core.utilities.Emoji
|
|
||||||
import com.r3corda.core.utilities.LogHelper
|
|
||||||
import com.r3corda.core.utilities.ProgressTracker
|
|
||||||
import com.r3corda.node.internal.Node
|
|
||||||
import com.r3corda.node.services.config.ConfigHelper
|
|
||||||
import com.r3corda.node.services.config.FullNodeConfiguration
|
|
||||||
import com.r3corda.node.services.messaging.NodeMessagingClient
|
|
||||||
import com.r3corda.node.services.network.NetworkMapService
|
|
||||||
import com.r3corda.node.services.persistence.NodeAttachmentService
|
|
||||||
import com.r3corda.node.services.transactions.ValidatingNotaryService
|
|
||||||
import com.r3corda.node.utilities.databaseTransaction
|
|
||||||
import com.r3corda.protocols.NotaryProtocol
|
|
||||||
import com.r3corda.protocols.TwoPartyTradeProtocol
|
|
||||||
import joptsimple.OptionParser
|
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.security.PublicKey
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.concurrent.thread
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
// TRADING DEMO
|
|
||||||
//
|
|
||||||
// Please see docs/build/html/running-the-demos.html
|
|
||||||
//
|
|
||||||
// This program is a simple driver for exercising the two party trading protocol. Until Corda has a unified node server
|
|
||||||
// programs like this are required to wire up the pieces and run a demo scenario end to end.
|
|
||||||
//
|
|
||||||
// If you are creating a new scenario, you can use this program as a template for creating your own driver. Make sure to
|
|
||||||
// copy/paste the right parts of the build.gradle file to make sure it gets a script to run it deposited in
|
|
||||||
// build/install/r3prototyping/bin
|
|
||||||
//
|
|
||||||
// In this scenario, a buyer wants to purchase some commercial paper by swapping his cash for the CP. The seller learns
|
|
||||||
// that the buyer exists, and sends them a message to kick off the trade. The seller, having obtained his CP, then quits
|
|
||||||
// and the buyer goes back to waiting. The buyer will sell as much CP as he can!
|
|
||||||
//
|
|
||||||
// The different roles in the scenario this program can adopt are:
|
|
||||||
|
|
||||||
enum class Role {
|
|
||||||
BUYER,
|
|
||||||
SELLER
|
|
||||||
}
|
|
||||||
|
|
||||||
// And this is the directory under the current working directory where each node will create its own server directory,
|
|
||||||
// which holds things like checkpoints, keys, databases, message logs etc.
|
|
||||||
val DEFAULT_BASE_DIRECTORY = "./build/trader-demo"
|
|
||||||
|
|
||||||
private val log: Logger = LoggerFactory.getLogger("TraderDemo")
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
val parser = OptionParser()
|
|
||||||
|
|
||||||
val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required()
|
|
||||||
val myNetworkAddress = parser.accepts("network-address").withRequiredArg().defaultsTo("localhost")
|
|
||||||
val theirNetworkAddress = parser.accepts("other-network-address").withRequiredArg().defaultsTo("localhost")
|
|
||||||
val apiNetworkAddress = parser.accepts("api-address").withRequiredArg().defaultsTo("localhost")
|
|
||||||
val baseDirectoryArg = parser.accepts("base-directory").withRequiredArg().defaultsTo(DEFAULT_BASE_DIRECTORY)
|
|
||||||
val h2PortArg = parser.accepts("h2-port").withRequiredArg().ofType(Int::class.java).defaultsTo(-1)
|
|
||||||
val options = try {
|
|
||||||
parser.parse(*args)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error(e.message)
|
|
||||||
printHelp(parser)
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val role = options.valueOf(roleArg)!!
|
|
||||||
|
|
||||||
val myNetAddr = HostAndPort.fromString(options.valueOf(myNetworkAddress)).withDefaultPort(
|
|
||||||
when (role) {
|
|
||||||
Role.BUYER -> 31337
|
|
||||||
Role.SELLER -> 31340
|
|
||||||
}
|
|
||||||
)
|
|
||||||
val theirNetAddr = HostAndPort.fromString(options.valueOf(theirNetworkAddress)).withDefaultPort(
|
|
||||||
when (role) {
|
|
||||||
Role.BUYER -> 31340
|
|
||||||
Role.SELLER -> 31337
|
|
||||||
}
|
|
||||||
)
|
|
||||||
val apiNetAddr = HostAndPort.fromString(options.valueOf(apiNetworkAddress)).withDefaultPort(myNetAddr.port + 1)
|
|
||||||
val h2Port = if (options.valueOf(h2PortArg) < 0) {
|
|
||||||
myNetAddr.port + 2
|
|
||||||
} else options.valueOf(h2PortArg)
|
|
||||||
|
|
||||||
val baseDirectory = options.valueOf(baseDirectoryArg)!!
|
|
||||||
|
|
||||||
// Suppress the Artemis MQ noise, and activate the demo logging.
|
|
||||||
//
|
|
||||||
// The first two strings correspond to the first argument to StateMachineManager.add() but the way we handle logging
|
|
||||||
// for protocols will change in future.
|
|
||||||
LogHelper.setLevel("+demo.buyer", "+demo.seller", "-org.apache.activemq")
|
|
||||||
|
|
||||||
val directory = Paths.get(baseDirectory, role.name.toLowerCase())
|
|
||||||
log.info("Using base demo directory $directory")
|
|
||||||
|
|
||||||
// Override the default config file (which you can find in the file "reference.conf") to give each node a name.
|
|
||||||
val config = run {
|
|
||||||
val myLegalName = when (role) {
|
|
||||||
Role.BUYER -> "Bank A"
|
|
||||||
Role.SELLER -> "Bank B"
|
|
||||||
}
|
|
||||||
val configOverrides = mapOf(
|
|
||||||
"myLegalName" to myLegalName,
|
|
||||||
"artemisAddress" to myNetAddr.toString(),
|
|
||||||
"webAddress" to apiNetAddr.toString(),
|
|
||||||
"h2port" to h2Port.toString()
|
|
||||||
)
|
|
||||||
FullNodeConfiguration(ConfigHelper.loadConfig(directory, allowMissingConfig = true, configOverrides = configOverrides))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Which services will this instance of the node provide to the network?
|
|
||||||
val advertisedServices: Set<ServiceInfo>
|
|
||||||
|
|
||||||
// One of the two servers needs to run the network map and notary services. In such a trivial two-node network
|
|
||||||
// the map is not very helpful, but we need one anyway. So just make the buyer side run the network map as it's
|
|
||||||
// the side that sticks around waiting for the seller.
|
|
||||||
val networkMapId = if (role == Role.BUYER) {
|
|
||||||
advertisedServices = setOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type))
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
advertisedServices = emptySet()
|
|
||||||
NodeMessagingClient.makeNetworkMapAddress(theirNetAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// And now construct then start the node object. It takes a little while.
|
|
||||||
val node = logElapsedTime("Node startup", log) {
|
|
||||||
Node(config, networkMapId, advertisedServices).setup().start()
|
|
||||||
}
|
|
||||||
|
|
||||||
// What happens next depends on the role. The buyer sits around waiting for a trade to start. The seller role
|
|
||||||
// will contact the buyer and actually make something happen.
|
|
||||||
val amount = 1000.DOLLARS
|
|
||||||
if (role == Role.BUYER) {
|
|
||||||
runBuyer(node, amount)
|
|
||||||
} else {
|
|
||||||
node.networkMapRegistrationFuture.success {
|
|
||||||
val party = node.netMapCache.getNodeByLegalName("Bank A")?.legalIdentity ?: throw IllegalStateException("Cannot find other node?!")
|
|
||||||
runSeller(node, amount, party)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runSeller(node: Node, amount: Amount<Currency>, otherSide: Party) {
|
|
||||||
// The seller will sell some commercial paper to the buyer, who will pay with (self issued) cash.
|
|
||||||
//
|
|
||||||
// The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an
|
|
||||||
// attachment. Make sure we have the transaction prospectus attachment loaded into our store.
|
|
||||||
//
|
|
||||||
// This can also be done via an HTTP upload, but here we short-circuit and do it from code.
|
|
||||||
if (node.storage.attachments.openAttachment(TraderDemoProtocolSeller.PROSPECTUS_HASH) == null) {
|
|
||||||
TraderDemoProtocolSeller::class.java.getResourceAsStream("bank-of-london-cp.jar").use {
|
|
||||||
val id = node.storage.attachments.importAttachment(it)
|
|
||||||
assertEquals(TraderDemoProtocolSeller.PROSPECTUS_HASH, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val tradeTX: ListenableFuture<SignedTransaction>
|
|
||||||
if (node.isPreviousCheckpointsPresent) {
|
|
||||||
tradeTX = node.smm.findStateMachines(TraderDemoProtocolSeller::class.java).single().second
|
|
||||||
} else {
|
|
||||||
val seller = TraderDemoProtocolSeller(otherSide, amount)
|
|
||||||
tradeTX = node.services.startProtocol(seller)
|
|
||||||
}
|
|
||||||
|
|
||||||
tradeTX.success {
|
|
||||||
log.info("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(it.tx)}")
|
|
||||||
thread {
|
|
||||||
node.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runBuyer(node: Node, amount: Amount<Currency>) {
|
|
||||||
// Buyer will fetch the attachment from the seller automatically when it resolves the transaction.
|
|
||||||
// For demo purposes just extract attachment jars when saved to disk, so the user can explore them.
|
|
||||||
val attachmentsPath = (node.storage.attachments as NodeAttachmentService).let {
|
|
||||||
it.automaticallyExtractAttachments = true
|
|
||||||
it.storePath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Self issue some cash.
|
|
||||||
//
|
|
||||||
// TODO: At some point this demo should be extended to have a central bank node.
|
|
||||||
databaseTransaction(node.database) {
|
|
||||||
node.services.fillWithSomeTestCash(300000.DOLLARS,
|
|
||||||
outputNotary = node.info.notaryIdentity, // In this demo, the buyer and notary are on the same node, but need to use right key.
|
|
||||||
ownedBy = node.info.legalIdentity.owningKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait around until a node asks to start a trade with us. In a real system, this part would happen out of band
|
|
||||||
// via some other system like an exchange or maybe even a manual messaging system like Bloomberg. But for the
|
|
||||||
// next stage in our building site, we will just auto-generate fake trades to give our nodes something to do.
|
|
||||||
//
|
|
||||||
// As the seller initiates the two-party trade protocol, here, we will be the buyer.
|
|
||||||
node.services.registerProtocolInitiator(TraderDemoProtocolSeller::class) { otherParty ->
|
|
||||||
TraderDemoProtocolBuyer(otherParty, attachmentsPath, amount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We create a couple of ad-hoc test protocols that wrap the two party trade protocol, to give us the demo logic.
|
|
||||||
|
|
||||||
private class TraderDemoProtocolBuyer(val otherSide: Party,
|
|
||||||
private val attachmentsPath: Path,
|
|
||||||
val amount: Amount<Currency>,
|
|
||||||
override val progressTracker: ProgressTracker = ProgressTracker(STARTING_BUY)) : ProtocolLogic<Unit>() {
|
|
||||||
|
|
||||||
object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset")
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
override fun call() {
|
|
||||||
progressTracker.currentStep = STARTING_BUY
|
|
||||||
|
|
||||||
val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
|
|
||||||
val buyer = TwoPartyTradeProtocol.Buyer(
|
|
||||||
otherSide,
|
|
||||||
notary.notaryIdentity,
|
|
||||||
amount,
|
|
||||||
CommercialPaper.State::class.java)
|
|
||||||
|
|
||||||
// This invokes the trading protocol and out pops our finished transaction.
|
|
||||||
val tradeTX: SignedTransaction = subProtocol(buyer, shareParentSessions = true)
|
|
||||||
// TODO: This should be moved into the protocol itself.
|
|
||||||
serviceHub.recordTransactions(listOf(tradeTX))
|
|
||||||
|
|
||||||
log.info("Purchase complete - we are a happy customer! Final transaction is: " +
|
|
||||||
"\n\n${Emoji.renderIfSupported(tradeTX.tx)}")
|
|
||||||
|
|
||||||
logIssuanceAttachment(tradeTX)
|
|
||||||
logBalance()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun logBalance() {
|
|
||||||
val balances = serviceHub.vaultService.cashBalances.entries.map { "${it.key.currencyCode} ${it.value}" }
|
|
||||||
logger.info("Remaining balance: ${balances.joinToString()}")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun logIssuanceAttachment(tradeTX: SignedTransaction) {
|
|
||||||
// Find the original CP issuance.
|
|
||||||
val search = TransactionGraphSearch(serviceHub.storageService.validatedTransactions, listOf(tradeTX.tx))
|
|
||||||
search.query = TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java,
|
|
||||||
followInputsOfType = CommercialPaper.State::class.java)
|
|
||||||
val cpIssuance = search.call().single()
|
|
||||||
|
|
||||||
cpIssuance.attachments.first().let {
|
|
||||||
val p = attachmentsPath.toAbsolutePath().resolve("$it.jar")
|
|
||||||
log.info("""
|
|
||||||
|
|
||||||
The issuance of the commercial paper came with an attachment. You can find it expanded in this directory:
|
|
||||||
$p
|
|
||||||
|
|
||||||
${Emoji.renderIfSupported(cpIssuance)}""")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TraderDemoProtocolSeller(val otherSide: Party,
|
|
||||||
val amount: Amount<Currency>,
|
|
||||||
override val progressTracker: ProgressTracker = TraderDemoProtocolSeller.tracker()) : ProtocolLogic<SignedTransaction>() {
|
|
||||||
companion object {
|
|
||||||
val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9")
|
|
||||||
|
|
||||||
object SELF_ISSUING : ProgressTracker.Step("Got session ID back, issuing and timestamping some commercial paper")
|
|
||||||
|
|
||||||
object TRADING : ProgressTracker.Step("Starting the trade protocol") {
|
|
||||||
override fun childProgressTracker(): ProgressTracker = TwoPartyTradeProtocol.Seller.tracker()
|
|
||||||
}
|
|
||||||
|
|
||||||
// We vend a progress tracker that already knows there's going to be a TwoPartyTradingProtocol involved at some
|
|
||||||
// point: by setting up the tracker in advance, the user can see what's coming in more detail, instead of being
|
|
||||||
// surprised when it appears as a new set of tasks below the current one.
|
|
||||||
fun tracker() = ProgressTracker(SELF_ISSUING, TRADING)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
override fun call(): SignedTransaction {
|
|
||||||
progressTracker.currentStep = SELF_ISSUING
|
|
||||||
|
|
||||||
val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
|
|
||||||
val cpOwnerKey = serviceHub.legalIdentityKey
|
|
||||||
val commercialPaper = selfIssueSomeCommercialPaper(cpOwnerKey.public, notary)
|
|
||||||
|
|
||||||
progressTracker.currentStep = TRADING
|
|
||||||
|
|
||||||
val seller = TwoPartyTradeProtocol.Seller(
|
|
||||||
otherSide,
|
|
||||||
notary,
|
|
||||||
commercialPaper,
|
|
||||||
amount,
|
|
||||||
cpOwnerKey,
|
|
||||||
progressTracker.getChildProgressTracker(TRADING)!!)
|
|
||||||
val tradeTX: SignedTransaction = subProtocol(seller, shareParentSessions = true)
|
|
||||||
serviceHub.recordTransactions(listOf(tradeTX))
|
|
||||||
|
|
||||||
return tradeTX
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
fun selfIssueSomeCommercialPaper(ownedBy: PublicKey, notaryNode: NodeInfo): StateAndRef<CommercialPaper.State> {
|
|
||||||
// Make a fake company that's issued its own paper.
|
|
||||||
val keyPair = generateKeyPair()
|
|
||||||
val party = Party("Bank of London", keyPair.public)
|
|
||||||
|
|
||||||
val issuance: SignedTransaction = run {
|
|
||||||
val tx = CommercialPaper().generateIssue(party.ref(1, 2, 3), 1100.DOLLARS `issued by` DUMMY_CASH_ISSUER,
|
|
||||||
Instant.now() + 10.days, notaryNode.notaryIdentity)
|
|
||||||
|
|
||||||
// TODO: Consider moving these two steps below into generateIssue.
|
|
||||||
|
|
||||||
// Attach the prospectus.
|
|
||||||
tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!.id)
|
|
||||||
|
|
||||||
// Requesting timestamping, all CP must be timestamped.
|
|
||||||
tx.setTime(Instant.now(), 30.seconds)
|
|
||||||
|
|
||||||
// Sign it as ourselves.
|
|
||||||
tx.signWith(keyPair)
|
|
||||||
|
|
||||||
// Get the notary to sign the timestamp
|
|
||||||
val notarySig = subProtocol(NotaryProtocol.Client(tx.toSignedTransaction(false)))
|
|
||||||
tx.addSignatureUnchecked(notarySig)
|
|
||||||
|
|
||||||
// Commit it to local storage.
|
|
||||||
val stx = tx.toSignedTransaction(true)
|
|
||||||
serviceHub.recordTransactions(listOf(stx))
|
|
||||||
|
|
||||||
stx
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now make a dummy transaction that moves it to a new key, just to show that resolving dependencies works.
|
|
||||||
val move: SignedTransaction = run {
|
|
||||||
val builder = TransactionType.General.Builder(notaryNode.notaryIdentity)
|
|
||||||
CommercialPaper().generateMove(builder, issuance.tx.outRef(0), ownedBy)
|
|
||||||
builder.signWith(keyPair)
|
|
||||||
val notarySignature = subProtocol(NotaryProtocol.Client(builder.toSignedTransaction(false)))
|
|
||||||
builder.addSignatureUnchecked(notarySignature)
|
|
||||||
val tx = builder.toSignedTransaction(true)
|
|
||||||
serviceHub.recordTransactions(listOf(tx))
|
|
||||||
tx
|
|
||||||
}
|
|
||||||
|
|
||||||
return move.tx.outRef(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun printHelp(parser: OptionParser) {
|
|
||||||
println("""
|
|
||||||
Usage: trader-demo --role [BUYER|SELLER] [options]
|
|
||||||
Please refer to the documentation in docs/build/index.html for more info.
|
|
||||||
|
|
||||||
""".trimIndent())
|
|
||||||
parser.printHelpOn(System.out)
|
|
||||||
}
|
|
||||||
|
|
@ -1,200 +0,0 @@
|
|||||||
package com.r3corda.demos.attachment
|
|
||||||
|
|
||||||
import com.google.common.net.HostAndPort
|
|
||||||
import com.r3corda.core.contracts.TransactionType
|
|
||||||
import com.r3corda.core.crypto.Party
|
|
||||||
import com.r3corda.core.crypto.SecureHash
|
|
||||||
import com.r3corda.core.failure
|
|
||||||
import com.r3corda.core.logElapsedTime
|
|
||||||
import com.r3corda.core.node.services.ServiceInfo
|
|
||||||
import com.r3corda.core.success
|
|
||||||
import com.r3corda.core.utilities.Emoji
|
|
||||||
import com.r3corda.core.utilities.LogHelper
|
|
||||||
import com.r3corda.node.internal.Node
|
|
||||||
import com.r3corda.node.services.config.ConfigHelper
|
|
||||||
import com.r3corda.node.services.config.FullNodeConfiguration
|
|
||||||
import com.r3corda.node.services.messaging.NodeMessagingClient
|
|
||||||
import com.r3corda.node.services.network.NetworkMapService
|
|
||||||
import com.r3corda.node.services.transactions.SimpleNotaryService
|
|
||||||
import com.r3corda.protocols.FinalityProtocol
|
|
||||||
import com.r3corda.testing.ALICE_KEY
|
|
||||||
import joptsimple.OptionParser
|
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import kotlin.concurrent.thread
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
// ATTACHMENT DEMO
|
|
||||||
//
|
|
||||||
// Please see docs/build/html/running-the-demos.html and docs/build/html/tutorial-attachments.html
|
|
||||||
//
|
|
||||||
// This program is a simple demonstration of sending a transaction with an attachment from one node to another, and
|
|
||||||
// then accessing the attachment on the remote node.
|
|
||||||
//
|
|
||||||
// The different roles in the scenario this program can adopt are:
|
|
||||||
|
|
||||||
enum class Role(val legalName: String, val port: Int) {
|
|
||||||
SENDER("Bank A", 31337),
|
|
||||||
RECIPIENT("Bank B", 31340);
|
|
||||||
|
|
||||||
val other: Role
|
|
||||||
get() = when (this) {
|
|
||||||
SENDER -> RECIPIENT
|
|
||||||
RECIPIENT -> SENDER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// And this is the directory under the current working directory where each node will create its own server directory,
|
|
||||||
// which holds things like checkpoints, keys, databases, message logs etc.
|
|
||||||
val DEFAULT_BASE_DIRECTORY = "./build/attachment-demo"
|
|
||||||
|
|
||||||
val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9")
|
|
||||||
|
|
||||||
private val log: Logger = LoggerFactory.getLogger("AttachmentDemo")
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
val parser = OptionParser()
|
|
||||||
|
|
||||||
val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required()
|
|
||||||
val myNetworkAddress = parser.accepts("network-address").withRequiredArg().defaultsTo("localhost")
|
|
||||||
val theirNetworkAddress = parser.accepts("other-network-address").withRequiredArg().defaultsTo("localhost")
|
|
||||||
val apiNetworkAddress = parser.accepts("api-address").withRequiredArg().defaultsTo("localhost")
|
|
||||||
val baseDirectoryArg = parser.accepts("base-directory").withRequiredArg().defaultsTo(DEFAULT_BASE_DIRECTORY)
|
|
||||||
|
|
||||||
val options = try {
|
|
||||||
parser.parse(*args)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error(e.message)
|
|
||||||
printHelp(parser)
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val role = options.valueOf(roleArg)!!
|
|
||||||
|
|
||||||
val myNetAddr = HostAndPort.fromString(options.valueOf(myNetworkAddress)).withDefaultPort(role.port)
|
|
||||||
val theirNetAddr = HostAndPort.fromString(options.valueOf(theirNetworkAddress)).withDefaultPort(role.other.port)
|
|
||||||
val apiNetAddr = HostAndPort.fromString(options.valueOf(apiNetworkAddress)).withDefaultPort(myNetAddr.port + 1)
|
|
||||||
|
|
||||||
val baseDirectory = options.valueOf(baseDirectoryArg)!!
|
|
||||||
|
|
||||||
// Suppress the Artemis MQ noise, and activate the demo logging.
|
|
||||||
//
|
|
||||||
// The first two strings correspond to the first argument to StateMachineManager.add() but the way we handle logging
|
|
||||||
// for protocols will change in future.
|
|
||||||
LogHelper.setLevel("-org.apache.activemq")
|
|
||||||
|
|
||||||
val directory = Paths.get(baseDirectory, role.name.toLowerCase())
|
|
||||||
log.info("Using base demo directory $directory")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Override the default config file (which you can find in the file "reference.conf") to give each node a name.
|
|
||||||
val config = run {
|
|
||||||
val myLegalName = role.legalName
|
|
||||||
val configOverrides = mapOf(
|
|
||||||
"myLegalName" to myLegalName,
|
|
||||||
"artemisAddress" to myNetAddr.toString(),
|
|
||||||
"webAddress" to apiNetAddr.toString()
|
|
||||||
)
|
|
||||||
FullNodeConfiguration(ConfigHelper.loadConfig(directory, allowMissingConfig = true, configOverrides = configOverrides))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Which services will this instance of the node provide to the network?
|
|
||||||
val advertisedServices: Set<ServiceInfo>
|
|
||||||
|
|
||||||
// One of the two servers needs to run the network map and notary services. In such a trivial two-node network
|
|
||||||
// the map is not very helpful, but we need one anyway. So just make the recipient side run the network map as it's
|
|
||||||
// the side that sticks around waiting for the sender.
|
|
||||||
val networkMapId = if (role == Role.SENDER) {
|
|
||||||
advertisedServices = setOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
advertisedServices = emptySet()
|
|
||||||
NodeMessagingClient.makeNetworkMapAddress(theirNetAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// And now construct then start the node object. It takes a little while.
|
|
||||||
val node = logElapsedTime("Node startup", log) {
|
|
||||||
Node(config, networkMapId, advertisedServices).setup().start()
|
|
||||||
}
|
|
||||||
|
|
||||||
// What happens next depends on the role. The recipient sits around waiting for a transaction. The sender role
|
|
||||||
// will contact the recipient and actually make something happen.
|
|
||||||
when (role) {
|
|
||||||
Role.RECIPIENT -> runRecipient(node)
|
|
||||||
Role.SENDER -> {
|
|
||||||
node.networkMapRegistrationFuture.success {
|
|
||||||
// Pause a moment to give the network map time to update
|
|
||||||
Thread.sleep(100L)
|
|
||||||
val party = node.netMapCache.getNodeByLegalName(Role.RECIPIENT.legalName)?.legalIdentity ?: throw IllegalStateException("Cannot find other node?!")
|
|
||||||
runSender(node, party)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runRecipient(node: Node) {
|
|
||||||
val serviceHub = node.services
|
|
||||||
|
|
||||||
// Normally we would receive the transaction from a more specific protocol, but in this case we let [FinalityProtocol]
|
|
||||||
// handle receiving it for us.
|
|
||||||
serviceHub.storageService.validatedTransactions.updates.subscribe { event ->
|
|
||||||
// When the transaction is received, it's passed through [ResolveTransactionsProtocol], which first fetches any
|
|
||||||
// attachments for us, then verifies the transaction. As such, by the time it hits the validated transaction store,
|
|
||||||
// we have a copy of the attachment.
|
|
||||||
val tx = event.tx
|
|
||||||
if (tx.attachments.isNotEmpty()) {
|
|
||||||
val attachment = serviceHub.storageService.attachments.openAttachment(tx.attachments.first())
|
|
||||||
assertEquals(PROSPECTUS_HASH, attachment?.id)
|
|
||||||
|
|
||||||
println("File received - we're happy!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(event.tx)}")
|
|
||||||
thread {
|
|
||||||
node.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runSender(node: Node, otherSide: Party) {
|
|
||||||
val serviceHub = node.services
|
|
||||||
// Make sure we have the file in storage
|
|
||||||
// TODO: We should have our own demo file, not share the trader demo file
|
|
||||||
if (serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH) == null) {
|
|
||||||
com.r3corda.demos.Role::class.java.getResourceAsStream("bank-of-london-cp.jar").use {
|
|
||||||
val id = node.storage.attachments.importAttachment(it)
|
|
||||||
assertEquals(PROSPECTUS_HASH, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a trivial transaction that just passes across the attachment - in normal cases there would be
|
|
||||||
// inputs, outputs and commands that refer to this attachment.
|
|
||||||
val ptx = TransactionType.General.Builder(notary = null)
|
|
||||||
ptx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!.id)
|
|
||||||
|
|
||||||
// Despite not having any states, we have to have at least one signature on the transaction
|
|
||||||
ptx.signWith(ALICE_KEY)
|
|
||||||
|
|
||||||
// Send the transaction to the other recipient
|
|
||||||
val tx = ptx.toSignedTransaction()
|
|
||||||
serviceHub.startProtocol(FinalityProtocol(tx, emptySet(), setOf(otherSide))).success {
|
|
||||||
thread {
|
|
||||||
Thread.sleep(1000L) // Give the other side time to request the attachment
|
|
||||||
node.stop()
|
|
||||||
}
|
|
||||||
}.failure {
|
|
||||||
println("Failed to relay message ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun printHelp(parser: OptionParser) {
|
|
||||||
println("""
|
|
||||||
Usage: attachment-demo --role [RECIPIENT|SENDER] [options]
|
|
||||||
Please refer to the documentation in docs/build/index.html for more info.
|
|
||||||
|
|
||||||
""".trimIndent())
|
|
||||||
parser.printHelpOn(System.out)
|
|
||||||
}
|
|
Binary file not shown.
@ -44,6 +44,9 @@ dependencies {
|
|||||||
|
|
||||||
// Guava: Google test library (collections test suite)
|
// 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')
|
||||||
|
@ -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))
|
@ -0,0 +1,17 @@
|
|||||||
|
package com.r3corda.testing.http
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.google.common.net.HostAndPort
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
class HttpApi(val root: URL) {
|
||||||
|
fun putJson(path: String, data: Any = Unit) = HttpUtils.putJson(URL(root, path), toJson(data))
|
||||||
|
fun postJson(path: String, data: Any = Unit) = HttpUtils.postJson(URL(root, path), toJson(data))
|
||||||
|
|
||||||
|
private fun toJson(any: Any) = ObjectMapper().writeValueAsString(any)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromHostAndPort(hostAndPort: HostAndPort, base: String, protocol: String = "http"): HttpApi
|
||||||
|
= HttpApi(URL("$protocol://$hostAndPort/$base/"))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package com.r3corda.testing.http
|
||||||
|
|
||||||
|
import com.r3corda.core.utilities.loggerFor
|
||||||
|
import okhttp3.*
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A small set of utilities for making HttpCalls, aimed at demos and tests.
|
||||||
|
*/
|
||||||
|
object HttpUtils {
|
||||||
|
private val logger = loggerFor<HttpUtils>()
|
||||||
|
private val client by lazy {
|
||||||
|
OkHttpClient.Builder()
|
||||||
|
.connectTimeout(5, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(60, TimeUnit.SECONDS).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putJson(url: URL, data: String) : Boolean {
|
||||||
|
val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), data)
|
||||||
|
return makeRequest(Request.Builder().url(url).put(body).build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun postJson(url: URL, data: String) : Boolean {
|
||||||
|
val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), data)
|
||||||
|
return makeRequest(Request.Builder().url(url).post(body).build())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeRequest(request: Request): Boolean {
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
logger.error("Could not fulfill HTTP request of type ${request.method()} to ${request.url()}. Status Code: ${response.code()}. Message: ${response.body().string()}")
|
||||||
|
}
|
||||||
|
return response.isSuccessful
|
||||||
|
}
|
||||||
|
}
|
@ -164,7 +164,7 @@ class MockStorageService(override val attachments: AttachmentStorage = MockAttac
|
|||||||
fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties {
|
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
|
||||||
|
Loading…
Reference in New Issue
Block a user