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
/experimental/build
/docs/build/doctrees
/test-utils/build
/client/build
# gradle's 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_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/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_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" />
@ -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_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_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_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_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_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>
</component>
</project>

View File

@ -42,7 +42,7 @@ Install the Oracle JDK 8u45 or higher. It is possible that OpenJDK will also wor
## 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/

View File

@ -1,6 +1,6 @@
buildscript {
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.artemis_version = '1.3.0'
ext.jackson_version = '2.8.0.rc2'
@ -48,7 +48,7 @@ allprojects {
// Our version: bump this on release.
group 'com.r3corda'
version '0.3-SNAPSHOT'
version '0.4-SNAPSHOT'
}
repositories {
@ -102,21 +102,24 @@ mainClassName = 'com.r3corda.demos.TraderDemoKt'
dependencies {
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-reflect:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.2"
compile 'com.squareup.okhttp3:okhttp:3.3.1'
compile 'co.paralleluniverse:capsule:1.0.3'
// Unit testing helpers.
testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:3.4.1'
testCompile 'com.pholser:junit-quickcheck-core:0.6'
// Integration test helpers
integrationTestCompile 'junit:junit:4.12'
integrationTestCompile 'org.assertj:assertj-core:${assertj_version}'
integrationTestCompile project(':test-utils')
}
// 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
classpath = sourceSets.integrationTest.runtimeClasspath
}
test.finalizedBy(integrationTest)
task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
@ -196,46 +198,49 @@ applicationDistribution.into("bin") {
fileMode = 0755
}
task createCapsule(type: FatCapsule, dependsOn: 'quasarScan') {
task buildCordaJAR(type: FatCapsule, dependsOn: 'quasarScan') {
applicationClass 'com.r3corda.node.MainKt'
archiveName 'corda.jar'
applicationSource = files(project.tasks.findByName('jar'), 'build/classes/main/CordaCaplet.class')
capsuleManifest {
appClassPath = ["jolokia-agent-war-${project.ext.jolokia_version}.war"]
systemProperties['log4j.configuration'] = 'log4j2.xml'
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar"]
minJavaVersion = '1.8.0'
caplets = ['CordaCaplet']
}
}
task createStandalone(dependsOn: 'createCapsule') << {
task installTemplateNodes(dependsOn: 'buildCordaJAR') << {
copy {
from createCapsule.outputs.getFiles()
from buildCordaJAR.outputs.getFiles()
from 'config/dev/nameservernode.conf'
into "${buildDir}/standalone/nameserver"
into "${buildDir}/nodes/nameserver"
rename 'nameservernode.conf', 'node.conf'
}
copy {
from createCapsule.outputs.getFiles()
from buildCordaJAR.outputs.getFiles()
from 'config/dev/generalnodea.conf'
into "${buildDir}/standalone/nodea"
into "${buildDir}/nodes/nodea"
rename 'generalnodea.conf', 'node.conf'
}
copy {
from createCapsule.outputs.getFiles()
from buildCordaJAR.outputs.getFiles()
from 'config/dev/generalnodeb.conf'
into "${buildDir}/standalone/nodeb"
into "${buildDir}/nodes/nodeb"
rename 'generalnodeb.conf', 'node.conf'
}
delete("${buildDir}/standalone/runstandalone")
def jarName = createCapsule.outputs.getFiles().getSingleFile().getName()
delete("${buildDir}/nodes/runnodes")
def jarName = buildCordaJAR.outputs.getFiles().getSingleFile().getName()
copy {
from "buildSrc/scripts/runstandalone"
from "buildSrc/scripts/runnodes"
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"))
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
# Creates three nodes. A network map and notary node and two regular nodes that can be extended with cordapps.
set -euo pipefail
trap 'kill $(jobs -p)' SIGINT SIGTERM EXIT
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",
myLegalName : "Bank A",
nearestCity : "London",
keyStorePassword : "cordacadevpass",
trustStorePassword : "trustpass",
artemisAddress : "localhost:31337",
webAddress : "localhost:31339",
hostNotaryServiceLocally: false,
extraAdvertisedServiceIds: "corda.interest_rates",
mapService : {
hostServiceLocally : false,
address : "localhost:12345",
identity : "Notary Service"
}
basedir : "./nodea"
myLegalName : "Bank A"
nearestCity : "London"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
artemisAddress : "localhost:31337"
webAddress : "localhost:31339"
hostNotaryServiceLocally: false
extraAdvertisedServiceIds: "corda.interest_rates"
networkMapAddress : "localhost:12345"
useHTTPS : false

View File

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

View File

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

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: CanonicalizerPlugin
repositories {
mavenCentral()
mavenLocal()
mavenCentral()
jcenter()
maven {
url 'http://oss.sonatype.org/content/repositories/snapshots'
}
jcenter()
maven {
url 'https://dl.bintray.com/kotlin/exposed'
}
}
dependencies {
compile project(':core')
testCompile project(':test-utils')
testCompile 'junit:junit:4.12'
}
@ -87,4 +26,4 @@ sourceSets {
srcDir "../config/test"
}
}
}
}

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: CanonicalizerPlugin
repositories {
@ -85,4 +21,4 @@ sourceSets {
srcDir "../../config/test"
}
}
}
}

View File

@ -11,6 +11,7 @@ package com.r3corda.contracts.isolated
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.transactions.TransactionBuilder
import java.security.PublicKey
// 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.PartyAndReference
import com.r3corda.core.contracts.TransactionBuilder
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.crypto.Party
interface DummyContractBackdoor {

View File

@ -3,19 +3,18 @@ package com.r3corda.contracts;
import com.google.common.collect.*;
import com.r3corda.contracts.asset.*;
import com.r3corda.core.contracts.*;
import static com.r3corda.core.contracts.ContractsDSL.requireThat;
import com.r3corda.core.contracts.Timestamp;
import com.r3corda.core.contracts.TransactionForContract.*;
import com.r3corda.core.contracts.clauses.*;
import com.r3corda.core.crypto.*;
import kotlin.Unit;
import com.r3corda.core.transactions.*;
import kotlin.*;
import org.jetbrains.annotations.*;
import java.security.*;
import java.time.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.*;
import static com.r3corda.core.contracts.ContractsDSL.*;
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.
*/
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();
public static class State implements ContractState, ICommercialPaperState {
public static class State implements OwnableState, ICommercialPaperState {
private PartyAndReference issuance;
private PublicKey owner;
private Amount<Issued<Currency>> faceValue;
@ -54,6 +52,12 @@ public class JavaCommercialPaper implements Contract {
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) {
return new State(newIssuance, this.owner, this.faceValue, this.maturityDate);
}
@ -70,6 +74,7 @@ public class JavaCommercialPaper implements Contract {
return issuance;
}
@NotNull
public PublicKey getOwner() {
return owner;
}
@ -86,7 +91,6 @@ public class JavaCommercialPaper implements Contract {
@Override
public Contract getContract() {
return JCP_PROGRAM_ID;
//return SecureHash.sha256("java commercial paper (this should be a bytecode hash)");
}
@Override
@ -128,44 +132,17 @@ public class JavaCommercialPaper implements Contract {
}
}
public interface Clause {
abstract class AbstractGroup implements GroupClause<State, State> {
@NotNull
@Override
public MatchBehaviour getIfNotMatched() {
return MatchBehaviour.CONTINUE;
}
@NotNull
@Override
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;
public interface Clauses {
class Group extends GroupClauseVerifier<State, Commands, State> {
// This complains because we're passing generic types into a varargs, but it is valid so we suppress the
// warning.
@SuppressWarnings("unchecked")
Group() {
super(new AnyComposition<>(
new Clauses.Redeem(),
new Clauses.Move(),
new Clauses.Issue()
));
}
@NotNull
@ -175,7 +152,7 @@ public class JavaCommercialPaper implements Contract {
}
}
class Move extends AbstractGroup {
class Move extends Clause<State, Commands, State> {
@NotNull
@Override
public Set<Class<? extends CommandData>> getRequiredCommands() {
@ -184,11 +161,11 @@ public class JavaCommercialPaper implements Contract {
@NotNull
@Override
public Set<CommandData> verify(@NotNull TransactionForContract tx,
public Set<Commands> verify(@NotNull TransactionForContract tx,
@NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs,
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
@NotNull State token) {
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
@NotNull State groupingKey) {
AuthenticatedObject<Commands.Move> cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class);
// There should be only a single input due to aggregation above
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
@Override
public Set<Class<? extends CommandData>> getRequiredCommands() {
@ -215,11 +192,11 @@ public class JavaCommercialPaper implements Contract {
@NotNull
@Override
public Set<CommandData> verify(@NotNull TransactionForContract tx,
public Set<Commands> verify(@NotNull TransactionForContract tx,
@NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs,
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
@NotNull State token) {
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
@NotNull State groupingKey) {
AuthenticatedObject<Commands.Redeem> cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class);
// 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
@Override
public Set<Class<? extends CommandData>> getRequiredCommands() {
@ -257,14 +234,13 @@ public class JavaCommercialPaper implements Contract {
@NotNull
@Override
public Set<CommandData> verify(@NotNull TransactionForContract tx,
public Set<Commands> verify(@NotNull TransactionForContract tx,
@NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs,
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
@NotNull State token) {
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
@NotNull State groupingKey) {
AuthenticatedObject<Commands.Issue> cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class);
State output = single(outputs);
Party notary = cmd.getValue().notary;
Timestamp timestampCommand = tx.getTimestamp();
Instant time = null == timestampCommand
? null
@ -291,49 +267,28 @@ public class JavaCommercialPaper implements Contract {
}
class Redeem implements Commands {
private final Party notary;
public Redeem(Party setNotary) {
this.notary = setNotary;
}
@Override
public boolean equals(Object obj) { return obj instanceof Redeem; }
}
class Issue implements Commands {
private final Party notary;
public Issue(Party setNotary) {
this.notary = setNotary;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Issue) {
Issue other = (Issue)obj;
return notary.equals(other.notary);
} else {
return false;
}
}
@Override
public int hashCode() { return notary.hashCode(); }
public boolean equals(Object obj) { return obj instanceof Issue; }
}
}
@NotNull
private Collection<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) {
private List<AuthenticatedObject<Commands>> extractCommands(@NotNull TransactionForContract tx) {
return tx.getCommands()
.stream()
.filter((AuthenticatedObject<CommandData> command) -> command.getValue() instanceof Commands)
.map((AuthenticatedObject<CommandData> command) -> new AuthenticatedObject<>(command.getSigners(), command.getSigningParties(), (Commands) command.getValue()))
.collect(Collectors.toList());
}
@Override
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
@ -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) {
State state = new State(issuance, issuance.getParty().getOwningKey(), faceValue, maturityDate);
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 {
new Cash().generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), wallet, null);
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) {

View File

@ -1,6 +1,7 @@
package com.r3corda.contracts
import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.asset.FungibleAsset
import com.r3corda.contracts.asset.InsufficientBalanceException
import com.r3corda.contracts.asset.sumCashBy
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.toStringShort
import com.r3corda.core.random63BitValue
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.utilities.Emoji
import java.security.PublicKey
import java.time.Instant
@ -49,10 +51,7 @@ class CommercialPaper : Contract {
val maturityDate: Instant
)
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>>
= tx.commands.select<Commands>()
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clauses.Group()), extractCommands(tx))
override fun verify(tx: TransactionForContract) = verifyClause(tx, Clauses.Group(), tx.commands.select<Commands>())
data class State(
val issuance: PartyAndReference,
@ -79,25 +78,16 @@ class CommercialPaper : Contract {
}
interface Clauses {
class Group : GroupClauseVerifier<State, Issued<Terms>>() {
override val ifNotMatched = MatchBehaviour.ERROR
override val ifMatched = MatchBehaviour.END
override val clauses = listOf(
Redeem(),
Move(),
Issue()
)
class Group : GroupClauseVerifier<State, Commands, Issued<Terms>>(
AnyComposition(
Redeem(),
Move(),
Issue())) {
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
= tx.groupStates<State, Issued<Terms>> { it.token }
}
abstract class AbstractGroupClause: GroupClause<State, Issued<Terms>> {
override val ifNotMatched = MatchBehaviour.CONTINUE
override val ifMatched = MatchBehaviour.END
}
class Issue : AbstractIssue<State, Terms>(
class Issue : AbstractIssue<State, Commands, Terms>(
{ map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() },
{ token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
@ -105,10 +95,10 @@ class CommercialPaper : Contract {
override fun verify(tx: TransactionForContract,
inputs: List<State>,
outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<Terms>): Set<CommandData> {
val consumedCommands = super.verify(tx, inputs, outputs, commands, token)
val command = commands.requireSingleCommand<Commands.Issue>()
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Issued<Terms>?): Set<Commands> {
val consumedCommands = super.verify(tx, inputs, outputs, commands, groupingKey)
commands.requireSingleCommand<Commands.Issue>()
val timestamp = tx.timestamp
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 fun verify(tx: TransactionForContract,
inputs: List<State>,
outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<Terms>): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Issued<Terms>?): Set<Commands> {
val command = commands.requireSingleCommand<Commands.Move>()
val input = inputs.single()
requireThat {
@ -138,15 +128,14 @@ class CommercialPaper : Contract {
}
}
class Redeem(): AbstractGroupClause() {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Redeem::class.java)
class Redeem(): Clause<State, Commands, Issued<Terms>>() {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Redeem::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State>,
outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<Terms>): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Issued<Terms>?): Set<Commands> {
// TODO: This should filter commands down to those with compatible subjects (underlying product and maturity date)
// before requiring a single command
val command = commands.requireSingleCommand<Commands.Redeem>()
@ -169,9 +158,9 @@ class CommercialPaper : Contract {
}
interface Commands : CommandData {
class Move : TypeOnlyCommandData(), Commands
data class Redeem(val notary: Party) : Commands
data class Issue(val notary: Party, override val nonce: Long = random63BitValue()) : IssueCommand, Commands
data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands
class Redeem : TypeOnlyCommandData(), 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 {
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) }
Cash().generateSpend(tx, amount, paper.state.data.owner, wallet)
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
import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.asset.InsufficientBalanceException
import com.r3corda.contracts.asset.sumCashBy
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.NullPublicKey
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
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 java.security.PublicKey
import java.time.Instant
@ -30,8 +34,7 @@ class CommercialPaperLegacy : Contract {
val maturityDate: Instant
) : OwnableState, ICommercialPaperState {
override val contract = CP_LEGACY_PROGRAM_ID
override val participants: List<PublicKey>
get() = listOf(owner)
override val participants = listOf(owner)
fun withoutOwner() = copy(owner = NullPublicKey)
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
@ -46,11 +49,11 @@ class CommercialPaperLegacy : Contract {
}
interface Commands : CommandData {
class Move: TypeOnlyCommandData(), Commands
data class Redeem(val notary: Party) : Commands
class Move : TypeOnlyCommandData(), 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.
// 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) {
@ -74,8 +77,8 @@ class CommercialPaperLegacy : Contract {
}
}
// Redemption of the paper requires movement of on-ledger cash.
is Commands.Redeem -> {
// Redemption of the paper requires movement of on-ledger cash.
val input = inputs.single()
val received = tx.outputs.sumCashBy(input.owner)
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)
"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.
// 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.
"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.SecureHash
import com.r3corda.core.protocols.ProtocolLogicRefFactory
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.utilities.suggestInterestRateAnnouncementTimeWindow
import com.r3corda.protocols.TwoPartyDealProtocol
import org.apache.commons.jexl3.JexlBuilder
@ -447,20 +448,14 @@ class InterestRateSwap() : Contract {
fixingCalendar, index, indexSource, indexTenor)
}
fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
= tx.commands.select<Commands>()
override fun verify(tx: TransactionForContract) = verifyClause(tx, AllComposition(Clauses.Timestamped(), Clauses.Group()), tx.commands.select<Commands>())
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clause.Timestamped(), Clause.Group()), extractCommands(tx))
interface Clause {
interface Clauses {
/**
* Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides
* helper functions for the clauses.
*/
abstract class AbstractIRSClause : GroupClause<State, String> {
override val ifMatched = MatchBehaviour.END
override val ifNotMatched = MatchBehaviour.CONTINUE
abstract class AbstractIRSClause : Clause<State, Commands, UniqueIdentifier>() {
// These functions may make more sense to use for basket types, but for now let's leave them here
fun checkLegDates(legs: List<CommonLeg>) {
requireThat {
@ -502,23 +497,18 @@ class InterestRateSwap() : Contract {
}
}
class Group : GroupClauseVerifier<State, String>() {
override val ifMatched = MatchBehaviour.END
override val ifNotMatched = MatchBehaviour.ERROR
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, String>>
class Group : GroupClauseVerifier<State, Commands, UniqueIdentifier>(AnyComposition(Agree(), Fix(), Pay(), Mature())) {
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, UniqueIdentifier>>
// Group by Trade ID for in / out states
= tx.groupStates() { state -> state.common.tradeID }
override val clauses = listOf(Agree(), Fix(), Pay(), Mature())
= tx.groupStates() { state -> state.linearId }
}
class Timestamped : SingleClause {
override val ifMatched = MatchBehaviour.CONTINUE
override val ifNotMatched = MatchBehaviour.ERROR
override val requiredCommands = emptySet<Class<out CommandData>>()
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
class Timestamped : Clause<ContractState, Commands, Unit>() {
override fun verify(tx: TransactionForContract,
inputs: List<ContractState>,
outputs: List<ContractState>,
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Unit?): Set<Commands> {
require(tx.timestamp?.midpoint != null) { "must be timestamped" }
// We return an empty set because we don't process any commands
return emptySet()
@ -526,13 +516,13 @@ class InterestRateSwap() : Contract {
}
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,
inputs: List<State>,
outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: String): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Agree>()
val irs = outputs.filterIsInstance<State>().single()
requireThat {
@ -562,13 +552,13 @@ class InterestRateSwap() : Contract {
}
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,
inputs: List<State>,
outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: String): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Refix>()
val irs = outputs.filterIsInstance<State>().single()
val prevIrs = inputs.filterIsInstance<State>().single()
@ -607,13 +597,13 @@ class InterestRateSwap() : Contract {
}
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,
inputs: List<State>,
outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: String): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Pay>()
requireThat {
"Payments not supported / verifiable yet" by false
@ -623,17 +613,18 @@ class InterestRateSwap() : Contract {
}
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,
inputs: List<State>,
outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: String): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: UniqueIdentifier?): Set<Commands> {
val command = tx.commands.requireSingleCommand<Commands.Mature>()
val irs = inputs.filterIsInstance<State>().single()
requireThat {
"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)
@ -656,11 +647,12 @@ class InterestRateSwap() : Contract {
val fixedLeg: FixedLeg,
val floatingLeg: FloatingLeg,
val calculation: Calculation,
val common: Common
val common: Common,
override val linearId: UniqueIdentifier = UniqueIdentifier(common.tradeID)
) : FixableDealState, SchedulableState {
override val contract = IRS_PROGRAM_ID
override val thread = SecureHash.sha256(common.tradeID)
override val ref = common.tradeID
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.NoZeroSizedOutputs
import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.GroupClauseVerifier
import com.r3corda.core.contracts.clauses.MatchBehaviour
import com.r3corda.core.contracts.clauses.*
import com.r3corda.core.crypto.*
import com.r3corda.core.node.services.Wallet
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.utilities.Emoji
import java.math.BigInteger
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
* 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:
* 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.
*/
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 clauses = listOf(Clauses.Group())
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
= tx.commands.select<Cash.Commands>()
override val conserveClause: AbstractConserveAmount<State, Commands, Currency> = Clauses.ConserveAmount()
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Cash.Commands>>
= commands.select<Cash.Commands>()
interface Clauses {
class Group : GroupClauseVerifier<State, Issued<Currency>>() {
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
override val clauses = listOf(
NoZeroSizedOutputs<State, Currency>(),
class Group : GroupClauseVerifier<State, Commands, Issued<Currency>>(AllComposition<State, Commands, Issued<Currency>>(
NoZeroSizedOutputs<State, Commands, Currency>(),
FirstComposition<State, Commands, Issued<Currency>>(
Issue(),
ConserveAmount())
)
) {
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Currency>>>
= tx.groupStates<State, Issued<Currency>> { it.issuanceDef }
}
class Issue : AbstractIssue<State, Currency>(
class Issue : AbstractIssue<State, Commands, Currency>(
sum = { sumCash() },
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. */
@ -86,7 +84,7 @@ class Cash : OnLedgerAsset<Currency, Cash.State>() {
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
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 issuanceDef = amount.token
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 generateIssueCommand() = Commands.Issue()
override fun generateMoveCommand() = Commands.Move()
override fun verify(tx: TransactionForContract)
= verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
}
// Small DSL extensions.

View File

@ -5,11 +5,13 @@ import com.r3corda.contracts.clause.AbstractIssue
import com.r3corda.contracts.clause.NoZeroSizedOutputs
import com.r3corda.core.contracts.*
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.SecureHash
import com.r3corda.core.crypto.newSecureRandom
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.transactions.TransactionBuilder
import java.security.PublicKey
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
* 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).
*
* 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.
class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.Commands, CommodityContract.State>() {
/**
* TODO:
* 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 conserveClause: AbstractConserveAmount<State, Commodity> = Clauses.ConserveAmount()
override val conserveClause: AbstractConserveAmount<State, Commands, Commodity> = Clauses.ConserveAmount()
/**
* 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
* each group.
*/
class Group : GroupClauseVerifier<State, Issued<Commodity>>() {
/**
* The group clause does not depend on any commands being present, so something has gone terribly wrong if
* it doesn't match.
*/
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()
)
class Group : GroupClauseVerifier<State, Commands, Issued<Commodity>>(AnyComposition(
NoZeroSizedOutputs<State, Commands, Commodity>(),
Issue(),
ConserveAmount())) {
/**
* 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.
*/
class Issue : AbstractIssue<State, Commodity>(
class Issue : AbstractIssue<State, Commands, Commodity>(
sum = { sumCommodities() },
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.
*/
class ConserveAmount : AbstractConserveAmount<State, Commodity>()
class ConserveAmount : AbstractConserveAmount<State, Commands, Commodity>()
}
/** 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>
}
override val clauses = listOf(Clauses.Group())
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
= tx.commands.select<CommodityContract.Commands>()
override fun verify(tx: TransactionForContract)
= verifyClause(tx, Clauses.Group(), extractCommands(tx.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.

View File

@ -29,7 +29,10 @@ interface FungibleAsset<T> : OwnableState {
val deposit: PartyAndReference
val issuanceDef: Issued<T>
val amount: Amount<Issued<T>>
/** There must be an ExitCommand signed by these keys to destroy the amount */
/**
* 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>
/** There must be a MoveCommand signed by this key to claim the amount */
override val owner: PublicKey

View File

@ -4,16 +4,11 @@ import com.google.common.annotations.VisibleForTesting
import com.r3corda.contracts.clause.*
import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.*
import com.r3corda.core.crypto.NullPublicKey
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.crypto.*
import com.r3corda.core.random63BitValue
import com.r3corda.core.testing.MINI_CORP
import com.r3corda.core.testing.TEST_TX_TIME
import com.r3corda.core.utilities.Emoji
import com.r3corda.core.utilities.NonEmptySet
import com.r3corda.core.utilities.nonEmptySetOf
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.utilities.*
import java.math.BigInteger
import java.security.PublicKey
import java.time.Duration
import java.time.Instant
@ -43,25 +38,27 @@ class Obligation<P> : 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")
private val clauses = listOf(InterceptorClause(Clauses.VerifyLifecycle<P>(), Clauses.Net<P>()),
Clauses.Group<P>())
interface Clauses {
/**
* Parent clause for clauses that operate on grouped states (those which are fungible).
*/
class Group<P> : GroupClauseVerifier<State<P>, Issued<Terms<P>>>() {
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
override val clauses = listOf(
NoZeroSizedOutputs<State<P>, Terms<P>>(),
SetLifecycle<P>(),
VerifyLifecycle<P>(),
Settle<P>(),
Issue(),
ConserveAmount()
)
class Group<P> : GroupClauseVerifier<State<P>, Commands, Issued<Terms<P>>>(
AllComposition(
NoZeroSizedOutputs<State<P>, Commands, Terms<P>>(),
FirstComposition(
SetLifecycle<P>(),
AllComposition(
VerifyLifecycle<State<P>, Commands, Issued<Terms<P>>, P>(),
FirstComposition(
Settle<P>(),
Issue(),
ConserveAmount()
)
)
)
)
) {
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<Obligation.State<P>, Issued<Terms<P>>>>
= tx.groupStates<Obligation.State<P>, Issued<Terms<P>>> { it.issuanceDef }
}
@ -69,58 +66,64 @@ class Obligation<P> : Contract {
/**
* Generic issuance clause
*/
class Issue<P> : AbstractIssue<State<P>, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) {
override val requiredCommands = setOf(Obligation.Commands.Issue::class.java)
class Issue<P> : AbstractIssue<State<P>, Commands, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
}
/**
* 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.
*/
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.
*/
class SetLifecycle<P> : GroupClause<State<P>, Issued<Terms<P>>> {
override val requiredCommands = setOf(Commands.SetLifecycle::class.java)
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
class SetLifecycle<P> : Clause<State<P>, Commands, Issued<Terms<P>>>() {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.SetLifecycle::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State<P>>,
outputs: List<State<P>>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<Terms<P>>): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Issued<Terms<P>>?): Set<Commands> {
val command = commands.requireSingleCommand<Commands.SetLifecycle>()
Obligation<P>().verifySetLifecycleCommand(inputs, outputs, tx, command)
return setOf(command.value)
}
override fun toString(): String = "Set obligation lifecycle"
}
/**
* Obligation-specific clause for settling an outstanding obligation by witnessing
* change of ownership of other states to fulfil
*/
class Settle<P> : GroupClause<State<P>, Issued<Terms<P>>> {
override val requiredCommands = setOf(Commands.Settle::class.java)
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
class Settle<P> : Clause<State<P>, Commands, Issued<Terms<P>>>() {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Settle::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State<P>>,
outputs: List<State<P>>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<Terms<P>>): Set<CommandData> {
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Issued<Terms<P>>?): Set<Commands> {
require(groupingKey != null)
val command = commands.requireSingleCommand<Commands.Settle<P>>()
val obligor = token.issuer.party
val template = token.product
val obligor = groupingKey!!.issuer.party
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 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
@ -166,7 +169,7 @@ class Obligation<P> : Contract {
for ((beneficiary, obligations) in inputs.groupBy { it.owner }) {
val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull<P>()
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" }
totalPenniesSettled += settled.quantity
}
@ -185,7 +188,7 @@ class Obligation<P> : Contract {
"signatures are present from all obligors" by command.signers.containsAll(requiredSigners)
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
"at obligor ${obligor.name} the obligations after settlement balance" by
(inputAmount == outputAmount + Amount(totalPenniesSettled, token))
(inputAmount == outputAmount + Amount(totalPenniesSettled, groupingKey))
}
return setOf(command.value)
}
@ -197,26 +200,15 @@ class Obligation<P> : Contract {
* any lifecycle change clause, which is the only clause that involve
* non-standard lifecycle states on input/output.
*/
class VerifyLifecycle<P> : SingleClause, GroupClause<State<P>, Issued<Terms<P>>> {
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>>()
)
class VerifyLifecycle<S: ContractState, C: CommandData, T: Any, P> : Clause<S, C, T>() {
override fun verify(tx: TransactionForContract,
inputs: List<State<P>>,
outputs: List<State<P>>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<Terms<P>>): Set<CommandData>
= verify(inputs, outputs)
fun verify(inputs: List<State<P>>,
outputs: List<State<P>>): Set<CommandData> {
inputs: List<S>,
outputs: List<S>,
commands: List<AuthenticatedObject<C>>,
groupingKey: T?): Set<C>
= verify(inputs.filterIsInstance<State<P>>(), outputs.filterIsInstance<State<P>>())
private fun verify(inputs: List<State<P>>,
outputs: List<State<P>>): Set<C> {
requireThat {
"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 }
@ -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
* 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.
@ -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>>
}
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
= tx.commands.select<Obligation.Commands>()
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx))
override fun verify(tx: TransactionForContract) = verifyClause<Commands>(tx, FirstComposition<ContractState, Commands, Unit>(
Clauses.Net<Commands, P>(),
Clauses.Group<P>()
), tx.commands.select<Obligation.Commands>())
/**
* 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 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
* 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.
*/
@Suppress("unused")
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<Terms<P>>>,
changeKey: PublicKey, assetStates: List<StateAndRef<Obligation.State<P>>>): PublicKey
= Clauses.ConserveAmount<P>().generateExit(tx, amountIssued, changeKey, assetStates,
assetStates: List<StateAndRef<Obligation.State<P>>>): PublicKey
= Clauses.ConserveAmount<P>().generateExit(tx, amountIssued, assetStates,
deriveState = { state, amount, owner -> state.copy(data = state.data.move(amount, owner)) },
generateMoveCommand = { -> Commands.Move() },
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>.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>
get() = Obligation.Terms(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(this), TEST_TX_TIME)
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.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.transactions.TransactionBuilder
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
* the issuer/depositRefs and just examine the amount fields.
*/
abstract class OnLedgerAsset<T : Any, S : FungibleAsset<T>> : Contract {
abstract val clauses: List<SingleClause>
abstract fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
abstract val conserveClause: AbstractConserveAmount<S, T>
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx))
abstract class OnLedgerAsset<T : Any, C: CommandData, S : FungibleAsset<T>> : Contract {
abstract fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): Collection<AuthenticatedObject<C>>
abstract val conserveClause: AbstractConserveAmount<S, C, T>
/**
* 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.
*/
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
changeKey: PublicKey, assetStates: List<StateAndRef<S>>): PublicKey
= conserveClause.generateExit(tx, amountIssued, changeKey, assetStates,
assetStates: List<StateAndRef<S>>): PublicKey
= conserveClause.generateExit(tx, amountIssued, assetStates,
deriveState = { state, amount, owner -> deriveState(state, amount, owner) },
generateMoveCommand = { -> generateMoveCommand() },
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.sumFungibleOrZero
import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.GroupClause
import com.r3corda.core.contracts.clauses.MatchBehaviour
import com.r3corda.core.contracts.clauses.Clause
import com.r3corda.core.crypto.Party
import com.r3corda.core.transactions.TransactionBuilder
import java.security.PublicKey
import java.util.*
@ -16,14 +16,7 @@ import java.util.*
* Move command is provided, and errors if absent. Must be the last clause under a grouping clause;
* errors on no-match, ends on match.
*/
abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause<S, 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()
abstract class AbstractConserveAmount<S : FungibleAsset<T>, C : CommandData, T : Any> : Clause<S, C, Issued<T>>() {
/**
* 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 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
* 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.
*/
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>,
generateMoveCommand: () -> CommandData,
generateExitCommand: (Amount<Issued<T>>) -> CommandData): PublicKey {
val owner = assetStates.map { it.state.data.owner }.toSet().single()
val currency = amountIssued.token.product
val amount = Amount(amountIssued.quantity, currency)
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) {
// 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()
for (state in gathered) tx.addInputState(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
}
@ -178,26 +172,31 @@ abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause
override fun verify(tx: TransactionForContract,
inputs: List<S>,
outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<T>): Set<CommandData> {
val inputAmount: Amount<Issued<T>> = inputs.sumFungibleOrNull<T>() ?: throw IllegalArgumentException("there is at least one asset input for group $token")
val deposit = token.issuer
val outputAmount: Amount<Issued<T>> = outputs.sumFungibleOrZero(token)
commands: List<AuthenticatedObject<C>>,
groupingKey: Issued<T>?): Set<C> {
require(groupingKey != null) { "Conserve amount clause can only be used on grouped states" }
val matchedCommands = commands.filter { command -> command.value is FungibleAsset.Commands.Move || command.value is FungibleAsset.Commands.Exit<*> }
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.
// A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero.
// If we want to remove assets from the ledger, that must be signed for by the issuer and owner.
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 amountExitingLedger: Amount<Issued<T>> = exitCommand?.value?.amount ?: Amount(0, token)
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, groupingKey)
requireThat {
"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)
}
return listOf(exitCommand?.value, verifyMoveCommand<FungibleAsset.Commands.Move>(inputs, tx))
.filter { it != null }
.requireNoNulls().toSet()
verifyMoveCommand<FungibleAsset.Commands.Move>(inputs, commands)
// 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
import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.GroupClause
import com.r3corda.core.contracts.clauses.MatchBehaviour
import com.r3corda.core.contracts.clauses.Clause
/**
* 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
* 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 sumOrZero: List<S>.(token: Issued<T>) -> Amount<Issued<T>>
) : GroupClause<S, Issued<T>> {
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
) : Clause<S, C, Issued<T>>() {
override fun verify(tx: TransactionForContract,
inputs: List<S>,
outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<T>): Set<CommandData> {
commands: List<AuthenticatedObject<C>>,
groupingKey: Issued<T>?): Set<C> {
require(groupingKey != null)
// TODO: Take in matched commands as a parameter
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).
// The grouping already ensures that all outputs have the same deposit reference and token.
val issuer = token.issuer.party
val inputAmount = inputs.sumOrZero(token)
val issuer = groupingKey!!.issuer.party
val inputAmount = inputs.sumOrZero(groupingKey)
val outputAmount = outputs.sum()
requireThat {
"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)
}
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.sumAmountsDue
import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.MatchBehaviour
import com.r3corda.core.contracts.clauses.SingleClause
import com.r3corda.core.contracts.clauses.Clause
import java.security.PublicKey
/**
@ -43,25 +42,25 @@ data class MultilateralNetState<P>(
* Clause for netting contract states. Currently only supports obligation contract.
*/
// TODO: Make this usable for any nettable contract states
open class NetClause<P> : SingleClause {
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Obligation.Commands.Net::class.java)
open class NetClause<C: CommandData, P> : Clause<ContractState, C, Unit>() {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Obligation.Commands.Net::class.java)
@Suppress("ConvertLambdaToReference")
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
val command = commands.requireSingleCommand<Obligation.Commands.Net>()
override fun verify(tx: TransactionForContract,
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) {
NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State<P> -> it.bilateralNetState }
NetType.PAYMENT -> tx.groupStates { it: Obligation.State<P> -> it.multilateralNetState }
}
for ((inputs, outputs, key) in groups) {
verifyNetCommand(inputs, outputs, command, key)
for ((groupInputs, groupOutputs, key) in groups) {
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
fun verifyNetCommand(inputs: List<Obligation.State<P>>,
outputs: List<Obligation.State<P>>,
command: AuthenticatedObject<Obligation.Commands.Net>,
command: AuthenticatedObject<NetCommand>,
netState: NetState<P>) {
val template = netState.template
// 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.core.contracts.*
import com.r3corda.core.contracts.clauses.GroupClause
import com.r3corda.core.contracts.clauses.MatchBehaviour
import com.r3corda.core.contracts.clauses.Clause
/**
* Clause for fungible asset contracts, which enforces that no output state should have
* a balance of zero.
*/
open class NoZeroSizedOutputs<in S: FungibleAsset<T>, T: Any> : GroupClause<S, Issued<T>> {
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
override val requiredCommands: Set<Class<CommandData>>
get() = emptySet()
open class NoZeroSizedOutputs<in S : FungibleAsset<T>, C : CommandData, T : Any> : Clause<S, C, Issued<T>>() {
override fun verify(tx: TransactionForContract,
inputs: List<S>,
outputs: List<S>,
commands: Collection<AuthenticatedObject<CommandData>>,
token: Issued<T>): Set<CommandData> {
commands: List<AuthenticatedObject<C>>,
groupingKey: Issued<T>?): Set<C> {
requireThat {
"there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L }
}
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.core.contracts.Amount
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.crypto.Party
import com.r3corda.core.node.ServiceHub
import com.r3corda.core.node.services.Wallet
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.testing.DUMMY_NOTARY
import com.r3corda.core.testing.DUMMY_NOTARY_KEY
import java.security.KeyPair
import com.r3corda.core.utilities.DUMMY_NOTARY
import java.security.PublicKey
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 {
val numStates = min + Math.floor(rng.nextDouble() * (max - min)).toInt()
val amounts = LongArray(numStates)
val baseSize = howMuch.quantity / numStates
val numSlots = min + Math.floor(rng.nextDouble() * (max - min)).toInt()
val baseSize = howMuch.quantity / numSlots
check(baseSize > 0) { baseSize }
var filledSoFar = 0L
for (i in 0..numStates - 1) {
if (i < numStates - 1) {
// Adjust the amount a bit up or down, to give more realistic amounts (not all identical).
amounts[i] = baseSize + (baseSize / 2 * (rng.nextDouble() - 0.5)).toLong()
filledSoFar += amounts[i]
val amounts = LongArray(numSlots) { baseSize }
var distanceFromGoal = 0L
// If we want 10 slots then max adjust is 0.1, so even if all random numbers come out to the largest downward
// adjustment possible, the last slot ends at zero. With 20 slots, max adjust is 0.05 etc.
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 {
// Handle inexact rounding.
amounts[i] = howMuch.quantity - filledSoFar
amounts[i] += distanceFromGoal
}
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
}

View File

@ -7,16 +7,17 @@ import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.signWithECDSA
import com.r3corda.core.crypto.toStringsShort
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.random63BitValue
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.trace
import java.security.KeyPair
import java.security.PublicKey
import java.security.SignatureException
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.
val ourSignature = signWithOurKey(partialTX)
val notarySignature = getNotarySignature(partialTX)
return sendSignatures(partialTX, ourSignature, notarySignature)
}
@ -118,16 +118,11 @@ object TwoPartyTradeProtocol {
progressTracker.currentStep = VERIFYING
maybeSTX.validate {
maybeSTX.unwrap {
progressTracker.nextStep()
// Check that the tx proposed by the buyer is valid.
val missingSigs: Set<PublicKey> = it.verifySignatures(throwIfSignaturesAreMissing = false)
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
val wtx: WireTransaction = it.verifySignatures(myKeyPair.public, notaryNode.identity.owningKey)
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,
@ -212,7 +207,7 @@ object TwoPartyTradeProtocol {
val maybeTradeRequest = receive<SellerTradeInfo>(sessionID)
progressTracker.currentStep = VERIFYING
maybeTradeRequest.validate {
maybeTradeRequest.unwrap {
// What is the seller trying to sell us?
val asset = it.assetForSale.state.data
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.
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 {

View File

@ -6,7 +6,8 @@ import kotlin.*;
import org.junit.*;
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

View File

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

View File

@ -1,9 +1,16 @@
package com.r3corda.contracts
import com.r3corda.core.contracts.*
import com.r3corda.core.node.services.testing.MockServices
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 java.math.BigDecimal
import java.time.LocalDate
@ -394,9 +401,10 @@ class IRSTests {
@Test
fun `ensure failure occurs when there are inbound states for an agreement command`() {
val irs = singleIRS()
transaction {
input() { singleIRS() }
output("irs post agreement") { singleIRS() }
input() { irs }
output("irs post agreement") { irs }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails with` "There are no in states for an agreement"
@ -665,10 +673,11 @@ class IRSTests {
transaction("Agreement") {
output("irs post agreement2") {
irs.copy(
irs.fixedLeg,
irs.floatingLeg,
irs.calculation,
irs.common.copy(tradeID = "t2")
linearId = UniqueIdentifier("t2"),
fixedLeg = irs.fixedLeg,
floatingLeg = irs.floatingLeg,
calculation = irs.calculation,
common = irs.common.copy(tradeID = "t2")
)
}
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.SecureHash
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 java.security.PublicKey
import java.util.*
@ -17,7 +21,9 @@ class CashTests {
amount = 1000.DOLLARS `issued by` defaultIssuer,
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(
amount = Amount(amount.quantity, token = amount.token.copy(deposit.copy(reference = OpaqueBytes.of(ref))))
@ -59,7 +65,7 @@ class CashTests {
}
@Test
fun issueMoney() {
fun `issue by move`() {
// Check we can't "move" money into existence.
transaction {
input { DummyState() }
@ -68,7 +74,10 @@ class CashTests {
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
// institution is allowed to issue as much cash as they want.
transaction {
@ -90,28 +99,41 @@ class CashTests {
command(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
this.verifies()
}
}
@Test
fun generateIssueRaw() {
// Test generation works.
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
assertTrue(ptx.inputStates().isEmpty())
val s = ptx.outputStates()[0].data as Cash.State
val tx: WireTransaction = TransactionType.General.Builder(notary = null).apply {
Cash().generateIssue(this, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
signWith(MINI_CORP_KEY)
}.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(MINI_CORP, s.deposit.party)
assertEquals(DUMMY_PUBKEY_1, s.owner)
assertTrue(ptx.commands()[0].value is Cash.Commands.Issue)
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0])
assertTrue(tx.commands[0].value is Cash.Commands.Issue)
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 templatePtx = TransactionType.General.Builder(DUMMY_NOTARY)
Cash().generateIssue(templatePtx, amount, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
assertTrue(templatePtx.inputStates().isEmpty())
assertEquals(ptx.outputStates()[0], templatePtx.outputStates()[0])
val tx: WireTransaction = TransactionType.General.Builder(notary = null).apply {
Cash().generateIssue(this, amount, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
signWith(MINI_CORP_KEY)
}.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.
transaction {
input { inState }
input { issuerInState }
output { inState.copy(amount = inState.amount * 2) }
// Move fails: not allowed to summon money.
@ -154,11 +176,11 @@ class CashTests {
}
tweak {
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 {
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()
}
@ -279,12 +301,12 @@ class CashTests {
fun exitLedger() {
// Single input/output straightforward case.
transaction {
input { inState }
output { outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
input { issuerInState }
output { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
tweak {
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"
}
@ -293,20 +315,24 @@ class CashTests {
this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
tweak {
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
this.verifies()
}
}
}
}
@Test
fun `exit ledger with multiple issuers`() {
// Multi-issuer case.
transaction {
input { inState }
input { inState `issued by` MINI_CORP }
input { issuerInState }
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 { inState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
output { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) `issued by` MINI_CORP }
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"
@ -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
fun multiIssuer() {
transaction {
@ -385,7 +423,7 @@ class CashTests {
*/
fun makeExit(amount: Amount<Currency>, corp: Party, depositRef: Byte = 1): WireTransaction {
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()
}
@ -404,9 +442,10 @@ class CashTests {
assertEquals(WALLET[0].ref, wtx.inputs[0])
assertEquals(0, wtx.outputs.size)
val expected = Cash.Commands.Exit(Amount(10000, Issued(MEGA_CORP.ref(1), USD)))
val actual = wtx.commands.single().value
assertEquals(expected, actual)
val expectedMove = Cash.Commands.Move()
val expectedExit = Cash.Commands.Exit(Amount(10000, Issued(MEGA_CORP.ref(1), USD)))
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.SecureHash
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.testing.*
import com.r3corda.core.utilities.nonEmptySetOf
import com.r3corda.core.utilities.*
import com.r3corda.testing.*
import org.junit.Test
import java.security.PublicKey
import java.time.Duration
@ -119,19 +119,21 @@ class ObligationTests {
}
// Test generation works.
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
Obligation<Currency>().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
beneficiary = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
assertTrue(ptx.inputStates().isEmpty())
val tx = TransactionType.General.Builder(notary = null).apply {
Obligation<Currency>().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
beneficiary = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
signWith(MINI_CORP_KEY)
}.toSignedTransaction().tx
assertTrue(tx.inputs.isEmpty())
val expected = Obligation.State(
obligor = MINI_CORP,
quantity = 100.DOLLARS.quantity,
beneficiary = DUMMY_PUBKEY_1,
template = megaCorpDollarSettlement
)
assertEquals(ptx.outputStates()[0].data, expected)
assertTrue(ptx.commands()[0].value is Obligation.Commands.Issue)
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0])
assertEquals(tx.outputs[0].data, expected)
assertTrue(tx.commands[0].value is Obligation.Commands.Issue)
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.
transaction {
@ -178,11 +180,11 @@ class ObligationTests {
}
tweak {
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 {
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()
}

View File

@ -40,6 +40,7 @@ dependencies {
// Bring in the MockNode infrastructure for writing protocol unit tests.
testCompile project(":node")
testCompile project(":test-utils")
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
@ -55,6 +56,9 @@ dependencies {
// AssertJ: for fluent assertions for testing
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.
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.SecureHash
import com.r3corda.core.transactions.TransactionBuilder
import java.security.PublicKey
// The dummy contract doesn't do anything useful. It exists for testing purposes.
@ -53,14 +54,16 @@ class DummyContract : Contract {
return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owner.party.owningKey))
}
fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: PublicKey): TransactionBuilder {
val priorState = prior.state.data
fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: PublicKey) = move(listOf(prior), newOwner)
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)
return TransactionType.General.Builder(notary = prior.state.notary).withItems(
/* INPUT */ prior,
return TransactionType.General.Builder(notary = priors[0].state.notary).withItems(
/* INPUTS */ *priors.toTypedArray(),
/* COMMAND */ Command(cmd, priorState.owner),
/* OUTPUT */ state
)
}
}
}
}

View File

@ -3,7 +3,7 @@ package com.r3corda.core.contracts
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 {
override val contract = DUMMY_PROGRAM_ID

View File

@ -423,15 +423,43 @@ enum class NetType {
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 commodityCode: String = symbol,
val defaultFractionDigits: Int = 0) {
companion object {
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"))
)
fun getInstance(symbol: String): Commodity?
= registry[symbol]
fun getInstance(commodityCode: String): Commodity?
= 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
import com.r3corda.core.contracts.clauses.Clause
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
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.serialization.OpaqueBytes
import com.r3corda.core.serialization.serialize
import com.r3corda.core.transactions.TransactionBuilder
import java.io.FileNotFoundException
import java.io.InputStream
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
/**
* 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.
*/
interface LinearState : ContractState {
/** Unique thread id within the wallets of all parties */
val thread: SecureHash
interface LinearState: ContractState {
/**
* 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
/**
* 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 {
@ -348,6 +375,12 @@ interface MoveCommand : CommandData {
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. */
data class AuthenticatedObject<out T : Any>(
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.node.services.ReadOnlyTransactionStorage
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.WireTransaction
import java.util.*
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
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
/** 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)
val requiredKeys = getRequiredSigners(tx) + notaryKey
val missing = requiredKeys - tx.signers
val missing = requiredKeys - tx.mustSign
return missing
}

View File

@ -3,6 +3,7 @@ package com.r3corda.core.contracts
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.transactions.LedgerTransaction
import java.security.PublicKey
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.CommandData
import com.r3corda.core.contracts.ContractState
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
// "or", "and", "single" etc. composition of sets of clauses.
interface 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
}
abstract class Clause<in S : ContractState, C : CommandData, in K : Any> {
companion object {
val log = loggerFor<Clause<*, *, *>>()
}
enum class MatchBehaviour {
CONTINUE,
END,
ERROR
}
/** Determine whether this clause runs or not */
open val requiredCommands: Set<Class<out CommandData>> = emptySet()
/**
* 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
* would check each of the output states that it applies to, looking for a zero amount, and throw IllegalStateException
* 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
* 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
* commands that were not required (for example the Exit command for fungible assets is optional).
*/
@Throws(IllegalStateException::class)
fun verify(tx: TransactionForContract,
commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
abstract fun verify(tx: TransactionForContract,
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
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.
*
@ -13,27 +11,15 @@ import java.util.*
* @param commands commands extracted from the transaction, which are relevant to the
* clauses.
*/
fun verifyClauses(tx: TransactionForContract,
clauses: List<SingleClause>,
commands: Collection<AuthenticatedObject<CommandData>>) {
val unmatchedCommands = ArrayList(commands.map { it.value })
verify@ for (clause in clauses) {
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
fun <C: CommandData> verifyClause(tx: TransactionForContract,
clause: Clause<ContractState, C, Unit>,
commands: List<AuthenticatedObject<C>>) {
if (Clause.log.isTraceEnabled) {
clause.getExecutionPath(commands).forEach {
Clause.log.trace("Tx ${tx.origHash} clause: ${clause}")
}
}
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 java.util.*
interface GroupVerify<in S, in T : Any> {
/**
*
* @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>
}
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>>
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 {
abstract val clauses: List<GroupClause<S, T>>
override val requiredCommands: Set<Class<out CommandData>>
get() = emptySet()
abstract fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<S, T>>
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
override fun verify(tx: TransactionForContract,
inputs: List<ContractState>,
outputs: List<ContractState>,
commands: List<AuthenticatedObject<C>>,
groupingKey: Unit?): Set<C> {
val groups = groupStates(tx)
val matchedCommands = HashSet<CommandData>()
val unmatchedCommands = ArrayList(commands.map { it.value })
val matchedCommands = HashSet<C>()
for ((inputs, outputs, token) in groups) {
val temp = verifyGroup(commands, inputs, outputs, token, tx, unmatchedCommands)
matchedCommands.addAll(temp)
unmatchedCommands.removeAll(temp)
for ((groupInputs, groupOutputs, groupToken) in groups) {
matchedCommands.addAll(clause.verify(tx, groupInputs, groupOutputs, commands, groupToken))
}
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 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 CA_CERT_ALIAS = "CA Cert"

View File

@ -1,7 +1,7 @@
package com.r3corda.core.node
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.TransactionResolutionException
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.crypto.Party
import com.r3corda.core.messaging.MessagingService
import com.r3corda.core.messaging.SingleMessageRecipient
import com.r3corda.core.node.NodeInfo
import org.slf4j.LoggerFactory
import java.security.PublicKey
@ -20,8 +21,8 @@ interface NetworkMapCache {
val logger = LoggerFactory.getLogger(NetworkMapCache::class.java)
}
enum class MapChangeType { Added, Removed }
data class MapChange(val node: NodeInfo, val type: MapChangeType )
enum class MapChangeType { Added, Removed, Modified }
data class MapChange(val node: NodeInfo, val prevNodeInfo: NodeInfo?, val type: MapChangeType )
/** A list of nodes that advertise a network map service */
val networkMapNodes: List<NodeInfo>
@ -73,12 +74,12 @@ interface NetworkMapCache {
* updates.
*
* @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 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.
*/
fun addMapService(net: MessagingService, service: NodeInfo,
fun addMapService(net: MessagingService, networkMapAddress: SingleMessageRecipient,
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.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.transactions.WireTransaction
import java.security.KeyPair
import java.security.PrivateKey
import java.security.PublicKey
@ -85,13 +86,13 @@ interface WalletService {
/**
* 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.
/** Returns the [linearHeads] only when the type of the state would be considered an 'instanceof' the given type. */
@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) }
}

View File

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

View File

@ -1,26 +1,35 @@
package com.r3corda.core.protocols
import co.paralleluniverse.fibers.Suspendable
import com.google.common.util.concurrent.ListenableFuture
import com.r3corda.core.crypto.Party
import com.r3corda.core.node.ServiceHub
import com.r3corda.core.utilities.UntrustworthyData
import org.slf4j.Logger
/**
* The interface of [ProtocolStateMachineImpl] exposing methods and properties required by ProtocolLogic for compilation.
*/
interface ProtocolStateMachine<R> {
@Suspendable
fun <T : Any> sendAndReceive(topic: String, destination: Party, sessionIDForSend: Long, sessionIDForReceive: Long,
payload: Any, recvType: Class<T>): UntrustworthyData<T>
fun <T : Any> sendAndReceive(topic: String,
destination: Party,
sessionIDForSend: Long,
sessionIDForReceive: Long,
payload: Any,
receiveType: Class<T>): UntrustworthyData<T>
@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
fun send(topic: String, destination: Party, sessionID: Long, payload: Any)
val serviceHub: ServiceHub
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.node.AttachmentsClassLoader
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.NonEmptySetSerializer
import de.javakaffee.kryoserializers.ArraysAsListSerializer
@ -232,7 +234,7 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
kryo.writeClassAndObject(output, obj.outputs)
kryo.writeClassAndObject(output, obj.commands)
kryo.writeClassAndObject(output, obj.notary)
kryo.writeClassAndObject(output, obj.signers)
kryo.writeClassAndObject(output, obj.mustSign)
kryo.writeClassAndObject(output, obj.type)
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.WalletService
import com.r3corda.core.serialization.SingletonSerializeAsToken
import com.r3corda.core.transactions.WireTransaction
import com.r3corda.core.utilities.loggerFor
import com.r3corda.core.utilities.trace
import rx.Observable
@ -22,9 +23,6 @@ import javax.annotation.concurrent.ThreadSafe
*/
@ThreadSafe
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>()
// 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.
*/
override val linearHeads: Map<SecureHash, StateAndRef<LinearState>>
override val linearHeads: Map<UniqueIdentifier, StateAndRef<LinearState>>
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 {
@ -78,14 +76,6 @@ open class InMemoryWalletService(protected val services: ServiceHub) : Singleton
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
netDelta = walletAndNetDelta.second
return@locked wallet
@ -133,23 +123,4 @@ open class InMemoryWalletService(protected val services: ServiceHub) : Singleton
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.serialization.serialize
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.WireTransaction
import java.security.KeyPair
import java.security.PublicKey
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.
* 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.
*
* @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(
protected val type: TransactionType = TransactionType.General(),
@ -30,7 +37,6 @@ open class TransactionBuilder(
protected val signers: MutableSet<PublicKey> = mutableSetOf(),
protected var timestamp: Timestamp? = null) {
@Deprecated("use timestamp instead")
val time: Timestamp? get() = timestamp
init {
@ -91,10 +97,11 @@ open class TransactionBuilder(
/** The signatures that have been collected so far - might be incomplete! */
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}" }
val data = toWireTransaction().serialize()
addSignatureUnchecked(key.signWithECDSA(data.bits))
return this
}
/**
@ -139,13 +146,12 @@ open class TransactionBuilder(
return SignedTransaction(toWireTransaction().serialize(), ArrayList(currentSigs))
}
open fun addInputState(stateAndRef: StateAndRef<*>) = addInputState(stateAndRef.ref, stateAndRef.state.notary)
fun addInputState(stateRef: StateRef, notary: Party) {
open fun addInputState(stateAndRef: StateAndRef<*>) {
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}\"." }
signers.add(notary.owningKey)
inputs.add(stateRef)
inputs.add(stateAndRef.ref)
}
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.
inline fun <reified T : Any> loggerFor(): org.slf4j.Logger = LoggerFactory.getLogger(T::class.java)
inline fun org.slf4j.Logger.trace(msg: () -> String) {
if (isTraceEnabled) trace(msg())
}
inline fun org.slf4j.Logger.trace(msg: () -> String) { 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 */
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
@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)
}

View File

@ -1,7 +1,9 @@
package com.r3corda.protocols
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.Party
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.protocols.ProtocolLogic
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.UntrustworthyData
import com.r3corda.protocols.AbstractStateReplacementProtocol.Acceptor
import com.r3corda.protocols.AbstractStateReplacementProtocol.Instigator
import java.security.PublicKey
@ -98,7 +103,7 @@ abstract class AbstractStateReplacementProtocol<T> {
sendAndReceive<Ack>(node.identity, 0, sessionIdForReceive, handshake)
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!!)
else {
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 sessionIdForReceive: Long,
override val progressTracker: ProgressTracker = tracker()) : ProtocolLogic<Unit>() {
@ -135,24 +140,21 @@ abstract class AbstractStateReplacementProtocol<T> {
@Suspendable
override fun call() {
progressTracker.currentStep = VERIFYING
val proposal = receive<Proposal<T>>(sessionIdForReceive).validate { it }
val maybeProposal: UntrustworthyData<Proposal<T>> = receive(sessionIdForReceive)
try {
verifyProposal(proposal)
verifyTx(proposal.stx)
val stx: SignedTransaction = maybeProposal.unwrap { verifyProposal(maybeProposal).stx }
verifyTx(stx)
approve(stx)
} catch(e: Exception) {
// TODO: catch only specific exceptions. However, there are numerous validation exceptions
// that might occur (tx validation/resolution, invalid proposal). Need to rethink how
// we manage exceptions and maybe introduce some platform exception hierarchy
val myIdentity = serviceHub.storageService.myLegalIdentity
val state = proposal.stateRef
val state = maybeProposal.unwrap { it.stateRef }
val reason = StateReplacementRefused(myIdentity, state, e.message)
reject(reason)
return
}
approve(proposal.stx)
}
@Suspendable
@ -164,7 +166,7 @@ abstract class AbstractStateReplacementProtocol<T> {
val swapSignatures = sendAndReceive<List<DigitalSignature.WithKey>>(otherSide, sessionIdForSend, sessionIdForReceive, response)
// 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
}
@ -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
* 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
private fun verifyTx(stx: SignedTransaction) {
@ -199,7 +202,7 @@ abstract class AbstractStateReplacementProtocol<T> {
private fun checkMySignatureRequired(tx: WireTransaction) {
// TODO: use keys from the keyManagementService instead
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

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?>>,
requests: List<SecureHash>): List<T> =
maybeItems.validate { response ->
maybeItems.unwrap { response ->
if (response.size != requests.size)
throw BadAnswer()
for ((index, resp) in response.withIndex()) {

View File

@ -1,6 +1,6 @@
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.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
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.transactions.SignedTransaction
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
/**
@ -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
*/
@Suspendable
override fun verifyProposal(proposal: AbstractStateReplacementProtocol.Proposal<Party>) {
val newNotary = proposal.modification
val isNotary = serviceHub.networkMapCache.notaryNodes.any { it.identity == newNotary }
require(isNotary) { "The proposed node $newNotary does not run a Notary service " }
override fun verifyProposal(maybeProposal: UntrustworthyData<AbstractStateReplacementProtocol.Proposal<Party>>): AbstractStateReplacementProtocol.Proposal<Party> {
return maybeProposal.unwrap { proposal ->
val newNotary = proposal.modification
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 proposedTx = proposal.stx.tx
require(proposedTx.inputs.contains(state)) { "The proposed state $state is not in the proposed transaction inputs" }
val state = proposal.stateRef
val proposedTx = proposal.stx.tx
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
val blacklist = listOf("Evil Notary")
require(!blacklist.contains(newNotary.name)) { "The proposed new notary $newNotary is not trusted by the party" }
// An example requirement
val blacklist = listOf("Evil Notary")
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
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.Timestamp
import com.r3corda.core.contracts.WireTransaction
import com.r3corda.core.transactions.WireTransaction
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SignedData
@ -72,7 +72,7 @@ object NotaryProtocol {
private fun validateResponse(response: UntrustworthyData<Result>): Result {
progressTracker.currentStep = VALIDATING
response.validate {
response.unwrap {
if (it.sig != null) validateSignature(it.sig, stx.txBits)
else if (it.error is NotaryError.Conflict) it.error.conflict.verified()
else if (it.error == null || it.error !is NotaryError)
@ -105,7 +105,7 @@ object NotaryProtocol {
@Suspendable
override fun call() {
val (stx, reqIdentity) = receive<SignRequest>(receiveSessionID).validate { it }
val (stx, reqIdentity) = receive<SignRequest>(receiveSessionID).unwrap { it }
val wtx = stx.tx
val result = try {
@ -205,5 +205,5 @@ sealed class 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 com.r3corda.core.contracts.Fix
import com.r3corda.core.contracts.FixOf
import com.r3corda.core.contracts.TransactionBuilder
import com.r3corda.core.contracts.WireTransaction
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.transactions.WireTransaction
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party
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 resp = sendAndReceive<DigitalSignature.LegallyIdentifiable>(oracle, 0, sessionID, req)
return resp.validate { sig ->
return resp.unwrap { sig ->
check(sig.signer == oracle)
tx.checkSignature(sig)
sig
@ -100,7 +100,7 @@ open class RatesFixProtocol(protected val tx: TransactionBuilder,
// TODO: add deadline to receive
val resp = sendAndReceive<ArrayList<Fix>>(oracle, 0, sessionID, req)
return resp.validate {
return resp.unwrap {
val fix = it.first()
// Check the returned fix is for what we asked for.
check(fix.of == fixOf)

View File

@ -2,13 +2,12 @@ package com.r3corda.protocols
import co.paralleluniverse.fibers.Suspendable
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.SecureHash
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.*
// 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 {
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()
@ -61,7 +93,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
@Suspendable
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
// 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.
//
// If 'stx' is set, then 'wtx' is the contents (from the c'tor).
stx?.verifySignatures()
val wtx = stx?.verifySignatures() ?: wtx
wtx?.let {
fetchMissingAttachments(listOf(it))
val ltx = it.toLedgerTransaction(serviceHub)
@ -97,7 +129,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
override val topic: String get() = throw UnsupportedOperationException()
@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
// first traversal across the dependency graph.
//
@ -147,7 +179,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
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.random63BitValue
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.UntrustworthyData
import com.r3corda.core.utilities.trace
import java.math.BigDecimal
import java.security.KeyPair
import java.security.PublicKey
import java.security.SignatureException
import java.time.Duration
/**
@ -98,15 +100,11 @@ object TwoPartyDealProtocol {
fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData<SignedTransaction>): SignedTransaction {
progressTracker.currentStep = VERIFYING
untrustedPartialTX.validate { stx ->
untrustedPartialTX.unwrap { stx ->
progressTracker.nextStep()
// Check that the tx proposed by the buyer is valid.
val missingSigs = stx.verifySignatures(throwIfSignaturesAreMissing = false)
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
val wtx: WireTransaction = stx.verifySignatures(myKeyPair.public, notaryNode.identity.owningKey)
logger.trace { "Received partially signed transaction: ${stx.id}" }
checkDependencies(stx)
@ -242,7 +240,7 @@ object TwoPartyDealProtocol {
val handshake = receive<Handshake<U>>(sessionID)
progressTracker.currentStep = VERIFYING
handshake.validate {
handshake.unwrap {
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.
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 {

View File

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

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