Merge with master

# Conflicts:
#	docs/build/html/_sources/index.txt
#	docs/build/html/_sources/protocol-state-machines.txt
#	docs/build/html/_sources/tutorial-contract-clauses.txt
#	docs/build/html/index.html
#	docs/build/html/protocol-state-machines.html
#	docs/build/html/searchindex.js
#	docs/build/html/tutorial-contract-clauses.html
#	docs/build/html/tutorial-contract.html
This commit is contained in:
jamescarlyle 2016-09-09 09:43:14 +01:00
commit b7e6c210d9
992 changed files with 25453 additions and 8284 deletions

2
.gitignore vendored
View File

@ -16,6 +16,8 @@ tags
/core/build /core/build
/experimental/build /experimental/build
/docs/build/doctrees /docs/build/doctrees
/test-utils/build
/client/build
# gradle's buildSrc build/ # gradle's buildSrc build/
/buildSrc/build/ /buildSrc/build/

8
.idea/modules.xml generated
View File

@ -5,6 +5,10 @@
<module fileurl="file://$PROJECT_DIR$/.idea/modules/buildSrc.iml" filepath="$PROJECT_DIR$/.idea/modules/buildSrc.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/buildSrc.iml" filepath="$PROJECT_DIR$/.idea/modules/buildSrc.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/buildSrc_main.iml" filepath="$PROJECT_DIR$/.idea/modules/buildSrc_main.iml" group="buildSrc" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/buildSrc_main.iml" filepath="$PROJECT_DIR$/.idea/modules/buildSrc_main.iml" group="buildSrc" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/buildSrc_test.iml" filepath="$PROJECT_DIR$/.idea/modules/buildSrc_test.iml" group="buildSrc" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/buildSrc_test.iml" filepath="$PROJECT_DIR$/.idea/modules/buildSrc_test.iml" group="buildSrc" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/client/client.iml" filepath="$PROJECT_DIR$/.idea/modules/client/client.iml" group="client" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/client/client_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/client/client_integrationTest.iml" group="client" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/client/client_main.iml" filepath="$PROJECT_DIR$/.idea/modules/client/client_main.iml" group="client" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/client/client_test.iml" filepath="$PROJECT_DIR$/.idea/modules/client/client_test.iml" group="client" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts.iml" group="contracts" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts.iml" group="contracts" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts_main.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts_main.iml" group="contracts" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts_main.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts_main.iml" group="contracts" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts_test.iml" group="contracts" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts_test.iml" group="contracts" />
@ -18,12 +22,16 @@
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" group="contracts/isolated" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" group="contracts/isolated" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" group="contracts/isolated" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" group="contracts/isolated" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node.iml" group="node" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node.iml" group="node" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_integrationTest.iml" group="node" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_main.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_main.iml" group="node" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_main.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_main.iml" group="node" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_test.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_test.iml" group="node" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_test.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_test.iml" group="node" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_integrationTest.iml" group="r3prototyping" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_integrationTest.iml" group="r3prototyping" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" group="r3prototyping" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" group="r3prototyping" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_test.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_test.iml" group="r3prototyping" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_test.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_test.iml" group="r3prototyping" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils.iml" group="test-utils" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils_main.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils_main.iml" group="test-utils" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils_test.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils_test.iml" group="test-utils" />
</modules> </modules>
</component> </component>
</project> </project>

View File

@ -42,7 +42,7 @@ Install the Oracle JDK 8u45 or higher. It is possible that OpenJDK will also wor
## Using IntelliJ ## Using IntelliJ
It's a good idea to use a modern IDE. We use IntelliJ. Install IntelliJ version 15 community edition (which is free): It's a good idea to use a modern IDE. We use IntelliJ. Install the __latest version__ of IntelliJ community edition (which is free):
https://www.jetbrains.com/idea/download/ https://www.jetbrains.com/idea/download/

View File

@ -1,6 +1,6 @@
buildscript { buildscript {
ext.kotlin_version = '1.0.3' ext.kotlin_version = '1.0.3'
ext.quasar_version = '0.7.5' ext.quasar_version = '0.7.6'
ext.asm_version = '0.5.3' ext.asm_version = '0.5.3'
ext.artemis_version = '1.3.0' ext.artemis_version = '1.3.0'
ext.jackson_version = '2.8.0.rc2' ext.jackson_version = '2.8.0.rc2'
@ -48,7 +48,7 @@ allprojects {
// Our version: bump this on release. // Our version: bump this on release.
group 'com.r3corda' group 'com.r3corda'
version '0.3-SNAPSHOT' version '0.4-SNAPSHOT'
} }
repositories { repositories {
@ -102,21 +102,24 @@ mainClassName = 'com.r3corda.demos.TraderDemoKt'
dependencies { dependencies {
compile project(':node') compile project(':node')
// TODO: Demos should not depend on test code, but only use production APIs
compile project(':test-utils')
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.2" compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.2"
compile 'com.squareup.okhttp3:okhttp:3.3.1' compile 'com.squareup.okhttp3:okhttp:3.3.1'
compile 'co.paralleluniverse:capsule:1.0.3'
// Unit testing helpers. // Unit testing helpers.
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:3.4.1' testCompile 'org.assertj:assertj-core:3.4.1'
testCompile 'com.pholser:junit-quickcheck-core:0.6'
// Integration test helpers // Integration test helpers
integrationTestCompile 'junit:junit:4.12' integrationTestCompile 'junit:junit:4.12'
integrationTestCompile 'org.assertj:assertj-core:${assertj_version}' integrationTestCompile 'org.assertj:assertj-core:${assertj_version}'
integrationTestCompile project(':test-utils')
} }
// Package up the demo programs. // Package up the demo programs.
@ -155,11 +158,10 @@ tasks.withType(CreateStartScripts) {
} }
} }
task integrationTest(type: Test) { 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
} }
test.finalizedBy(integrationTest)
task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) { task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
@ -196,46 +198,49 @@ applicationDistribution.into("bin") {
fileMode = 0755 fileMode = 0755
} }
task createCapsule(type: FatCapsule, dependsOn: 'quasarScan') { task buildCordaJAR(type: FatCapsule, dependsOn: 'quasarScan') {
applicationClass 'com.r3corda.node.MainKt' applicationClass 'com.r3corda.node.MainKt'
archiveName 'corda.jar'
applicationSource = files(project.tasks.findByName('jar'), 'build/classes/main/CordaCaplet.class')
capsuleManifest { capsuleManifest {
appClassPath = ["jolokia-agent-war-${project.ext.jolokia_version}.war"] appClassPath = ["jolokia-agent-war-${project.ext.jolokia_version}.war"]
systemProperties['log4j.configuration'] = 'log4j2.xml' systemProperties['log4j.configuration'] = 'log4j2.xml'
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar"] javaAgents = ["quasar-core-${quasar_version}-jdk8.jar"]
minJavaVersion = '1.8.0' minJavaVersion = '1.8.0'
caplets = ['CordaCaplet']
} }
} }
task createStandalone(dependsOn: 'createCapsule') << { task installTemplateNodes(dependsOn: 'buildCordaJAR') << {
copy { copy {
from createCapsule.outputs.getFiles() from buildCordaJAR.outputs.getFiles()
from 'config/dev/nameservernode.conf' from 'config/dev/nameservernode.conf'
into "${buildDir}/standalone/nameserver" into "${buildDir}/nodes/nameserver"
rename 'nameservernode.conf', 'node.conf' rename 'nameservernode.conf', 'node.conf'
} }
copy { copy {
from createCapsule.outputs.getFiles() from buildCordaJAR.outputs.getFiles()
from 'config/dev/generalnodea.conf' from 'config/dev/generalnodea.conf'
into "${buildDir}/standalone/nodea" into "${buildDir}/nodes/nodea"
rename 'generalnodea.conf', 'node.conf' rename 'generalnodea.conf', 'node.conf'
} }
copy { copy {
from createCapsule.outputs.getFiles() from buildCordaJAR.outputs.getFiles()
from 'config/dev/generalnodeb.conf' from 'config/dev/generalnodeb.conf'
into "${buildDir}/standalone/nodeb" into "${buildDir}/nodes/nodeb"
rename 'generalnodeb.conf', 'node.conf' rename 'generalnodeb.conf', 'node.conf'
} }
delete("${buildDir}/standalone/runstandalone") delete("${buildDir}/nodes/runnodes")
def jarName = createCapsule.outputs.getFiles().getSingleFile().getName() def jarName = buildCordaJAR.outputs.getFiles().getSingleFile().getName()
copy { copy {
from "buildSrc/scripts/runstandalone" from "buildSrc/scripts/runnodes"
filter { String line -> line.replace("JAR_NAME", jarName) } filter { String line -> line.replace("JAR_NAME", jarName) }
filter(org.apache.tools.ant.filters.FixCrLfFilter.class, eol: org.apache.tools.ant.filters.FixCrLfFilter.CrLf.newInstance("lf")) filter(org.apache.tools.ant.filters.FixCrLfFilter.class, eol: org.apache.tools.ant.filters.FixCrLfFilter.CrLf.newInstance("lf"))
into "${buildDir}/standalone" into "${buildDir}/nodes"
} }
} }

7
buildSrc/build.gradle Normal file
View File

@ -0,0 +1,7 @@
repositories {
mavenCentral()
}
dependencies {
compile "com.google.guava:guava:19.0"
}

View File

@ -1,4 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Creates three nodes. A network map and notary node and two regular nodes that can be extended with cordapps.
set -euo pipefail set -euo pipefail
trap 'kill $(jobs -p)' SIGINT SIGTERM EXIT trap 'kill $(jobs -p)' SIGINT SIGTERM EXIT
export CAPSULE_CACHE_DIR=cache export CAPSULE_CACHE_DIR=cache

View File

@ -0,0 +1,52 @@
import com.google.common.io.ByteStreams
import org.gradle.api.*
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
import java.nio.file.Files
import java.nio.file.attribute.FileTime
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
// Custom Gradle plugin that attempts to make the resulting jar file deterministic.
// Ie. same contract definition should result when compiled in same jar file.
// This is done by removing date time stamps from the files inside the jar.
class CanonicalizerPlugin implements Plugin<Project> {
void apply(Project project) {
project.getTasks().getByName('jar').doLast() {
def zipPath = (String) project.jar.archivePath
def destPath = Files.createTempFile("processzip", null)
def zeroTime = FileTime.fromMillis(0)
def input = new ZipFile(zipPath)
def entries = input.entries().toList().sort { it.name }
def output = new ZipOutputStream(new FileOutputStream(destPath.toFile()))
output.setMethod(ZipOutputStream.DEFLATED)
entries.each {
def newEntry = new ZipEntry( it.name )
newEntry.setLastModifiedTime(zeroTime)
newEntry.setCreationTime(zeroTime)
newEntry.compressedSize = -1
newEntry.size = it.size
newEntry.crc = it.crc
output.putNextEntry(newEntry)
ByteStreams.copy(input.getInputStream(it), output)
output.closeEntry()
}
output.close()
input.close()
Files.move(destPath, Paths.get(zipPath), StandardCopyOption.REPLACE_EXISTING)
}
}
}

73
client/build.gradle Normal file
View File

@ -0,0 +1,73 @@
apply plugin: 'kotlin'
apply plugin: QuasarPlugin
repositories {
mavenLocal()
mavenCentral()
maven {
url 'http://oss.sonatype.org/content/repositories/snapshots'
}
jcenter()
maven {
url 'https://dl.bintray.com/kotlin/exposed'
}
}
//noinspection GroovyAssignabilityCheck
configurations {
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
runtime.exclude module: 'isolated'
integrationTestCompile.extendsFrom testCompile
integrationTestRuntime.extendsFrom testRuntime
}
sourceSets {
integrationTest {
kotlin {
compileClasspath += main.output + test.output
runtimeClasspath += main.output + test.output
srcDir file('src/integration-test/kotlin')
}
}
test {
resources {
srcDir "../config/test"
}
}
}
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
// build/reports/project/dependencies/index.html for green highlighted parts of the tree.
dependencies {
compile project(':node')
// Log4J: logging framework (with SLF4J bindings)
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
compile "org.apache.logging.log4j:log4j-core:${log4j_version}"
compile "com.google.guava:guava:19.0"
// ReactFX: Functional reactive UI programming.
compile 'org.reactfx:reactfx:2.0-M5'
compile 'org.fxmisc.easybind:easybind:1.0.3'
// Unit testing helpers.
testCompile 'junit:junit:4.12'
testCompile "org.assertj:assertj-core:${assertj_version}"
testCompile project(':test-utils')
// Integration test helpers
integrationTestCompile 'junit:junit:4.12'
}
quasarScan.dependsOn('classes', ':core:classes', ':contracts:classes')
task integrationTest(type: Test) {
testClassesDir = sourceSets.integrationTest.output.classesDir
classpath = sourceSets.integrationTest.runtimeClasspath
}

View File

@ -0,0 +1,251 @@
package com.r3corda.client
import com.r3corda.core.contracts.*
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.node.driver.driver
import com.r3corda.node.driver.startClient
import com.r3corda.node.services.monitor.ServiceToClientEvent
import com.r3corda.node.services.monitor.TransactionBuildResult
import com.r3corda.node.services.transactions.SimpleNotaryService
import com.r3corda.node.utilities.AddOrRemove
import com.r3corda.testing.*
import org.junit.Test
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import rx.subjects.PublishSubject
import kotlin.test.fail
val log: Logger = LoggerFactory.getLogger(WalletMonitorClientTests::class.java)
class WalletMonitorClientTests {
@Test
fun cashIssueWorksEndToEnd() {
driver {
val aliceNodeFuture = startNode("Alice")
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(SimpleNotaryService.Type))
val aliceNode = aliceNodeFuture.get()
val notaryNode = notaryNodeFuture.get()
val client = startClient(aliceNode).get()
log.info("Alice is ${aliceNode.identity}")
log.info("Notary is ${notaryNode.identity}")
val aliceInStream = PublishSubject.create<ServiceToClientEvent>()
val aliceOutStream = PublishSubject.create<ClientToServiceCommand>()
val aliceMonitorClient = WalletMonitorClient(client, aliceNode, aliceOutStream, aliceInStream, PublishSubject.create())
require(aliceMonitorClient.register().get())
aliceOutStream.onNext(ClientToServiceCommand.IssueCash(
amount = Amount(100, USD),
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
recipient = aliceNode.identity,
notary = notaryNode.identity
))
aliceInStream.expectEvents(isStrict = false) {
parallel(
expect { build: ServiceToClientEvent.TransactionBuild ->
val state = build.state
if (state is TransactionBuildResult.Failed) {
fail(state.message)
}
},
expect { output: ServiceToClientEvent.OutputState ->
require(output.consumed.size == 0)
require(output.produced.size == 1)
}
)
}
}
}
@Test
fun issueAndMoveWorks() {
driver {
val aliceNodeFuture = startNode("Alice")
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(SimpleNotaryService.Type))
val aliceNode = aliceNodeFuture.get()
val notaryNode = notaryNodeFuture.get()
val client = startClient(aliceNode).get()
log.info("Alice is ${aliceNode.identity}")
log.info("Notary is ${notaryNode.identity}")
val aliceInStream = PublishSubject.create<ServiceToClientEvent>()
val aliceOutStream = PublishSubject.create<ClientToServiceCommand>()
val aliceMonitorClient = WalletMonitorClient(client, aliceNode, aliceOutStream, aliceInStream, PublishSubject.create())
require(aliceMonitorClient.register().get())
aliceOutStream.onNext(ClientToServiceCommand.IssueCash(
amount = Amount(100, USD),
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
recipient = aliceNode.identity,
notary = notaryNode.identity
))
aliceOutStream.onNext(ClientToServiceCommand.PayCash(
amount = Amount(100, Issued(PartyAndReference(aliceNode.identity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
recipient = aliceNode.identity
))
aliceInStream.expectEvents {
sequence(
// ISSUE
parallel(
sequence(
expect { add: ServiceToClientEvent.StateMachine ->
require(add.addOrRemove == AddOrRemove.ADD)
},
expect { remove: ServiceToClientEvent.StateMachine ->
require(remove.addOrRemove == AddOrRemove.REMOVE)
}
),
expect { tx: ServiceToClientEvent.Transaction ->
require(tx.transaction.tx.inputs.isEmpty())
require(tx.transaction.tx.outputs.size == 1)
val signaturePubKeys = tx.transaction.sigs.map { it.by }.toSet()
// Only Alice signed
require(signaturePubKeys.size == 1)
require(signaturePubKeys.contains(aliceNode.identity.owningKey))
},
expect { build: ServiceToClientEvent.TransactionBuild ->
val state = build.state
when (state) {
is TransactionBuildResult.ProtocolStarted -> {
}
is TransactionBuildResult.Failed -> fail(state.message)
}
},
expect { output: ServiceToClientEvent.OutputState ->
require(output.consumed.size == 0)
require(output.produced.size == 1)
}
),
// MOVE
parallel(
sequence(
expect { add: ServiceToClientEvent.StateMachine ->
require(add.addOrRemove == AddOrRemove.ADD)
},
expect { add: ServiceToClientEvent.StateMachine ->
require(add.addOrRemove == AddOrRemove.REMOVE)
}
),
expect { tx: ServiceToClientEvent.Transaction ->
require(tx.transaction.tx.inputs.size == 1)
require(tx.transaction.tx.outputs.size == 1)
val signaturePubKeys = tx.transaction.sigs.map { it.by }.toSet()
// Alice and Notary signed
require(signaturePubKeys.size == 2)
require(signaturePubKeys.contains(aliceNode.identity.owningKey))
require(signaturePubKeys.contains(notaryNode.identity.owningKey))
},
sequence(
expect { build: ServiceToClientEvent.TransactionBuild ->
val state = build.state
when (state) {
is TransactionBuildResult.ProtocolStarted -> {
log.info("${state.message}")
}
is TransactionBuildResult.Failed -> fail(state.message)
}
},
replicate(7) {
expect { build: ServiceToClientEvent.Progress -> }
}
),
expect { output: ServiceToClientEvent.OutputState ->
require(output.consumed.size == 1)
require(output.produced.size == 1)
}
)
)
}
}
}
@Test
fun movingCashOfDifferentIssueRefsFails() {
driver {
val aliceNodeFuture = startNode("Alice")
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(SimpleNotaryService.Type))
val aliceNode = aliceNodeFuture.get()
val notaryNode = notaryNodeFuture.get()
val client = startClient(aliceNode).get()
log.info("Alice is ${aliceNode.identity}")
log.info("Notary is ${notaryNode.identity}")
val aliceInStream = PublishSubject.create<ServiceToClientEvent>()
val aliceOutStream = PublishSubject.create<ClientToServiceCommand>()
val aliceMonitorClient = WalletMonitorClient(client, aliceNode, aliceOutStream, aliceInStream, PublishSubject.create())
require(aliceMonitorClient.register().get())
aliceOutStream.onNext(ClientToServiceCommand.IssueCash(
amount = Amount(100, USD),
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
recipient = aliceNode.identity,
notary = notaryNode.identity
))
aliceOutStream.onNext(ClientToServiceCommand.IssueCash(
amount = Amount(100, USD),
issueRef = OpaqueBytes(ByteArray(1, { 2 })),
recipient = aliceNode.identity,
notary = notaryNode.identity
))
aliceOutStream.onNext(ClientToServiceCommand.PayCash(
amount = Amount(200, Issued(PartyAndReference(aliceNode.identity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
recipient = aliceNode.identity
))
aliceInStream.expectEvents {
sequence(
// ISSUE 1
parallel(
sequence(
expect { add: ServiceToClientEvent.StateMachine ->
require(add.addOrRemove == AddOrRemove.ADD)
},
expect { remove: ServiceToClientEvent.StateMachine ->
require(remove.addOrRemove == AddOrRemove.REMOVE)
}
),
expect { tx: ServiceToClientEvent.Transaction -> },
expect { build: ServiceToClientEvent.TransactionBuild -> },
expect { output: ServiceToClientEvent.OutputState -> }
),
// ISSUE 2
parallel(
sequence(
expect { add: ServiceToClientEvent.StateMachine ->
require(add.addOrRemove == AddOrRemove.ADD)
},
expect { remove: ServiceToClientEvent.StateMachine ->
require(remove.addOrRemove == AddOrRemove.REMOVE)
}
),
expect { tx: ServiceToClientEvent.Transaction -> },
expect { build: ServiceToClientEvent.TransactionBuild -> },
expect { output: ServiceToClientEvent.OutputState -> }
),
// MOVE, should fail
expect { build: ServiceToClientEvent.TransactionBuild ->
val state = build.state
require(state is TransactionBuildResult.Failed)
}
)
}
}
}
}

View File

@ -0,0 +1,64 @@
package com.r3corda.client
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.messaging.MessagingService
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.random63BitValue
import com.r3corda.core.serialization.deserialize
import com.r3corda.core.serialization.serialize
import com.r3corda.node.services.monitor.*
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import rx.Observable
import rx.Observer
/**
* Worked example of a client which communicates with the wallet monitor service.
*/
private val log: Logger = LoggerFactory.getLogger("WalletMonitorClient")
class WalletMonitorClient(
val net: MessagingService,
val node: NodeInfo,
val outEvents: Observable<ClientToServiceCommand>,
val inEvents: Observer<ServiceToClientEvent>,
val snapshot: Observer<StateSnapshotMessage>
) {
private val sessionID = random63BitValue()
fun register(): ListenableFuture<Boolean> {
val future = SettableFuture.create<Boolean>()
log.info("Registering with ID $sessionID. I am ${net.myAddress}")
net.addMessageHandler(WalletMonitorService.REGISTER_TOPIC, sessionID) { msg, reg ->
val resp = msg.data.deserialize<RegisterResponse>()
net.removeMessageHandler(reg)
future.set(resp.success)
}
net.addMessageHandler(WalletMonitorService.STATE_TOPIC, sessionID) { msg, reg ->
val snapshotMessage = msg.data.deserialize<StateSnapshotMessage>()
net.removeMessageHandler(reg)
snapshot.onNext(snapshotMessage)
}
net.addMessageHandler(WalletMonitorService.IN_EVENT_TOPIC, sessionID) { msg, reg ->
val event = msg.data.deserialize<ServiceToClientEvent>()
inEvents.onNext(event)
}
val req = RegisterRequest(net.myAddress, sessionID)
val registerMessage = net.createMessage(WalletMonitorService.REGISTER_TOPIC, 0, req.serialize().bits)
net.send(registerMessage, node.address)
outEvents.subscribe { event ->
val envelope = ClientToServiceCommandMessage(sessionID, net.myAddress, event)
val message = net.createMessage(WalletMonitorService.OUT_EVENT_TOPIC, 0, envelope.serialize().bits)
net.send(message, node.address)
}
return future
}
}

View File

@ -0,0 +1,133 @@
package com.r3corda.client.fxutils
import javafx.collections.FXCollections
import javafx.collections.ListChangeListener
import javafx.collections.ObservableList
import javafx.collections.transformation.TransformationList
import kotlin.comparisons.compareValues
/**
* Given an [ObservableList]<[E]> and a grouping key [K], [AggregatedList] groups the elements by the key into a fresh
* [ObservableList]<[E]> for each group and exposes the groups as an observable list of [A]s by calling [assemble] on each.
*
* Changes done to elements of the input list are reflected in the observable list of the respective group, whereas
* additions/removals of elements in the underlying list are reflected in the exposed [ObservableList]<[A]> by
* adding/deleting aggregations as expected.
*
* The ordering of the exposed list is based on the [hashCode] of keys.
*
* Example:
* val statesGroupedByCurrency = AggregatedList(states, { state -> state.currency }) { currency, group ->
* object {
* val currency = currency
* val states = group
* }
* }
*
* The above creates an observable list of (currency, statesOfCurrency) pairs.
*
* Note that update events to the source list are discarded, assuming the key of elements does not change.
* TODO Should we handle this case? It requires additional bookkeeping of sourceIndex->(aggregationIndex, groupIndex)
*
* @param list The underlying list.
* @param toKey Function to extract the key from an element.
* @param assemble Function to assemble the aggregation into the exposed [A].
*/
class AggregatedList<A, E, K : Any>(
list: ObservableList<out E>,
val toKey: (E) -> K,
val assemble: (K, ObservableList<E>) -> A
) : TransformationList<A, E>(list) {
private class AggregationGroup<E, out A>(
val keyHashCode: Int,
val value: A,
val elements: ObservableList<E>
)
// Invariant: sorted by K.hashCode()
private val aggregationList = mutableListOf<AggregationGroup<E, A>>()
init {
list.forEach { addItem(it) }
}
override fun get(index: Int): A? = aggregationList.getOrNull(index)?.value
/**
* We cannot implement this as aggregations are one to many
*/
override fun getSourceIndex(index: Int): Int {
throw UnsupportedOperationException()
}
override val size: Int get() = aggregationList.size
override fun sourceChanged(c: ListChangeListener.Change<out E>) {
beginChange()
while (c.next()) {
if (c.wasPermutated()) {
// Permutation should not change aggregation
} else if (c.wasUpdated()) {
// Update should not change aggregation
} else {
for (removedSourceItem in c.removed) {
val removedPair = removeItem(removedSourceItem)
if (removedPair != null) {
nextRemove(removedPair.first, removedPair.second.value)
}
}
for (addedItem in c.addedSubList) {
val insertIndex = addItem(addedItem)
if (insertIndex != null) {
nextAdd(insertIndex, insertIndex + 1)
}
}
}
}
endChange()
}
private fun removeItem(removedItem: E): Pair<Int, AggregationGroup<E, A>>? {
val key = toKey(removedItem)
val keyHashCode = key.hashCode()
val index = aggregationList.binarySearch(
comparison = { group -> compareValues(keyHashCode, group.keyHashCode.hashCode()) }
)
if (index < 0) {
throw IllegalStateException("Removed element $removedItem does not map to an existing aggregation")
} else {
val aggregationGroup = aggregationList[index]
if (aggregationGroup.elements.size == 1) {
return Pair(index, aggregationList.removeAt(index))
}
aggregationGroup.elements.remove(removedItem)
}
return null
}
private fun addItem(addedItem: E): Int? {
val key = toKey(addedItem)
val keyHashCode = key.hashCode()
val index = aggregationList.binarySearch(
comparison = { group -> compareValues(keyHashCode, group.keyHashCode.hashCode()) }
)
if (index < 0) {
// New aggregation
val observableGroupElements = FXCollections.observableArrayList<E>()
observableGroupElements.add(addedItem)
val aggregationGroup = AggregationGroup(
keyHashCode = keyHashCode,
value = assemble(key, observableGroupElements),
elements = observableGroupElements
)
val insertIndex = -index - 1
aggregationList.add(insertIndex, aggregationGroup)
return insertIndex
} else {
aggregationList[index].elements.add(addedItem)
return null
}
}
}

View File

@ -0,0 +1,51 @@
package com.r3corda.client.fxutils
import com.r3corda.client.model.ExchangeRate
import com.r3corda.core.contracts.Amount
import javafx.beans.binding.Bindings
import javafx.beans.value.ObservableValue
import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.stream
import org.fxmisc.easybind.EasyBind
import java.util.*
import java.util.stream.Collectors
/**
* Utility bindings for the [Amount] type, similar in spirit to [Bindings]
*/
object AmountBindings {
fun <T> sum(amounts: ObservableList<Amount<T>>, token: T) = EasyBind.map(
Bindings.createLongBinding({
amounts.stream().collect(Collectors.summingLong {
require(it.token == token)
it.quantity
})
}, arrayOf(amounts))
) { sum -> Amount(sum.toLong(), token) }
fun exchange(
currency: ObservableValue<Currency>,
exchangeRate: ObservableValue<ExchangeRate>
): ObservableValue<Pair<Currency, (Amount<Currency>) -> Long>> {
return EasyBind.combine(currency, exchangeRate) { currency, exchangeRate ->
Pair(currency) { amount: Amount<Currency> ->
(exchangeRate.rate(amount.token, currency) * amount.quantity).toLong()
}
}
}
fun sumAmountExchange(
amounts: ObservableList<Amount<Currency>>,
currency: ObservableValue<Currency>,
exchangeRate: ObservableValue<ExchangeRate>
): ObservableValue<Amount<Currency>> {
return EasyBind.monadic(exchange(currency, exchangeRate)).flatMap {
val (currencyValue, exchange: (Amount<Currency>) -> Long) = it
EasyBind.map(
Bindings.createLongBinding({
amounts.stream().collect(Collectors.summingLong { exchange(it) })
} , arrayOf(amounts))
) { Amount(it.toLong(), currencyValue) }
}
}
}

View File

@ -0,0 +1,61 @@
package com.r3corda.client.fxutils
import javafx.beans.Observable
import javafx.beans.value.ObservableValue
import javafx.collections.ListChangeListener
import javafx.collections.ObservableList
import javafx.collections.ObservableListBase
/**
* [ChosenList] manages an [ObservableList] that may be changed by the wrapping [ObservableValue]. Whenever the underlying
* [ObservableValue] changes the exposed list changes to the new value. Changes to the list are simply propagated.
*
* Example:
* val filteredStates = ChosenList(EasyBind.map(filterCriteriaType) { type ->
* when (type) {
* is (ByCurrency) -> statesFilteredByCurrency
* is (ByIssuer) -> statesFilteredByIssuer
* }
* })
*
* The above will create a list that chooses and delegates to the appropriate filtered list based on the type of filter.
*/
class ChosenList<E>(
private val chosenListObservable: ObservableValue<ObservableList<E>>
): ObservableListBase<E>() {
private var currentList = chosenListObservable.value
private val listener = object : ListChangeListener<E> {
override fun onChanged(change: ListChangeListener.Change<out E>) = fireChange(change)
}
init {
chosenListObservable.addListener { observable: Observable -> rechoose() }
currentList.addListener(listener)
beginChange()
nextAdd(0, currentList.size)
endChange()
}
override fun get(index: Int) = currentList.get(index)
override val size: Int get() = currentList.size
private fun rechoose() {
val chosenList = chosenListObservable.value
if (currentList != chosenList) {
pick(chosenList)
}
}
private fun pick(list: ObservableList<E>) {
currentList.removeListener(listener)
list.addListener(listener)
beginChange()
nextRemove(0, currentList)
currentList = list
nextAdd(0, list.size)
endChange()
}
}

View File

@ -0,0 +1,38 @@
package com.r3corda.client.fxutils
import javafx.application.Platform
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import rx.Observable
/**
* Simple utilities for converting an [rx.Observable] into a javafx [ObservableValue]/[ObservableList]
*/
fun <A, B> Observable<A>.foldToObservableValue(initial: B, folderFun: (A, B) -> B): ObservableValue<B> {
val result = SimpleObjectProperty<B>(initial)
subscribe {
Platform.runLater {
result.set(folderFun(it, result.get()))
}
}
return result
}
fun <A, B, C> Observable<A>.foldToObservableList(
initialAccumulator: C, folderFun: (A, C, ObservableList<B>) -> C
): ObservableList<B> {
val result = FXCollections.observableArrayList<B>()
/**
* This capture is fine, as [Platform.runLater] runs closures in order
*/
var currentAccumulator = initialAccumulator
subscribe {
Platform.runLater {
currentAccumulator = folderFun(it, currentAccumulator, result)
}
}
return result
}

View File

@ -0,0 +1,40 @@
package com.r3corda.client.mock
class ErrorOr<out A> private constructor(
val value: A?,
val error: Exception?
) {
constructor(value: A): this(value, null)
constructor(error: Exception): this(null, error)
fun <T> match(onValue: (A) -> T, onError: (Exception) -> T): T {
if (value != null) {
return onValue(value)
} else {
return onError(error!!)
}
}
fun getValueOrThrow(): A {
if (value != null) {
return value
} else {
throw error!!
}
}
// Functor
fun <B> map(function: (A) -> B): ErrorOr<B> {
return ErrorOr(value?.let(function), error)
}
// Applicative
fun <B, C> combine(other: ErrorOr<B>, function: (A, B) -> C): ErrorOr<C> {
return ErrorOr(value?.let { a -> other.value?.let { b -> function(a, b) } }, error ?: other.error)
}
// Monad
fun <B> bind(function: (A) -> ErrorOr<B>): ErrorOr<B> {
return value?.let(function) ?: ErrorOr<B>(error!!)
}
}

View File

@ -0,0 +1,99 @@
package com.r3corda.client.mock
import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.node.services.monitor.ServiceToClientEvent
import java.time.Instant
/**
* [Generator]s for incoming/outgoing events to/from the [WalletMonitorService]. Internally it keeps track of owned
* state/ref pairs, but it doesn't necessarily generate "correct" events!
*/
class EventGenerator(
val parties: List<Party>,
val notary: Party
) {
private var wallet = listOf<StateAndRef<Cash.State>>()
val issuerGenerator =
Generator.pickOne(parties).combine(Generator.intRange(0, 1)) { party, ref -> party.ref(ref.toByte()) }
val currencies = setOf(USD, GBP, CHF).toList() // + Currency.getAvailableCurrencies().toList().subList(0, 3).toSet()).toList()
val currencyGenerator = Generator.pickOne(currencies)
val amountIssuedGenerator =
Generator.intRange(1, 10000).combine(issuerGenerator, currencyGenerator) { amount, issuer, currency ->
Amount(amount.toLong(), Issued(issuer, currency))
}
val publicKeyGenerator = Generator.oneOf(parties.map { it.owningKey })
val partyGenerator = Generator.oneOf(parties)
val cashStateGenerator = amountIssuedGenerator.combine(publicKeyGenerator) { amount, from ->
val builder = TransactionBuilder()
builder.addOutputState(Cash.State(amount, from))
builder.addCommand(Command(Cash.Commands.Issue(), amount.token.issuer.party.owningKey))
builder.toWireTransaction().outRef<Cash.State>(0)
}
val consumedGenerator: Generator<Set<StateRef>> = Generator.frequency(
0.7 to Generator.pure(setOf()),
0.3 to Generator.impure { wallet }.bind { states ->
Generator.sampleBernoulli(states, 0.2).map { someStates ->
val consumedSet = someStates.map { it.ref }.toSet()
wallet = wallet.filter { it.ref !in consumedSet }
consumedSet
}
}
)
val producedGenerator: Generator<Set<StateAndRef<ContractState>>> = Generator.frequency(
// 0.1 to Generator.pure(setOf())
0.9 to Generator.impure { wallet }.bind { states ->
Generator.replicate(2, cashStateGenerator).map {
wallet = states + it
it.toSet()
}
}
)
val outputStateGenerator = consumedGenerator.combine(producedGenerator) { consumed, produced ->
ServiceToClientEvent.OutputState(Instant.now(), consumed, produced)
}
val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) }
val amountGenerator = Generator.intRange(0, 10000).combine(currencyGenerator) { quantity, currency -> Amount(quantity.toLong(), currency) }
val issueCashGenerator =
amountGenerator.combine(partyGenerator, issueRefGenerator) { amount, to, issueRef ->
ClientToServiceCommand.IssueCash(
amount,
issueRef,
to,
notary
)
}
val moveCashGenerator =
amountIssuedGenerator.combine(
partyGenerator
) { amountIssued, recipient ->
ClientToServiceCommand.PayCash(
amount = amountIssued,
recipient = recipient
)
}
val serviceToClientEventGenerator = Generator.frequency<ServiceToClientEvent>(
1.0 to outputStateGenerator
)
val clientToServiceCommandGenerator = Generator.frequency(
0.33 to issueCashGenerator,
0.33 to moveCashGenerator
)
}

View File

@ -0,0 +1,171 @@
package com.r3corda.client.mock
import java.util.*
/**
* This file defines a basic [Generator] library for composing random generators of objects.
*
* An object of type [Generator]<[A]> captures a generator of [A]s. Generators may be composed in several ways.
*
* [Generator.choice] picks a generator from the specified list and runs that.
* [Generator.frequency] is similar to [choice] but the probability may be specified for each generator (it is normalised before picking).
* [Generator.combine] combines two generators of A and B with a function (A, B) -> C. Variants exist for other arities.
* [Generator.bind] sequences two generators using an arbitrary A->Generator<B> function. Keep the usage of this
* function minimal as it may explode the stack, especially when using recursion.
*
* There are other utilities as well, the type of which are usually descriptive.
*
* Example:
* val birdNameGenerator = Generator.pickOne(listOf("raven", "pigeon"))
* val birdHeightGenerator = Generator.doubleRange(from = 10.0, to = 30.0)
* val birdGenerator = birdNameGenerator.combine(birdHeightGenerator) { name, height -> Bird(name, height) }
* val birdsGenerator = Generator.replicate(2, birdGenerator)
* val mammalsGenerator = Generator.sampleBernoulli(listOf(Mammal("fox"), Mammal("elephant")))
* val animalsGenerator = Generator.frequency(
* 0.2 to birdsGenerator,
* 0.8 to mammalsGenerator
* )
* val animals = animalsGenerator.generate(Random()).getOrThrow()
*
* The above will generate a random list of animals.
*/
class Generator<out A>(val generate: (Random) -> ErrorOr<A>) {
// Functor
fun <B> map(function: (A) -> B): Generator<B> =
Generator { generate(it).map(function) }
// Applicative
fun <B> product(other: Generator<(A) -> B>) =
Generator { generate(it).combine(other.generate(it)) { a, f -> f(a) } }
fun <B, R> combine(other1: Generator<B>, function: (A, B) -> R) =
product<R>(other1.product(pure({ b -> { a -> function(a, b) } })))
fun <B, C, R> combine(other1: Generator<B>, other2: Generator<C>, function: (A, B, C) -> R) =
product<R>(other1.product(other2.product(pure({ c -> { b -> { a -> function(a, b, c) } } }))))
fun <B, C, D, R> combine(other1: Generator<B>, other2: Generator<C>, other3: Generator<D>, function: (A, B, C, D) -> R) =
product<R>(other1.product(other2.product(other3.product(pure({ d -> { c -> { b -> { a -> function(a, b, c, d) } } } })))))
fun <B, C, D, E, R> combine(other1: Generator<B>, other2: Generator<C>, other3: Generator<D>, other4: Generator<E>, function: (A, B, C, D, E) -> R) =
product<R>(other1.product(other2.product(other3.product(other4.product(pure({ e -> { d -> { c -> { b -> { a -> function(a, b, c, d, e) } } } } }))))))
// Monad
fun <B> bind(function: (A) -> Generator<B>) =
Generator { generate(it).bind { a -> function(a).generate(it) } }
companion object {
fun <A> pure(value: A) = Generator { ErrorOr(value) }
fun <A> impure(valueClosure: () -> A) = Generator { ErrorOr(valueClosure()) }
fun <A> fail(error: Exception) = Generator<A> { ErrorOr(error) }
// Alternative
fun <A> choice(generators: List<Generator<A>>) = intRange(0, generators.size - 1).bind { generators[it] }
fun <A> success(generate: (Random) -> A) = Generator { ErrorOr(generate(it)) }
fun <A> frequency(vararg generators: Pair<Double, Generator<A>>): Generator<A> {
val ranges = mutableListOf<Pair<Double, Double>>()
var current = 0.0
generators.forEach {
val next = current + it.first
ranges.add(Pair(current, next))
current = next
}
return doubleRange(0.0, current).bind { value ->
generators[ranges.binarySearch { range ->
if (value < range.first) {
1
} else if (value < range.second) {
0
} else {
-1
}
}].second
}
}
fun <A> sequence(generators: List<Generator<A>>) = Generator<List<A>> {
val result = mutableListOf<A>()
for (generator in generators) {
val element = generator.generate(it)
if (element.value != null) {
result.add(element.value)
} else {
return@Generator ErrorOr(element.error!!)
}
}
ErrorOr(result)
}
}
}
fun <A> Generator.Companion.oneOf(list: List<A>) = intRange(0, list.size - 1).map { list[it] }
fun <A> Generator<A>.generateOrFail(random: Random, numberOfTries: Int = 1): A {
var error: Exception? = null
for (i in 0 .. numberOfTries - 1) {
val result = generate(random)
if (result.value != null) {
return result.value
} else {
error = result.error
}
}
if (error == null) {
throw IllegalArgumentException("numberOfTries cannot be <= 0")
} else {
throw Exception("Failed to generate", error)
}
}
fun Generator.Companion.int() = Generator.success { it.nextInt() }
fun Generator.Companion.intRange(from: Int, to: Int): Generator<Int> = Generator.success {
(from + Math.abs(it.nextInt()) % (to - from + 1)).toInt()
}
fun Generator.Companion.double() = Generator.success { it.nextDouble() }
fun Generator.Companion.doubleRange(from: Double, to: Double): Generator<Double> = Generator.success {
from + it.nextDouble() % (to - from)
}
fun <A> Generator.Companion.replicate(number: Int, generator: Generator<A>): Generator<List<A>> {
val generators = mutableListOf<Generator<A>>()
for (i in 1 .. number) {
generators.add(generator)
}
return sequence(generators)
}
fun <A> Generator.Companion.replicatePoisson(meanSize: Double, generator: Generator<A>) = Generator<List<A>> {
val chance = (meanSize - 1) / meanSize
val result = mutableListOf<A>()
var finish = false
while (!finish) {
val errorOr = Generator.doubleRange(0.0, 1.0).generate(it).bind { value ->
if (value < chance) {
generator.generate(it).map { result.add(it) }
} else {
finish = true
ErrorOr(Unit)
}
}
if (errorOr.error != null) {
return@Generator ErrorOr(errorOr.error)
}
}
ErrorOr(result)
}
fun <A> Generator.Companion.pickOne(list: List<A>) = Generator.intRange(0, list.size - 1).map { list[it] }
fun <A> Generator.Companion.sampleBernoulli(maxRatio: Double = 1.0, vararg collection: A) =
sampleBernoulli(listOf(collection), maxRatio)
fun <A> Generator.Companion.sampleBernoulli(collection: Collection<A>, maxRatio: Double = 1.0): Generator<List<A>> =
intRange(0, (maxRatio * collection.size).toInt()).bind { howMany ->
replicate(collection.size, Generator.doubleRange(0.0, 1.0)).map { chances ->
val result = mutableListOf<A>()
collection.forEachIndexed { index, element ->
if (chances[index] < howMany.toDouble() / collection.size.toDouble()) {
result.add(element)
}
}
result
}
}

View File

@ -0,0 +1,39 @@
package com.r3corda.client.model
import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.contracts.StateRef
import com.r3corda.client.fxutils.foldToObservableList
import com.r3corda.node.services.monitor.ServiceToClientEvent
import com.r3corda.node.services.monitor.StateSnapshotMessage
import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.removeIf
import rx.Observable
class StatesDiff<out T : ContractState>(
val added: Collection<StateAndRef<T>>,
val removed: Collection<StateRef>
)
/**
* This model exposes the list of owned contract states.
*/
class ContractStateModel {
private val serviceToClient: Observable<ServiceToClientEvent> by observable(WalletMonitorModel::serviceToClient)
private val snapshot: Observable<StateSnapshotMessage> by observable(WalletMonitorModel::snapshot)
private val outputStates = serviceToClient.ofType(ServiceToClientEvent.OutputState::class.java)
val contractStatesDiff = outputStates.map { StatesDiff(it.produced, it.consumed) }
// We filter the diff first rather than the complete contract state list.
// TODO wire up snapshot once it holds StateAndRefs
val cashStatesDiff = contractStatesDiff.map {
StatesDiff(it.added.filterIsInstance<StateAndRef<Cash.State>>(), it.removed)
}
val cashStates: ObservableList<StateAndRef<Cash.State>> =
cashStatesDiff.foldToObservableList(Unit) { statesDiff, _accumulator, observableList ->
observableList.removeIf { it.ref in statesDiff.removed }
observableList.addAll(statesDiff.added)
}
}

View File

@ -0,0 +1,25 @@
package com.r3corda.client.model
import com.r3corda.core.contracts.Amount
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue
import java.util.*
interface ExchangeRate {
fun rate(from: Currency, to: Currency): Double
}
fun ExchangeRate.exchangeAmount(amount: Amount<Currency>, to: Currency) =
Amount(exchangeDouble(amount, to).toLong(), to)
fun ExchangeRate.exchangeDouble(amount: Amount<Currency>, to: Currency) =
rate(amount.token, to) * amount.quantity
/**
* This model provides an exchange rate from arbitrary currency to arbitrary currency.
* TODO hook up an actual oracle
*/
class ExchangeRateModel {
val exchangeRate: ObservableValue<ExchangeRate> = SimpleObjectProperty<ExchangeRate>(object : ExchangeRate {
override fun rate(from: Currency, to: Currency) = 1.0
})
}

View File

@ -0,0 +1,172 @@
package com.r3corda.client.model
import com.r3corda.client.fxutils.foldToObservableList
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.node.services.monitor.ServiceToClientEvent
import com.r3corda.node.services.monitor.TransactionBuildResult
import com.r3corda.node.utilities.AddOrRemove
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue
import javafx.collections.ObservableList
import rx.Observable
import java.time.Instant
import java.util.UUID
interface GatheredTransactionData {
val fiberId: ObservableValue<Long?>
val uuid: ObservableValue<UUID?>
val protocolName: ObservableValue<String?>
val protocolStatus: ObservableValue<ProtocolStatus?>
val transaction: ObservableValue<SignedTransaction?>
val status: ObservableValue<TransactionCreateStatus?>
val lastUpdate: ObservableValue<Instant>
}
sealed class TransactionCreateStatus(val message: String?) {
class Started(message: String?) : TransactionCreateStatus(message)
class Failed(message: String?) : TransactionCreateStatus(message)
override fun toString(): String = message ?: javaClass.simpleName
}
sealed class ProtocolStatus(val status: String?) {
object Added: ProtocolStatus(null)
object Removed: ProtocolStatus(null)
class InProgress(status: String): ProtocolStatus(status)
override fun toString(): String = status ?: javaClass.simpleName
}
data class GatheredTransactionDataWritable(
override val fiberId: SimpleObjectProperty<Long?> = SimpleObjectProperty(null),
override val uuid: SimpleObjectProperty<UUID?> = SimpleObjectProperty(null),
override val protocolName: SimpleObjectProperty<String?> = SimpleObjectProperty(null),
override val protocolStatus: SimpleObjectProperty<ProtocolStatus?> = SimpleObjectProperty(null),
override val transaction: SimpleObjectProperty<SignedTransaction?> = SimpleObjectProperty(null),
override val status: SimpleObjectProperty<TransactionCreateStatus?> = SimpleObjectProperty(null),
override val lastUpdate: SimpleObjectProperty<Instant>
) : GatheredTransactionData
/**
* This model provides an observable list of states relating to the creation of a transaction not yet on ledger.
*/
class GatheredTransactionDataModel {
private val serviceToClient: Observable<ServiceToClientEvent> by observable(WalletMonitorModel::serviceToClient)
/**
* Aggregation of updates to transactions. We use the observable list as the only container and do linear search for
* matching transactions because we have two keys(fiber ID and UUID) and this way it's easier to avoid syncing issues.
*
* The Fiber ID is used to identify events that relate to the same transaction server-side, whereas the UUID is
* generated on the UI and is used to identify events with the UI action that triggered them. Currently a UUID is
* generated for each outgoing [ClientToServiceCommand].
*
* TODO: Make this more efficient by maintaining and syncing two maps (for the two keys) in the accumulator
* (Note that a transaction may be mapped by one or both)
* TODO: Expose a writable stream to combine [serviceToClient] with to allow recording of transactions made locally(UUID)
*/
val gatheredGatheredTransactionDataList: ObservableList<out GatheredTransactionData> =
serviceToClient.foldToObservableList<ServiceToClientEvent, GatheredTransactionDataWritable, Unit>(
initialAccumulator = Unit,
folderFun = { serviceToClientEvent, _unit, transactionStates ->
return@foldToObservableList when (serviceToClientEvent) {
is ServiceToClientEvent.Transaction -> {
// TODO handle this once we have some id to associate the tx with
}
is ServiceToClientEvent.OutputState -> {}
is ServiceToClientEvent.StateMachine -> {
newFiberIdTransactionStateOrModify(transactionStates,
fiberId = serviceToClientEvent.fiberId,
lastUpdate = serviceToClientEvent.time,
tweak = {
protocolName.set(serviceToClientEvent.label)
protocolStatus.set(when (serviceToClientEvent.addOrRemove) {
AddOrRemove.ADD -> ProtocolStatus.Added
AddOrRemove.REMOVE -> ProtocolStatus.Removed
})
}
)
}
is ServiceToClientEvent.Progress -> {
newFiberIdTransactionStateOrModify(transactionStates,
fiberId = serviceToClientEvent.fiberId,
lastUpdate = serviceToClientEvent.time,
tweak = {
protocolStatus.set(ProtocolStatus.InProgress(serviceToClientEvent.message))
}
)
}
is ServiceToClientEvent.TransactionBuild -> {
val state = serviceToClientEvent.state
newUuidTransactionStateOrModify(transactionStates,
uuid = serviceToClientEvent.id,
fiberId = when (state) {
is TransactionBuildResult.ProtocolStarted -> state.fiberId
is TransactionBuildResult.Failed -> null
},
lastUpdate = serviceToClientEvent.time,
tweak = {
return@newUuidTransactionStateOrModify when (state) {
is TransactionBuildResult.ProtocolStarted -> {
transaction.set(state.transaction)
status.set(TransactionCreateStatus.Started(state.message))
}
is TransactionBuildResult.Failed -> {
status.set(TransactionCreateStatus.Failed(state.message))
}
}
}
)
}
}
}
)
companion object {
private fun newFiberIdTransactionStateOrModify(
transactionStates: ObservableList<GatheredTransactionDataWritable>,
fiberId: Long,
lastUpdate: Instant,
tweak: GatheredTransactionDataWritable.() -> Unit
) {
val index = transactionStates.indexOfFirst { it.fiberId.value == fiberId }
if (index < 0) {
val newState = GatheredTransactionDataWritable(
fiberId = SimpleObjectProperty(fiberId),
lastUpdate = SimpleObjectProperty(lastUpdate)
)
tweak(newState)
transactionStates.add(newState)
} else {
val existingState = transactionStates[index]
existingState.lastUpdate.set(lastUpdate)
tweak(existingState)
}
}
private fun newUuidTransactionStateOrModify(
transactionStates: ObservableList<GatheredTransactionDataWritable>,
uuid: UUID,
fiberId: Long?,
lastUpdate: Instant,
tweak: GatheredTransactionDataWritable.() -> Unit
) {
val index = transactionStates.indexOfFirst {
it.uuid.value == uuid || (fiberId != null && it.fiberId.value == fiberId)
}
if (index < 0) {
val newState = GatheredTransactionDataWritable(
uuid = SimpleObjectProperty(uuid),
fiberId = SimpleObjectProperty(fiberId),
lastUpdate = SimpleObjectProperty(lastUpdate)
)
tweak(newState)
transactionStates.add(newState)
} else {
val existingState = transactionStates[index]
existingState.lastUpdate.set(lastUpdate)
tweak(existingState)
}
}
}
}

View File

@ -0,0 +1,166 @@
package com.r3corda.client.model
import javafx.beans.property.ObjectProperty
import javafx.beans.value.ObservableValue
import javafx.beans.value.WritableValue
import javafx.collections.ObservableList
import org.reactfx.EventSink
import org.reactfx.EventStream
import rx.Observable
import rx.Observer
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
/**
* This file defines a global [Models] store and delegates to inject event streams/sinks. Note that all streams here
* are global.
*
* This allows decoupling of UI logic from stream initialisation and provides us with a central place to inspect data
* flows. It also allows detecting of looping logic by constructing a stream dependency graph TODO do this.
*
* Usage:
* // Inject service -> client event stream
* private val serviceToClient: EventStream<ServiceToClientEvent> by eventStream(WalletMonitorModel::serviceToClient)
*
* Each Screen code should have a code layout like this:
*
* class Screen {
* val root = (..)
*
* [ inject UI elements using fxid()/inject() ]
*
* [ inject observable dependencies using observable()/eventSink() etc]
*
* [ define screen-specific observables ]
*
* init {
* [ wire up UI elements ]
* }
* }
*
* For example if I wanted to display a list of all USD cash states:
* class USDCashStatesScreen {
* val root: Pane by fxml()
*
* val usdCashStatesListView: ListView<Cash.State> by fxid("USDCashStatesListView")
*
* val cashStates: ObservableList<Cash.State> by observableList(ContractStateModel::cashStates)
*
* val usdCashStates = cashStates.filter { it.(..).currency == USD }
*
* init {
* Bindings.bindContent(usdCashStatesListView.items, usdCashStates)
* usdCashStatesListView.setCellValueFactory(somethingsomething)
* }
* }
*
* The UI code can just assume that the cash state list comes from somewhere outside. The initialisation of that
* observable is decoupled, it may be mocked or be streamed from the network etc.
*
* Later on we may even want to move all screen-specific observables to a separate Model as well (like usdCashStates) - this
* would allow moving all of the aggregation logic to e.g. a different machine, all the UI will do is inject these and wire
* them up with the UI elements.
*
* Another advantage of this separation is that once we start adding a lot of screens we can still track data dependencies
* in a central place as opposed to ad-hoc wiring up the observables.
*/
inline fun <reified M : Any, T> observable(noinline observableProperty: (M) -> Observable<T>) =
TrackedDelegate.ObservableDelegate(M::class, observableProperty)
inline fun <reified M : Any, T> observer(noinline observerProperty: (M) -> Observer<T>) =
TrackedDelegate.ObserverDelegate(M::class, observerProperty)
inline fun <reified M : Any, T> eventStream(noinline streamProperty: (M) -> EventStream<T>) =
TrackedDelegate.EventStreamDelegate(M::class, streamProperty)
inline fun <reified M : Any, T> eventSink(noinline sinkProperty: (M) -> EventSink<T>) =
TrackedDelegate.EventSinkDelegate(M::class, sinkProperty)
inline fun <reified M : Any, T> observableValue(noinline observableValueProperty: (M) -> ObservableValue<T>) =
TrackedDelegate.ObservableValueDelegate(M::class, observableValueProperty)
inline fun <reified M : Any, T> writableValue(noinline writableValueProperty: (M) -> WritableValue<T>) =
TrackedDelegate.WritableValueDelegate(M::class, writableValueProperty)
inline fun <reified M : Any, T> objectProperty(noinline objectProperty: (M) -> ObjectProperty<T>) =
TrackedDelegate.ObjectPropertyDelegate(M::class, objectProperty)
inline fun <reified M : Any, T> observableList(noinline observableListProperty: (M) -> ObservableList<T>) =
TrackedDelegate.ObservableListDelegate(M::class, observableListProperty)
inline fun <reified M : Any, T> observableListReadOnly(noinline observableListProperty: (M) -> ObservableList<out T>) =
TrackedDelegate.ObservableListReadOnlyDelegate(M::class, observableListProperty)
object Models {
private val modelStore = HashMap<KClass<*>, Any>()
/**
* Holds a class->dependencies map that tracks what screens are depending on what model.
*/
private val dependencyGraph = HashMap<KClass<*>, MutableSet<KClass<*>>>()
fun <M : Any> initModel(klass: KClass<M>) = modelStore.getOrPut(klass) { klass.java.newInstance() }
fun <M : Any> get(klass: KClass<M>, origin: KClass<*>) : M {
dependencyGraph.getOrPut(origin) { mutableSetOf<KClass<*>>() }.add(klass)
val model = initModel(klass)
if (model.javaClass != klass.java) {
throw IllegalStateException("Model stored as ${klass.qualifiedName} has type ${model.javaClass}")
}
@Suppress("UNCHECKED_CAST")
return model as M
}
inline fun <reified M : Any> get(origin: KClass<*>) : M = get(M::class, origin)
}
sealed class TrackedDelegate<M : Any>(val klass: KClass<M>) {
init { Models.initModel(klass) }
class ObservableDelegate<M : Any, T> (klass: KClass<M>, val eventStreamProperty: (M) -> Observable<T>) : TrackedDelegate<M>(klass) {
operator fun getValue(thisRef: Any, property: KProperty<*>): Observable<T> {
return eventStreamProperty(Models.get(klass, thisRef.javaClass.kotlin))
}
}
class ObserverDelegate<M : Any, T> (klass: KClass<M>, val eventStreamProperty: (M) -> Observer<T>) : TrackedDelegate<M>(klass) {
operator fun getValue(thisRef: Any, property: KProperty<*>): Observer<T> {
return eventStreamProperty(Models.get(klass, thisRef.javaClass.kotlin))
}
}
class EventStreamDelegate<M : Any, T> (klass: KClass<M>, val eventStreamProperty: (M) -> org.reactfx.EventStream<T>) : TrackedDelegate<M>(klass) {
operator fun getValue(thisRef: Any, property: KProperty<*>): org.reactfx.EventStream<T> {
return eventStreamProperty(Models.get(klass, thisRef.javaClass.kotlin))
}
}
class EventSinkDelegate<M : Any, T> (klass: KClass<M>, val eventSinkProperty: (M) -> org.reactfx.EventSink<T>) : TrackedDelegate<M>(klass) {
operator fun getValue(thisRef: Any, property: KProperty<*>): org.reactfx.EventSink<T> {
return eventSinkProperty(Models.get(klass, thisRef.javaClass.kotlin))
}
}
class ObservableValueDelegate<M : Any, T>(klass: KClass<M>, val observableValueProperty: (M) -> ObservableValue<T>) : TrackedDelegate<M>(klass) {
operator fun getValue(thisRef: Any, property: KProperty<*>): ObservableValue<T> {
return observableValueProperty(Models.get(klass, thisRef.javaClass.kotlin))
}
}
class WritableValueDelegate<M : Any, T>(klass: KClass<M>, val writableValueProperty: (M) -> WritableValue<T>) : TrackedDelegate<M>(klass) {
operator fun getValue(thisRef: Any, property: KProperty<*>): WritableValue<T> {
return writableValueProperty(Models.get(klass, thisRef.javaClass.kotlin))
}
}
class ObservableListDelegate<M : Any, T>(klass: KClass<M>, val observableListProperty: (M) -> ObservableList<T>) : TrackedDelegate<M>(klass) {
operator fun getValue(thisRef: Any, property: KProperty<*>): ObservableList<T> {
return observableListProperty(Models.get(klass, thisRef.javaClass.kotlin))
}
}
class ObservableListReadOnlyDelegate<M : Any, out T>(klass: KClass<M>, val observableListReadOnlyProperty: (M) -> ObservableList<out T>) : TrackedDelegate<M>(klass) {
operator fun getValue(thisRef: Any, property: KProperty<*>): ObservableList<out T> {
return observableListReadOnlyProperty(Models.get(klass, thisRef.javaClass.kotlin))
}
}
class ObjectPropertyDelegate<M : Any, T>(klass: KClass<M>, val objectPropertyProperty: (M) -> ObjectProperty<T>) : TrackedDelegate<M>(klass) {
operator fun getValue(thisRef: Any, property: KProperty<*>): ObjectProperty<T> {
return objectPropertyProperty(Models.get(klass, thisRef.javaClass.kotlin))
}
}
}

View File

@ -0,0 +1,42 @@
package com.r3corda.client.model
import com.r3corda.client.WalletMonitorClient
import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.messaging.MessagingService
import com.r3corda.core.node.NodeInfo
import com.r3corda.node.services.monitor.ServiceToClientEvent
import com.r3corda.node.services.monitor.StateSnapshotMessage
import rx.Observable
import rx.Observer
import rx.subjects.PublishSubject
/**
* This model exposes raw event streams to and from the [WalletMonitorService] through a [WalletMonitorClient]
*/
class WalletMonitorModel {
private val clientToServiceSource = PublishSubject.create<ClientToServiceCommand>()
val clientToService: Observer<ClientToServiceCommand> = clientToServiceSource
private val serviceToClientSource = PublishSubject.create<ServiceToClientEvent>()
val serviceToClient: Observable<ServiceToClientEvent> = serviceToClientSource
private val snapshotSource = PublishSubject.create<StateSnapshotMessage>()
val snapshot: Observable<StateSnapshotMessage> = snapshotSource
/**
* Register for updates to/from a given wallet.
* @param messagingService The messaging to use for communication.
* @param walletMonitorNodeInfo the [Node] to connect to.
* TODO provide an unsubscribe mechanism
*/
fun register(messagingService: MessagingService, walletMonitorNodeInfo: NodeInfo) {
val monitorClient = WalletMonitorClient(
messagingService,
walletMonitorNodeInfo,
clientToServiceSource,
serviceToClientSource,
snapshotSource
)
require(monitorClient.register().get())
}
}

View File

@ -0,0 +1,82 @@
package com.r3corda.client.fxutils
import javafx.collections.FXCollections
import org.junit.Before
import org.junit.Test
import kotlin.test.fail
class AggregatedListTest {
var sourceList = FXCollections.observableArrayList<Int>()
@Before
fun setup() {
sourceList = FXCollections.observableArrayList<Int>()
}
@Test
fun addWorks() {
val aggregatedList = AggregatedList(sourceList, { it % 3 }) { mod3, group -> Pair(mod3, group) }
require(aggregatedList.size == 0) { "Aggregation is empty is source list is" }
sourceList.add(9)
require(aggregatedList.size == 1) { "Aggregation list has one element if one was added to source list" }
require(aggregatedList[0]!!.first == 0)
sourceList.add(8)
require(aggregatedList.size == 2) { "Aggregation list has two elements if two were added to source list with different keys" }
sourceList.add(6)
require(aggregatedList.size == 2) { "Aggregation list's size doesn't change if element with existing key is added" }
aggregatedList.forEach {
when (it.first) {
0 -> require(it.second.toSet() == setOf(6, 9))
2 -> require(it.second.size == 1)
else -> fail("No aggregation expected with key ${it.first}")
}
}
}
@Test
fun removeWorks() {
val aggregatedList = AggregatedList(sourceList, { it % 3 }) { mod3, group -> Pair(mod3, group) }
sourceList.addAll(0, 1, 2, 3, 4)
require(aggregatedList.size == 3)
aggregatedList.forEach {
when (it.first) {
0 -> require(it.second.toSet() == setOf(0, 3))
1 -> require(it.second.toSet() == setOf(1, 4))
2 -> require(it.second.toSet() == setOf(2))
else -> fail("No aggregation expected with key ${it.first}")
}
}
sourceList.remove(4)
require(aggregatedList.size == 3)
aggregatedList.forEach {
when (it.first) {
0 -> require(it.second.toSet() == setOf(0, 3))
1 -> require(it.second.toSet() == setOf(1))
2 -> require(it.second.toSet() == setOf(2))
else -> fail("No aggregation expected with key ${it.first}")
}
}
sourceList.remove(2, 4)
require(aggregatedList.size == 2)
aggregatedList.forEach {
when (it.first) {
0 -> require(it.second.toSet() == setOf(0))
1 -> require(it.second.toSet() == setOf(1))
else -> fail("No aggregation expected with key ${it.first}")
}
}
sourceList.removeAll(0, 1)
require(aggregatedList.size == 0)
}
}

View File

@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIICCjCCAbCgAwIBAgIINo1Qd4LJ1RUwCgYIKoZIzj0EAwIwWDEbMBkGA1UEAwwS
Q29yZGEgTm9kZSBSb290IENBMQswCQYDVQQKDAJSMzEOMAwGA1UECwwFY29yZGEx
DzANBgNVBAcMBkxvbmRvbjELMAkGA1UEBhMCVUswHhcNMTYwNzE5MDAwMDAwWhcN
MjYwNzE3MDAwMDAwWjBYMRswGQYDVQQDDBJDb3JkYSBOb2RlIFJvb3QgQ0ExCzAJ
BgNVBAoMAlIzMQ4wDAYDVQQLDAVjb3JkYTEPMA0GA1UEBwwGTG9uZG9uMQswCQYD
VQQGEwJVSzBWMBAGByqGSM49AgEGBSuBBAAKA0IABHtaVzCVCvNqp+Jhy5/hC25h
yHomwW5gJpNCPUdgVpLnSUXm+NRzf0ia+1SevkaEPSf5kzk47K1Po6KBWVTPbUaj
ZzBlMB0GA1UdDgQWBBSNU2aUShmUQCadxpzs+WWMQTSMJTASBgNVHRMBAf8ECDAG
AQH/AgECMAsGA1UdDwQEAwIBtjAjBgNVHSUEHDAaBggrBgEFBQcDAQYIKwYBBQUH
AwIGBFUdJQAwCgYIKoZIzj0EAwIDSAAwRQIgaHrsbm0ZZ5uFXCfntTcDddJmttiX
IXi6aN7mIIsl/5kCIQC4RCyclxoeZD/TaraTDzOVeSaooxLA/SrwQMzd0pxCcg==
-----END CERTIFICATE-----

View File

@ -1,14 +1,11 @@
basedir : "./nodea", basedir : "./nodea"
myLegalName : "Bank A", myLegalName : "Bank A"
nearestCity : "London", nearestCity : "London"
keyStorePassword : "cordacadevpass", keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass", trustStorePassword : "trustpass"
artemisAddress : "localhost:31337", artemisAddress : "localhost:31337"
webAddress : "localhost:31339", webAddress : "localhost:31339"
hostNotaryServiceLocally: false, hostNotaryServiceLocally: false
extraAdvertisedServiceIds: "corda.interest_rates", extraAdvertisedServiceIds: "corda.interest_rates"
mapService : { networkMapAddress : "localhost:12345"
hostServiceLocally : false, useHTTPS : false
address : "localhost:12345",
identity : "Notary Service"
}

View File

@ -1,14 +1,11 @@
basedir : "./nodeb", basedir : "./nodeb"
myLegalName : "Bank B", myLegalName : "Bank B"
nearestCity : "London", nearestCity : "London"
keyStorePassword : "cordacadevpass", keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass", trustStorePassword : "trustpass"
artemisAddress : "localhost:31338", artemisAddress : "localhost:31338"
webAddress : "localhost:31340", webAddress : "localhost:31340"
hostNotaryServiceLocally: false, hostNotaryServiceLocally: false
extraAdvertisedServiceIds: "corda.interest_rates", extraAdvertisedServiceIds: "corda.interest_rates"
mapService : { networkMapAddress : "localhost:12345"
hostServiceLocally : false, useHTTPS : false
address : "localhost:12345",
identity : "Notary Service"
}

View File

@ -1,14 +1,10 @@
basedir : "./nameserver", basedir : "./nameserver"
myLegalName : "Notary Service", myLegalName : "Notary Service"
nearestCity : "London", nearestCity : "London"
keyStorePassword : "cordacadevpass", keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass", trustStorePassword : "trustpass"
artemisAddress : "localhost:12345", artemisAddress : "localhost:12345"
webAddress : "localhost:12346", webAddress : "localhost:12346"
hostNotaryServiceLocally: true, hostNotaryServiceLocally: true
extraAdvertisedServiceIds: "", extraAdvertisedServiceIds: ""
mapService : { useHTTPS : false
hostServiceLocally : true,
address : ${artemisAddress},
identity : ${myLegalName}
}

View File

@ -1,83 +1,22 @@
import com.google.common.io.ByteStreams
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.nio.file.attribute.FileTime
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "com.google.guava:guava:19.0"
}
}
// Custom Gradle plugin that attempts to make the resulting jar file deterministic.
// Ie. same contract definition should result when compiled in same jar file.
// This is done by removing date time stamps from the files inside the jar.
class CanonicalizerPlugin implements Plugin<Project> {
void apply(Project project) {
project.getTasks().getByName('jar').doLast() {
def zipPath = (String) project.jar.archivePath
def destPath = Files.createTempFile("processzip", null)
def zeroTime = FileTime.fromMillis(0)
def input = new ZipFile(zipPath)
def entries = input.entries().toList().sort { it.name }
def output = new ZipOutputStream(new FileOutputStream(destPath.toFile()))
output.setMethod(ZipOutputStream.DEFLATED)
entries.each {
def newEntry = new ZipEntry(it.name)
newEntry.setLastModifiedTime(zeroTime)
newEntry.setCreationTime(zeroTime)
newEntry.compressedSize = -1
newEntry.size = it.size
newEntry.crc = it.crc
output.putNextEntry(newEntry)
ByteStreams.copy(input.getInputStream(it), output)
output.closeEntry()
}
output.close()
input.close()
Files.move(destPath, Paths.get(zipPath), StandardCopyOption.REPLACE_EXISTING)
}
}
}
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: CanonicalizerPlugin apply plugin: CanonicalizerPlugin
repositories { repositories {
mavenCentral()
mavenLocal() mavenLocal()
mavenCentral() mavenCentral()
jcenter()
maven { maven {
url 'http://oss.sonatype.org/content/repositories/snapshots' url 'http://oss.sonatype.org/content/repositories/snapshots'
} }
jcenter()
maven {
url 'https://dl.bintray.com/kotlin/exposed'
}
} }
dependencies { dependencies {
compile project(':core') compile project(':core')
testCompile project(':test-utils')
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
} }

View File

@ -1,68 +1,4 @@
import com.google.common.io.ByteStreams
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.nio.file.attribute.FileTime
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "com.google.guava:guava:19.0"
}
}
// Custom Gradle plugin that attempts to make the resulting jar file deterministic.
// Ie. same contract definition should result when compiled in same jar file.
// This is done by removing date time stamps from the files inside the jar.
class CanonicalizerPlugin implements Plugin<Project> {
void apply(Project project) {
project.getTasks().getByName('jar').doLast() {
def zipPath = (String) project.jar.archivePath
def destPath = Files.createTempFile("processzip", null)
def zeroTime = FileTime.fromMillis(0)
def input = new ZipFile(zipPath)
def entries = input.entries().toList().sort { it.name }
def output = new ZipOutputStream(new FileOutputStream(destPath.toFile()))
output.setMethod(ZipOutputStream.DEFLATED)
entries.each {
def newEntry = new ZipEntry( it.name )
newEntry.setLastModifiedTime(zeroTime)
newEntry.setCreationTime(zeroTime)
newEntry.compressedSize = -1
newEntry.size = it.size
newEntry.crc = it.crc
output.putNextEntry(newEntry)
ByteStreams.copy(input.getInputStream(it), output)
output.closeEntry()
}
output.close()
input.close()
Files.move(destPath, Paths.get(zipPath), StandardCopyOption.REPLACE_EXISTING)
}
}
}
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: CanonicalizerPlugin apply plugin: CanonicalizerPlugin
repositories { repositories {

View File

@ -11,6 +11,7 @@ package com.r3corda.contracts.isolated
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.transactions.TransactionBuilder
import java.security.PublicKey import java.security.PublicKey
// The dummy contract doesn't do anything useful. It exists for testing purposes. // The dummy contract doesn't do anything useful. It exists for testing purposes.

View File

@ -2,7 +2,7 @@ package com.r3corda.core.node
import com.r3corda.core.contracts.ContractState import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.PartyAndReference import com.r3corda.core.contracts.PartyAndReference
import com.r3corda.core.contracts.TransactionBuilder import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
interface DummyContractBackdoor { interface DummyContractBackdoor {

View File

@ -3,19 +3,18 @@ package com.r3corda.contracts;
import com.google.common.collect.*; import com.google.common.collect.*;
import com.r3corda.contracts.asset.*; import com.r3corda.contracts.asset.*;
import com.r3corda.core.contracts.*; import com.r3corda.core.contracts.*;
import static com.r3corda.core.contracts.ContractsDSL.requireThat;
import com.r3corda.core.contracts.Timestamp; import com.r3corda.core.contracts.Timestamp;
import com.r3corda.core.contracts.TransactionForContract.*; import com.r3corda.core.contracts.TransactionForContract.*;
import com.r3corda.core.contracts.clauses.*; import com.r3corda.core.contracts.clauses.*;
import com.r3corda.core.crypto.*; import com.r3corda.core.crypto.*;
import kotlin.Unit; import com.r3corda.core.transactions.*;
import kotlin.*;
import org.jetbrains.annotations.*; import org.jetbrains.annotations.*;
import java.security.*; import java.security.*;
import java.time.*; import java.time.*;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.*;
import static com.r3corda.core.contracts.ContractsDSL.*; import static com.r3corda.core.contracts.ContractsDSL.*;
import static kotlin.collections.CollectionsKt.*; import static kotlin.collections.CollectionsKt.*;
@ -26,10 +25,9 @@ import static kotlin.collections.CollectionsKt.*;
* use of Kotlin for implementation of the framework does not impose the same language choice on contract developers. * use of Kotlin for implementation of the framework does not impose the same language choice on contract developers.
*/ */
public class JavaCommercialPaper implements Contract { public class JavaCommercialPaper implements Contract {
//public static SecureHash JCP_PROGRAM_ID = SecureHash.sha256("java commercial paper (this should be a bytecode hash)");
private static final Contract JCP_PROGRAM_ID = new JavaCommercialPaper(); private static final Contract JCP_PROGRAM_ID = new JavaCommercialPaper();
public static class State implements ContractState, ICommercialPaperState { public static class State implements OwnableState, ICommercialPaperState {
private PartyAndReference issuance; private PartyAndReference issuance;
private PublicKey owner; private PublicKey owner;
private Amount<Issued<Currency>> faceValue; private Amount<Issued<Currency>> faceValue;
@ -54,6 +52,12 @@ public class JavaCommercialPaper implements Contract {
return new State(this.issuance, newOwner, this.faceValue, this.maturityDate); return new State(this.issuance, newOwner, this.faceValue, this.maturityDate);
} }
@NotNull
@Override
public Pair<CommandData, OwnableState> withNewOwner(@NotNull PublicKey newOwner) {
return new Pair<>(new Commands.Move(), new State(this.issuance, newOwner, this.faceValue, this.maturityDate));
}
public ICommercialPaperState withIssuance(PartyAndReference newIssuance) { public ICommercialPaperState withIssuance(PartyAndReference newIssuance) {
return new State(newIssuance, this.owner, this.faceValue, this.maturityDate); return new State(newIssuance, this.owner, this.faceValue, this.maturityDate);
} }
@ -70,6 +74,7 @@ public class JavaCommercialPaper implements Contract {
return issuance; return issuance;
} }
@NotNull
public PublicKey getOwner() { public PublicKey getOwner() {
return owner; return owner;
} }
@ -86,7 +91,6 @@ public class JavaCommercialPaper implements Contract {
@Override @Override
public Contract getContract() { public Contract getContract() {
return JCP_PROGRAM_ID; return JCP_PROGRAM_ID;
//return SecureHash.sha256("java commercial paper (this should be a bytecode hash)");
} }
@Override @Override
@ -128,44 +132,17 @@ public class JavaCommercialPaper implements Contract {
} }
} }
public interface Clause { public interface Clauses {
abstract class AbstractGroup implements GroupClause<State, State> { class Group extends GroupClauseVerifier<State, Commands, State> {
@NotNull // This complains because we're passing generic types into a varargs, but it is valid so we suppress the
@Override // warning.
public MatchBehaviour getIfNotMatched() { @SuppressWarnings("unchecked")
return MatchBehaviour.CONTINUE; Group() {
} super(new AnyComposition<>(
new Clauses.Redeem(),
@NotNull new Clauses.Move(),
@Override new Clauses.Issue()
public MatchBehaviour getIfMatched() { ));
return MatchBehaviour.END;
}
}
class Group extends GroupClauseVerifier<State, State> {
@NotNull
@Override
public MatchBehaviour getIfMatched() {
return MatchBehaviour.END;
}
@NotNull
@Override
public MatchBehaviour getIfNotMatched() {
return MatchBehaviour.ERROR;
}
@NotNull
@Override
public List<GroupClause<State, State>> getClauses() {
final List<GroupClause<State, State>> clauses = new ArrayList<>();
clauses.add(new Clause.Redeem());
clauses.add(new Clause.Move());
clauses.add(new Clause.Issue());
return clauses;
} }
@NotNull @NotNull
@ -175,7 +152,7 @@ public class JavaCommercialPaper implements Contract {
} }
} }
class Move extends AbstractGroup { class Move extends Clause<State, Commands, State> {
@NotNull @NotNull
@Override @Override
public Set<Class<? extends CommandData>> getRequiredCommands() { public Set<Class<? extends CommandData>> getRequiredCommands() {
@ -184,11 +161,11 @@ public class JavaCommercialPaper implements Contract {
@NotNull @NotNull
@Override @Override
public Set<CommandData> verify(@NotNull TransactionForContract tx, public Set<Commands> verify(@NotNull TransactionForContract tx,
@NotNull List<? extends State> inputs, @NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs, @NotNull List<? extends State> outputs,
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands, @NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
@NotNull State token) { @NotNull State groupingKey) {
AuthenticatedObject<Commands.Move> cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class); AuthenticatedObject<Commands.Move> cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class);
// There should be only a single input due to aggregation above // There should be only a single input due to aggregation above
State input = single(inputs); State input = single(inputs);
@ -206,7 +183,7 @@ public class JavaCommercialPaper implements Contract {
} }
} }
class Redeem extends AbstractGroup { class Redeem extends Clause<State, Commands, State> {
@NotNull @NotNull
@Override @Override
public Set<Class<? extends CommandData>> getRequiredCommands() { public Set<Class<? extends CommandData>> getRequiredCommands() {
@ -215,11 +192,11 @@ public class JavaCommercialPaper implements Contract {
@NotNull @NotNull
@Override @Override
public Set<CommandData> verify(@NotNull TransactionForContract tx, public Set<Commands> verify(@NotNull TransactionForContract tx,
@NotNull List<? extends State> inputs, @NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs, @NotNull List<? extends State> outputs,
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands, @NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
@NotNull State token) { @NotNull State groupingKey) {
AuthenticatedObject<Commands.Redeem> cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class); AuthenticatedObject<Commands.Redeem> cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class);
// There should be only a single input due to aggregation above // There should be only a single input due to aggregation above
@ -248,7 +225,7 @@ public class JavaCommercialPaper implements Contract {
} }
} }
class Issue extends AbstractGroup { class Issue extends Clause<State, Commands, State> {
@NotNull @NotNull
@Override @Override
public Set<Class<? extends CommandData>> getRequiredCommands() { public Set<Class<? extends CommandData>> getRequiredCommands() {
@ -257,14 +234,13 @@ public class JavaCommercialPaper implements Contract {
@NotNull @NotNull
@Override @Override
public Set<CommandData> verify(@NotNull TransactionForContract tx, public Set<Commands> verify(@NotNull TransactionForContract tx,
@NotNull List<? extends State> inputs, @NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs, @NotNull List<? extends State> outputs,
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands, @NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
@NotNull State token) { @NotNull State groupingKey) {
AuthenticatedObject<Commands.Issue> cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class); AuthenticatedObject<Commands.Issue> cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class);
State output = single(outputs); State output = single(outputs);
Party notary = cmd.getValue().notary;
Timestamp timestampCommand = tx.getTimestamp(); Timestamp timestampCommand = tx.getTimestamp();
Instant time = null == timestampCommand Instant time = null == timestampCommand
? null ? null
@ -291,49 +267,28 @@ public class JavaCommercialPaper implements Contract {
} }
class Redeem implements Commands { class Redeem implements Commands {
private final Party notary;
public Redeem(Party setNotary) {
this.notary = setNotary;
}
@Override @Override
public boolean equals(Object obj) { return obj instanceof Redeem; } public boolean equals(Object obj) { return obj instanceof Redeem; }
} }
class Issue implements Commands { class Issue implements Commands {
private final Party notary;
public Issue(Party setNotary) {
this.notary = setNotary;
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) { return obj instanceof Issue; }
if (obj instanceof Issue) {
Issue other = (Issue)obj;
return notary.equals(other.notary);
} else {
return false;
}
}
@Override
public int hashCode() { return notary.hashCode(); }
} }
} }
@NotNull @NotNull
private Collection<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) { private List<AuthenticatedObject<Commands>> extractCommands(@NotNull TransactionForContract tx) {
return tx.getCommands() return tx.getCommands()
.stream() .stream()
.filter((AuthenticatedObject<CommandData> command) -> command.getValue() instanceof Commands) .filter((AuthenticatedObject<CommandData> command) -> command.getValue() instanceof Commands)
.map((AuthenticatedObject<CommandData> command) -> new AuthenticatedObject<>(command.getSigners(), command.getSigningParties(), (Commands) command.getValue()))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override @Override
public void verify(@NotNull TransactionForContract tx) throws IllegalArgumentException { public void verify(@NotNull TransactionForContract tx) throws IllegalArgumentException {
ClauseVerifier.verifyClauses(tx, Collections.singletonList(new Clause.Group()), extractCommands(tx)); ClauseVerifier.verifyClause(tx, new Clauses.Group(), extractCommands(tx));
} }
@NotNull @NotNull
@ -346,13 +301,13 @@ public class JavaCommercialPaper implements Contract {
public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount<Issued<Currency>> faceValue, @Nullable Instant maturityDate, @NotNull Party notary) { public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount<Issued<Currency>> faceValue, @Nullable Instant maturityDate, @NotNull Party notary) {
State state = new State(issuance, issuance.getParty().getOwningKey(), faceValue, maturityDate); State state = new State(issuance, issuance.getParty().getOwningKey(), faceValue, maturityDate);
TransactionState output = new TransactionState<>(state, notary); TransactionState output = new TransactionState<>(state, notary);
return new TransactionType.General.Builder(notary).withItems(output, new Command(new Commands.Issue(notary), issuance.getParty().getOwningKey())); return new TransactionType.General.Builder(notary).withItems(output, new Command(new Commands.Issue(), issuance.getParty().getOwningKey()));
} }
public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, List<StateAndRef<Cash.State>> wallet) throws InsufficientBalanceException { public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, List<StateAndRef<Cash.State>> wallet) throws InsufficientBalanceException {
new Cash().generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), wallet, null); new Cash().generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), wallet, null);
tx.addInputState(paper); tx.addInputState(paper);
tx.addCommand(new Command(new Commands.Redeem(paper.getState().getNotary()), paper.getState().getData().getOwner())); tx.addCommand(new Command(new Commands.Redeem(), paper.getState().getData().getOwner()));
} }
public void generateMove(TransactionBuilder tx, StateAndRef<State> paper, PublicKey newOwner) { public void generateMove(TransactionBuilder tx, StateAndRef<State> paper, PublicKey newOwner) {

View File

@ -1,6 +1,7 @@
package com.r3corda.contracts package com.r3corda.contracts
import com.r3corda.contracts.asset.Cash import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.asset.FungibleAsset
import com.r3corda.contracts.asset.InsufficientBalanceException import com.r3corda.contracts.asset.InsufficientBalanceException
import com.r3corda.contracts.asset.sumCashBy import com.r3corda.contracts.asset.sumCashBy
import com.r3corda.contracts.clause.AbstractIssue import com.r3corda.contracts.clause.AbstractIssue
@ -10,6 +11,7 @@ import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.random63BitValue import com.r3corda.core.random63BitValue
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.utilities.Emoji import com.r3corda.core.utilities.Emoji
import java.security.PublicKey import java.security.PublicKey
import java.time.Instant import java.time.Instant
@ -49,10 +51,7 @@ class CommercialPaper : Contract {
val maturityDate: Instant val maturityDate: Instant
) )
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>> override fun verify(tx: TransactionForContract) = verifyClause(tx, Clauses.Group(), tx.commands.select<Commands>())
= tx.commands.select<Commands>()
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clauses.Group()), extractCommands(tx))
data class State( data class State(
val issuance: PartyAndReference, val issuance: PartyAndReference,
@ -79,25 +78,16 @@ class CommercialPaper : Contract {
} }
interface Clauses { interface Clauses {
class Group : GroupClauseVerifier<State, Issued<Terms>>() { class Group : GroupClauseVerifier<State, Commands, Issued<Terms>>(
override val ifNotMatched = MatchBehaviour.ERROR AnyComposition(
override val ifMatched = MatchBehaviour.END Redeem(),
override val clauses = listOf( Move(),
Redeem(), Issue())) {
Move(),
Issue()
)
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>> override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
= tx.groupStates<State, Issued<Terms>> { it.token } = tx.groupStates<State, Issued<Terms>> { it.token }
} }
abstract class AbstractGroupClause: GroupClause<State, Issued<Terms>> { class Issue : AbstractIssue<State, Commands, Terms>(
override val ifNotMatched = MatchBehaviour.CONTINUE
override val ifMatched = MatchBehaviour.END
}
class Issue : AbstractIssue<State, Terms>(
{ map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() }, { map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() },
{ token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) { { token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java) override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
@ -105,10 +95,10 @@ class CommercialPaper : Contract {
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State>, inputs: List<State>,
outputs: List<State>, outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: Issued<Terms>): Set<CommandData> { groupingKey: Issued<Terms>?): Set<Commands> {
val consumedCommands = super.verify(tx, inputs, outputs, commands, token) val consumedCommands = super.verify(tx, inputs, outputs, commands, groupingKey)
val command = commands.requireSingleCommand<Commands.Issue>() commands.requireSingleCommand<Commands.Issue>()
val timestamp = tx.timestamp val timestamp = tx.timestamp
val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped") val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped")
@ -118,14 +108,14 @@ class CommercialPaper : Contract {
} }
} }
class Move: AbstractGroupClause() { class Move: Clause<State, Commands, Issued<Terms>>() {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Move::class.java) override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Move::class.java)
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State>, inputs: List<State>,
outputs: List<State>, outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: Issued<Terms>): Set<CommandData> { groupingKey: Issued<Terms>?): Set<Commands> {
val command = commands.requireSingleCommand<Commands.Move>() val command = commands.requireSingleCommand<Commands.Move>()
val input = inputs.single() val input = inputs.single()
requireThat { requireThat {
@ -138,15 +128,14 @@ class CommercialPaper : Contract {
} }
} }
class Redeem(): AbstractGroupClause() { class Redeem(): Clause<State, Commands, Issued<Terms>>() {
override val requiredCommands: Set<Class<out CommandData>> override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Redeem::class.java)
get() = setOf(Commands.Redeem::class.java)
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State>, inputs: List<State>,
outputs: List<State>, outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: Issued<Terms>): Set<CommandData> { groupingKey: Issued<Terms>?): Set<Commands> {
// TODO: This should filter commands down to those with compatible subjects (underlying product and maturity date) // TODO: This should filter commands down to those with compatible subjects (underlying product and maturity date)
// before requiring a single command // before requiring a single command
val command = commands.requireSingleCommand<Commands.Redeem>() val command = commands.requireSingleCommand<Commands.Redeem>()
@ -169,9 +158,9 @@ class CommercialPaper : Contract {
} }
interface Commands : CommandData { interface Commands : CommandData {
class Move : TypeOnlyCommandData(), Commands data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands
data class Redeem(val notary: Party) : Commands class Redeem : TypeOnlyCommandData(), Commands
data class Issue(val notary: Party, override val nonce: Long = random63BitValue()) : IssueCommand, Commands data class Issue(override val nonce: Long = random63BitValue()) : IssueCommand, Commands
} }
/** /**
@ -181,7 +170,7 @@ class CommercialPaper : Contract {
*/ */
fun generateIssue(issuance: PartyAndReference, faceValue: Amount<Issued<Currency>>, maturityDate: Instant, notary: Party): TransactionBuilder { fun generateIssue(issuance: PartyAndReference, faceValue: Amount<Issued<Currency>>, maturityDate: Instant, notary: Party): TransactionBuilder {
val state = TransactionState(State(issuance, issuance.party.owningKey, faceValue, maturityDate), notary) val state = TransactionState(State(issuance, issuance.party.owningKey, faceValue, maturityDate), notary)
return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Issue(notary), issuance.party.owningKey)) return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey))
} }
/** /**
@ -206,7 +195,7 @@ class CommercialPaper : Contract {
val amount = paper.state.data.faceValue.let { amount -> Amount(amount.quantity, amount.token.product) } val amount = paper.state.data.faceValue.let { amount -> Amount(amount.quantity, amount.token.product) }
Cash().generateSpend(tx, amount, paper.state.data.owner, wallet) Cash().generateSpend(tx, amount, paper.state.data.owner, wallet)
tx.addInputState(paper) tx.addInputState(paper)
tx.addCommand(CommercialPaper.Commands.Redeem(paper.state.notary), paper.state.data.owner) tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.data.owner)
} }
} }

View File

@ -1,11 +1,15 @@
package com.r3corda.contracts package com.r3corda.contracts
import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.asset.InsufficientBalanceException
import com.r3corda.contracts.asset.sumCashBy import com.r3corda.contracts.asset.sumCashBy
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.NullPublicKey import com.r3corda.core.crypto.NullPublicKey
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.node.services.Wallet
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.utilities.Emoji import com.r3corda.core.utilities.Emoji
import java.security.PublicKey import java.security.PublicKey
import java.time.Instant import java.time.Instant
@ -30,8 +34,7 @@ class CommercialPaperLegacy : Contract {
val maturityDate: Instant val maturityDate: Instant
) : OwnableState, ICommercialPaperState { ) : OwnableState, ICommercialPaperState {
override val contract = CP_LEGACY_PROGRAM_ID override val contract = CP_LEGACY_PROGRAM_ID
override val participants: List<PublicKey> override val participants = listOf(owner)
get() = listOf(owner)
fun withoutOwner() = copy(owner = NullPublicKey) fun withoutOwner() = copy(owner = NullPublicKey)
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
@ -46,11 +49,11 @@ class CommercialPaperLegacy : Contract {
} }
interface Commands : CommandData { interface Commands : CommandData {
class Move: TypeOnlyCommandData(), Commands class Move : TypeOnlyCommandData(), Commands
data class Redeem(val notary: Party) : Commands class Redeem : TypeOnlyCommandData(), Commands
// We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP. // We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP.
// However, nothing in the platform enforces that uniqueness: it's up to the issuer. // However, nothing in the platform enforces that uniqueness: it's up to the issuer.
data class Issue(val notary: Party) : Commands class Issue : TypeOnlyCommandData(), Commands
} }
override fun verify(tx: TransactionForContract) { override fun verify(tx: TransactionForContract) {
@ -74,8 +77,8 @@ class CommercialPaperLegacy : Contract {
} }
} }
// Redemption of the paper requires movement of on-ledger cash.
is Commands.Redeem -> { is Commands.Redeem -> {
// Redemption of the paper requires movement of on-ledger cash.
val input = inputs.single() val input = inputs.single()
val received = tx.outputs.sumCashBy(input.owner) val received = tx.outputs.sumCashBy(input.owner)
val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped") val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped")
@ -97,6 +100,7 @@ class CommercialPaperLegacy : Contract {
"output values sum to more than the inputs" by (output.faceValue.quantity > 0) "output values sum to more than the inputs" by (output.faceValue.quantity > 0)
"the maturity date is not in the past" by (time < output.maturityDate) "the maturity date is not in the past" by (time < output.maturityDate)
// Don't allow an existing CP state to be replaced by this issuance. // Don't allow an existing CP state to be replaced by this issuance.
// TODO: this has a weird/incorrect assertion string because it doesn't quite match the logic in the clause version.
// TODO: Consider how to handle the case of mistaken issuances, or other need to patch. // TODO: Consider how to handle the case of mistaken issuances, or other need to patch.
"output values sum to more than the inputs" by inputs.isEmpty() "output values sum to more than the inputs" by inputs.isEmpty()
} }
@ -107,4 +111,25 @@ class CommercialPaperLegacy : Contract {
} }
} }
} }
fun generateIssue(issuance: PartyAndReference, faceValue: Amount<Issued<Currency>>, maturityDate: Instant,
notary: Party): TransactionBuilder {
val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate)
return TransactionBuilder(notary = notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey))
}
fun generateMove(tx: TransactionBuilder, paper: StateAndRef<State>, newOwner: PublicKey) {
tx.addInputState(paper)
tx.addOutputState(paper.state.data.withOwner(newOwner))
tx.addCommand(Command(Commands.Move(), paper.state.data.owner))
}
@Throws(InsufficientBalanceException::class)
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, wallet: Wallet) {
// Add the cash movement using the states in our wallet.
Cash().generateSpend(tx, paper.state.data.faceValue.withoutIssuer(),
paper.state.data.owner, wallet.statesOfType<Cash.State>())
tx.addInputState(paper)
tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner))
}
} }

View File

@ -5,6 +5,7 @@ import com.r3corda.core.contracts.clauses.*
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.protocols.ProtocolLogicRefFactory import com.r3corda.core.protocols.ProtocolLogicRefFactory
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.utilities.suggestInterestRateAnnouncementTimeWindow import com.r3corda.core.utilities.suggestInterestRateAnnouncementTimeWindow
import com.r3corda.protocols.TwoPartyDealProtocol import com.r3corda.protocols.TwoPartyDealProtocol
import org.apache.commons.jexl3.JexlBuilder import org.apache.commons.jexl3.JexlBuilder
@ -447,20 +448,14 @@ class InterestRateSwap() : Contract {
fixingCalendar, index, indexSource, indexTenor) fixingCalendar, index, indexSource, indexTenor)
} }
fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>> override fun verify(tx: TransactionForContract) = verifyClause(tx, AllComposition(Clauses.Timestamped(), Clauses.Group()), tx.commands.select<Commands>())
= tx.commands.select<Commands>()
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clause.Timestamped(), Clause.Group()), extractCommands(tx)) interface Clauses {
interface Clause {
/** /**
* Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides * Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides
* helper functions for the clauses. * helper functions for the clauses.
*/ */
abstract class AbstractIRSClause : GroupClause<State, String> { abstract class AbstractIRSClause : Clause<State, Commands, UniqueIdentifier>() {
override val ifMatched = MatchBehaviour.END
override val ifNotMatched = MatchBehaviour.CONTINUE
// These functions may make more sense to use for basket types, but for now let's leave them here // These functions may make more sense to use for basket types, but for now let's leave them here
fun checkLegDates(legs: List<CommonLeg>) { fun checkLegDates(legs: List<CommonLeg>) {
requireThat { requireThat {
@ -502,23 +497,18 @@ class InterestRateSwap() : Contract {
} }
} }
class Group : GroupClauseVerifier<State, String>() { class Group : GroupClauseVerifier<State, Commands, UniqueIdentifier>(AnyComposition(Agree(), Fix(), Pay(), Mature())) {
override val ifMatched = MatchBehaviour.END override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, UniqueIdentifier>>
override val ifNotMatched = MatchBehaviour.ERROR
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, String>>
// Group by Trade ID for in / out states // Group by Trade ID for in / out states
= tx.groupStates() { state -> state.common.tradeID } = tx.groupStates() { state -> state.linearId }
override val clauses = listOf(Agree(), Fix(), Pay(), Mature())
} }
class Timestamped : SingleClause { class Timestamped : Clause<ContractState, Commands, Unit>() {
override val ifMatched = MatchBehaviour.CONTINUE override fun verify(tx: TransactionForContract,
override val ifNotMatched = MatchBehaviour.ERROR inputs: List<ContractState>,
override val requiredCommands = emptySet<Class<out CommandData>>() outputs: List<ContractState>,
commands: List<AuthenticatedObject<Commands>>,
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> { groupingKey: Unit?): Set<Commands> {
require(tx.timestamp?.midpoint != null) { "must be timestamped" } require(tx.timestamp?.midpoint != null) { "must be timestamped" }
// We return an empty set because we don't process any commands // We return an empty set because we don't process any commands
return emptySet() return emptySet()
@ -526,13 +516,13 @@ class InterestRateSwap() : Contract {
} }
class Agree : AbstractIRSClause() { class Agree : AbstractIRSClause() {
override val requiredCommands = setOf(Commands.Agree::class.java) override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Agree::class.java)
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State>, inputs: List<State>,
outputs: List<State>, outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: String): Set<CommandData> { groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Agree>() val command = tx.commands.requireSingleCommand<Commands.Agree>()
val irs = outputs.filterIsInstance<State>().single() val irs = outputs.filterIsInstance<State>().single()
requireThat { requireThat {
@ -562,13 +552,13 @@ class InterestRateSwap() : Contract {
} }
class Fix : AbstractIRSClause() { class Fix : AbstractIRSClause() {
override val requiredCommands = setOf(Commands.Refix::class.java) override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Refix::class.java)
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State>, inputs: List<State>,
outputs: List<State>, outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: String): Set<CommandData> { groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Refix>() val command = tx.commands.requireSingleCommand<Commands.Refix>()
val irs = outputs.filterIsInstance<State>().single() val irs = outputs.filterIsInstance<State>().single()
val prevIrs = inputs.filterIsInstance<State>().single() val prevIrs = inputs.filterIsInstance<State>().single()
@ -607,13 +597,13 @@ class InterestRateSwap() : Contract {
} }
class Pay : AbstractIRSClause() { class Pay : AbstractIRSClause() {
override val requiredCommands = setOf(Commands.Pay::class.java) override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Pay::class.java)
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State>, inputs: List<State>,
outputs: List<State>, outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: String): Set<CommandData> { groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Pay>() val command = tx.commands.requireSingleCommand<Commands.Pay>()
requireThat { requireThat {
"Payments not supported / verifiable yet" by false "Payments not supported / verifiable yet" by false
@ -623,17 +613,18 @@ class InterestRateSwap() : Contract {
} }
class Mature : AbstractIRSClause() { class Mature : AbstractIRSClause() {
override val requiredCommands = setOf(Commands.Mature::class.java) override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Mature::class.java)
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State>, inputs: List<State>,
outputs: List<State>, outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: String): Set<CommandData> { groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Mature>() val command = tx.commands.requireSingleCommand<Commands.Mature>()
val irs = inputs.filterIsInstance<State>().single() val irs = inputs.filterIsInstance<State>().single()
requireThat { requireThat {
"No more fixings to be applied" by (irs.calculation.nextFixingDate() == null) "No more fixings to be applied" by (irs.calculation.nextFixingDate() == null)
"The irs is fully consumed and there is no id matched output state" by outputs.isEmpty()
} }
return setOf(command.value) return setOf(command.value)
@ -656,11 +647,12 @@ class InterestRateSwap() : Contract {
val fixedLeg: FixedLeg, val fixedLeg: FixedLeg,
val floatingLeg: FloatingLeg, val floatingLeg: FloatingLeg,
val calculation: Calculation, val calculation: Calculation,
val common: Common val common: Common,
override val linearId: UniqueIdentifier = UniqueIdentifier(common.tradeID)
) : FixableDealState, SchedulableState { ) : FixableDealState, SchedulableState {
override val contract = IRS_PROGRAM_ID override val contract = IRS_PROGRAM_ID
override val thread = SecureHash.sha256(common.tradeID)
override val ref = common.tradeID override val ref = common.tradeID
override val participants: List<PublicKey> override val participants: List<PublicKey>

View File

@ -4,10 +4,10 @@ import com.r3corda.contracts.clause.AbstractConserveAmount
import com.r3corda.contracts.clause.AbstractIssue import com.r3corda.contracts.clause.AbstractIssue
import com.r3corda.contracts.clause.NoZeroSizedOutputs import com.r3corda.contracts.clause.NoZeroSizedOutputs
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.GroupClauseVerifier import com.r3corda.core.contracts.clauses.*
import com.r3corda.core.contracts.clauses.MatchBehaviour
import com.r3corda.core.crypto.* import com.r3corda.core.crypto.*
import com.r3corda.core.node.services.Wallet import com.r3corda.core.node.services.Wallet
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.utilities.Emoji import com.r3corda.core.utilities.Emoji
import java.math.BigInteger import java.math.BigInteger
import java.security.PublicKey import java.security.PublicKey
@ -34,7 +34,7 @@ val CASH_PROGRAM_ID = Cash()
* At the same time, other contracts that just want money and don't care much who is currently holding it in their * At the same time, other contracts that just want money and don't care much who is currently holding it in their
* vaults can ignore the issuer/depositRefs and just examine the amount fields. * vaults can ignore the issuer/depositRefs and just examine the amount fields.
*/ */
class Cash : OnLedgerAsset<Currency, Cash.State>() { class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
/** /**
* TODO: * TODO:
* 1) hash should be of the contents, not the URI * 1) hash should be of the contents, not the URI
@ -46,32 +46,30 @@ class Cash : OnLedgerAsset<Currency, Cash.State>() {
* that is inconsistent with the legal contract. * that is inconsistent with the legal contract.
*/ */
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html") override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html")
override val conserveClause: AbstractConserveAmount<State, Currency> = Clauses.ConserveAmount() override val conserveClause: AbstractConserveAmount<State, Commands, Currency> = Clauses.ConserveAmount()
override val clauses = listOf(Clauses.Group()) override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Cash.Commands>>
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>> = commands.select<Cash.Commands>()
= tx.commands.select<Cash.Commands>()
interface Clauses { interface Clauses {
class Group : GroupClauseVerifier<State, Issued<Currency>>() { class Group : GroupClauseVerifier<State, Commands, Issued<Currency>>(AllComposition<State, Commands, Issued<Currency>>(
override val ifMatched: MatchBehaviour = MatchBehaviour.END NoZeroSizedOutputs<State, Commands, Currency>(),
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR FirstComposition<State, Commands, Issued<Currency>>(
override val clauses = listOf(
NoZeroSizedOutputs<State, Currency>(),
Issue(), Issue(),
ConserveAmount()) ConserveAmount())
)
) {
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.issuanceDef }
} }
class Issue : AbstractIssue<State, Currency>( class Issue : AbstractIssue<State, Commands, Currency>(
sum = { sumCash() }, sum = { sumCash() },
sumOrZero = { sumCashOrZero(it) } sumOrZero = { sumCashOrZero(it) }
) { ) {
override val requiredCommands = setOf(Commands.Issue::class.java) override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
} }
class ConserveAmount : AbstractConserveAmount<State, Currency>() class ConserveAmount : AbstractConserveAmount<State, Commands, Currency>()
} }
/** A state representing a cash claim against some party. */ /** A state representing a cash claim against some party. */
@ -86,7 +84,7 @@ class Cash : OnLedgerAsset<Currency, Cash.State>() {
: 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 deposit = amount.token.issuer
override val exitKeys = setOf(deposit.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 issuanceDef = amount.token
override val participants = listOf(owner) override val participants = listOf(owner)
@ -145,6 +143,9 @@ class Cash : OnLedgerAsset<Currency, Cash.State>() {
override fun generateExitCommand(amount: Amount<Issued<Currency>>) = Commands.Exit(amount) override fun generateExitCommand(amount: Amount<Issued<Currency>>) = Commands.Exit(amount)
override fun generateIssueCommand() = Commands.Issue() override fun generateIssueCommand() = Commands.Issue()
override fun generateMoveCommand() = Commands.Move() override fun generateMoveCommand() = Commands.Move()
override fun verify(tx: TransactionForContract)
= verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
} }
// Small DSL extensions. // Small DSL extensions.

View File

@ -5,11 +5,13 @@ import com.r3corda.contracts.clause.AbstractIssue
import com.r3corda.contracts.clause.NoZeroSizedOutputs import com.r3corda.contracts.clause.NoZeroSizedOutputs
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.GroupClauseVerifier import com.r3corda.core.contracts.clauses.GroupClauseVerifier
import com.r3corda.core.contracts.clauses.MatchBehaviour import com.r3corda.core.contracts.clauses.AnyComposition
import com.r3corda.core.contracts.clauses.verifyClause
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.newSecureRandom import com.r3corda.core.crypto.newSecureRandom
import com.r3corda.core.crypto.toStringShort import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.transactions.TransactionBuilder
import java.security.PublicKey import java.security.PublicKey
import java.util.* import java.util.*
@ -28,9 +30,12 @@ val COMMODITY_PROGRAM_ID = CommodityContract()
* differences are in representation of the underlying commodity. Issuer in this context means the party who has the * differences are in representation of the underlying commodity. Issuer in this context means the party who has the
* commodity, or is otherwise responsible for delivering the commodity on demand, and the deposit reference is use for * commodity, or is otherwise responsible for delivering the commodity on demand, and the deposit reference is use for
* internal accounting by the issuer (it might be, for example, a warehouse and/or location within a warehouse). * internal accounting by the issuer (it might be, for example, a warehouse and/or location within a warehouse).
*
* This is an early stage example contract used to illustrate non-cash fungible assets, and is likely to change significantly
* in future.
*/ */
// TODO: Need to think about expiry of commodities, how to require payment of storage costs, etc. // TODO: Need to think about expiry of commodities, how to require payment of storage costs, etc.
class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() { class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.Commands, CommodityContract.State>() {
/** /**
* TODO: * TODO:
* 1) hash should be of the contents, not the URI * 1) hash should be of the contents, not the URI
@ -43,7 +48,7 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
*/ */
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html") override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html")
override val conserveClause: AbstractConserveAmount<State, Commodity> = Clauses.ConserveAmount() override val conserveClause: AbstractConserveAmount<State, Commands, Commodity> = Clauses.ConserveAmount()
/** /**
* The clauses for this contract are essentially: * The clauses for this contract are essentially:
@ -59,24 +64,10 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
* Grouping clause to extract input and output states into matched groups and then run a set of clauses over * Grouping clause to extract input and output states into matched groups and then run a set of clauses over
* each group. * each group.
*/ */
class Group : GroupClauseVerifier<State, Issued<Commodity>>() { class Group : GroupClauseVerifier<State, Commands, Issued<Commodity>>(AnyComposition(
/** NoZeroSizedOutputs<State, Commands, Commodity>(),
* The group clause does not depend on any commands being present, so something has gone terribly wrong if Issue(),
* it doesn't match. ConserveAmount())) {
*/
override val ifNotMatched = MatchBehaviour.ERROR
/**
* The group clause is the only top level clause, so end after processing it. If there are any commands left
* after this clause has run, the clause verifier will trigger an error.
*/
override val ifMatched = MatchBehaviour.END
// Subclauses to run on each group
override val clauses = listOf(
NoZeroSizedOutputs<State, Commodity>(),
Issue(),
ConserveAmount()
)
/** /**
* Group commodity states by issuance definition (issuer and underlying commodity). * Group commodity states by issuance definition (issuer and underlying commodity).
*/ */
@ -87,17 +78,17 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
/** /**
* Standard issue clause, specialised to match the commodity issue command. * Standard issue clause, specialised to match the commodity issue command.
*/ */
class Issue : AbstractIssue<State, Commodity>( class Issue : AbstractIssue<State, Commands, Commodity>(
sum = { sumCommodities() }, sum = { sumCommodities() },
sumOrZero = { sumCommoditiesOrZero(it) } sumOrZero = { sumCommoditiesOrZero(it) }
) { ) {
override val requiredCommands = setOf(Commands.Issue::class.java) override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
} }
/** /**
* Standard clause for conserving the amount from input to output. * Standard clause for conserving the amount from input to output.
*/ */
class ConserveAmount : AbstractConserveAmount<State, Commodity>() class ConserveAmount : AbstractConserveAmount<State, Commands, Commodity>()
} }
/** A state representing a commodity claim against some party */ /** A state representing a commodity claim against some party */
@ -147,9 +138,10 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
*/ */
data class Exit(override val amount: Amount<Issued<Commodity>>) : Commands, FungibleAsset.Commands.Exit<Commodity> data class Exit(override val amount: Amount<Issued<Commodity>>) : Commands, FungibleAsset.Commands.Exit<Commodity>
} }
override val clauses = listOf(Clauses.Group()) override fun verify(tx: TransactionForContract)
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>> = verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
= tx.commands.select<CommodityContract.Commands>() override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Commands>>
= commands.select<CommodityContract.Commands>()
/** /**
* Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey. * Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.

View File

@ -29,7 +29,10 @@ interface FungibleAsset<T> : OwnableState {
val deposit: PartyAndReference val deposit: PartyAndReference
val issuanceDef: Issued<T> 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 */ /**
* There must be an ExitCommand signed by these keys to destroy the amount. While all states require their
* owner to sign, some (i.e. cash) also require the issuer.
*/
val exitKeys: Collection<PublicKey> val exitKeys: Collection<PublicKey>
/** There must be a MoveCommand signed by this key to claim the amount */ /** There must be a MoveCommand signed by this key to claim the amount */
override val owner: PublicKey override val owner: PublicKey

View File

@ -4,16 +4,11 @@ import com.google.common.annotations.VisibleForTesting
import com.r3corda.contracts.clause.* import com.r3corda.contracts.clause.*
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.* import com.r3corda.core.contracts.clauses.*
import com.r3corda.core.crypto.NullPublicKey import com.r3corda.core.crypto.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.random63BitValue import com.r3corda.core.random63BitValue
import com.r3corda.core.testing.MINI_CORP import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.testing.TEST_TX_TIME import com.r3corda.core.utilities.*
import com.r3corda.core.utilities.Emoji import java.math.BigInteger
import com.r3corda.core.utilities.NonEmptySet
import com.r3corda.core.utilities.nonEmptySetOf
import java.security.PublicKey import java.security.PublicKey
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
@ -43,25 +38,27 @@ class Obligation<P> : Contract {
* that is inconsistent with the legal contract. * that is inconsistent with the legal contract.
*/ */
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.example.gov/cash-settlement.html") override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.example.gov/cash-settlement.html")
private val clauses = listOf(InterceptorClause(Clauses.VerifyLifecycle<P>(), Clauses.Net<P>()),
Clauses.Group<P>())
interface Clauses { interface Clauses {
/** /**
* Parent clause for clauses that operate on grouped states (those which are fungible). * Parent clause for clauses that operate on grouped states (those which are fungible).
*/ */
class Group<P> : GroupClauseVerifier<State<P>, Issued<Terms<P>>>() { class Group<P> : GroupClauseVerifier<State<P>, Commands, Issued<Terms<P>>>(
override val ifMatched: MatchBehaviour = MatchBehaviour.END AllComposition(
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR NoZeroSizedOutputs<State<P>, Commands, Terms<P>>(),
override val clauses = listOf( FirstComposition(
NoZeroSizedOutputs<State<P>, Terms<P>>(), SetLifecycle<P>(),
SetLifecycle<P>(), AllComposition(
VerifyLifecycle<P>(), VerifyLifecycle<State<P>, Commands, Issued<Terms<P>>, P>(),
Settle<P>(), FirstComposition(
Issue(), Settle<P>(),
ConserveAmount() Issue(),
) ConserveAmount()
)
)
)
)
) {
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.issuanceDef }
} }
@ -69,58 +66,64 @@ class Obligation<P> : Contract {
/** /**
* Generic issuance clause * Generic issuance clause
*/ */
class Issue<P> : AbstractIssue<State<P>, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) { class Issue<P> : AbstractIssue<State<P>, Commands, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) {
override val requiredCommands = setOf(Obligation.Commands.Issue::class.java) override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
} }
/** /**
* Generic move/exit clause for fungible assets * Generic move/exit clause for fungible assets
*/ */
class ConserveAmount<P> : AbstractConserveAmount<State<P>, Terms<P>>() class ConserveAmount<P> : AbstractConserveAmount<State<P>, Commands, Terms<P>>()
/** /**
* Clause for supporting netting of obligations. * Clause for supporting netting of obligations.
*/ */
class Net<P> : NetClause<P>() class Net<C: CommandData, P> : NetClause<C, P>() {
val lifecycleClause = Clauses.VerifyLifecycle<ContractState, C, Unit, P>()
override fun toString(): String = "Net obligations"
override fun verify(tx: TransactionForContract, inputs: List<ContractState>, outputs: List<ContractState>, commands: List<AuthenticatedObject<C>>, groupingKey: Unit?): Set<C> {
lifecycleClause.verify(tx, inputs, outputs, commands, groupingKey)
return super.verify(tx, inputs, outputs, commands, groupingKey)
}
}
/** /**
* Obligation-specific clause for changing the lifecycle of one or more states. * Obligation-specific clause for changing the lifecycle of one or more states.
*/ */
class SetLifecycle<P> : GroupClause<State<P>, Issued<Terms<P>>> { class SetLifecycle<P> : Clause<State<P>, Commands, Issued<Terms<P>>>() {
override val requiredCommands = setOf(Commands.SetLifecycle::class.java) override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.SetLifecycle::class.java)
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State<P>>, inputs: List<State<P>>,
outputs: List<State<P>>, outputs: List<State<P>>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: Issued<Terms<P>>): Set<CommandData> { groupingKey: Issued<Terms<P>>?): Set<Commands> {
val command = commands.requireSingleCommand<Commands.SetLifecycle>() val command = commands.requireSingleCommand<Commands.SetLifecycle>()
Obligation<P>().verifySetLifecycleCommand(inputs, outputs, tx, command) Obligation<P>().verifySetLifecycleCommand(inputs, outputs, tx, command)
return setOf(command.value) return setOf(command.value)
} }
override fun toString(): String = "Set obligation lifecycle"
} }
/** /**
* Obligation-specific clause for settling an outstanding obligation by witnessing * Obligation-specific clause for settling an outstanding obligation by witnessing
* change of ownership of other states to fulfil * change of ownership of other states to fulfil
*/ */
class Settle<P> : GroupClause<State<P>, Issued<Terms<P>>> { class Settle<P> : Clause<State<P>, Commands, Issued<Terms<P>>>() {
override val requiredCommands = setOf(Commands.Settle::class.java) override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Settle::class.java)
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State<P>>, inputs: List<State<P>>,
outputs: List<State<P>>, outputs: List<State<P>>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<Commands>>,
token: Issued<Terms<P>>): Set<CommandData> { groupingKey: Issued<Terms<P>>?): Set<Commands> {
require(groupingKey != null)
val command = commands.requireSingleCommand<Commands.Settle<P>>() val command = commands.requireSingleCommand<Commands.Settle<P>>()
val obligor = token.issuer.party val obligor = groupingKey!!.issuer.party
val template = token.product val template = groupingKey.product
val inputAmount: Amount<Issued<Terms<P>>> = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group") val inputAmount: Amount<Issued<Terms<P>>> = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
val outputAmount: Amount<Issued<Terms<P>>> = outputs.sumObligationsOrZero(token) val outputAmount: Amount<Issued<Terms<P>>> = outputs.sumObligationsOrZero(groupingKey)
// Sum up all asset state objects that are moving and fulfil our requirements // Sum up all asset state objects that are moving and fulfil our requirements
@ -166,7 +169,7 @@ class Obligation<P> : Contract {
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>()
if (settled != null) { if (settled != null) {
val debt = obligations.sumObligationsOrZero(token) val debt = obligations.sumObligationsOrZero(groupingKey)
require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" } require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" }
totalPenniesSettled += settled.quantity totalPenniesSettled += settled.quantity
} }
@ -185,7 +188,7 @@ class Obligation<P> : Contract {
"signatures are present from all obligors" by command.signers.containsAll(requiredSigners) "signatures are present from all obligors" by command.signers.containsAll(requiredSigners)
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L } "there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
"at obligor ${obligor.name} the obligations after settlement balance" by "at obligor ${obligor.name} the obligations after settlement balance" by
(inputAmount == outputAmount + Amount(totalPenniesSettled, token)) (inputAmount == outputAmount + Amount(totalPenniesSettled, groupingKey))
} }
return setOf(command.value) return setOf(command.value)
} }
@ -197,26 +200,15 @@ class Obligation<P> : Contract {
* any lifecycle change clause, which is the only clause that involve * any lifecycle change clause, which is the only clause that involve
* non-standard lifecycle states on input/output. * non-standard lifecycle states on input/output.
*/ */
class VerifyLifecycle<P> : SingleClause, GroupClause<State<P>, Issued<Terms<P>>> { class VerifyLifecycle<S: ContractState, C: CommandData, T: Any, P> : Clause<S, C, T>() {
override val requiredCommands: Set<Class<out CommandData>> = emptySet()
override val ifMatched: MatchBehaviour = MatchBehaviour.CONTINUE
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
= verify(
tx.inputs.filterIsInstance<State<P>>(),
tx.outputs.filterIsInstance<State<P>>()
)
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State<P>>, inputs: List<S>,
outputs: List<State<P>>, outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<C>>,
token: Issued<Terms<P>>): Set<CommandData> groupingKey: T?): Set<C>
= verify(inputs, outputs) = verify(inputs.filterIsInstance<State<P>>(), outputs.filterIsInstance<State<P>>())
private fun verify(inputs: List<State<P>>,
fun verify(inputs: List<State<P>>, outputs: List<State<P>>): Set<C> {
outputs: List<State<P>>): Set<CommandData> {
requireThat { requireThat {
"all inputs are in the normal state " by inputs.all { it.lifecycle == Lifecycle.NORMAL } "all inputs are in the normal state " by inputs.all { it.lifecycle == Lifecycle.NORMAL }
"all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL } "all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL }
@ -334,7 +326,7 @@ class Obligation<P> : Contract {
* Net two or more obligation states together in a close-out netting style. Limited to bilateral netting * Net two or more obligation states together in a close-out netting style. Limited to bilateral netting
* as only the beneficiary (not the obligor) needs to sign. * as only the beneficiary (not the obligor) needs to sign.
*/ */
data class Net(val type: NetType) : Obligation.Commands data class Net(override val type: NetType) : NetCommand, Commands
/** /**
* A command stating that a debt has been moved, optionally to fulfil another contract. * A command stating that a debt has been moved, optionally to fulfil another contract.
@ -377,9 +369,10 @@ class Obligation<P> : Contract {
data class Exit<P>(override val amount: Amount<Issued<Terms<P>>>) : Commands, FungibleAsset.Commands.Exit<Terms<P>> data class Exit<P>(override val amount: Amount<Issued<Terms<P>>>) : Commands, FungibleAsset.Commands.Exit<Terms<P>>
} }
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>> override fun verify(tx: TransactionForContract) = verifyClause<Commands>(tx, FirstComposition<ContractState, Commands, Unit>(
= tx.commands.select<Obligation.Commands>() Clauses.Net<Commands, P>(),
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx)) Clauses.Group<P>()
), tx.commands.select<Obligation.Commands>())
/** /**
* A default command mutates inputs and produces identical outputs, except that the lifecycle changes. * A default command mutates inputs and produces identical outputs, except that the lifecycle changes.
@ -451,17 +444,16 @@ class Obligation<P> : Contract {
* *
* @param tx transaction builder to add states and commands to. * @param tx transaction builder to add states and commands to.
* @param amountIssued the amount to be exited, represented as a quantity of issued currency. * @param amountIssued the amount to be exited, represented as a quantity of issued currency.
* @param changeKey the key to send any change to. This needs to be explicitly stated as the input states are not
* necessarily owned by us.
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
* the responsibility of the caller to check that they do not exit funds held by others. * the responsibility of the caller to check that they do not exit funds held by others.
* @return the public key of the assets issuer, who must sign the transaction for it to be valid. * @return the public key of the assets issuer, who must sign the transaction for it to be valid.
*/ */
@Suppress("unused") @Suppress("unused")
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<Terms<P>>>, fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<Terms<P>>>,
changeKey: PublicKey, assetStates: List<StateAndRef<Obligation.State<P>>>): PublicKey assetStates: List<StateAndRef<Obligation.State<P>>>): PublicKey
= Clauses.ConserveAmount<P>().generateExit(tx, amountIssued, changeKey, assetStates, = Clauses.ConserveAmount<P>().generateExit(tx, amountIssued, assetStates,
deriveState = { state, amount, owner -> state.copy(data = state.data.move(amount, owner)) }, deriveState = { state, amount, owner -> state.copy(data = state.data.move(amount, owner)) },
generateMoveCommand = { -> Commands.Move() },
generateExitCommand = { amount -> Commands.Exit(amount) } generateExitCommand = { amount -> Commands.Exit(amount) }
) )
@ -718,7 +710,12 @@ infix fun <T> Obligation.State<T>.`issued by`(party: Party) = copy(obligor = par
@Suppress("unused") fun <T> Obligation.State<T>.ownedBy(owner: PublicKey) = copy(beneficiary = owner) @Suppress("unused") fun <T> Obligation.State<T>.ownedBy(owner: PublicKey) = copy(beneficiary = owner)
@Suppress("unused") fun <T> Obligation.State<T>.issuedBy(party: Party) = copy(obligor = party) @Suppress("unused") fun <T> Obligation.State<T>.issuedBy(party: Party) = copy(obligor = party)
/** A randomly generated key. */
val DUMMY_OBLIGATION_ISSUER_KEY by lazy { entropyToKeyPair(BigInteger.valueOf(10)) }
/** A dummy, randomly generated issuer party by the name of "Snake Oil Issuer" */
val DUMMY_OBLIGATION_ISSUER by lazy { Party("Snake Oil Issuer", DUMMY_OBLIGATION_ISSUER_KEY.public) }
val Issued<Currency>.OBLIGATION_DEF: Obligation.Terms<Currency> val Issued<Currency>.OBLIGATION_DEF: Obligation.Terms<Currency>
get() = Obligation.Terms(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(this), TEST_TX_TIME) get() = Obligation.Terms(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(this), TEST_TX_TIME)
val Amount<Issued<Currency>>.OBLIGATION: Obligation.State<Currency> val Amount<Issued<Currency>>.OBLIGATION: Obligation.State<Currency>
get() = Obligation.State(Obligation.Lifecycle.NORMAL, MINI_CORP, token.OBLIGATION_DEF, quantity, NullPublicKey) get() = Obligation.State(Obligation.Lifecycle.NORMAL, DUMMY_OBLIGATION_ISSUER, token.OBLIGATION_DEF, quantity, NullPublicKey)

View File

@ -2,9 +2,8 @@ package com.r3corda.contracts.asset
import com.r3corda.contracts.clause.AbstractConserveAmount import com.r3corda.contracts.clause.AbstractConserveAmount
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.SingleClause
import com.r3corda.core.contracts.clauses.verifyClauses
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.transactions.TransactionBuilder
import java.security.PublicKey import java.security.PublicKey
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -25,12 +24,9 @@ import java.security.PublicKey
* At the same time, other contracts that just want assets and don't care much who is currently holding it can ignore * At the same time, other contracts that just want assets and don't care much who is currently holding it can ignore
* the issuer/depositRefs and just examine the amount fields. * the issuer/depositRefs and just examine the amount fields.
*/ */
abstract class OnLedgerAsset<T : Any, S : FungibleAsset<T>> : Contract { abstract class OnLedgerAsset<T : Any, C: CommandData, S : FungibleAsset<T>> : Contract {
abstract val clauses: List<SingleClause> abstract fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): Collection<AuthenticatedObject<C>>
abstract fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>> abstract val conserveClause: AbstractConserveAmount<S, C, T>
abstract val conserveClause: AbstractConserveAmount<S, T>
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx))
/** /**
* Generate an transaction exiting assets from the ledger. * Generate an transaction exiting assets from the ledger.
@ -44,9 +40,10 @@ abstract class OnLedgerAsset<T : Any, S : FungibleAsset<T>> : Contract {
* @return the public key of the assets issuer, who must sign the transaction for it to be valid. * @return the public key of the assets issuer, who must sign the transaction for it to be valid.
*/ */
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>, fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
changeKey: PublicKey, assetStates: List<StateAndRef<S>>): PublicKey assetStates: List<StateAndRef<S>>): PublicKey
= conserveClause.generateExit(tx, amountIssued, changeKey, assetStates, = conserveClause.generateExit(tx, amountIssued, assetStates,
deriveState = { state, amount, owner -> deriveState(state, amount, owner) }, deriveState = { state, amount, owner -> deriveState(state, amount, owner) },
generateMoveCommand = { -> generateMoveCommand() },
generateExitCommand = { amount -> generateExitCommand(amount) } generateExitCommand = { amount -> generateExitCommand(amount) }
) )

View File

@ -5,9 +5,9 @@ import com.r3corda.contracts.asset.InsufficientBalanceException
import com.r3corda.contracts.asset.sumFungibleOrNull import com.r3corda.contracts.asset.sumFungibleOrNull
import com.r3corda.contracts.asset.sumFungibleOrZero import com.r3corda.contracts.asset.sumFungibleOrZero
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.GroupClause import com.r3corda.core.contracts.clauses.Clause
import com.r3corda.core.contracts.clauses.MatchBehaviour
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.transactions.TransactionBuilder
import java.security.PublicKey import java.security.PublicKey
import java.util.* import java.util.*
@ -16,14 +16,7 @@ import java.util.*
* Move command is provided, and errors if absent. Must be the last clause under a grouping clause; * Move command is provided, and errors if absent. Must be the last clause under a grouping clause;
* errors on no-match, ends on match. * errors on no-match, ends on match.
*/ */
abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause<S, Issued<T>> { abstract class AbstractConserveAmount<S : FungibleAsset<T>, C : CommandData, T : Any> : Clause<S, C, Issued<T>>() {
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
override val requiredCommands: Set<Class<out CommandData>>
get() = emptySet()
/** /**
* Gather assets from the given list of states, sufficient to match or exceed the given amount. * Gather assets from the given list of states, sufficient to match or exceed the given amount.
* *
@ -53,16 +46,16 @@ abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause
* *
* @param tx transaction builder to add states and commands to. * @param tx transaction builder to add states and commands to.
* @param amountIssued the amount to be exited, represented as a quantity of issued currency. * @param amountIssued the amount to be exited, represented as a quantity of issued currency.
* @param changeKey the key to send any change to. This needs to be explicitly stated as the input states are not
* necessarily owned by us.
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
* the responsibility of the caller to check that they do not exit funds held by others. * the responsibility of the caller to check that they do not attempt to exit funds held by others.
* @return the public key of the assets issuer, who must sign the transaction for it to be valid. * @return the public key of the assets issuer, who must sign the transaction for it to be valid.
*/ */
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>, fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
changeKey: PublicKey, assetStates: List<StateAndRef<S>>, assetStates: List<StateAndRef<S>>,
deriveState: (TransactionState<S>, Amount<Issued<T>>, PublicKey) -> TransactionState<S>, deriveState: (TransactionState<S>, Amount<Issued<T>>, PublicKey) -> TransactionState<S>,
generateMoveCommand: () -> CommandData,
generateExitCommand: (Amount<Issued<T>>) -> CommandData): PublicKey { generateExitCommand: (Amount<Issued<T>>) -> CommandData): PublicKey {
val owner = assetStates.map { it.state.data.owner }.toSet().single()
val currency = amountIssued.token.product val currency = amountIssued.token.product
val amount = Amount(amountIssued.quantity, currency) val amount = Amount(amountIssued.quantity, currency)
var acceptableCoins = assetStates.filter { ref -> ref.state.data.amount.token == amountIssued.token } var acceptableCoins = assetStates.filter { ref -> ref.state.data.amount.token == amountIssued.token }
@ -82,12 +75,13 @@ abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause
val outputs = if (change != null) { val outputs = if (change != null) {
// Add a change output and adjust the last output downwards. // Add a change output and adjust the last output downwards.
listOf(deriveState(gathered.last().state, change, changeKey)) listOf(deriveState(gathered.last().state, change, owner))
} else emptyList() } else emptyList()
for (state in gathered) tx.addInputState(state) for (state in gathered) tx.addInputState(state)
for (state in outputs) tx.addOutputState(state) for (state in outputs) tx.addOutputState(state)
tx.addCommand(generateExitCommand(amountIssued), amountIssued.token.issuer.party.owningKey) tx.addCommand(generateMoveCommand(), gathered.map { it.state.data.owner })
tx.addCommand(generateExitCommand(amountIssued), gathered.flatMap { it.state.data.exitKeys })
return amountIssued.token.issuer.party.owningKey return amountIssued.token.issuer.party.owningKey
} }
@ -178,26 +172,31 @@ abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<S>, inputs: List<S>,
outputs: List<S>, outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<C>>,
token: Issued<T>): Set<CommandData> { groupingKey: Issued<T>?): Set<C> {
val inputAmount: Amount<Issued<T>> = inputs.sumFungibleOrNull<T>() ?: throw IllegalArgumentException("there is at least one asset input for group $token") require(groupingKey != null) { "Conserve amount clause can only be used on grouped states" }
val deposit = token.issuer val matchedCommands = commands.filter { command -> command.value is FungibleAsset.Commands.Move || command.value is FungibleAsset.Commands.Exit<*> }
val outputAmount: Amount<Issued<T>> = outputs.sumFungibleOrZero(token) val inputAmount: Amount<Issued<T>> = inputs.sumFungibleOrNull<T>() ?: throw IllegalArgumentException("there is at least one asset input for group $groupingKey")
val deposit = groupingKey!!.issuer
val outputAmount: Amount<Issued<T>> = outputs.sumFungibleOrZero(groupingKey)
// If we want to remove assets from the ledger, that must be signed for by the issuer. // If we want to remove assets from the ledger, that must be signed for by the issuer and owner.
// A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero.
val exitKeys: Set<PublicKey> = inputs.flatMap { it.exitKeys }.toSet() val exitKeys: Set<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
val exitCommand = tx.commands.select<FungibleAsset.Commands.Exit<T>>(parties = null, signers = exitKeys).filter {it.value.amount.token == token}.singleOrNull() val exitCommand = matchedCommands.select<FungibleAsset.Commands.Exit<T>>(parties = null, signers = exitKeys).filter { it.value.amount.token == groupingKey }.singleOrNull()
val amountExitingLedger: Amount<Issued<T>> = exitCommand?.value?.amount ?: Amount(0, token) val amountExitingLedger: Amount<Issued<T>> = exitCommand?.value?.amount ?: Amount(0, groupingKey)
requireThat { requireThat {
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L } "there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
"for reference ${deposit.reference} at issuer ${deposit.party.name} the amounts balance" by "for reference ${deposit.reference} at issuer ${deposit.party.name} the amounts balance: ${inputAmount.quantity} - ${amountExitingLedger.quantity} != ${outputAmount.quantity}" by
(inputAmount == outputAmount + amountExitingLedger) (inputAmount == outputAmount + amountExitingLedger)
} }
return listOf(exitCommand?.value, verifyMoveCommand<FungibleAsset.Commands.Move>(inputs, tx)) verifyMoveCommand<FungibleAsset.Commands.Move>(inputs, commands)
.filter { it != null }
.requireNoNulls().toSet() // This is safe because we've taken the commands from a collection of C objects at the start
@Suppress("UNCHECKED_CAST")
return matchedCommands.map { it.value }.toSet()
} }
override fun toString(): String = "Conserve amount between inputs and outputs"
} }

View File

@ -1,8 +1,7 @@
package com.r3corda.contracts.clause package com.r3corda.contracts.clause
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.GroupClause import com.r3corda.core.contracts.clauses.Clause
import com.r3corda.core.contracts.clauses.MatchBehaviour
/** /**
* Standard issue clause for contracts that issue fungible assets. * Standard issue clause for contracts that issue fungible assets.
@ -14,20 +13,16 @@ import com.r3corda.core.contracts.clauses.MatchBehaviour
* @param sumOrZero function to convert a list of states into an amount of the token, and returns zero if there are * @param sumOrZero function to convert a list of states into an amount of the token, and returns zero if there are
* no states in the list. Takes in an instance of the token definition for constructing the zero amount if needed. * no states in the list. Takes in an instance of the token definition for constructing the zero amount if needed.
*/ */
abstract class AbstractIssue<in S: ContractState, T: Any>( abstract class AbstractIssue<in S: ContractState, C: CommandData, T: Any>(
val sum: List<S>.() -> Amount<Issued<T>>, val sum: List<S>.() -> Amount<Issued<T>>,
val sumOrZero: List<S>.(token: Issued<T>) -> Amount<Issued<T>> val sumOrZero: List<S>.(token: Issued<T>) -> Amount<Issued<T>>
) : GroupClause<S, Issued<T>> { ) : Clause<S, C, Issued<T>>() {
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<S>, inputs: List<S>,
outputs: List<S>, outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<C>>,
token: Issued<T>): Set<CommandData> { groupingKey: Issued<T>?): Set<C> {
require(groupingKey != null)
// TODO: Take in matched commands as a parameter // TODO: Take in matched commands as a parameter
val issueCommand = commands.requireSingleCommand<IssueCommand>() val issueCommand = commands.requireSingleCommand<IssueCommand>()
@ -42,8 +37,8 @@ abstract class AbstractIssue<in S: ContractState, T: Any>(
// external mechanism (such as locally defined rules on which parties are trustworthy). // external mechanism (such as locally defined rules on which parties are trustworthy).
// The grouping already ensures that all outputs have the same deposit reference and token. // The grouping already ensures that all outputs have the same deposit reference and token.
val issuer = token.issuer.party val issuer = groupingKey!!.issuer.party
val inputAmount = inputs.sumOrZero(token) val inputAmount = inputs.sumOrZero(groupingKey)
val outputAmount = outputs.sum() val outputAmount = outputs.sum()
requireThat { requireThat {
"the issue command has a nonce" by (issueCommand.value.nonce != 0L) "the issue command has a nonce" by (issueCommand.value.nonce != 0L)
@ -53,6 +48,8 @@ abstract class AbstractIssue<in S: ContractState, T: Any>(
"output values sum to more than the inputs" by (outputAmount > inputAmount) "output values sum to more than the inputs" by (outputAmount > inputAmount)
} }
return setOf(issueCommand.value) // This is safe because we've taken the command from a collection of C objects at the start
@Suppress("UNCHECKED_CAST")
return setOf(issueCommand.value as C)
} }
} }

View File

@ -5,8 +5,7 @@ import com.r3corda.contracts.asset.Obligation
import com.r3corda.contracts.asset.extractAmountsDue import com.r3corda.contracts.asset.extractAmountsDue
import com.r3corda.contracts.asset.sumAmountsDue import com.r3corda.contracts.asset.sumAmountsDue
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.MatchBehaviour import com.r3corda.core.contracts.clauses.Clause
import com.r3corda.core.contracts.clauses.SingleClause
import java.security.PublicKey import java.security.PublicKey
/** /**
@ -43,25 +42,25 @@ data class MultilateralNetState<P>(
* Clause for netting contract states. Currently only supports obligation contract. * Clause for netting contract states. Currently only supports obligation contract.
*/ */
// TODO: Make this usable for any nettable contract states // TODO: Make this usable for any nettable contract states
open class NetClause<P> : SingleClause { open class NetClause<C: CommandData, P> : Clause<ContractState, C, Unit>() {
override val ifNotMatched: MatchBehaviour override val requiredCommands: Set<Class<out CommandData>> = setOf(Obligation.Commands.Net::class.java)
get() = MatchBehaviour.CONTINUE
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Obligation.Commands.Net::class.java)
@Suppress("ConvertLambdaToReference") @Suppress("ConvertLambdaToReference")
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> { override fun verify(tx: TransactionForContract,
val command = commands.requireSingleCommand<Obligation.Commands.Net>() inputs: List<ContractState>,
outputs: List<ContractState>,
commands: List<AuthenticatedObject<C>>,
groupingKey: Unit?): Set<C> {
val matchedCommands: List<AuthenticatedObject<C>> = commands.filter { it.value is NetCommand }
val command = matchedCommands.requireSingleCommand<Obligation.Commands.Net>()
val groups = when (command.value.type) { val groups = when (command.value.type) {
NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State<P> -> it.bilateralNetState } NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State<P> -> it.bilateralNetState }
NetType.PAYMENT -> tx.groupStates { it: Obligation.State<P> -> it.multilateralNetState } NetType.PAYMENT -> tx.groupStates { it: Obligation.State<P> -> it.multilateralNetState }
} }
for ((inputs, outputs, key) in groups) { for ((groupInputs, groupOutputs, key) in groups) {
verifyNetCommand(inputs, outputs, command, key) verifyNetCommand(groupInputs, groupOutputs, command, key)
} }
return setOf(command.value) return matchedCommands.map { it.value }.toSet()
} }
/** /**
@ -70,7 +69,7 @@ open class NetClause<P> : SingleClause {
@VisibleForTesting @VisibleForTesting
fun verifyNetCommand(inputs: List<Obligation.State<P>>, fun verifyNetCommand(inputs: List<Obligation.State<P>>,
outputs: List<Obligation.State<P>>, outputs: List<Obligation.State<P>>,
command: AuthenticatedObject<Obligation.Commands.Net>, command: AuthenticatedObject<NetCommand>,
netState: NetState<P>) { netState: NetState<P>) {
val template = netState.template val template = netState.template
// Create two maps of balances from obligors to beneficiaries, one for input states, the other for output states. // Create two maps of balances from obligors to beneficiaries, one for input states, the other for output states.

View File

@ -2,29 +2,23 @@ package com.r3corda.contracts.clause
import com.r3corda.contracts.asset.FungibleAsset import com.r3corda.contracts.asset.FungibleAsset
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.GroupClause import com.r3corda.core.contracts.clauses.Clause
import com.r3corda.core.contracts.clauses.MatchBehaviour
/** /**
* Clause for fungible asset contracts, which enforces that no output state should have * Clause for fungible asset contracts, which enforces that no output state should have
* a balance of zero. * a balance of zero.
*/ */
open class NoZeroSizedOutputs<in S: FungibleAsset<T>, T: Any> : GroupClause<S, Issued<T>> { open class NoZeroSizedOutputs<in S : FungibleAsset<T>, C : CommandData, T : Any> : Clause<S, C, Issued<T>>() {
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
override val requiredCommands: Set<Class<CommandData>>
get() = emptySet()
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<S>, inputs: List<S>,
outputs: List<S>, outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: List<AuthenticatedObject<C>>,
token: Issued<T>): Set<CommandData> { groupingKey: Issued<T>?): Set<C> {
requireThat { requireThat {
"there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L } "there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L }
} }
return emptySet() return emptySet()
} }
override fun toString(): String = "No zero sized outputs"
} }

View File

@ -0,0 +1,87 @@
package com.r3corda.contracts.testing
import com.pholser.junit.quickcheck.generator.GenerationStatus
import com.pholser.junit.quickcheck.generator.Generator
import com.pholser.junit.quickcheck.generator.java.util.ArrayListGenerator
import com.pholser.junit.quickcheck.random.SourceOfRandomness
import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.NullSignature
import com.r3corda.core.testing.*
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.WireTransaction
/**
* This file contains generators for quickcheck style testing. The idea is that we can write random instance generators
* for each type we have in the code and test against those instead of predefined mock data. This style of testing can
* catch corner case bugs and test algebraic properties of the code, for example deserialize(serialize(generatedThing)) == generatedThing
*
* TODO add combinators for easier Generator writing
*/
class ContractStateGenerator : Generator<ContractState>(ContractState::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): ContractState {
return Cash.State(
amount = AmountGenerator(IssuedGenerator(CurrencyGenerator())).generate(random, status),
owner = PublicKeyGenerator().generate(random, status)
)
}
}
class MoveGenerator : Generator<Cash.Commands.Move>(Cash.Commands.Move::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Cash.Commands.Move {
return Cash.Commands.Move(SecureHashGenerator().generate(random, status))
}
}
class IssueGenerator : Generator<Cash.Commands.Issue>(Cash.Commands.Issue::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Cash.Commands.Issue {
return Cash.Commands.Issue(random.nextLong())
}
}
class ExitGenerator : Generator<Cash.Commands.Exit>(Cash.Commands.Exit::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Cash.Commands.Exit {
return Cash.Commands.Exit(AmountGenerator(IssuedGenerator(CurrencyGenerator())).generate(random, status))
}
}
class CommandDataGenerator : Generator<CommandData>(CommandData::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): CommandData {
val generators = listOf(MoveGenerator(), IssueGenerator(), ExitGenerator())
return generators[random.nextInt(0, generators.size - 1)].generate(random, status)
}
}
class CommandGenerator : Generator<Command>(Command::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Command {
val signersGenerator = ArrayListGenerator()
signersGenerator.addComponentGenerators(listOf(PublicKeyGenerator()))
return Command(CommandDataGenerator().generate(random, status), PublicKeyGenerator().generate(random, status))
}
}
class WiredTransactionGenerator: Generator<WireTransaction>(WireTransaction::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): WireTransaction {
val commands = CommandGenerator().generateList(random, status) + listOf(CommandGenerator().generate(random, status))
return WireTransaction(
inputs = StateRefGenerator().generateList(random, status),
attachments = SecureHashGenerator().generateList(random, status),
outputs = TransactionStateGenerator(ContractStateGenerator()).generateList(random, status),
commands = commands,
notary = PartyGenerator().generate(random, status),
signers = commands.flatMap { it.signers },
type = TransactionType.General(),
timestamp = TimestampGenerator().generate(random, status)
)
}
}
class SignedTransactionGenerator: Generator<SignedTransaction>(SignedTransaction::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): SignedTransaction {
val wireTransaction = WiredTransactionGenerator().generate(random, status)
return SignedTransaction(
txBits = wireTransaction.serialized,
sigs = listOf(NullSignature)
)
}
}

View File

@ -6,15 +6,13 @@ import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER
import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER_KEY import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER_KEY
import com.r3corda.core.contracts.Amount import com.r3corda.core.contracts.Amount
import com.r3corda.core.contracts.Issued import com.r3corda.core.contracts.Issued
import com.r3corda.core.contracts.SignedTransaction import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.contracts.TransactionType import com.r3corda.core.contracts.TransactionType
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.node.ServiceHub import com.r3corda.core.node.ServiceHub
import com.r3corda.core.node.services.Wallet import com.r3corda.core.node.services.Wallet
import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.utilities.DUMMY_NOTARY
import com.r3corda.core.testing.DUMMY_NOTARY_KEY
import java.security.KeyPair
import java.security.PublicKey import java.security.PublicKey
import java.util.* import java.util.*
@ -61,22 +59,29 @@ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount<Currency>,
} }
private fun calculateRandomlySizedAmounts(howMuch: Amount<Currency>, min: Int, max: Int, rng: Random): LongArray { private fun calculateRandomlySizedAmounts(howMuch: Amount<Currency>, min: Int, max: Int, rng: Random): LongArray {
val numStates = min + Math.floor(rng.nextDouble() * (max - min)).toInt() val numSlots = min + Math.floor(rng.nextDouble() * (max - min)).toInt()
val amounts = LongArray(numStates) val baseSize = howMuch.quantity / numSlots
val baseSize = howMuch.quantity / numStates
check(baseSize > 0) { baseSize } check(baseSize > 0) { baseSize }
var filledSoFar = 0L
for (i in 0..numStates - 1) { val amounts = LongArray(numSlots) { baseSize }
if (i < numStates - 1) { var distanceFromGoal = 0L
// Adjust the amount a bit up or down, to give more realistic amounts (not all identical). // If we want 10 slots then max adjust is 0.1, so even if all random numbers come out to the largest downward
amounts[i] = baseSize + (baseSize / 2 * (rng.nextDouble() - 0.5)).toLong() // adjustment possible, the last slot ends at zero. With 20 slots, max adjust is 0.05 etc.
filledSoFar += amounts[i] val maxAdjust = 1.0 / numSlots
for (i in amounts.indices) {
if (i != amounts.lastIndex) {
val adjustBy = rng.nextDouble() * maxAdjust - (maxAdjust / 2)
val adjustment = (1 + adjustBy)
val adjustTo = (amounts[i] * adjustment).toLong()
amounts[i] = adjustTo
distanceFromGoal += baseSize - adjustTo
} else { } else {
// Handle inexact rounding. amounts[i] += distanceFromGoal
amounts[i] = howMuch.quantity - filledSoFar
} }
check(amounts[i] >= 0) { "${amounts[i]} : $filledSoFar : $howMuch" }
} }
check(amounts.sum() == howMuch.quantity)
// The desired amount may not have divided equally to start with, so adjust the first value to make up.
amounts[0] += howMuch.quantity - amounts.sum()
return amounts return amounts
} }

View File

@ -7,16 +7,17 @@ import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.DigitalSignature import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.signWithECDSA import com.r3corda.core.crypto.signWithECDSA
import com.r3corda.core.crypto.toStringsShort
import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.NodeInfo
import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.random63BitValue import com.r3corda.core.random63BitValue
import com.r3corda.core.seconds import com.r3corda.core.seconds
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.transactions.WireTransaction
import com.r3corda.core.utilities.ProgressTracker import com.r3corda.core.utilities.ProgressTracker
import com.r3corda.core.utilities.trace import com.r3corda.core.utilities.trace
import java.security.KeyPair import java.security.KeyPair
import java.security.PublicKey import java.security.PublicKey
import java.security.SignatureException
import java.util.* import java.util.*
/** /**
@ -95,7 +96,6 @@ object TwoPartyTradeProtocol {
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though. // These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
val ourSignature = signWithOurKey(partialTX) val ourSignature = signWithOurKey(partialTX)
val notarySignature = getNotarySignature(partialTX) val notarySignature = getNotarySignature(partialTX)
return sendSignatures(partialTX, ourSignature, notarySignature) return sendSignatures(partialTX, ourSignature, notarySignature)
} }
@ -118,16 +118,11 @@ object TwoPartyTradeProtocol {
progressTracker.currentStep = VERIFYING progressTracker.currentStep = VERIFYING
maybeSTX.validate { maybeSTX.unwrap {
progressTracker.nextStep() progressTracker.nextStep()
// Check that the tx proposed by the buyer is valid. // Check that the tx proposed by the buyer is valid.
val missingSigs: Set<PublicKey> = it.verifySignatures(throwIfSignaturesAreMissing = false) val wtx: WireTransaction = it.verifySignatures(myKeyPair.public, notaryNode.identity.owningKey)
val expected = setOf(myKeyPair.public, notaryNode.identity.owningKey)
if (missingSigs != expected)
throw SignatureException("The set of missing signatures is not as expected: ${missingSigs.toStringsShort()} vs ${expected.toStringsShort()}")
val wtx: WireTransaction = it.tx
logger.trace { "Received partially signed transaction: ${it.id}" } logger.trace { "Received partially signed transaction: ${it.id}" }
// Download and check all the things that this transaction depends on and verify it is contract-valid, // Download and check all the things that this transaction depends on and verify it is contract-valid,
@ -212,7 +207,7 @@ object TwoPartyTradeProtocol {
val maybeTradeRequest = receive<SellerTradeInfo>(sessionID) val maybeTradeRequest = receive<SellerTradeInfo>(sessionID)
progressTracker.currentStep = VERIFYING progressTracker.currentStep = VERIFYING
maybeTradeRequest.validate { maybeTradeRequest.unwrap {
// What is the seller trying to sell us? // What is the seller trying to sell us?
val asset = it.assetForSale.state.data val asset = it.assetForSale.state.data
val assetTypeName = asset.javaClass.name val assetTypeName = asset.javaClass.name
@ -240,7 +235,7 @@ object TwoPartyTradeProtocol {
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx. // TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
return sendAndReceive<SignaturesFromSeller>(otherSide, theirSessionID, sessionID, stx).validate { it } return sendAndReceive<SignaturesFromSeller>(otherSide, theirSessionID, sessionID, stx).unwrap { it }
} }
private fun signWithOurKeys(cashSigningPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction { private fun signWithOurKeys(cashSigningPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {

View File

@ -6,7 +6,8 @@ import kotlin.*;
import org.junit.*; import org.junit.*;
import static com.r3corda.core.contracts.ContractsDSL.*; import static com.r3corda.core.contracts.ContractsDSL.*;
import static com.r3corda.core.testing.CoreTestUtils.*; import static com.r3corda.core.utilities.TestConstants.*;
import static com.r3corda.testing.CoreTestUtils.*;
/** /**
* This is an incomplete Java replica of CashTests.kt to show how to use the Java test DSL * This is an incomplete Java replica of CashTests.kt to show how to use the Java test DSL

View File

@ -6,9 +6,15 @@ import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.days import com.r3corda.core.days
import com.r3corda.core.node.services.testing.MockServices
import com.r3corda.core.seconds import com.r3corda.core.seconds
import com.r3corda.core.testing.* import com.r3corda.core.transactions.LedgerTransaction
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.utilities.DUMMY_NOTARY
import com.r3corda.core.utilities.DUMMY_NOTARY_KEY
import com.r3corda.core.utilities.DUMMY_PUBKEY_1
import com.r3corda.core.utilities.TEST_TX_TIME
import com.r3corda.testing.node.MockServices
import com.r3corda.testing.*
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.Parameterized import org.junit.runners.Parameterized
@ -17,6 +23,8 @@ import java.util.*
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertTrue import kotlin.test.assertTrue
// TODO: The generate functions aren't tested by these tests: add them.
interface ICommercialPaperTestTemplate { interface ICommercialPaperTestTemplate {
fun getPaper(): ICommercialPaperState fun getPaper(): ICommercialPaperState
fun getIssueCommand(notary: Party): CommandData fun getIssueCommand(notary: Party): CommandData
@ -32,8 +40,8 @@ class JavaCommercialPaperTest() : ICommercialPaperTestTemplate {
TEST_TX_TIME + 7.days TEST_TX_TIME + 7.days
) )
override fun getIssueCommand(notary: Party): CommandData = JavaCommercialPaper.Commands.Issue(notary) override fun getIssueCommand(notary: Party): CommandData = JavaCommercialPaper.Commands.Issue()
override fun getRedeemCommand(notary: Party): CommandData = JavaCommercialPaper.Commands.Redeem(notary) override fun getRedeemCommand(notary: Party): CommandData = JavaCommercialPaper.Commands.Redeem()
override fun getMoveCommand(): CommandData = JavaCommercialPaper.Commands.Move() override fun getMoveCommand(): CommandData = JavaCommercialPaper.Commands.Move()
} }
@ -45,8 +53,8 @@ class KotlinCommercialPaperTest() : ICommercialPaperTestTemplate {
maturityDate = TEST_TX_TIME + 7.days maturityDate = TEST_TX_TIME + 7.days
) )
override fun getIssueCommand(notary: Party): CommandData = CommercialPaper.Commands.Issue(notary) override fun getIssueCommand(notary: Party): CommandData = CommercialPaper.Commands.Issue()
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaper.Commands.Redeem(notary) override fun getRedeemCommand(notary: Party): CommandData = CommercialPaper.Commands.Redeem()
override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move() override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move()
} }
@ -58,8 +66,8 @@ class KotlinCommercialPaperLegacyTest() : ICommercialPaperTestTemplate {
maturityDate = TEST_TX_TIME + 7.days maturityDate = TEST_TX_TIME + 7.days
) )
override fun getIssueCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Issue(notary) override fun getIssueCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Issue()
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Redeem(notary) override fun getRedeemCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Redeem()
override fun getMoveCommand(): CommandData = CommercialPaperLegacy.Commands.Move() override fun getMoveCommand(): CommandData = CommercialPaperLegacy.Commands.Move()
} }

View File

@ -1,9 +1,16 @@
package com.r3corda.contracts package com.r3corda.contracts
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.node.services.testing.MockServices
import com.r3corda.core.seconds import com.r3corda.core.seconds
import com.r3corda.core.testing.* import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.utilities.DUMMY_NOTARY
import com.r3corda.core.utilities.DUMMY_NOTARY_KEY
import com.r3corda.core.utilities.TEST_TX_TIME
import com.r3corda.testing.LedgerDSL
import com.r3corda.testing.TestLedgerDSLInterpreter
import com.r3corda.testing.TestTransactionDSLInterpreter
import com.r3corda.testing.node.MockServices
import com.r3corda.testing.*
import org.junit.Test import org.junit.Test
import java.math.BigDecimal import java.math.BigDecimal
import java.time.LocalDate import java.time.LocalDate
@ -394,9 +401,10 @@ class IRSTests {
@Test @Test
fun `ensure failure occurs when there are inbound states for an agreement command`() { fun `ensure failure occurs when there are inbound states for an agreement command`() {
val irs = singleIRS()
transaction { transaction {
input() { singleIRS() } input() { irs }
output("irs post agreement") { singleIRS() } output("irs post agreement") { irs }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME) timestamp(TEST_TX_TIME)
this `fails with` "There are no in states for an agreement" this `fails with` "There are no in states for an agreement"
@ -665,10 +673,11 @@ class IRSTests {
transaction("Agreement") { transaction("Agreement") {
output("irs post agreement2") { output("irs post agreement2") {
irs.copy( irs.copy(
irs.fixedLeg, linearId = UniqueIdentifier("t2"),
irs.floatingLeg, fixedLeg = irs.fixedLeg,
irs.calculation, floatingLeg = irs.floatingLeg,
irs.common.copy(tradeID = "t2") calculation = irs.calculation,
common = irs.common.copy(tradeID = "t2")
) )
} }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }

View File

@ -4,7 +4,11 @@ import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.testing.* import com.r3corda.core.transactions.WireTransaction
import com.r3corda.core.utilities.DUMMY_NOTARY
import com.r3corda.core.utilities.DUMMY_PUBKEY_1
import com.r3corda.core.utilities.DUMMY_PUBKEY_2
import com.r3corda.testing.*
import org.junit.Test import org.junit.Test
import java.security.PublicKey import java.security.PublicKey
import java.util.* import java.util.*
@ -17,7 +21,9 @@ class CashTests {
amount = 1000.DOLLARS `issued by` defaultIssuer, amount = 1000.DOLLARS `issued by` defaultIssuer,
owner = DUMMY_PUBKEY_1 owner = DUMMY_PUBKEY_1
) )
val outState = inState.copy(owner = DUMMY_PUBKEY_2) // Input state held by the issuer
val issuerInState = inState.copy(owner = defaultIssuer.party.owningKey)
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(deposit.copy(reference = OpaqueBytes.of(ref))))
@ -59,7 +65,7 @@ class CashTests {
} }
@Test @Test
fun issueMoney() { fun `issue by move`() {
// Check we can't "move" money into existence. // Check we can't "move" money into existence.
transaction { transaction {
input { DummyState() } input { DummyState() }
@ -68,7 +74,10 @@ class CashTests {
this `fails with` "there is at least one asset input" this `fails with` "there is at least one asset input"
} }
}
@Test
fun issue() {
// Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised // Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised
// institution is allowed to issue as much cash as they want. // institution is allowed to issue as much cash as they want.
transaction { transaction {
@ -90,28 +99,41 @@ class CashTests {
command(MINI_CORP_PUBKEY) { Cash.Commands.Issue() } command(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
this.verifies() this.verifies()
} }
}
@Test
fun generateIssueRaw() {
// Test generation works. // Test generation works.
val ptx = TransactionType.General.Builder(DUMMY_NOTARY) val tx: WireTransaction = TransactionType.General.Builder(notary = null).apply {
Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) Cash().generateIssue(this, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
assertTrue(ptx.inputStates().isEmpty()) signWith(MINI_CORP_KEY)
val s = ptx.outputStates()[0].data as Cash.State }.toSignedTransaction().tx
assertTrue(tx.inputs.isEmpty())
val s = tx.outputs[0].data as Cash.State
assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount) assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount)
assertEquals(MINI_CORP, s.deposit.party) assertEquals(MINI_CORP, s.deposit.party)
assertEquals(DUMMY_PUBKEY_1, s.owner) assertEquals(DUMMY_PUBKEY_1, s.owner)
assertTrue(ptx.commands()[0].value is Cash.Commands.Issue) assertTrue(tx.commands[0].value is Cash.Commands.Issue)
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0]) assertEquals(MINI_CORP_PUBKEY, tx.commands[0].signers[0])
}
// Test issuance from the issuance definition @Test
fun generateIssueFromAmount() {
// Test issuance from an issued amount
val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34) val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34)
val templatePtx = TransactionType.General.Builder(DUMMY_NOTARY) val tx: WireTransaction = TransactionType.General.Builder(notary = null).apply {
Cash().generateIssue(templatePtx, amount, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) Cash().generateIssue(this, amount, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
assertTrue(templatePtx.inputStates().isEmpty()) signWith(MINI_CORP_KEY)
assertEquals(ptx.outputStates()[0], templatePtx.outputStates()[0]) }.toSignedTransaction().tx
assertTrue(tx.inputs.isEmpty())
assertEquals(tx.outputs[0], tx.outputs[0])
}
@Test
fun `extended issue examples`() {
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer. // We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
transaction { transaction {
input { inState } input { issuerInState }
output { inState.copy(amount = inState.amount * 2) } output { inState.copy(amount = inState.amount * 2) }
// Move fails: not allowed to summon money. // Move fails: not allowed to summon money.
@ -154,11 +176,11 @@ class CashTests {
} }
tweak { tweak {
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
this `fails with` "All commands must be matched at end of execution." this `fails with` "The following commands were not matched at the end of execution"
} }
tweak { tweak {
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) } command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) }
this `fails with` "All commands must be matched at end of execution." this `fails with` "The following commands were not matched at the end of execution"
} }
this.verifies() this.verifies()
} }
@ -279,12 +301,12 @@ class CashTests {
fun exitLedger() { fun exitLedger() {
// Single input/output straightforward case. // Single input/output straightforward case.
transaction { transaction {
input { inState } input { issuerInState }
output { outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) } output { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
tweak { tweak {
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer) } command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer) }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
this `fails with` "the amounts balance" this `fails with` "the amounts balance"
} }
@ -293,20 +315,24 @@ class CashTests {
this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command" this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
tweak { tweak {
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
this.verifies() this.verifies()
} }
} }
} }
}
@Test
fun `exit ledger with multiple issuers`() {
// Multi-issuer case. // Multi-issuer case.
transaction { transaction {
input { inState } input { issuerInState }
input { inState `issued by` MINI_CORP } input { issuerInState.copy(owner = MINI_CORP_PUBKEY) `issued by` MINI_CORP }
output { inState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) `issued by` MINI_CORP } output { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) `issued by` MINI_CORP }
output { inState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) } output { issuerInState.copy(owner = MINI_CORP_PUBKEY, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { Cash.Commands.Move() }
this `fails with` "at issuer MegaCorp the amounts balance" this `fails with` "at issuer MegaCorp the amounts balance"
@ -318,6 +344,18 @@ class CashTests {
} }
} }
@Test
fun `exit cash not held by its issuer`() {
// Single input/output straightforward case.
transaction {
input { inState }
output { outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails with` "at issuer MegaCorp the amounts balance"
}
}
@Test @Test
fun multiIssuer() { fun multiIssuer() {
transaction { transaction {
@ -385,7 +423,7 @@ class CashTests {
*/ */
fun makeExit(amount: Amount<Currency>, corp: Party, depositRef: Byte = 1): WireTransaction { fun makeExit(amount: Amount<Currency>, corp: Party, depositRef: Byte = 1): WireTransaction {
val tx = TransactionType.General.Builder(DUMMY_NOTARY) val tx = TransactionType.General.Builder(DUMMY_NOTARY)
Cash().generateExit(tx, Amount(amount.quantity, Issued(corp.ref(depositRef), amount.token)), OUR_PUBKEY_1, WALLET) Cash().generateExit(tx, Amount(amount.quantity, Issued(corp.ref(depositRef), amount.token)), WALLET)
return tx.toWireTransaction() return tx.toWireTransaction()
} }
@ -404,9 +442,10 @@ class CashTests {
assertEquals(WALLET[0].ref, wtx.inputs[0]) assertEquals(WALLET[0].ref, wtx.inputs[0])
assertEquals(0, wtx.outputs.size) assertEquals(0, wtx.outputs.size)
val expected = Cash.Commands.Exit(Amount(10000, Issued(MEGA_CORP.ref(1), USD))) val expectedMove = Cash.Commands.Move()
val actual = wtx.commands.single().value val expectedExit = Cash.Commands.Exit(Amount(10000, Issued(MEGA_CORP.ref(1), USD)))
assertEquals(expected, actual)
assertEquals(listOf(expectedMove, expectedExit), wtx.commands.map { it.value })
} }
/** /**

View File

@ -5,8 +5,8 @@ import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.NullPublicKey import com.r3corda.core.crypto.NullPublicKey
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.testing.* import com.r3corda.core.utilities.*
import com.r3corda.core.utilities.nonEmptySetOf import com.r3corda.testing.*
import org.junit.Test import org.junit.Test
import java.security.PublicKey import java.security.PublicKey
import java.time.Duration import java.time.Duration
@ -119,19 +119,21 @@ class ObligationTests {
} }
// Test generation works. // Test generation works.
val ptx = TransactionType.General.Builder(DUMMY_NOTARY) val tx = TransactionType.General.Builder(notary = null).apply {
Obligation<Currency>().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, Obligation<Currency>().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
beneficiary = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) beneficiary = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
assertTrue(ptx.inputStates().isEmpty()) signWith(MINI_CORP_KEY)
}.toSignedTransaction().tx
assertTrue(tx.inputs.isEmpty())
val expected = Obligation.State( val expected = Obligation.State(
obligor = MINI_CORP, obligor = MINI_CORP,
quantity = 100.DOLLARS.quantity, quantity = 100.DOLLARS.quantity,
beneficiary = DUMMY_PUBKEY_1, beneficiary = DUMMY_PUBKEY_1,
template = megaCorpDollarSettlement template = megaCorpDollarSettlement
) )
assertEquals(ptx.outputStates()[0].data, expected) assertEquals(tx.outputs[0].data, expected)
assertTrue(ptx.commands()[0].value is Obligation.Commands.Issue) assertTrue(tx.commands[0].value is Obligation.Commands.Issue)
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0]) assertEquals(MINI_CORP_PUBKEY, tx.commands[0].signers[0])
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer. // We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
transaction { transaction {
@ -178,11 +180,11 @@ class ObligationTests {
} }
tweak { tweak {
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Move() } command(MEGA_CORP_PUBKEY) { Obligation.Commands.Move() }
this `fails with` "All commands must be matched at end of execution." this `fails with` "The following commands were not matched at the end of execution"
} }
tweak { tweak {
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.amount / 2) } command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.amount / 2) }
this `fails with` "All commands must be matched at end of execution." this `fails with` "The following commands were not matched at the end of execution"
} }
this.verifies() this.verifies()
} }

View File

@ -40,6 +40,7 @@ dependencies {
// Bring in the MockNode infrastructure for writing protocol unit tests. // Bring in the MockNode infrastructure for writing protocol unit tests.
testCompile project(":node") testCompile project(":node")
testCompile project(":test-utils")
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
@ -55,6 +56,9 @@ dependencies {
// AssertJ: for fluent assertions for testing // AssertJ: for fluent assertions for testing
testCompile "org.assertj:assertj-core:${assertj_version}" testCompile "org.assertj:assertj-core:${assertj_version}"
compile 'com.pholser:junit-quickcheck-core:0.6'
compile 'com.pholser:junit-quickcheck-generators:0.6'
// Guava: Google utilities library. // Guava: Google utilities library.
compile "com.google.guava:guava:19.0" compile "com.google.guava:guava:19.0"

View File

@ -0,0 +1,49 @@
package com.r3corda.core.contracts
import com.r3corda.core.crypto.Party
import com.r3corda.core.serialization.OpaqueBytes
import java.security.PublicKey
import java.util.*
/**
* A command from the monitoring client, to the node.
*
* @param id ID used to tag event(s) resulting from a command.
*/
sealed class ClientToServiceCommand(val id: UUID) {
/**
* Issue cash state objects.
*
* @param amount the amount of currency to issue on to the ledger.
* @param issueRef the reference to specify on the issuance, used to differentiate pools of cash. Convention is
* to use the single byte "0x01" as a default.
* @param recipient the party to issue the cash to.
* @param notary the notary to use for this transaction.
* @param id the ID to be provided in events resulting from this request.
*/
class IssueCash(val amount: Amount<Currency>,
val issueRef: OpaqueBytes,
val recipient: Party,
val notary: Party,
id: UUID = UUID.randomUUID()) : ClientToServiceCommand(id)
/**
* Pay cash to someone else.
*
* @param amount the amount of currency to issue on to the ledger.
* @param recipient the party to issue the cash to.
* @param id the ID to be provided in events resulting from this request.
*/
class PayCash(val amount: Amount<Issued<Currency>>, val recipient: Party,
id: UUID = UUID.randomUUID()) : ClientToServiceCommand(id)
/**
* Exit cash from the ledger.
*
* @param amount the amount of currency to exit from the ledger.
* @param issueRef the reference previously specified on the issuance.
* @param id the ID to be provided in events resulting from this request.
*/
class ExitCash(val amount: Amount<Currency>, val issueRef: OpaqueBytes,
id: UUID = UUID.randomUUID()) : ClientToServiceCommand(id)
}

View File

@ -2,6 +2,7 @@ package com.r3corda.core.contracts
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.transactions.TransactionBuilder
import java.security.PublicKey import java.security.PublicKey
// The dummy contract doesn't do anything useful. It exists for testing purposes. // The dummy contract doesn't do anything useful. It exists for testing purposes.
@ -53,11 +54,13 @@ class DummyContract : Contract {
return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owner.party.owningKey)) return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owner.party.owningKey))
} }
fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: PublicKey): TransactionBuilder { fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: PublicKey) = move(listOf(prior), newOwner)
val priorState = prior.state.data fun move(priors: List<StateAndRef<DummyContract.SingleOwnerState>>, newOwner: PublicKey): TransactionBuilder {
require(priors.size > 0)
val priorState = priors[0].state.data
val (cmd, state) = priorState.withNewOwner(newOwner) val (cmd, state) = priorState.withNewOwner(newOwner)
return TransactionType.General.Builder(notary = prior.state.notary).withItems( return TransactionType.General.Builder(notary = priors[0].state.notary).withItems(
/* INPUT */ prior, /* INPUTS */ *priors.toTypedArray(),
/* COMMAND */ Command(cmd, priorState.owner), /* COMMAND */ Command(cmd, priorState.owner),
/* OUTPUT */ state /* OUTPUT */ state
) )

View File

@ -3,7 +3,7 @@ package com.r3corda.core.contracts
import java.security.PublicKey import java.security.PublicKey
/** /**
* Dummy state for use in testing. Not part of any real contract. * Dummy state for use in testing. Not part of any contract, not even the [DummyContract].
*/ */
data class DummyState(val magicNumber: Int = 0) : ContractState { data class DummyState(val magicNumber: Int = 0) : ContractState {
override val contract = DUMMY_PROGRAM_ID override val contract = DUMMY_PROGRAM_ID

View File

@ -423,15 +423,43 @@ enum class NetType {
PAYMENT PAYMENT
} }
data class Commodity(val symbol: String, /**
* Class representing a commodity, as an equivalent to the [Currency] class. This exists purely to enable the
* [CommodityContract] contract, and is likely to change in future.
*
* @param commodityCode a unique code for the commodity. No specific registry for these is currently defined, although
* this is likely to change in future.
* @param displayName human readable name for the commodity.
* @param defaultFractionDigits the number of digits normally after the decimal point when referring to quantities of
* this commodity.
*/
data class Commodity(val commodityCode: String,
val displayName: String, val displayName: String,
val commodityCode: String = symbol,
val defaultFractionDigits: Int = 0) { val defaultFractionDigits: Int = 0) {
companion object { companion object {
private val registry = mapOf( private val registry = mapOf(
// Simple example commodity, as in http://www.investopedia.com/university/commodities/commodities14.asp
Pair("FCOJ", Commodity("FCOJ", "Frozen concentrated orange juice")) Pair("FCOJ", Commodity("FCOJ", "Frozen concentrated orange juice"))
) )
fun getInstance(symbol: String): Commodity? fun getInstance(commodityCode: String): Commodity?
= registry[symbol] = registry[commodityCode]
}
}
/**
* This class provides a truly unique identifier of a trade, state, or other business object.
* @param externalId If there is an existing weak identifer e.g. trade reference id.
* This should be set here the first time a UniqueIdentifier identifier is created as part of an issue,
* or ledger on-boarding activity. This ensure that the human readable identity is paired with the strong id.
* @param id Should never be set by user code and left as default initialised.
* So that the first time a state is issued this should be given a new UUID.
* Subsequent copies and evolutions of a state should just copy the externalId and Id fields unmodified.
*/
data class UniqueIdentifier(val externalId: String? = null, val id: UUID = UUID.randomUUID()) {
override fun toString(): String {
if (externalId != null) {
return "${externalId}_${id.toString()}"
}
return id.toString()
} }
} }

View File

@ -1,5 +1,6 @@
package com.r3corda.core.contracts package com.r3corda.core.contracts
import com.r3corda.core.contracts.clauses.Clause
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort import com.r3corda.core.crypto.toStringShort
@ -7,6 +8,7 @@ import com.r3corda.core.protocols.ProtocolLogicRef
import com.r3corda.core.protocols.ProtocolLogicRefFactory import com.r3corda.core.protocols.ProtocolLogicRefFactory
import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.serialization.serialize import com.r3corda.core.serialization.serialize
import com.r3corda.core.transactions.TransactionBuilder
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@ -215,16 +217,41 @@ data class ScheduledStateRef(val ref: StateRef, override val scheduledAt: Instan
data class ScheduledActivity(val logicRef: ProtocolLogicRef, override val scheduledAt: Instant) : Scheduled data class ScheduledActivity(val logicRef: ProtocolLogicRef, override val scheduledAt: Instant) : Scheduled
/** /**
* A state that evolves by superseding itself, all of which share the common "thread". * A state that evolves by superseding itself, all of which share the common "linearId".
* *
* This simplifies the job of tracking the current version of certain types of state in e.g. a wallet. * This simplifies the job of tracking the current version of certain types of state in e.g. a wallet.
*/ */
interface LinearState : ContractState { interface LinearState: ContractState {
/** Unique thread id within the wallets of all parties */ /**
val thread: SecureHash * Unique id shared by all LinearState states throughout history within the wallets of all parties.
* Verify methods should check that one input and one output share the id in a transaction,
* except at issuance/termination.
*/
val linearId: UniqueIdentifier
/** true if this should be tracked by our wallet(s) */ /**
* True if this should be tracked by our wallet(s).
* */
fun isRelevant(ourKeys: Set<PublicKey>): Boolean fun isRelevant(ourKeys: Set<PublicKey>): Boolean
/**
* Standard clause to verify the LinearState safety properties.
*/
class ClauseVerifier<S : LinearState>(val stateClass: Class<S>) : Clause<ContractState, CommandData, Unit>() {
override fun verify(tx: TransactionForContract,
inputs: List<ContractState>,
outputs: List<ContractState>,
commands: List<AuthenticatedObject<CommandData>>,
groupingKey: Unit?): Set<CommandData> {
val filteredInputs = inputs.filterIsInstance(stateClass)
val inputIds = filteredInputs.map { it.linearId }.distinct()
require(inputIds.count() == filteredInputs.count()) { "LinearStates cannot be merged" }
val filteredOutputs = outputs.filterIsInstance(stateClass)
val outputIds = filteredOutputs.map { it.linearId }.distinct()
require(outputIds.count() == filteredOutputs.count()) { "LinearStates cannot be split" }
return emptySet()
}
}
} }
interface SchedulableState : ContractState { interface SchedulableState : ContractState {
@ -348,6 +375,12 @@ interface MoveCommand : CommandData {
val contractHash: SecureHash? val contractHash: SecureHash?
} }
/** A common netting command for contracts whose states can be netted. */
interface NetCommand : CommandData {
/** The type of netting to apply, see [NetType] for options. */
val type: NetType
}
/** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */ /** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */
data class AuthenticatedObject<out T : Any>( data class AuthenticatedObject<out T : Any>(
val signers: List<PublicKey>, val signers: List<PublicKey>,

View File

@ -2,6 +2,8 @@ package com.r3corda.core.contracts
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.ReadOnlyTransactionStorage import com.r3corda.core.node.services.ReadOnlyTransactionStorage
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.WireTransaction
import java.util.* import java.util.*
import java.util.concurrent.Callable import java.util.concurrent.Callable

View File

@ -1,39 +0,0 @@
package com.r3corda.core.contracts
import com.r3corda.core.node.ServiceHub
import java.io.FileNotFoundException
// TODO: Move these into the actual classes (i.e. where people would expect to find them) and split Transactions.kt into multiple files
/**
* Looks up identities and attachments from storage to generate a [LedgerTransaction]. A transaction is expected to
* have been fully resolved using the resolution protocol by this point.
*
* @throws FileNotFoundException if a required attachment was not found in storage.
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
*/
fun WireTransaction.toLedgerTransaction(services: ServiceHub): LedgerTransaction {
// Look up random keys to authenticated identities. This is just a stub placeholder and will all change in future.
val authenticatedArgs = commands.map {
val parties = it.signers.mapNotNull { pk -> services.identityService.partyFromKey(pk) }
AuthenticatedObject(it.signers, parties, it.value)
}
// Open attachments specified in this transaction. If we haven't downloaded them, we fail.
val attachments = attachments.map {
services.storageService.attachments.openAttachment(it) ?: throw FileNotFoundException(it.toString())
}
val resolvedInputs = inputs.map { StateAndRef(services.loadState(it), it) }
return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, signers, timestamp, type)
}
/**
* Calls [verify] to check all required signatures are present, and then calls [WireTransaction.toLedgerTransaction]
* with the passed in [ServiceHub] to resolve the dependencies, returning an unverified LedgerTransaction.
*
* @throws FileNotFoundException if a required attachment was not found in storage.
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
*/
fun SignedTransaction.toLedgerTransaction(services: ServiceHub): LedgerTransaction {
verifySignatures()
return tx.toLedgerTransaction(services)
}

View File

@ -1,7 +1,8 @@
package com.r3corda.core.contracts package com.r3corda.core.contracts
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.noneOrSingle import com.r3corda.core.transactions.LedgerTransaction
import com.r3corda.core.transactions.TransactionBuilder
import java.security.PublicKey import java.security.PublicKey
/** Defines transaction build & validation logic for a specific transaction type */ /** Defines transaction build & validation logic for a specific transaction type */
@ -29,7 +30,7 @@ sealed class TransactionType {
if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx) if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx)
val requiredKeys = getRequiredSigners(tx) + notaryKey val requiredKeys = getRequiredSigners(tx) + notaryKey
val missing = requiredKeys - tx.signers val missing = requiredKeys - tx.mustSign
return missing return missing
} }

View File

@ -3,6 +3,7 @@ package com.r3corda.core.contracts
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.transactions.LedgerTransaction
import java.security.PublicKey import java.security.PublicKey
import java.util.* import java.util.*

View File

@ -1,218 +0,0 @@
package com.r3corda.core.contracts
import com.esotericsoftware.kryo.Kryo
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringsShort
import com.r3corda.core.indexOfOrThrow
import com.r3corda.core.serialization.SerializedBytes
import com.r3corda.core.serialization.THREAD_LOCAL_KRYO
import com.r3corda.core.serialization.deserialize
import com.r3corda.core.serialization.serialize
import com.r3corda.core.utilities.Emoji
import java.security.PublicKey
import java.security.SignatureException
import java.util.*
/**
* Views of a transaction as it progresses through the pipeline, from bytes loaded from disk/network to the object
* tree passed into a contract.
*
* SignedTransaction wraps a serialized WireTransaction. It contains one or more signatures, each one for
* a public key that is mentioned inside a transaction command. SignedTransaction is the top level transaction type
* and the type most frequently passed around the network and stored. The identity of a transaction is the hash
* of a WireTransaction, therefore if you are storing data keyed by WT hash be aware that multiple different STs may
* map to the same key (and they could be different in important ways, like validity!). The signatures on a
* SignedTransaction might be invalid or missing: the type does not imply validity.
*
* WireTransaction is a transaction in a form ready to be serialised/unserialised. A WireTransaction can be hashed
* in various ways to calculate a *signature hash* (or sighash), this is the hash that is signed by the various involved
* keypairs.
*
* LedgerTransaction is derived from WireTransaction. It is the result of doing the following operations:
*
* - Downloading and locally storing all the dependencies of the transaction.
* - Resolving the input states and loading them into memory.
* - Doing some basic key lookups on WireCommand to see if any keys are from a recognised party, thus converting the
* WireCommand objects into AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->party map.
* In future it'd make more sense to use a certificate scheme and so that logic would get more complex.
* - Deserialising the output states.
*
* All the above refer to inputs using a (txhash, output index) pair.
*
* There is also TransactionForContract, which is a lightly red-acted form of LedgerTransaction that's fed into the
* contract's verify function. It may be removed in future.
*/
/** Transaction ready for serialisation, without any signatures attached. */
data class WireTransaction(val inputs: List<StateRef>,
val attachments: List<SecureHash>,
val outputs: List<TransactionState<ContractState>>,
val commands: List<Command>,
val notary: Party?,
val signers: List<PublicKey>,
val type: TransactionType,
val timestamp: Timestamp?) : NamedByHash {
// Cache the serialised form of the transaction and its hash to give us fast access to it.
@Volatile @Transient private var cachedBits: SerializedBytes<WireTransaction>? = null
val serialized: SerializedBytes<WireTransaction> get() = cachedBits ?: serialize().apply { cachedBits = this }
override val id: SecureHash get() = serialized.hash
companion object {
fun deserialize(bits: SerializedBytes<WireTransaction>, kryo: Kryo = THREAD_LOCAL_KRYO.get()): WireTransaction {
val wtx = bits.bits.deserialize<WireTransaction>(kryo)
wtx.cachedBits = bits
return wtx
}
}
/** Returns a [StateAndRef] for the given output index. */
@Suppress("UNCHECKED_CAST")
fun <T : ContractState> outRef(index: Int): StateAndRef<T> {
require(index >= 0 && index < outputs.size)
return StateAndRef(outputs[index] as TransactionState<T>, StateRef(id, index))
}
/** Returns a [StateAndRef] for the requested output state, or throws [IllegalArgumentException] if not found. */
fun <T : ContractState> outRef(state: ContractState): StateAndRef<T> = outRef(outputs.map { it.data }.indexOfOrThrow(state))
override fun toString(): String {
val buf = StringBuilder()
buf.appendln("Transaction $id:")
for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input")
for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $output")
for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command")
for (attachment in attachments) buf.appendln("${Emoji.paperclip}ATTACHMENT: $attachment")
return buf.toString()
}
}
/** Container for a [WireTransaction] and attached signatures. */
data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
val sigs: List<DigitalSignature.WithKey>) : NamedByHash {
init {
check(sigs.isNotEmpty())
}
// TODO: This needs to be reworked to ensure that the inner WireTransaction is only ever deserialised sandboxed.
/** Lazily calculated access to the deserialised/hashed transaction data. */
val tx: WireTransaction by lazy { WireTransaction.deserialize(txBits) }
/** A transaction ID is the hash of the [WireTransaction]. Thus adding or removing a signature does not change it. */
override val id: SecureHash get() = txBits.hash
/**
* Verify the signatures, deserialise the wire transaction and then check that the set of signatures found contains
* the set of pubkeys in the signers list. If any signatures are missing, either throws an exception (by default) or
* returns the list of keys that have missing signatures, depending on the parameter.
*
* @throws SignatureException if a signature is invalid, does not match or if any signature is missing.
*/
fun verifySignatures(throwIfSignaturesAreMissing: Boolean = true): Set<PublicKey> {
// Embedded WireTransaction is not deserialised until after we check the signatures.
for (sig in sigs)
sig.verifyWithECDSA(txBits.bits)
// Now examine the contents and ensure the sigs we have line up with the advertised list of signers.
val missing = getMissingSignatures()
if (missing.isNotEmpty() && throwIfSignaturesAreMissing) {
val missingElements = getMissingKeyDescriptions(missing)
throw SignatureException("Missing signatures for ${missingElements} on transaction ${id.prefixChars()} for ${missing.toStringsShort()}")
}
return missing
}
/**
* Get a human readable description of where signatures are required from, and are missing, to assist in debugging
* the underlying cause.
*/
private fun getMissingKeyDescriptions(missing: Set<PublicKey>): ArrayList<String> {
// TODO: We need a much better way of structuring this data
val missingElements = ArrayList<String>()
this.tx.commands.forEach { command ->
if (command.signers.any { signer -> missing.contains(signer) })
missingElements.add(command.toString())
}
this.tx.notary?.owningKey.apply {
if (missing.contains(this))
missingElements.add("notary")
}
return missingElements
}
/** Returns the same transaction but with an additional (unchecked) signature */
fun withAdditionalSignature(sig: DigitalSignature.WithKey): SignedTransaction {
// TODO: need to make sure the Notary signs last
return copy(sigs = sigs + sig)
}
fun withAdditionalSignatures(sigList: Iterable<DigitalSignature.WithKey>): SignedTransaction {
return copy(sigs = sigs + sigList)
}
/** Alias for [withAdditionalSignature] to let you use Kotlin operator overloading. */
operator fun plus(sig: DigitalSignature.WithKey) = withAdditionalSignature(sig)
operator fun plus(sigList: Collection<DigitalSignature.WithKey>) = withAdditionalSignatures(sigList)
/**
* Returns the set of missing signatures - a signature must be present for each signer public key.
*/
private fun getMissingSignatures(): Set<PublicKey> {
val notaryKey = tx.notary?.owningKey
val requiredKeys = tx.signers.toSet()
val sigKeys = sigs.map { it.by }.toSet()
if (sigKeys.containsAll(requiredKeys)) return emptySet()
return requiredKeys - sigKeys
}
}
/**
* A LedgerTransaction wraps the data needed to calculate one or more successor states from a set of input states.
* It is the first step after extraction from a WireTransaction. The signatures at this point have been lined up
* with the commands from the wire, and verified/looked up.
*/
data class LedgerTransaction(
/** The input states which will be consumed/invalidated by the execution of this transaction. */
val inputs: List<StateAndRef<*>>,
/** The states that will be generated by the execution of this transaction. */
val outputs: List<TransactionState<*>>,
/** Arbitrary data passed to the program of each input state. */
val commands: List<AuthenticatedObject<CommandData>>,
/** A list of [Attachment] objects identified by the transaction that are needed for this transaction to verify. */
val attachments: List<Attachment>,
/** The hash of the original serialised WireTransaction. */
override val id: SecureHash,
/** The notary for this party, may be null for transactions with no notary. */
val notary: Party?,
/** The notary key and the command keys together: a signed transaction must provide signatures for all of these. */
val signers: List<PublicKey>,
val timestamp: Timestamp?,
val type: TransactionType
) : NamedByHash {
@Suppress("UNCHECKED_CAST")
fun <T : ContractState> outRef(index: Int) = StateAndRef(outputs[index] as TransactionState<T>, StateRef(id, index))
// TODO: Remove this concept.
// There isn't really a good justification for hiding this data from the contract, it's just a backwards compat hack.
/** Strips the transaction down to a form that is usable by the contract verify functions */
fun toTransactionForContract(): TransactionForContract {
return TransactionForContract(inputs.map { it.state.data }, outputs.map { it.data }, attachments, commands, id,
inputs.map { it.state.notary }.singleOrNull(), timestamp)
}
/**
* Verifies this transaction and throws an exception if not valid, depending on the type. For general transactions:
*
* - The contracts are run with the transaction as the input.
* - The list of keys mentioned in commands is compared against the signers list.
*
* @throws TransactionVerificationException if anything goes wrong.
*/
fun verify() = type.verify(this)
}

View File

@ -0,0 +1,38 @@
package com.r3corda.core.contracts.clauses
import com.r3corda.core.contracts.AuthenticatedObject
import com.r3corda.core.contracts.CommandData
import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.TransactionForContract
import java.util.*
/**
* Compose a number of clauses, such that all of the clauses must run for verification to pass.
*/
class AllComposition<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
override val clauses = ArrayList<Clause<S, C, K>>()
init {
clauses.add(firstClause)
clauses.addAll(remainingClauses)
}
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> {
clauses.forEach { clause ->
check(clause.matches(commands)) { "Failed to match clause ${clause}" }
}
return clauses
}
override fun verify(tx: TransactionForContract,
inputs: List<S>,
outputs: List<S>,
commands: List<AuthenticatedObject<C>>,
groupingKey: K?): Set<C> {
return matchedClauses(commands).flatMapTo(HashSet<C>()) { clause ->
clause.verify(tx, inputs, outputs, commands, groupingKey)
}
}
override fun toString() = "All: $clauses.toList()"
}

View File

@ -0,0 +1,24 @@
package com.r3corda.core.contracts.clauses
import com.r3corda.core.contracts.AuthenticatedObject
import com.r3corda.core.contracts.CommandData
import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.TransactionForContract
import java.util.*
/**
* Compose a number of clauses, such that any number of the clauses can run.
*/
class AnyComposition<in S : ContractState, C : CommandData, in K : Any>(vararg rawClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
override val clauses: List<Clause<S, C, K>> = rawClauses.asList()
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> = clauses.filter { it.matches(commands) }
override fun verify(tx: TransactionForContract, inputs: List<S>, outputs: List<S>, commands: List<AuthenticatedObject<C>>, groupingKey: K?): Set<C> {
return matchedClauses(commands).flatMapTo(HashSet<C>()) { clause ->
clause.verify(tx, inputs, outputs, commands, groupingKey)
}
}
override fun toString(): String = "Or: ${clauses.toList()}"
}

View File

@ -2,43 +2,68 @@ package com.r3corda.core.contracts.clauses
import com.r3corda.core.contracts.AuthenticatedObject import com.r3corda.core.contracts.AuthenticatedObject
import com.r3corda.core.contracts.CommandData import com.r3corda.core.contracts.CommandData
import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.TransactionForContract import com.r3corda.core.contracts.TransactionForContract
import com.r3corda.core.utilities.loggerFor
/** /**
* A clause that can be matched as part of execution of a contract. * A clause of a contract, containing a chunk of verification logic. That logic may be delegated to other clauses, or
* provided directly by this clause.
*
* @param S the type of contract state this clause operates on.
* @param C a common supertype of commands this clause operates on.
* @param K the type of the grouping key for states this clause operates on. Use [Unit] if not applicable.
*
* @see CompositeClause
*/ */
// TODO: ifNotMatched/ifMatched should be dropped, and replaced by logic in the calling code that understands abstract class Clause<in S : ContractState, C : CommandData, in K : Any> {
// "or", "and", "single" etc. composition of sets of clauses. companion object {
interface Clause { val log = loggerFor<Clause<*, *, *>>()
/** Classes for commands which must ALL be present in transaction for this clause to be triggered */ }
val requiredCommands: Set<Class<out CommandData>>
/** Behaviour if this clause is matched */
val ifNotMatched: MatchBehaviour
/** Behaviour if this clause is not matches */
val ifMatched: MatchBehaviour
}
enum class MatchBehaviour { /** Determine whether this clause runs or not */
CONTINUE, open val requiredCommands: Set<Class<out CommandData>> = emptySet()
END,
ERROR /**
} * Determine the subclauses which will be verified as a result of verifying this clause.
*/
open fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
= listOf(this)
interface SingleVerify {
/** /**
* Verify the transaction matches the conditions from this clause. For example, a "no zero amount output" clause * Verify the transaction matches the conditions from this clause. For example, a "no zero amount output" clause
* would check each of the output states that it applies to, looking for a zero amount, and throw IllegalStateException * would check each of the output states that it applies to, looking for a zero amount, and throw IllegalStateException
* if any matched. * if any matched.
* *
* @param tx the full transaction being verified. This is provided for cases where clauses need to access
* states or commands outside of their normal scope.
* @param inputs input states which are relevant to this clause. By default this is the set passed into [verifyClause],
* but may be further reduced by clauses such as [GroupClauseVerifier].
* @param outputs output states which are relevant to this clause. By default this is the set passed into [verifyClause],
* but may be further reduced by clauses such as [GroupClauseVerifier].
* @param commands commands which are relevant to this clause. By default this is the set passed into [verifyClause],
* but may be further reduced by clauses such as [GroupClauseVerifier].
* @param groupingKey a grouping key applied to states and commands, where applicable. Taken from
* [TransactionForContract.InOutGroup].
* @return the set of commands that are consumed IF this clause is matched, and cannot be used to match a * @return the set of commands that are consumed IF this clause is matched, and cannot be used to match a
* later clause. This would normally be all commands matching "requiredCommands" for this clause, but some * later clause. This would normally be all commands matching "requiredCommands" for this clause, but some
* verify() functions may do further filtering on possible matches, and return a subset. This may also include * verify() functions may do further filtering on possible matches, and return a subset. This may also include
* commands that were not required (for example the Exit command for fungible assets is optional). * commands that were not required (for example the Exit command for fungible assets is optional).
*/ */
@Throws(IllegalStateException::class) @Throws(IllegalStateException::class)
fun verify(tx: TransactionForContract, abstract fun verify(tx: TransactionForContract,
commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> inputs: List<S>,
outputs: List<S>,
commands: List<AuthenticatedObject<C>>,
groupingKey: K?): Set<C>
} }
interface SingleClause : Clause, SingleVerify /**
* Determine if the given list of commands matches the required commands for a clause to trigger.
*/
fun <C : CommandData> Clause<*, C, *>.matches(commands: List<AuthenticatedObject<C>>): Boolean {
return if (requiredCommands.isEmpty())
true
else
commands.map { it.value.javaClass }.toSet().containsAll(requiredCommands)
}

View File

@ -2,9 +2,7 @@
package com.r3corda.core.contracts.clauses package com.r3corda.core.contracts.clauses
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import java.util.*
// Wrapper object for exposing a JVM friend version of the clause verifier
/** /**
* Verify a transaction against the given list of clauses. * Verify a transaction against the given list of clauses.
* *
@ -13,27 +11,15 @@ import java.util.*
* @param commands commands extracted from the transaction, which are relevant to the * @param commands commands extracted from the transaction, which are relevant to the
* clauses. * clauses.
*/ */
fun verifyClauses(tx: TransactionForContract, fun <C: CommandData> verifyClause(tx: TransactionForContract,
clauses: List<SingleClause>, clause: Clause<ContractState, C, Unit>,
commands: Collection<AuthenticatedObject<CommandData>>) { commands: List<AuthenticatedObject<C>>) {
val unmatchedCommands = ArrayList(commands.map { it.value }) if (Clause.log.isTraceEnabled) {
clause.getExecutionPath(commands).forEach {
verify@ for (clause in clauses) { Clause.log.trace("Tx ${tx.origHash} clause: ${clause}")
val matchBehaviour = if (unmatchedCommands.map { command -> command.javaClass }.containsAll(clause.requiredCommands)) {
unmatchedCommands.removeAll(clause.verify(tx, commands))
clause.ifMatched
} else {
clause.ifNotMatched
}
when (matchBehaviour) {
MatchBehaviour.ERROR -> throw IllegalStateException()
MatchBehaviour.CONTINUE -> {
}
MatchBehaviour.END -> break@verify
} }
} }
val matchedCommands = clause.verify(tx, tx.inputs, tx.outputs, commands, null)
require(unmatchedCommands.isEmpty()) { "All commands must be matched at end of execution." } check(matchedCommands.containsAll(commands.map { it.value })) { "The following commands were not matched at the end of execution: " + (commands - matchedCommands) }
} }

View File

@ -0,0 +1,17 @@
package com.r3corda.core.contracts.clauses
import com.r3corda.core.contracts.AuthenticatedObject
import com.r3corda.core.contracts.CommandData
import com.r3corda.core.contracts.ContractState
/**
* Abstract supertype for clauses which compose other clauses together in some logical manner.
*/
abstract class CompositeClause<in S : ContractState, C: CommandData, in K : Any>: Clause<S, C, K>() {
/** List of clauses under this composite clause */
abstract val clauses: List<Clause<S, C, K>>
override fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
= matchedClauses(commands).flatMap { it.getExecutionPath(commands) }
/** Determine which clauses are matched by the supplied commands */
abstract fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>>
}

View File

@ -0,0 +1,30 @@
package com.r3corda.core.contracts.clauses
import com.r3corda.core.contracts.AuthenticatedObject
import com.r3corda.core.contracts.CommandData
import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.TransactionForContract
import com.r3corda.core.utilities.loggerFor
import java.util.*
/**
* Compose a number of clauses, such that the first match is run, and it errors if none is run.
*/
class FirstComposition<S : ContractState, C : CommandData, K : Any>(val firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
companion object {
val logger = loggerFor<FirstComposition<*, *, *>>()
}
override val clauses = ArrayList<Clause<S, C, K>>()
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> = listOf(clauses.first { it.matches(commands) })
init {
clauses.add(firstClause)
clauses.addAll(remainingClauses)
}
override fun verify(tx: TransactionForContract, inputs: List<S>, outputs: List<S>, commands: List<AuthenticatedObject<C>>, groupingKey: K?): Set<C>
= matchedClauses(commands).single().verify(tx, inputs, outputs, commands, groupingKey)
override fun toString() = "First: ${clauses.toList()}"
}

View File

@ -6,77 +6,24 @@ import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.TransactionForContract import com.r3corda.core.contracts.TransactionForContract
import java.util.* import java.util.*
interface GroupVerify<in S, in T : Any> { abstract class GroupClauseVerifier<S : ContractState, C : CommandData, K : Any>(val clause: Clause<S, C, K>) : Clause<ContractState, C, Unit>() {
/** abstract fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<S, K>>
*
* @return the set of commands that are consumed IF this clause is matched, and cannot be used to match a
* later clause.
*/
fun verify(tx: TransactionForContract,
inputs: List<S>,
outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: T): Set<CommandData>
}
interface GroupClause<in S : ContractState, in T : Any> : Clause, GroupVerify<S, T> override fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
= clause.getExecutionPath(commands)
abstract class GroupClauseVerifier<S : ContractState, T : Any> : SingleClause { override fun verify(tx: TransactionForContract,
abstract val clauses: List<GroupClause<S, T>> inputs: List<ContractState>,
override val requiredCommands: Set<Class<out CommandData>> outputs: List<ContractState>,
get() = emptySet() commands: List<AuthenticatedObject<C>>,
groupingKey: Unit?): Set<C> {
abstract fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<S, T>>
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
val groups = groupStates(tx) val groups = groupStates(tx)
val matchedCommands = HashSet<CommandData>() val matchedCommands = HashSet<C>()
val unmatchedCommands = ArrayList(commands.map { it.value })
for ((inputs, outputs, token) in groups) { for ((groupInputs, groupOutputs, groupToken) in groups) {
val temp = verifyGroup(commands, inputs, outputs, token, tx, unmatchedCommands) matchedCommands.addAll(clause.verify(tx, groupInputs, groupOutputs, commands, groupToken))
matchedCommands.addAll(temp)
unmatchedCommands.removeAll(temp)
} }
return matchedCommands return matchedCommands
} }
/**
* Verify a subset of a transaction's inputs and outputs matches the conditions from this clause. For example, a
* "no zero amount output" clause would check each of the output states within the group, looking for a zero amount,
* and throw IllegalStateException if any matched.
*
* @param commands the full set of commands which apply to this contract.
* @param inputs input states within this group.
* @param outputs output states within this group.
* @param token the object used as a key when grouping states.
* @param unmatchedCommands commands which have not yet been matched within this group.
* @return matchedCommands commands which are matched during the verification process.
*/
@Throws(IllegalStateException::class)
private fun verifyGroup(commands: Collection<AuthenticatedObject<CommandData>>,
inputs: List<S>,
outputs: List<S>,
token: T,
tx: TransactionForContract,
unmatchedCommands: List<CommandData>): Set<CommandData> {
val matchedCommands = HashSet<CommandData>()
verify@ for (clause in clauses) {
val matchBehaviour = if (unmatchedCommands.map { command -> command.javaClass }.containsAll(clause.requiredCommands)) {
matchedCommands.addAll(clause.verify(tx, inputs, outputs, commands, token))
clause.ifMatched
} else {
clause.ifNotMatched
}
when (matchBehaviour) {
MatchBehaviour.ERROR -> throw IllegalStateException()
MatchBehaviour.CONTINUE -> {
}
MatchBehaviour.END -> break@verify
}
}
return matchedCommands
}
} }

View File

@ -1,28 +0,0 @@
package com.r3corda.core.contracts.clauses
import com.r3corda.core.contracts.AuthenticatedObject
import com.r3corda.core.contracts.CommandData
import com.r3corda.core.contracts.TransactionForContract
import java.util.*
/**
* A clause which intercepts calls to a wrapped clause, and passes them through verification
* only from a pre-clause. This is similar to an inceptor in aspect orientated programming.
*/
data class InterceptorClause(
val preclause: SingleVerify,
val clause: SingleClause
) : SingleClause {
override val ifNotMatched: MatchBehaviour
get() = clause.ifNotMatched
override val ifMatched: MatchBehaviour
get() = clause.ifMatched
override val requiredCommands: Set<Class<out CommandData>>
get() = clause.requiredCommands
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
val consumed = HashSet(preclause.verify(tx, commands))
consumed.addAll(clause.verify(tx, commands))
return consumed
}
}

View File

@ -36,7 +36,9 @@ object X509Utilities {
val SIGNATURE_ALGORITHM = "SHA256withECDSA" val SIGNATURE_ALGORITHM = "SHA256withECDSA"
val KEY_GENERATION_ALGORITHM = "ECDSA" val KEY_GENERATION_ALGORITHM = "ECDSA"
val ECDSA_CURVE = "secp256k1" // TLS implementations only support standard SEC2 curves, although internally Corda uses newer EDDSA keys // TLS implementations only support standard SEC2 curves, although internally Corda uses newer EDDSA keys.
// Also browsers like Chrome don't seem to support the secp256k1, only the secp256r1 curve.
val ECDSA_CURVE = "secp256r1"
val KEYSTORE_TYPE = "JKS" val KEYSTORE_TYPE = "JKS"
val CA_CERT_ALIAS = "CA Cert" val CA_CERT_ALIAS = "CA Cert"

View File

@ -1,7 +1,7 @@
package com.r3corda.core.node package com.r3corda.core.node
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.r3corda.core.contracts.SignedTransaction import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.contracts.StateRef import com.r3corda.core.contracts.StateRef
import com.r3corda.core.contracts.TransactionResolutionException import com.r3corda.core.contracts.TransactionResolutionException
import com.r3corda.core.contracts.TransactionState import com.r3corda.core.contracts.TransactionState

View File

@ -4,6 +4,7 @@ import com.google.common.util.concurrent.ListenableFuture
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.messaging.MessagingService import com.r3corda.core.messaging.MessagingService
import com.r3corda.core.messaging.SingleMessageRecipient
import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.NodeInfo
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.security.PublicKey import java.security.PublicKey
@ -20,8 +21,8 @@ interface NetworkMapCache {
val logger = LoggerFactory.getLogger(NetworkMapCache::class.java) val logger = LoggerFactory.getLogger(NetworkMapCache::class.java)
} }
enum class MapChangeType { Added, Removed } enum class MapChangeType { Added, Removed, Modified }
data class MapChange(val node: NodeInfo, val type: MapChangeType ) data class MapChange(val node: NodeInfo, val prevNodeInfo: NodeInfo?, val type: MapChangeType )
/** A list of nodes that advertise a network map service */ /** A list of nodes that advertise a network map service */
val networkMapNodes: List<NodeInfo> val networkMapNodes: List<NodeInfo>
@ -73,12 +74,12 @@ interface NetworkMapCache {
* updates. * updates.
* *
* @param net the network messaging service. * @param net the network messaging service.
* @param service the network map service to fetch current state from. * @param networkMapAddress the network map service to fetch current state from.
* @param subscribe if the cache should subscribe to updates. * @param subscribe if the cache should subscribe to updates.
* @param ifChangedSinceVer an optional version number to limit updating the map based on. If the latest map * @param ifChangedSinceVer an optional version number to limit updating the map based on. If the latest map
* version is less than or equal to the given version, no update is fetched. * version is less than or equal to the given version, no update is fetched.
*/ */
fun addMapService(net: MessagingService, service: NodeInfo, fun addMapService(net: MessagingService, networkMapAddress: SingleMessageRecipient,
subscribe: Boolean, ifChangedSinceVer: Int? = null): ListenableFuture<Unit> subscribe: Boolean, ifChangedSinceVer: Int? = null): ListenableFuture<Unit>
/** /**

View File

@ -5,6 +5,7 @@ import com.google.common.util.concurrent.SettableFuture
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.transactions.WireTransaction
import java.security.KeyPair import java.security.KeyPair
import java.security.PrivateKey import java.security.PrivateKey
import java.security.PublicKey import java.security.PublicKey
@ -85,13 +86,13 @@ interface WalletService {
/** /**
* Returns a snapshot of the heads of LinearStates. * Returns a snapshot of the heads of LinearStates.
*/ */
val linearHeads: Map<SecureHash, StateAndRef<LinearState>> val linearHeads: Map<UniqueIdentifier, StateAndRef<LinearState>>
// TODO: When KT-10399 is fixed, rename this and remove the inline version below. // TODO: When KT-10399 is fixed, rename this and remove the inline version below.
/** Returns the [linearHeads] only when the type of the state would be considered an 'instanceof' the given type. */ /** Returns the [linearHeads] only when the type of the state would be considered an 'instanceof' the given type. */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : LinearState> linearHeadsOfType_(stateType: Class<T>): Map<SecureHash, StateAndRef<T>> { fun <T : LinearState> linearHeadsOfType_(stateType: Class<T>): Map<UniqueIdentifier, StateAndRef<T>> {
return linearHeads.filterValues { stateType.isInstance(it.state.data) }.mapValues { StateAndRef(it.value.state as TransactionState<T>, it.value.ref) } return linearHeads.filterValues { stateType.isInstance(it.state.data) }.mapValues { StateAndRef(it.value.state as TransactionState<T>, it.value.ref) }
} }

View File

@ -1,6 +1,6 @@
package com.r3corda.core.node.services package com.r3corda.core.node.services
import com.r3corda.core.contracts.SignedTransaction import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
/** /**

View File

@ -1,26 +1,35 @@
package com.r3corda.core.protocols package com.r3corda.core.protocols
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.google.common.util.concurrent.ListenableFuture
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.node.ServiceHub import com.r3corda.core.node.ServiceHub
import com.r3corda.core.utilities.UntrustworthyData import com.r3corda.core.utilities.UntrustworthyData
import org.slf4j.Logger import org.slf4j.Logger
/** /**
* The interface of [ProtocolStateMachineImpl] exposing methods and properties required by ProtocolLogic for compilation. * The interface of [ProtocolStateMachineImpl] exposing methods and properties required by ProtocolLogic for compilation.
*/ */
interface ProtocolStateMachine<R> { interface ProtocolStateMachine<R> {
@Suspendable @Suspendable
fun <T : Any> sendAndReceive(topic: String, destination: Party, sessionIDForSend: Long, sessionIDForReceive: Long, fun <T : Any> sendAndReceive(topic: String,
payload: Any, recvType: Class<T>): UntrustworthyData<T> destination: Party,
sessionIDForSend: Long,
sessionIDForReceive: Long,
payload: Any,
receiveType: Class<T>): UntrustworthyData<T>
@Suspendable @Suspendable
fun <T : Any> receive(topic: String, sessionIDForReceive: Long, recvType: Class<T>): UntrustworthyData<T> fun <T : Any> receive(topic: String, sessionIDForReceive: Long, receiveType: Class<T>): UntrustworthyData<T>
@Suspendable @Suspendable
fun send(topic: String, destination: Party, sessionID: Long, payload: Any) fun send(topic: String, destination: Party, sessionID: Long, payload: Any)
val serviceHub: ServiceHub val serviceHub: ServiceHub
val logger: Logger val logger: Logger
/** Unique ID for this machine, valid only while it is in memory. */
val machineId: Long
/** This future will complete when the call method returns. */
val resultFuture: ListenableFuture<R>
} }

View File

@ -13,6 +13,8 @@ import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.* import com.r3corda.core.crypto.*
import com.r3corda.core.node.AttachmentsClassLoader import com.r3corda.core.node.AttachmentsClassLoader
import com.r3corda.core.node.services.AttachmentStorage import com.r3corda.core.node.services.AttachmentStorage
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.WireTransaction
import com.r3corda.core.utilities.NonEmptySet import com.r3corda.core.utilities.NonEmptySet
import com.r3corda.core.utilities.NonEmptySetSerializer import com.r3corda.core.utilities.NonEmptySetSerializer
import de.javakaffee.kryoserializers.ArraysAsListSerializer import de.javakaffee.kryoserializers.ArraysAsListSerializer
@ -232,7 +234,7 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
kryo.writeClassAndObject(output, obj.outputs) kryo.writeClassAndObject(output, obj.outputs)
kryo.writeClassAndObject(output, obj.commands) kryo.writeClassAndObject(output, obj.commands)
kryo.writeClassAndObject(output, obj.notary) kryo.writeClassAndObject(output, obj.notary)
kryo.writeClassAndObject(output, obj.signers) kryo.writeClassAndObject(output, obj.mustSign)
kryo.writeClassAndObject(output, obj.type) kryo.writeClassAndObject(output, obj.type)
kryo.writeClassAndObject(output, obj.timestamp) kryo.writeClassAndObject(output, obj.timestamp)
} }

View File

@ -1,17 +0,0 @@
package com.r3corda.core.testing
import com.r3corda.core.contracts.Contract
import com.r3corda.core.contracts.LinearState
import com.r3corda.core.crypto.SecureHash
import java.security.PublicKey
class DummyLinearState(
override val thread: SecureHash = SecureHash.randomSHA256(),
override val contract: Contract = AlwaysSucceedContract(),
override val participants: List<PublicKey> = listOf(),
val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState {
override fun isRelevant(ourKeys: Set<PublicKey>): Boolean {
return participants.any { ourKeys.contains(it) }
}
}

View File

@ -0,0 +1,113 @@
package com.r3corda.core.testing
import com.pholser.junit.quickcheck.generator.GenerationStatus
import com.pholser.junit.quickcheck.generator.Generator
import com.pholser.junit.quickcheck.generator.java.lang.StringGenerator
import com.pholser.junit.quickcheck.generator.java.util.ArrayListGenerator
import com.pholser.junit.quickcheck.random.SourceOfRandomness
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.*
import com.r3corda.core.serialization.OpaqueBytes
import java.security.PrivateKey
import java.security.PublicKey
import java.time.Duration
import java.time.Instant
import java.util.*
/**
* Generators for quickcheck
*
* TODO Split this into several files
*/
fun <A> Generator<A>.generateList(random: SourceOfRandomness, status: GenerationStatus): List<A> {
val arrayGenerator = ArrayListGenerator()
arrayGenerator.addComponentGenerators(listOf(this))
@Suppress("UNCHECKED_CAST")
return arrayGenerator.generate(random, status) as List<A>
}
class PrivateKeyGenerator: Generator<PrivateKey>(PrivateKey::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): PrivateKey {
return entropyToKeyPair(random.nextBigInteger(32)).private
}
}
class PublicKeyGenerator: Generator<PublicKey>(PublicKey::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): PublicKey {
return entropyToKeyPair(random.nextBigInteger(32)).public
}
}
class PartyGenerator: Generator<Party>(Party::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Party {
return Party(StringGenerator().generate(random, status), PublicKeyGenerator().generate(random, status))
}
}
class PartyAndReferenceGenerator: Generator<PartyAndReference>(PartyAndReference::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): PartyAndReference {
return PartyAndReference(PartyGenerator().generate(random, status), OpaqueBytes(random.nextBytes(16)))
}
}
class SecureHashGenerator: Generator<SecureHash>(SecureHash::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): SecureHash {
return SecureHash.Companion.sha256(random.nextBytes(16))
}
}
class StateRefGenerator: Generator<StateRef>(StateRef::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): StateRef {
return StateRef(SecureHash.Companion.sha256(random.nextBytes(16)), random.nextInt(0, 10))
}
}
@Suppress("CAST_NEVER_SUCCEEDS")
class TransactionStateGenerator<T : ContractState>(val stateGenerator: Generator<T>) : Generator<TransactionState<T>>(TransactionState::class.java as Class<TransactionState<T>>) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): TransactionState<T> {
return TransactionState(stateGenerator.generate(random, status), PartyGenerator().generate(random, status))
}
}
@Suppress("CAST_NEVER_SUCCEEDS")
class IssuedGenerator<T>(val productGenerator: Generator<T>) : Generator<Issued<T>>(Issued::class.java as Class<Issued<T>>) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Issued<T> {
return Issued(PartyAndReferenceGenerator().generate(random, status), productGenerator.generate(random, status))
}
}
@Suppress("CAST_NEVER_SUCCEEDS")
class AmountGenerator<T>(val tokenGenerator: Generator<T>) : Generator<Amount<T>>(Amount::class.java as Class<Amount<T>>) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Amount<T> {
return Amount(random.nextLong(0, 1000000), tokenGenerator.generate(random, status))
}
}
class CurrencyGenerator() : Generator<Currency>(Currency::class.java) {
companion object {
val currencies = Currency.getAvailableCurrencies().toList()
}
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Currency {
return currencies[random.nextInt(0, currencies.size - 1)]
}
}
class InstantGenerator : Generator<Instant>(Instant::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Instant {
return Instant.ofEpochMilli(random.nextLong(0, 1000000))
}
}
class DurationGenerator : Generator<Duration>(Duration::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Duration {
return Duration.ofMillis(random.nextLong(0, 1000000))
}
}
class TimestampGenerator : Generator<Timestamp>(Timestamp::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Timestamp {
return Timestamp(InstantGenerator().generate(random, status), DurationGenerator().generate(random, status))
}
}

View File

@ -7,6 +7,7 @@ import com.r3corda.core.node.ServiceHub
import com.r3corda.core.node.services.Wallet import com.r3corda.core.node.services.Wallet
import com.r3corda.core.node.services.WalletService import com.r3corda.core.node.services.WalletService
import com.r3corda.core.serialization.SingletonSerializeAsToken import com.r3corda.core.serialization.SingletonSerializeAsToken
import com.r3corda.core.transactions.WireTransaction
import com.r3corda.core.utilities.loggerFor import com.r3corda.core.utilities.loggerFor
import com.r3corda.core.utilities.trace import com.r3corda.core.utilities.trace
import rx.Observable import rx.Observable
@ -22,9 +23,6 @@ import javax.annotation.concurrent.ThreadSafe
*/ */
@ThreadSafe @ThreadSafe
open class InMemoryWalletService(protected val services: ServiceHub) : SingletonSerializeAsToken(), WalletService { open class InMemoryWalletService(protected val services: ServiceHub) : SingletonSerializeAsToken(), WalletService {
class ClashingThreads(threads: Set<SecureHash>, transactions: Iterable<WireTransaction>) :
Exception("There are multiple linear head states after processing transactions $transactions. The clashing thread(s): $threads")
open protected val log = loggerFor<InMemoryWalletService>() open protected val log = loggerFor<InMemoryWalletService>()
// Variables inside InnerState are protected with a lock by the ThreadBox and aren't in scope unless you're // Variables inside InnerState are protected with a lock by the ThreadBox and aren't in scope unless you're
@ -46,9 +44,9 @@ open class InMemoryWalletService(protected val services: ServiceHub) : Singleton
/** /**
* Returns a snapshot of the heads of LinearStates. * Returns a snapshot of the heads of LinearStates.
*/ */
override val linearHeads: Map<SecureHash, StateAndRef<LinearState>> override val linearHeads: Map<UniqueIdentifier, StateAndRef<LinearState>>
get() = currentWallet.let { wallet -> get() = currentWallet.let { wallet ->
wallet.states.filterStatesOfType<LinearState>().associateBy { it.state.data.thread }.mapValues { it.value } wallet.states.filterStatesOfType<LinearState>().associateBy { it.state.data.linearId }.mapValues { it.value }
} }
override fun notifyAll(txns: Iterable<WireTransaction>): Wallet { override fun notifyAll(txns: Iterable<WireTransaction>): Wallet {
@ -78,14 +76,6 @@ open class InMemoryWalletService(protected val services: ServiceHub) : Singleton
Pair(wallet, combinedDelta) Pair(wallet, combinedDelta)
} }
// TODO: we need to remove the clashing threads concepts and support potential duplicate threads
// because two different nodes can have two different sets of threads and so currently it's possible
// for only one party to have a clash which interferes with determinism of the transactions.
val clashingThreads = walletAndNetDelta.first.clashingThreads
if (!clashingThreads.isEmpty()) {
throw ClashingThreads(clashingThreads, txns)
}
wallet = walletAndNetDelta.first wallet = walletAndNetDelta.first
netDelta = walletAndNetDelta.second netDelta = walletAndNetDelta.second
return@locked wallet return@locked wallet
@ -133,23 +123,4 @@ open class InMemoryWalletService(protected val services: ServiceHub) : Singleton
return Pair(Wallet(newStates), change) return Pair(Wallet(newStates), change)
} }
companion object {
// Returns the set of LinearState threads that clash in the wallet
val Wallet.clashingThreads: Set<SecureHash> get() {
val clashingThreads = HashSet<SecureHash>()
val threadsSeen = HashSet<SecureHash>()
for (linearState in states.filterStatesOfType<LinearState>()) {
val thread = linearState.state.data.thread
if (threadsSeen.contains(thread)) {
clashingThreads.add(thread)
} else {
threadsSeen.add(thread)
}
}
return clashingThreads
}
}
} }

View File

@ -0,0 +1,53 @@
package com.r3corda.core.transactions
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import java.security.PublicKey
import java.util.*
/**
* An abstract class defining fields shared by all transaction types in the system.
*/
abstract class BaseTransaction(
/** The inputs of this transaction. Note that in BaseTransaction subclasses the type of this list may change! */
open val inputs: List<*>,
/** Ordered list of states defined by this transaction, along with the associated notaries. */
val outputs: List<TransactionState<ContractState>>,
/**
* If present, the notary for this transaction. If absent then the transaction is not notarised at all.
* This is intended for issuance/genesis transactions that don't consume any other states and thus can't
* double spend anything.
*/
val notary: Party?,
/**
* Keys that are required to have signed the wrapping [SignedTransaction], ordered to match the list of
* signatures. There is nothing that forces the list to be the _correct_ list of signers for this
* transaction until the transaction is verified by using [LedgerTransaction.verify]. It includes the
* notary key, if the notary field is set.
*/
val mustSign: List<PublicKey>,
/**
* Pointer to a class that defines the behaviour of this transaction: either normal, or "notary changing".
*/
val type: TransactionType,
/**
* If specified, a time window in which this transaction may have been notarised. Contracts can check this
* time window to find out when a transaction is deemed to have occurred, from the ledger's perspective.
*/
val timestamp: Timestamp?
) : NamedByHash {
protected fun checkInvariants() {
if (notary == null) check(inputs.isEmpty()) { "The notary must be specified explicitly for any transaction that has inputs." }
if (timestamp != null) check(notary != null) { "If a timestamp is provided, there must be a notary." }
}
override fun equals(other: Any?) =
other is BaseTransaction &&
notary == other.notary &&
mustSign == other.mustSign &&
type == other.type &&
timestamp == other.timestamp
override fun hashCode() = Objects.hash(notary, mustSign, type, timestamp)
}

View File

@ -0,0 +1,84 @@
package com.r3corda.core.transactions
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import java.security.PublicKey
/**
* A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations:
*
* - Downloading and locally storing all the dependencies of the transaction.
* - Resolving the input states and loading them into memory.
* - Doing some basic key lookups on the [Command]s to see if any keys are from a recognised party, thus converting the
* [Command] objects into [AuthenticatedObject].
* - Deserialising the output states.
*
* All the above refer to inputs using a (txhash, output index) pair.
*/
class LedgerTransaction(
/** The resolved input states which will be consumed/invalidated by the execution of this transaction. */
override val inputs: List<StateAndRef<*>>,
outputs: List<TransactionState<ContractState>>,
/** Arbitrary data passed to the program of each input state. */
val commands: List<AuthenticatedObject<CommandData>>,
/** A list of [Attachment] objects identified by the transaction that are needed for this transaction to verify. */
val attachments: List<Attachment>,
/** The hash of the original serialised WireTransaction. */
override val id: SecureHash,
notary: Party?,
signers: List<PublicKey>,
timestamp: Timestamp?,
type: TransactionType
) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp) {
init { checkInvariants() }
@Suppress("UNCHECKED_CAST")
fun <T : ContractState> outRef(index: Int) = StateAndRef(outputs[index] as TransactionState<T>, StateRef(id, index))
// TODO: Remove this concept.
// There isn't really a good justification for hiding this data from the contract, it's just a backwards compat hack.
/** Strips the transaction down to a form that is usable by the contract verify functions */
fun toTransactionForContract(): TransactionForContract {
return TransactionForContract(inputs.map { it.state.data }, outputs.map { it.data }, attachments, commands, id,
inputs.map { it.state.notary }.singleOrNull(), timestamp)
}
/**
* Verifies this transaction and throws an exception if not valid, depending on the type. For general transactions:
*
* - The contracts are run with the transaction as the input.
* - The list of keys mentioned in commands is compared against the signers list.
*
* @throws TransactionVerificationException if anything goes wrong.
*/
fun verify() = type.verify(this)
// TODO: When we upgrade to Kotlin 1.1 we can make this a data class again and have the compiler generate these.
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
if (!super.equals(other)) return false
other as LedgerTransaction
if (inputs != other.inputs) return false
if (outputs != other.outputs) return false
if (commands != other.commands) return false
if (attachments != other.attachments) return false
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + inputs.hashCode()
result = 31 * result + outputs.hashCode()
result = 31 * result + commands.hashCode()
result = 31 * result + attachments.hashCode()
result = 31 * result + id.hashCode()
return result
}
}

View File

@ -0,0 +1,130 @@
package com.r3corda.core.transactions
import com.r3corda.core.contracts.NamedByHash
import com.r3corda.core.contracts.TransactionResolutionException
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringsShort
import com.r3corda.core.node.ServiceHub
import com.r3corda.core.serialization.SerializedBytes
import java.io.FileNotFoundException
import java.security.PublicKey
import java.security.SignatureException
import java.util.*
/**
* SignedTransaction wraps a serialized WireTransaction. It contains one or more signatures, each one for
* a public key that is mentioned inside a transaction command. SignedTransaction is the top level transaction type
* and the type most frequently passed around the network and stored. The identity of a transaction is the hash
* of a WireTransaction, therefore if you are storing data keyed by WT hash be aware that multiple different STs may
* map to the same key (and they could be different in important ways, like validity!). The signatures on a
* SignedTransaction might be invalid or missing: the type does not imply validity.
*/
data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
val sigs: List<DigitalSignature.WithKey>) : NamedByHash {
init {
require(sigs.isNotEmpty())
}
// TODO: This needs to be reworked to ensure that the inner WireTransaction is only ever deserialised sandboxed.
/** Lazily calculated access to the deserialised/hashed transaction data. */
val tx: WireTransaction by lazy { WireTransaction.deserialize(txBits) }
/** A transaction ID is the hash of the [WireTransaction]. Thus adding or removing a signature does not change it. */
override val id: SecureHash get() = txBits.hash
class SignaturesMissingException(val missing: Set<PublicKey>, val descriptions: List<String>, override val id: SecureHash) : NamedByHash, SignatureException() {
override fun toString(): String {
return "Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.toStringsShort()}"
}
}
/**
* Verifies the signatures on this transaction and throws if any are missing which aren't passed as parameters.
* In this context, "verifying" means checking they are valid signatures and that their public keys are in
* the contained transactions [BaseTransaction.mustSign] property.
*
* Normally you would not provide any keys to this function, but if you're in the process of building a partial
* transaction and you want to access the contents before you've signed it, you can specify your own keys here
* to bypass that check.
*
* @throws SignatureException if any signatures are invalid or unrecognised.
* @throws SignaturesMissingException if any signatures should have been present but were not.
*/
@Throws(SignatureException::class)
fun verifySignatures(vararg allowedToBeMissing: PublicKey): WireTransaction {
// Embedded WireTransaction is not deserialised until after we check the signatures.
checkSignaturesAreValid()
val missing = getMissingSignatures()
if (missing.isNotEmpty()) {
val allowed = setOf(*allowedToBeMissing)
val needed = missing - allowed
if (needed.isNotEmpty())
throw SignaturesMissingException(needed, getMissingKeyDescriptions(needed), id)
}
return tx
}
/**
* Mathematically validates the signatures that are present on this transaction. This does not imply that
* the signatures are by the right keys, or that there are sufficient signatures, just that they aren't
* corrupt. If you use this function directly you'll need to do the other checks yourself. Probably you
* want [verifySignatures] instead.
*
* @throws SignatureException if a signature fails to verify.
*/
@Throws(SignatureException::class)
fun checkSignaturesAreValid() {
for (sig in sigs)
sig.verifyWithECDSA(txBits.bits)
}
private fun getMissingSignatures(): Set<PublicKey> {
val requiredKeys = tx.mustSign.toSet()
val sigKeys = sigs.map { it.by }.toSet()
if (sigKeys.containsAll(requiredKeys)) return emptySet()
return requiredKeys - sigKeys
}
/**
* Get a human readable description of where signatures are required from, and are missing, to assist in debugging
* the underlying cause.
*/
private fun getMissingKeyDescriptions(missing: Set<PublicKey>): ArrayList<String> {
// TODO: We need a much better way of structuring this data
val missingElements = ArrayList<String>()
this.tx.commands.forEach { command ->
if (command.signers.any { it in missing })
missingElements.add(command.toString())
}
if (this.tx.notary?.owningKey in missing)
missingElements.add("notary")
return missingElements
}
/** Returns the same transaction but with an additional (unchecked) signature. */
fun withAdditionalSignature(sig: DigitalSignature.WithKey) = copy(sigs = sigs + sig)
/** Returns the same transaction but with an additional (unchecked) signatures. */
fun withAdditionalSignatures(sigList: Iterable<DigitalSignature.WithKey>) = copy(sigs = sigs + sigList)
/** Alias for [withAdditionalSignature] to let you use Kotlin operator overloading. */
operator fun plus(sig: DigitalSignature.WithKey) = withAdditionalSignature(sig)
/** Alias for [withAdditionalSignatures] to let you use Kotlin operator overloading. */
operator fun plus(sigList: Collection<DigitalSignature.WithKey>) = withAdditionalSignatures(sigList)
/**
* Calls [verifySignatures] to check all required signatures are present, and then calls
* [WireTransaction.toLedgerTransaction] with the passed in [ServiceHub] to resolve the dependencies,
* returning an unverified LedgerTransaction.
*
* @throws FileNotFoundException if a required attachment was not found in storage.
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
* @throws SignatureException if any signatures were invalid or unrecognised
* @throws SignaturesMissingException if any signatures that should have been present are missing.
*/
@Throws(FileNotFoundException::class, TransactionResolutionException::class, SignaturesMissingException::class)
fun toLedgerTransaction(services: ServiceHub) = verifySignatures().toLedgerTransaction(services)
}

View File

@ -1,7 +1,10 @@
package com.r3corda.core.contracts package com.r3corda.core.transactions
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.* import com.r3corda.core.crypto.*
import com.r3corda.core.serialization.serialize import com.r3corda.core.serialization.serialize
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.WireTransaction
import java.security.KeyPair import java.security.KeyPair
import java.security.PublicKey import java.security.PublicKey
import java.time.Duration import java.time.Duration
@ -19,6 +22,10 @@ import java.util.*
* @param notary Notary used for the transaction. If null, this indicates the transaction DOES NOT have a notary. * @param notary Notary used for the transaction. If null, this indicates the transaction DOES NOT have a notary.
* When this is set to a non-null value, an output state can be added by just passing in a [ContractState] a * When this is set to a non-null value, an output state can be added by just passing in a [ContractState] a
* [TransactionState] with this notary specified will be generated automatically. * [TransactionState] with this notary specified will be generated automatically.
*
* @param signers The set of public keys the transaction needs signatures for. The logic for building the signers set
* can be customised for every [TransactionType]. E.g. in the general case it contains the command and notary public keys,
* but for the [TransactionType.NotaryChange] transactions it is the set of all input [ContractState.participants].
*/ */
open class TransactionBuilder( open class TransactionBuilder(
protected val type: TransactionType = TransactionType.General(), protected val type: TransactionType = TransactionType.General(),
@ -30,7 +37,6 @@ open class TransactionBuilder(
protected val signers: MutableSet<PublicKey> = mutableSetOf(), protected val signers: MutableSet<PublicKey> = mutableSetOf(),
protected var timestamp: Timestamp? = null) { protected var timestamp: Timestamp? = null) {
@Deprecated("use timestamp instead")
val time: Timestamp? get() = timestamp val time: Timestamp? get() = timestamp
init { init {
@ -91,10 +97,11 @@ open class TransactionBuilder(
/** The signatures that have been collected so far - might be incomplete! */ /** The signatures that have been collected so far - might be incomplete! */
protected val currentSigs = arrayListOf<DigitalSignature.WithKey>() protected val currentSigs = arrayListOf<DigitalSignature.WithKey>()
fun signWith(key: KeyPair) { fun signWith(key: KeyPair): TransactionBuilder {
check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" } check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" }
val data = toWireTransaction().serialize() val data = toWireTransaction().serialize()
addSignatureUnchecked(key.signWithECDSA(data.bits)) addSignatureUnchecked(key.signWithECDSA(data.bits))
return this
} }
/** /**
@ -139,13 +146,12 @@ open class TransactionBuilder(
return SignedTransaction(toWireTransaction().serialize(), ArrayList(currentSigs)) return SignedTransaction(toWireTransaction().serialize(), ArrayList(currentSigs))
} }
open fun addInputState(stateAndRef: StateAndRef<*>) = addInputState(stateAndRef.ref, stateAndRef.state.notary) open fun addInputState(stateAndRef: StateAndRef<*>) {
fun addInputState(stateRef: StateRef, notary: Party) {
check(currentSigs.isEmpty()) check(currentSigs.isEmpty())
val notary = stateAndRef.state.notary
require(notary == this.notary) { "Input state requires notary \"${notary}\" which does not match the transaction notary \"${this.notary}\"." } require(notary == this.notary) { "Input state requires notary \"${notary}\" which does not match the transaction notary \"${this.notary}\"." }
signers.add(notary.owningKey) signers.add(notary.owningKey)
inputs.add(stateRef) inputs.add(stateAndRef.ref)
} }
fun addAttachment(attachmentId: SecureHash) { fun addAttachment(attachmentId: SecureHash) {

View File

@ -0,0 +1,118 @@
package com.r3corda.core.transactions
import com.esotericsoftware.kryo.Kryo
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.indexOfOrThrow
import com.r3corda.core.node.ServiceHub
import com.r3corda.core.serialization.SerializedBytes
import com.r3corda.core.serialization.THREAD_LOCAL_KRYO
import com.r3corda.core.serialization.deserialize
import com.r3corda.core.serialization.serialize
import com.r3corda.core.utilities.Emoji
import java.io.FileNotFoundException
import java.security.PublicKey
/**
* A transaction ready for serialisation, without any signatures attached. A WireTransaction is usually wrapped
* by a [SignedTransaction] that carries the signatures over this payload. The hash of the wire transaction is
* the identity of the transaction, that is, it's possible for two [SignedTransaction]s with different sets of
* signatures to have the same identity hash.
*/
class WireTransaction(
/** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
override val inputs: List<StateRef>,
/** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */
val attachments: List<SecureHash>,
outputs: List<TransactionState<ContractState>>,
/** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */
val commands: List<Command>,
notary: Party?,
signers: List<PublicKey>,
type: TransactionType,
timestamp: Timestamp?
) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp) {
init { checkInvariants() }
// Cache the serialised form of the transaction and its hash to give us fast access to it.
@Volatile @Transient private var cachedBits: SerializedBytes<WireTransaction>? = null
val serialized: SerializedBytes<WireTransaction> get() = cachedBits ?: serialize().apply { cachedBits = this }
override val id: SecureHash get() = serialized.hash
companion object {
fun deserialize(bits: SerializedBytes<WireTransaction>, kryo: Kryo = THREAD_LOCAL_KRYO.get()): WireTransaction {
val wtx = bits.bits.deserialize<WireTransaction>(kryo)
wtx.cachedBits = bits
return wtx
}
}
/** Returns a [StateAndRef] for the given output index. */
@Suppress("UNCHECKED_CAST")
fun <T : ContractState> outRef(index: Int): StateAndRef<T> {
require(index >= 0 && index < outputs.size)
return StateAndRef(outputs[index] as TransactionState<T>, StateRef(id, index))
}
/** Returns a [StateAndRef] for the requested output state, or throws [IllegalArgumentException] if not found. */
fun <T : ContractState> outRef(state: ContractState): StateAndRef<T> = outRef(outputs.map { it.data }.indexOfOrThrow(state))
/**
* Looks up identities and attachments from storage to generate a [LedgerTransaction]. A transaction is expected to
* have been fully resolved using the resolution protocol by this point.
*
* @throws FileNotFoundException if a required attachment was not found in storage.
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
*/
@Throws(FileNotFoundException::class, TransactionResolutionException::class)
fun toLedgerTransaction(services: ServiceHub): LedgerTransaction {
// Look up public keys to authenticated identities. This is just a stub placeholder and will all change in future.
val authenticatedArgs = commands.map {
val parties = it.signers.mapNotNull { pk -> services.identityService.partyFromKey(pk) }
AuthenticatedObject(it.signers, parties, it.value)
}
// Open attachments specified in this transaction. If we haven't downloaded them, we fail.
val attachments = attachments.map {
services.storageService.attachments.openAttachment(it) ?: throw FileNotFoundException(it.toString())
}
val resolvedInputs = inputs.map { StateAndRef(services.loadState(it), it) }
return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, mustSign, timestamp, type)
}
override fun toString(): String {
val buf = StringBuilder()
buf.appendln("Transaction $id:")
for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input")
for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $output")
for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command")
for (attachment in attachments) buf.appendln("${Emoji.paperclip}ATTACHMENT: $attachment")
return buf.toString()
}
// TODO: When Kotlin 1.1 comes out we can make this class a data class again, and have these be autogenerated.
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
if (!super.equals(other)) return false
other as WireTransaction
if (inputs != other.inputs) return false
if (attachments != other.attachments) return false
if (outputs != other.outputs) return false
if (commands != other.commands) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + inputs.hashCode()
result = 31 * result + attachments.hashCode()
result = 31 * result + outputs.hashCode()
result = 31 * result + commands.hashCode()
return result
}
}

View File

@ -13,9 +13,8 @@ import kotlin.reflect.KClass
// logging at that level is enabled. // logging at that level is enabled.
inline fun <reified T : Any> loggerFor(): org.slf4j.Logger = LoggerFactory.getLogger(T::class.java) inline fun <reified T : Any> loggerFor(): org.slf4j.Logger = LoggerFactory.getLogger(T::class.java)
inline fun org.slf4j.Logger.trace(msg: () -> String) { inline fun org.slf4j.Logger.trace(msg: () -> String) { if (isTraceEnabled) trace(msg()) }
if (isTraceEnabled) trace(msg()) inline fun org.slf4j.Logger.debug(msg: () -> String) { if (isDebugEnabled) debug(msg()) }
}
/** A configuration helper that allows modifying the log level for specific loggers */ /** A configuration helper that allows modifying the log level for specific loggers */
object LogHelper { object LogHelper {

View File

@ -0,0 +1,20 @@
@file:JvmName("TestConstants")
package com.r3corda.core.utilities
import com.r3corda.core.crypto.*
import java.math.BigInteger
import java.security.KeyPair
import java.security.PublicKey
import java.time.Instant
// A dummy time at which we will be pretending test transactions are created.
val TEST_TX_TIME: Instant get() = Instant.parse("2015-04-17T12:00:00.00Z")
val DUMMY_PUBKEY_1: PublicKey get() = DummyPublicKey("x1")
val DUMMY_PUBKEY_2: PublicKey get() = DummyPublicKey("x2")
val DUMMY_KEY_1: KeyPair by lazy { generateKeyPair() }
val DUMMY_KEY_2: KeyPair by lazy { generateKeyPair() }
val DUMMY_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) }
val DUMMY_NOTARY: Party get() = Party("Notary Service", DUMMY_NOTARY_KEY.public)

View File

@ -17,5 +17,9 @@ class UntrustworthyData<out T>(private val fromUntrustedWorld: T) {
get() = fromUntrustedWorld get() = fromUntrustedWorld
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
inline fun <R> unwrap(validator: (T) -> R) = validator(data)
@Suppress("DEPRECATION")
@Deprecated("This old name was confusing, use unwrap instead", replaceWith = ReplaceWith("unwrap"))
inline fun <R> validate(validator: (T) -> R) = validator(data) inline fun <R> validate(validator: (T) -> R) = validator(data)
} }

View File

@ -1,7 +1,9 @@
package com.r3corda.protocols package com.r3corda.protocols
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.contracts.StateRef
import com.r3corda.core.crypto.DigitalSignature import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.signWithECDSA import com.r3corda.core.crypto.signWithECDSA
@ -9,7 +11,10 @@ import com.r3corda.core.messaging.Ack
import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.NodeInfo
import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.random63BitValue import com.r3corda.core.random63BitValue
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.WireTransaction
import com.r3corda.core.utilities.ProgressTracker import com.r3corda.core.utilities.ProgressTracker
import com.r3corda.core.utilities.UntrustworthyData
import com.r3corda.protocols.AbstractStateReplacementProtocol.Acceptor import com.r3corda.protocols.AbstractStateReplacementProtocol.Acceptor
import com.r3corda.protocols.AbstractStateReplacementProtocol.Instigator import com.r3corda.protocols.AbstractStateReplacementProtocol.Instigator
import java.security.PublicKey import java.security.PublicKey
@ -98,7 +103,7 @@ abstract class AbstractStateReplacementProtocol<T> {
sendAndReceive<Ack>(node.identity, 0, sessionIdForReceive, handshake) sendAndReceive<Ack>(node.identity, 0, sessionIdForReceive, handshake)
val response = sendAndReceive<Result>(node.identity, sessionIdForSend, sessionIdForReceive, proposal) val response = sendAndReceive<Result>(node.identity, sessionIdForSend, sessionIdForReceive, proposal)
val participantSignature = response.validate { val participantSignature = response.unwrap {
if (it.sig == null) throw StateReplacementException(it.error!!) if (it.sig == null) throw StateReplacementException(it.error!!)
else { else {
check(it.sig.by == node.identity.owningKey) { "Not signed by the required participant" } check(it.sig.by == node.identity.owningKey) { "Not signed by the required participant" }
@ -117,7 +122,7 @@ abstract class AbstractStateReplacementProtocol<T> {
} }
} }
abstract class Acceptor<in T>(val otherSide: Party, abstract class Acceptor<T>(val otherSide: Party,
val sessionIdForSend: Long, val sessionIdForSend: Long,
val sessionIdForReceive: Long, val sessionIdForReceive: Long,
override val progressTracker: ProgressTracker = tracker()) : ProtocolLogic<Unit>() { override val progressTracker: ProgressTracker = tracker()) : ProtocolLogic<Unit>() {
@ -135,24 +140,21 @@ abstract class AbstractStateReplacementProtocol<T> {
@Suspendable @Suspendable
override fun call() { override fun call() {
progressTracker.currentStep = VERIFYING progressTracker.currentStep = VERIFYING
val proposal = receive<Proposal<T>>(sessionIdForReceive).validate { it } val maybeProposal: UntrustworthyData<Proposal<T>> = receive(sessionIdForReceive)
try { try {
verifyProposal(proposal) val stx: SignedTransaction = maybeProposal.unwrap { verifyProposal(maybeProposal).stx }
verifyTx(proposal.stx) verifyTx(stx)
approve(stx)
} catch(e: Exception) { } catch(e: Exception) {
// TODO: catch only specific exceptions. However, there are numerous validation exceptions // TODO: catch only specific exceptions. However, there are numerous validation exceptions
// that might occur (tx validation/resolution, invalid proposal). Need to rethink how // that might occur (tx validation/resolution, invalid proposal). Need to rethink how
// we manage exceptions and maybe introduce some platform exception hierarchy // we manage exceptions and maybe introduce some platform exception hierarchy
val myIdentity = serviceHub.storageService.myLegalIdentity val myIdentity = serviceHub.storageService.myLegalIdentity
val state = proposal.stateRef val state = maybeProposal.unwrap { it.stateRef }
val reason = StateReplacementRefused(myIdentity, state, e.message) val reason = StateReplacementRefused(myIdentity, state, e.message)
reject(reason) reject(reason)
return
} }
approve(proposal.stx)
} }
@Suspendable @Suspendable
@ -164,7 +166,7 @@ abstract class AbstractStateReplacementProtocol<T> {
val swapSignatures = sendAndReceive<List<DigitalSignature.WithKey>>(otherSide, sessionIdForSend, sessionIdForReceive, response) val swapSignatures = sendAndReceive<List<DigitalSignature.WithKey>>(otherSide, sessionIdForSend, sessionIdForReceive, response)
// TODO: This step should not be necessary, as signatures are re-checked in verifySignatures. // TODO: This step should not be necessary, as signatures are re-checked in verifySignatures.
val allSignatures = swapSignatures.validate { signatures -> val allSignatures = swapSignatures.unwrap { signatures ->
signatures.forEach { it.verifyWithECDSA(stx.txBits) } signatures.forEach { it.verifyWithECDSA(stx.txBits) }
signatures signatures
} }
@ -183,9 +185,10 @@ abstract class AbstractStateReplacementProtocol<T> {
/** /**
* Check the state change proposal to confirm that it's acceptable to this node. Rules for verification depend * Check the state change proposal to confirm that it's acceptable to this node. Rules for verification depend
* on the change proposed, and may further depend on the node itself (for example configuration). * on the change proposed, and may further depend on the node itself (for example configuration). The
* proposal is returned if acceptable, otherwise an exception is thrown.
*/ */
abstract internal fun verifyProposal(proposal: Proposal<T>) abstract fun verifyProposal(maybeProposal: UntrustworthyData<Proposal<T>>): Proposal<T>
@Suspendable @Suspendable
private fun verifyTx(stx: SignedTransaction) { private fun verifyTx(stx: SignedTransaction) {
@ -199,7 +202,7 @@ abstract class AbstractStateReplacementProtocol<T> {
private fun checkMySignatureRequired(tx: WireTransaction) { private fun checkMySignatureRequired(tx: WireTransaction) {
// TODO: use keys from the keyManagementService instead // TODO: use keys from the keyManagementService instead
val myKey = serviceHub.storageService.myLegalIdentity.owningKey val myKey = serviceHub.storageService.myLegalIdentity.owningKey
require(tx.signers.contains(myKey)) { "Party is not a participant for any of the input states of transaction ${tx.id}" } require(myKey in tx.mustSign) { "Party is not a participant for any of the input states of transaction ${tx.id}" }
} }
@Suspendable @Suspendable

View File

@ -0,0 +1,56 @@
package com.r3corda.protocols
import co.paralleluniverse.fibers.Suspendable
import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.crypto.Party
import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.random63BitValue
import com.r3corda.core.serialization.serialize
import java.util.*
/**
* Notify all involved parties about a transaction, including storing a copy. Normally this would be called via
* [FinalityProtocol].
*
* @param notarisedTransaction transaction which has been notarised (if needed) and is ready to notify nodes about.
* @param events information on the event(s) which triggered the transaction.
* @param participants a list of participants involved in the transaction.
* @return a list of participants who were successfully notified of the transaction.
*/
// TODO: Event needs to be replaced with something that's meaningful, but won't ever contain sensitive
// information (such as internal details of an account to take payment from). Suggest
// splitting ClientToServiceCommand into public and private parts, with only the public parts
// relayed here.
class BroadcastTransactionProtocol(val notarisedTransaction: SignedTransaction,
val events: Set<ClientToServiceCommand>,
val participants: Set<Party>) : ProtocolLogic<Unit>() {
companion object {
/** Topic for messages notifying a node of a new transaction */
val TOPIC = "platform.wallet.notify_tx"
}
override val topic: String = TOPIC
data class NotifyTxRequestMessage(
val tx: SignedTransaction,
val events: Set<ClientToServiceCommand>,
override val replyToParty: Party,
override val sessionID: Long
) : PartyRequestMessage
@Suspendable
override fun call() {
// Record it locally
serviceHub.recordTransactions(notarisedTransaction)
// TODO: Messaging layer should handle this broadcast for us (although we need to not be sending
// session ID, for that to work, as well).
participants.filter { it != serviceHub.storageService.myLegalIdentity }.forEach { participant ->
val sessionID = random63BitValue()
val msg = NotifyTxRequestMessage(notarisedTransaction, events, serviceHub.storageService.myLegalIdentity, sessionID)
send(participant, 0, msg)
}
}
}

View File

@ -81,7 +81,7 @@ abstract class FetchDataProtocol<T : NamedByHash, in W : Any>(
private fun validateFetchResponse(maybeItems: UntrustworthyData<ArrayList<W?>>, private fun validateFetchResponse(maybeItems: UntrustworthyData<ArrayList<W?>>,
requests: List<SecureHash>): List<T> = requests: List<SecureHash>): List<T> =
maybeItems.validate { response -> maybeItems.unwrap { response ->
if (response.size != requests.size) if (response.size != requests.size)
throw BadAnswer() throw BadAnswer()
for ((index, resp) in response.withIndex()) { for ((index, resp) in response.withIndex()) {

View File

@ -1,6 +1,6 @@
package com.r3corda.protocols package com.r3corda.protocols
import com.r3corda.core.contracts.SignedTransaction import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash

View File

@ -0,0 +1,57 @@
package com.r3corda.protocols
import co.paralleluniverse.fibers.Suspendable
import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.crypto.Party
import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.utilities.ProgressTracker
/**
* Finalise a transaction by notarising it, then recording it locally, and then sending it to all involved parties.
*
* @param transaction to commit.
* @param events information on the event(s) which triggered the transaction.
* @param participants a list of participants involved in the transaction.
* @return a list of participants who were successfully notified of the transaction.
*/
// TODO: Event needs to be replaced with something that's meaningful, but won't ever contain sensitive
// information (such as internal details of an account to take payment from). Suggest
// splitting ClientToServiceCommand into public and private parts, with only the public parts
// relayed here.
class FinalityProtocol(val transaction: SignedTransaction,
val events: Set<ClientToServiceCommand>,
val participants: Set<Party>,
override val progressTracker: ProgressTracker = tracker()): ProtocolLogic<Unit>() {
companion object {
object NOTARISING : ProgressTracker.Step("Requesting signature by notary service")
object BROADCASTING : ProgressTracker.Step("Broadcasting transaction to participants")
fun tracker() = ProgressTracker(NOTARISING, BROADCASTING)
}
override val topic: String
get() = throw UnsupportedOperationException()
@Suspendable
override fun call() {
// TODO: Resolve the tx here: it's probably already been done, but re-resolution is a no-op and it'll make the API more forgiving.
progressTracker.currentStep = NOTARISING
// Notarise the transaction if needed
val notarisedTransaction = if (needsNotarySignature(transaction)) {
val notarySig = subProtocol(NotaryProtocol.Client(transaction))
transaction.withAdditionalSignature(notarySig)
} else {
transaction
}
// Let everyone else know about the transaction
progressTracker.currentStep = BROADCASTING
subProtocol(BroadcastTransactionProtocol(notarisedTransaction, events, participants))
}
private fun needsNotarySignature(stx: SignedTransaction) = stx.tx.notary != null && hasNoNotarySignature(stx)
private fun hasNoNotarySignature(stx: SignedTransaction) = stx.tx.notary?.owningKey !in stx.sigs.map { it.by }
}

View File

@ -1,9 +1,16 @@
package com.r3corda.protocols package com.r3corda.protocols
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.contracts.StateRef
import com.r3corda.core.contracts.TransactionType
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.utilities.ProgressTracker import com.r3corda.core.utilities.ProgressTracker
import com.r3corda.core.utilities.UntrustworthyData
import com.r3corda.protocols.NotaryChangeProtocol.Acceptor
import com.r3corda.protocols.NotaryChangeProtocol.Instigator
import java.security.PublicKey import java.security.PublicKey
/** /**
@ -61,18 +68,25 @@ object NotaryChangeProtocol: AbstractStateReplacementProtocol<Party>() {
* TODO: In more difficult cases this should call for human attention to manually verify and approve the proposal * TODO: In more difficult cases this should call for human attention to manually verify and approve the proposal
*/ */
@Suspendable @Suspendable
override fun verifyProposal(proposal: AbstractStateReplacementProtocol.Proposal<Party>) { override fun verifyProposal(maybeProposal: UntrustworthyData<AbstractStateReplacementProtocol.Proposal<Party>>): AbstractStateReplacementProtocol.Proposal<Party> {
val newNotary = proposal.modification return maybeProposal.unwrap { proposal ->
val isNotary = serviceHub.networkMapCache.notaryNodes.any { it.identity == newNotary } val newNotary = proposal.modification
require(isNotary) { "The proposed node $newNotary does not run a Notary service " } val isNotary = serviceHub.networkMapCache.notaryNodes.any { it.identity == newNotary }
require(isNotary) { "The proposed node $newNotary does not run a Notary service " }
val state = proposal.stateRef val state = proposal.stateRef
val proposedTx = proposal.stx.tx val proposedTx = proposal.stx.tx
require(proposedTx.inputs.contains(state)) { "The proposed state $state is not in the proposed transaction inputs" } require(state in proposedTx.inputs) { "The proposed state $state is not in the proposed transaction inputs" }
require(proposedTx.type.javaClass == TransactionType.NotaryChange::class.java) {
"The proposed transaction is not a notary change transaction."
}
// An example requirement // An example requirement
val blacklist = listOf("Evil Notary") val blacklist = listOf("Evil Notary")
require(!blacklist.contains(newNotary.name)) { "The proposed new notary $newNotary is not trusted by the party" } require(!blacklist.contains(newNotary.name)) { "The proposed new notary $newNotary is not trusted by the party" }
proposal
}
} }
} }
} }

View File

@ -1,10 +1,10 @@
package com.r3corda.protocols package com.r3corda.protocols
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.r3corda.core.contracts.SignedTransaction import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.contracts.StateRef import com.r3corda.core.contracts.StateRef
import com.r3corda.core.contracts.Timestamp import com.r3corda.core.contracts.Timestamp
import com.r3corda.core.contracts.WireTransaction import com.r3corda.core.transactions.WireTransaction
import com.r3corda.core.crypto.DigitalSignature import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SignedData import com.r3corda.core.crypto.SignedData
@ -72,7 +72,7 @@ object NotaryProtocol {
private fun validateResponse(response: UntrustworthyData<Result>): Result { private fun validateResponse(response: UntrustworthyData<Result>): Result {
progressTracker.currentStep = VALIDATING progressTracker.currentStep = VALIDATING
response.validate { response.unwrap {
if (it.sig != null) validateSignature(it.sig, stx.txBits) if (it.sig != null) validateSignature(it.sig, stx.txBits)
else if (it.error is NotaryError.Conflict) it.error.conflict.verified() else if (it.error is NotaryError.Conflict) it.error.conflict.verified()
else if (it.error == null || it.error !is NotaryError) else if (it.error == null || it.error !is NotaryError)
@ -105,7 +105,7 @@ object NotaryProtocol {
@Suspendable @Suspendable
override fun call() { override fun call() {
val (stx, reqIdentity) = receive<SignRequest>(receiveSessionID).validate { it } val (stx, reqIdentity) = receive<SignRequest>(receiveSessionID).unwrap { it }
val wtx = stx.tx val wtx = stx.tx
val result = try { val result = try {
@ -205,5 +205,5 @@ sealed class NotaryError {
class TransactionInvalid : NotaryError() class TransactionInvalid : NotaryError()
class SignaturesMissing(val missingSigners: List<PublicKey>) : NotaryError() class SignaturesMissing(val missingSigners: Set<PublicKey>) : NotaryError()
} }

View File

@ -3,8 +3,8 @@ package com.r3corda.protocols
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.r3corda.core.contracts.Fix import com.r3corda.core.contracts.Fix
import com.r3corda.core.contracts.FixOf import com.r3corda.core.contracts.FixOf
import com.r3corda.core.contracts.TransactionBuilder import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.contracts.WireTransaction import com.r3corda.core.transactions.WireTransaction
import com.r3corda.core.crypto.DigitalSignature import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogic
@ -85,7 +85,7 @@ open class RatesFixProtocol(protected val tx: TransactionBuilder,
val req = SignRequest(wtx, serviceHub.storageService.myLegalIdentity, sessionID) val req = SignRequest(wtx, serviceHub.storageService.myLegalIdentity, sessionID)
val resp = sendAndReceive<DigitalSignature.LegallyIdentifiable>(oracle, 0, sessionID, req) val resp = sendAndReceive<DigitalSignature.LegallyIdentifiable>(oracle, 0, sessionID, req)
return resp.validate { sig -> return resp.unwrap { sig ->
check(sig.signer == oracle) check(sig.signer == oracle)
tx.checkSignature(sig) tx.checkSignature(sig)
sig sig
@ -100,7 +100,7 @@ open class RatesFixProtocol(protected val tx: TransactionBuilder,
// TODO: add deadline to receive // TODO: add deadline to receive
val resp = sendAndReceive<ArrayList<Fix>>(oracle, 0, sessionID, req) val resp = sendAndReceive<ArrayList<Fix>>(oracle, 0, sessionID, req)
return resp.validate { return resp.unwrap {
val fix = it.first() val fix = it.first()
// Check the returned fix is for what we asked for. // Check the returned fix is for what we asked for.
check(fix.of == fixOf) check(fix.of == fixOf)

View File

@ -2,13 +2,12 @@ package com.r3corda.protocols
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.r3corda.core.checkedAdd import com.r3corda.core.checkedAdd
import com.r3corda.core.contracts.LedgerTransaction
import com.r3corda.core.contracts.SignedTransaction
import com.r3corda.core.contracts.WireTransaction
import com.r3corda.core.contracts.toLedgerTransaction
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.transactions.LedgerTransaction
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.WireTransaction
import java.util.* import java.util.*
// TODO: This code is currently unit tested by TwoPartyTradeProtocolTests, it should have its own tests. // TODO: This code is currently unit tested by TwoPartyTradeProtocolTests, it should have its own tests.
@ -33,6 +32,39 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
companion object { companion object {
private fun dependencyIDs(wtx: WireTransaction) = wtx.inputs.map { it.txhash }.toSet() private fun dependencyIDs(wtx: WireTransaction) = wtx.inputs.map { it.txhash }.toSet()
private fun topologicalSort(transactions: Collection<SignedTransaction>): List<SignedTransaction> {
// Construct txhash -> dependent-txs map
val forwardGraph = HashMap<SecureHash, HashSet<SignedTransaction>>()
transactions.forEach { tx ->
tx.tx.inputs.forEach { input ->
// Note that we use a LinkedHashSet here to make the traversal deterministic (as long as the input list is)
forwardGraph.getOrPut(input.txhash) { LinkedHashSet() }.add(tx)
}
}
val visited = HashSet<SecureHash>(transactions.size)
val result = ArrayList<SignedTransaction>(transactions.size)
fun visit(transaction: SignedTransaction) {
if (transaction.id !in visited) {
visited.add(transaction.id)
forwardGraph[transaction.id]?.forEach {
visit(it)
}
result.add(transaction)
}
}
transactions.forEach {
visit(it)
}
result.reverse()
require(result.size == transactions.size)
return result
}
} }
class ExcessivelyLargeTransactionGraph() : Exception() class ExcessivelyLargeTransactionGraph() : Exception()
@ -61,7 +93,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
@Suspendable @Suspendable
override fun call(): List<LedgerTransaction> { override fun call(): List<LedgerTransaction> {
val newTxns: Iterable<SignedTransaction> = downloadDependencies(txHashes) val newTxns: Iterable<SignedTransaction> = topologicalSort(downloadDependencies(txHashes))
// For each transaction, verify it and insert it into the database. As we are iterating over them in a // For each transaction, verify it and insert it into the database. As we are iterating over them in a
// depth-first order, we should not encounter any verification failures due to missing data. If we fail // depth-first order, we should not encounter any verification failures due to missing data. If we fail
@ -83,7 +115,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
// be a clearer API if we do that. But for consistency with the other c'tor we currently do not. // be a clearer API if we do that. But for consistency with the other c'tor we currently do not.
// //
// If 'stx' is set, then 'wtx' is the contents (from the c'tor). // If 'stx' is set, then 'wtx' is the contents (from the c'tor).
stx?.verifySignatures() val wtx = stx?.verifySignatures() ?: wtx
wtx?.let { wtx?.let {
fetchMissingAttachments(listOf(it)) fetchMissingAttachments(listOf(it))
val ltx = it.toLedgerTransaction(serviceHub) val ltx = it.toLedgerTransaction(serviceHub)
@ -97,7 +129,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
override val topic: String get() = throw UnsupportedOperationException() override val topic: String get() = throw UnsupportedOperationException()
@Suspendable @Suspendable
private fun downloadDependencies(depsToCheck: Set<SecureHash>): List<SignedTransaction> { private fun downloadDependencies(depsToCheck: Set<SecureHash>): Collection<SignedTransaction> {
// Maintain a work queue of all hashes to load/download, initialised with our starting set. Then do a breadth // Maintain a work queue of all hashes to load/download, initialised with our starting set. Then do a breadth
// first traversal across the dependency graph. // first traversal across the dependency graph.
// //
@ -147,7 +179,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
throw ExcessivelyLargeTransactionGraph() throw ExcessivelyLargeTransactionGraph()
} }
return resultQ.values.reversed() return resultQ.values
} }
/** /**

View File

@ -11,13 +11,15 @@ import com.r3corda.core.node.services.DEFAULT_SESSION_ID
import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.random63BitValue import com.r3corda.core.random63BitValue
import com.r3corda.core.seconds import com.r3corda.core.seconds
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.transactions.WireTransaction
import com.r3corda.core.utilities.ProgressTracker import com.r3corda.core.utilities.ProgressTracker
import com.r3corda.core.utilities.UntrustworthyData import com.r3corda.core.utilities.UntrustworthyData
import com.r3corda.core.utilities.trace import com.r3corda.core.utilities.trace
import java.math.BigDecimal import java.math.BigDecimal
import java.security.KeyPair import java.security.KeyPair
import java.security.PublicKey import java.security.PublicKey
import java.security.SignatureException
import java.time.Duration import java.time.Duration
/** /**
@ -98,15 +100,11 @@ object TwoPartyDealProtocol {
fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData<SignedTransaction>): SignedTransaction { fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData<SignedTransaction>): SignedTransaction {
progressTracker.currentStep = VERIFYING progressTracker.currentStep = VERIFYING
untrustedPartialTX.validate { stx -> untrustedPartialTX.unwrap { stx ->
progressTracker.nextStep() progressTracker.nextStep()
// Check that the tx proposed by the buyer is valid. // Check that the tx proposed by the buyer is valid.
val missingSigs = stx.verifySignatures(throwIfSignaturesAreMissing = false) val wtx: WireTransaction = stx.verifySignatures(myKeyPair.public, notaryNode.identity.owningKey)
if (missingSigs != setOf(myKeyPair.public, notaryNode.identity.owningKey))
throw SignatureException("The set of missing signatures is not as expected: $missingSigs")
val wtx: WireTransaction = stx.tx
logger.trace { "Received partially signed transaction: ${stx.id}" } logger.trace { "Received partially signed transaction: ${stx.id}" }
checkDependencies(stx) checkDependencies(stx)
@ -242,7 +240,7 @@ object TwoPartyDealProtocol {
val handshake = receive<Handshake<U>>(sessionID) val handshake = receive<Handshake<U>>(sessionID)
progressTracker.currentStep = VERIFYING progressTracker.currentStep = VERIFYING
handshake.validate { handshake.unwrap {
return validateHandshake(it) return validateHandshake(it)
} }
} }
@ -254,7 +252,7 @@ object TwoPartyDealProtocol {
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx. // TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
return sendAndReceive<SignaturesFromPrimary>(otherSide, theirSessionID, sessionID, stx).validate { it } return sendAndReceive<SignaturesFromPrimary>(otherSide, theirSessionID, sessionID, stx).unwrap { it }
} }
private fun signWithOurKeys(signingPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction { private fun signWithOurKeys(signingPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {

View File

@ -1,13 +1,12 @@
package com.r3corda.protocols package com.r3corda.protocols
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.r3corda.core.contracts.SignedTransaction
import com.r3corda.core.contracts.TransactionVerificationException import com.r3corda.core.contracts.TransactionVerificationException
import com.r3corda.core.contracts.WireTransaction
import com.r3corda.core.contracts.toLedgerTransaction
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.node.services.TimestampChecker import com.r3corda.core.node.services.TimestampChecker
import com.r3corda.core.node.services.UniquenessProvider import com.r3corda.core.node.services.UniquenessProvider
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.WireTransaction
import java.security.SignatureException import java.security.SignatureException
/** /**
@ -38,10 +37,11 @@ class ValidatingNotaryProtocol(otherSide: Party,
} }
private fun checkSignatures(stx: SignedTransaction) { private fun checkSignatures(stx: SignedTransaction) {
val myKey = serviceHub.storageService.myLegalIdentity.owningKey try {
val missing = stx.verifySignatures(throwIfSignaturesAreMissing = false) - myKey stx.verifySignatures(serviceHub.storageService.myLegalIdentity.owningKey)
} catch(e: SignedTransaction.SignaturesMissingException) {
if (missing.isNotEmpty()) throw NotaryException(NotaryError.SignaturesMissing(missing.toList())) throw NotaryException(NotaryError.SignaturesMissing(e.missing))
}
} }
@Suspendable @Suspendable

Some files were not shown because too many files have changed in this diff Show More