Merge Open Source to Enterprise (pull request #147)

This commit is contained in:
szymonsztuka 2017-12-01 12:28:24 +00:00 committed by GitHub
commit 63fca3e7c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
221 changed files with 3624 additions and 3168 deletions

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="BankOfCordaDriverKt - Issue Web" type="JetRunConfigurationType" factoryName="Kotlin">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.bank.BankOfCordaDriverKt" />
<option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="--role ISSUE_CASH_WEB --quantity 100 --currency USD" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="bank-of-corda-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="BankOfCordaDriverKt - Run Stack" type="JetRunConfigurationType" factoryName="Kotlin">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.bank.BankOfCordaDriverKt" />
<option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="--role ISSUER" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="bank-of-corda-demo_main" />
<envs />
<method />
</configuration>
</component>

137
CONTRIBUTORS.md Normal file
View File

@ -0,0 +1,137 @@
# List of Contributors
We'd like to thank the following people for contributing ideas to Corda,
either during architecture review sessions of the R3 Architecture Working Group,
or in design reviews since Corda has been open-sourced. Some people have moved to
a different organisation since their contribution. Please forgive any omissions, and
create a pull request, or email <james@r3.com>, if you wish to see
changes to this list.
* Alberto Arri (R3)
* Andras Slemmer (R3)
* Andrius Dagys (R3)
* Andrzej Cichocki (R3)
* Anthony Coates (Deutsche Bank)
* Anton Semenov (Commerzbank)
* Antonio Cerrato (SEB)
* Anthony Woolley (Société Générale)
* Arnaud Stevens (Natixis)
* Arijit Das (Northern Trust)
* Arun Battu (BNY Mellon)
* Austin Moothart (R3)
* Barry Childe (HSBC)
* Barry Flower (Westpac)
* Benjamin Abineri (R3)
* Benoit Lafontaine (OCTO)
* Berit Bourgonje (ING)
* Bob Crozier (AIA)
* Bogdan Paunescu (R3)
* Cais Manai (R3)
* Carl Worrall (BCS)
* Chaitanya Jadhav (HSBC)
* Chris Akers (R3)
* Chris Burlinchon (R3)
* Chris Rankin (R3)
* Christian Kaufmann (Credit Suisse)
* Christian Sailer (R3)
* Christopher Saunders (Credit Suisse)
* Christopher Swanson (US Bank)
* Clark Thompson (R3)
* Clay Ratliff (Thoughtworks)
* Clemens Wan (R3)
* Clinton Alexander (R3)
* Daniel Roig (SEB)
* Dave Hudson (R3)
* David Lee (BCS)
* Farzad Pezeshkpour (RBS)
* Frederic Dalibard (Natixis)
* Garrett Macey (Wells Fargo)
* Gavin Thomas (R3)
* George Marcel Smetana (Bradesco)
* Giulio Katis (Westpac)
* Giuseppe Cardone (Intesa Sanpaolo)
* Guy Hochstetler (IBM)
* Ian Cusden (UBS)
* Ian Grigg (R3)
* Igor Nitto (R3)
* Igor Panov (CIBC)
* Ivan Schasny (R3)
* James Brown (R3)
* James Carlyle (R3)
* Jared Harwayne-Gidansky (BNY Mellon)
* Joel Dudley (R3)
* Johan Hörmark (SEB)
* Johann Palychata (BNP Paribas)
* Jonathan Sartin (R3)
* Jose Coll (R3)
* Jose Luu (Natixis)
* Josh Lindl (BCS)
* Justin Chapman (Northern Trust)
* Kai-Michael Schramm (Credit Suisse)
* Karel Hajek (Barclays Capital)
* Kasia Streich (R3)
* Kat Baker (R3)
* Khaild Ahmed (Northern Trust)
* Klaus Apolinario (Bradesco)
* Koen Vingerhoets (KBC)
* Kostas Chalkias (R3)
* Lars Stage Thomsen (Danske Bank)
* Lee Braine (Barclays)
* Lucas Salmen (Itau)
* Maksymillian Pawlak (R3)
* Marek Scocovsky (ABSA)
* Mark Lauer (Westpac)
* Mark Oldfield (R3)
* Mark Raynes (Thomson Reuters)
* Mark Simpson (RBS)
* Mark Tiggas (Wells Fargo)
* Massimo Morini (Banca IMI)
* Mat Rizzo (R3)
* Matt Britton (BCS)
* Matthew Nesbit (R3)
* Matthijs van den Bos (ING)
* Michal Kit (R3)
* Micheal Hinstridge (Thoughtworks)
* Michelle Sollecito (R3)
* Mike Hearn (R3)
* Mike Reichelt (US Bank)
* Mustafa Ozturk (Natixis)
* Nick Skinner (Northern Trust)
* Nigel King (R3)
* Nuam Athaweth (MUFG)
* Oscar Zibordi de Paiva (Bradesco)
* Patrick Kuo (R3)
* Pekka Kaipio (OP Financial)
* Piotr Piskorski (Nordea)
* Przemyslaw Bak (R3)
* Rex Maudsley (Société Générale)
* Richard Green (R3)
* Rick Parker (R3)
* Rhett Brewer (Goldman Sachs)
* Roberto Karpinski (Bradesco)
* Robin Green (CIBC)
* Rodrigo Bueno (Itau)
* Roger Willis (R3)
* Ross Burnett (Macquarie)
* Ross Nicoll (R3)
* Sajindra Jayasena (Deutsche Bank)
* Saket Sharma (BNY Mellon)
* Sam Chadwick (Thomson Reuters)
* Sasmit Sahu (Credit Suisse)
* Scott James (Credit Suisse)
* Shams Asari (R3)
* Simon Taylor (Barclays)
* Sofus Mortensen (Digital Asset Holdings)
* Szymon Sztuka (R3)
* Stephen Lane-Smith (BMO)
* Thomas O'Donnell (Macquarie)
* Thomas Schroeter (R3)
* Tom Menner (R3)
* Tudor Malene (R3)
* Tim Swanson (R3)
* Timothy Smith (Credit Suisse)
* Tommy Lillehagen (R3)
* Viktor Kolomeyko (R3)
* Wawrzek Niewodniczanski (R3)
* Wei Wu Zhang (Commonwealth Bank of Australia)
* Zabrina Smith (Northern Trust)

View File

@ -6,8 +6,7 @@ buildscript {
// Our version: bump this on release.
ext.corda_release_version = "0.16-SNAPSHOT"
// Increment this on any release that changes public APIs anywhere in the Corda platform
// TODO This is going to be difficult until we have a clear separation throughout the code of what is public and what is internal
ext.corda_platform_version = 1
ext.corda_platform_version = constants.getProperty("platformVersion")
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
// Dependency versions. Can run 'gradle dependencyUpdates' to find new versions of things.
@ -49,6 +48,7 @@ buildscript {
ext.commons_collections_version = '4.1'
ext.beanutils_version = '1.9.3'
ext.crash_version = 'faba68332800f21278c5b600bf14ad55cef5989e'
ext.jsr305_version = constants.getProperty("jsr305Version")
ext.spring_jdbc_version ='5.0.0.RELEASE'
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:

View File

@ -12,12 +12,15 @@ import net.corda.core.serialization.serialize
import net.corda.core.utilities.*
import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.nodeapi.RPCApi
import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.IntegrationTest
import net.corda.testing.driver.poll
import net.corda.testing.internal.*
import org.apache.activemq.artemis.api.core.SimpleString
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import rx.Observable
import rx.subjects.PublishSubject
@ -27,6 +30,14 @@ import java.util.concurrent.*
import java.util.concurrent.atomic.AtomicInteger
class RPCStabilityTests : IntegrationTest() {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
private val pool = Executors.newFixedThreadPool(10, testThreadFactory())
@After
fun shutdown() {
pool.shutdown()
}
object DummyOps : RPCOps {
override val protocolVersion = 0
@ -198,9 +209,9 @@ class RPCStabilityTests : IntegrationTest() {
val proxy = startRpcClient<LeakObservableOps>(server.get().broker.hostAndPort!!).get()
// Leak many observables
val N = 200
(1..N).toList().parallelStream().forEach {
proxy.leakObservable()
}
(1..N).map {
pool.fork { proxy.leakObservable(); Unit }
}.transpose().getOrThrow()
// In a loop force GC and check whether the server is notified
while (true) {
System.gc()
@ -232,7 +243,7 @@ class RPCStabilityTests : IntegrationTest() {
assertEquals("pong", client.ping())
serverFollower.shutdown()
startRpcServer<ReconnectOps>(ops = ops, customPort = serverPort).getOrThrow()
val pingFuture = ForkJoinPool.commonPool().fork(client::ping)
val pingFuture = pool.fork(client::ping)
assertEquals("pong", pingFuture.getOrThrow(10.seconds))
clientFollower.shutdown() // Driver would do this after the new server, causing hang.
}

View File

@ -74,7 +74,7 @@ public class StandaloneCordaRPCJavaClientTest {
}
private void copyFinanceCordapp() {
Path cordappsDir = (factory.baseDirectory(notaryConfig).resolve("cordapps"));
Path cordappsDir = (factory.baseDirectory(notaryConfig).resolve(NodeProcess.CORDAPPS_DIR_NAME));
try {
Files.createDirectories(cordappsDir);
} catch (IOException ex) {

View File

@ -86,7 +86,7 @@ class StandaloneCordaRPClientTest {
}
private fun copyFinanceCordapp() {
val cordappsDir = (factory.baseDirectory(notaryConfig) / "cordapps").createDirectories()
val cordappsDir = (factory.baseDirectory(notaryConfig) / NodeProcess.CORDAPPS_DIR_NAME).createDirectories()
// Find the finance jar file for the smoke tests of this module
val financeJar = Paths.get("build", "resources", "smokeTest").list {
it.filter { "corda-finance" in it.toString() }.toList().single()

View File

@ -6,14 +6,20 @@ import net.corda.core.internal.concurrent.map
import net.corda.core.messaging.RPCOps
import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.nodeapi.User
import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.internal.RPCDriverExposedDSLInterface
import net.corda.testing.internal.rpcTestUser
import net.corda.testing.internal.startInVmRpcClient
import net.corda.testing.internal.startRpcClient
import org.apache.activemq.artemis.api.core.client.ClientSession
import org.junit.Rule
import org.junit.runners.Parameterized
open class AbstractRPCTest {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
enum class RPCTestMode {
InVm,
Netty

View File

@ -5,19 +5,22 @@ import net.corda.core.messaging.RPCOps
import net.corda.core.utilities.millis
import net.corda.core.crypto.random63BitValue
import net.corda.core.internal.concurrent.fork
import net.corda.core.internal.concurrent.transpose
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.getOrThrow
import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.testing.internal.RPCDriverExposedDSLInterface
import net.corda.testing.internal.rpcDriver
import net.corda.testing.internal.testThreadFactory
import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import rx.Observable
import rx.subjects.UnicastSubject
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.*
@RunWith(Parameterized::class)
class RPCConcurrencyTests : AbstractRPCTest() {
@ -36,7 +39,7 @@ class RPCConcurrencyTests : AbstractRPCTest() {
fun getParallelObservableTree(depth: Int, branchingFactor: Int): ObservableRose<Int>
}
class TestOpsImpl : TestOps {
class TestOpsImpl(private val pool: Executor) : TestOps {
private val latches = ConcurrentHashMap<Long, CountDownLatch>()
override val protocolVersion = 0
@ -68,24 +71,22 @@ class RPCConcurrencyTests : AbstractRPCTest() {
val branches = if (depth == 0) {
Observable.empty<ObservableRose<Int>>()
} else {
val publish = UnicastSubject.create<ObservableRose<Int>>()
ForkJoinPool.commonPool().fork {
(1..branchingFactor).toList().parallelStream().forEach {
publish.onNext(getParallelObservableTree(depth - 1, branchingFactor))
UnicastSubject.create<ObservableRose<Int>>().also { publish ->
(1..branchingFactor).map {
pool.fork { publish.onNext(getParallelObservableTree(depth - 1, branchingFactor)) }
}.transpose().then {
it.getOrThrow()
publish.onCompleted()
}
publish.onCompleted()
}
publish
}
return ObservableRose(depth, branches)
}
}
private lateinit var testOpsImpl: TestOpsImpl
private fun RPCDriverExposedDSLInterface.testProxy(): TestProxy<TestOps> {
testOpsImpl = TestOpsImpl()
return testProxy<TestOps>(
testOpsImpl,
TestOpsImpl(pool),
clientConfiguration = RPCClientConfiguration.default.copy(
reapInterval = 100.millis,
cacheConcurrencyLevel = 16
@ -96,6 +97,12 @@ class RPCConcurrencyTests : AbstractRPCTest() {
)
}
private val pool = Executors.newFixedThreadPool(10, testThreadFactory())
@After
fun shutdown() {
pool.shutdown()
}
@Test
fun `call multiple RPCs in parallel`() {
rpcDriver {
@ -103,19 +110,17 @@ class RPCConcurrencyTests : AbstractRPCTest() {
val numberOfBlockedCalls = 2
val numberOfDownsRequired = 100
val id = proxy.ops.newLatch(numberOfDownsRequired)
val done = CountDownLatch(numberOfBlockedCalls)
// Start a couple of blocking RPC calls
(1..numberOfBlockedCalls).forEach {
ForkJoinPool.commonPool().fork {
val done = (1..numberOfBlockedCalls).map {
pool.fork {
proxy.ops.waitLatch(id)
done.countDown()
}
}
}.transpose()
// Down the latch that the others are waiting for concurrently
(1..numberOfDownsRequired).toList().parallelStream().forEach {
proxy.ops.downLatch(id)
}
done.await()
(1..numberOfDownsRequired).map {
pool.fork { proxy.ops.downLatch(id) }
}.transpose().getOrThrow()
done.getOrThrow()
}
}
@ -146,7 +151,7 @@ class RPCConcurrencyTests : AbstractRPCTest() {
fun ObservableRose<Int>.subscribeToAll() {
remainingLatch.countDown()
this.branches.subscribe { tree ->
(tree.value + 1..treeDepth - 1).forEach {
(tree.value + 1 until treeDepth).forEach {
require(it in depthsSeen) { "Got ${tree.value} before $it" }
}
depthsSeen.add(tree.value)
@ -165,11 +170,11 @@ class RPCConcurrencyTests : AbstractRPCTest() {
val treeDepth = 2
val treeBranchingFactor = 10
val remainingLatch = CountDownLatch((intPower(treeBranchingFactor, treeDepth + 1) - 1) / (treeBranchingFactor - 1))
val depthsSeen = Collections.synchronizedSet(HashSet<Int>())
val depthsSeen = ConcurrentHashSet<Int>()
fun ObservableRose<Int>.subscribeToAll() {
remainingLatch.countDown()
branches.subscribe { tree ->
(tree.value + 1..treeDepth - 1).forEach {
(tree.value + 1 until treeDepth).forEach {
require(it in depthsSeen) { "Got ${tree.value} before $it" }
}
depthsSeen.add(tree.value)

View File

@ -5,12 +5,18 @@ import net.corda.core.concurrent.CordaFuture
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.messaging.*
import net.corda.core.utilities.getOrThrow
import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.internal.rpcDriver
import net.corda.testing.internal.startRpcClient
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
class RPCFailureTests {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
class Unserializable
interface Ops : RPCOps {
fun getUnserializable(): Unserializable

View File

@ -49,8 +49,7 @@ class IdentitySyncFlowTests {
val alice: Party = aliceNode.info.singleIdentity()
val bob: Party = bobNode.info.singleIdentity()
val notary = mockNet.defaultNotaryIdentity
bobNode.internals.registerInitiatedFlow(Receive::class.java)
bobNode.registerInitiatedFlow(Receive::class.java)
// Alice issues then pays some cash to a new confidential identity that Bob doesn't know about
val anonymous = true
val ref = OpaqueBytes.of(0x01)
@ -80,8 +79,7 @@ class IdentitySyncFlowTests {
val bob: Party = bobNode.info.singleIdentity()
val charlie: Party = charlieNode.info.singleIdentity()
val notary = mockNet.defaultNotaryIdentity
bobNode.internals.registerInitiatedFlow(Receive::class.java)
bobNode.registerInitiatedFlow(Receive::class.java)
// Charlie issues then pays some cash to a new confidential identity
val anonymous = true
val ref = OpaqueBytes.of(0x01)

View File

@ -1,5 +1,7 @@
gradlePluginsVersion=2.0.9
gradlePluginsVersion=3.0.0
kotlinVersion=1.1.60
platformVersion=1
guavaVersion=21.0
bouncycastleVersion=1.57
typesafeConfigVersion=1.3.1
jsr305Version=3.0.2

View File

@ -78,7 +78,7 @@ dependencies {
compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8"
// Thread safety annotations
compile "com.google.code.findbugs:jsr305:3.0.1"
compile "com.google.code.findbugs:jsr305:$jsr305_version"
// Log4J: logging framework (ONLY explicitly referenced by net.corda.core.utilities.Logging.kt)
compile "org.apache.logging.log4j:log4j-core:${log4j_version}"

View File

@ -31,6 +31,8 @@ import java.time.Duration
import java.time.temporal.Temporal
import java.util.*
import java.util.Spliterator.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.TimeUnit
import java.util.stream.IntStream
import java.util.stream.Stream
import java.util.stream.StreamSupport
@ -307,3 +309,10 @@ fun TransactionBuilder.toLedgerTransaction(services: ServiceHub, serializationCo
val KClass<*>.packageName: String get() = java.`package`.name
fun URL.openHttpConnection(): HttpURLConnection = openConnection() as HttpURLConnection
/** Analogous to [Thread.join]. */
fun ExecutorService.join() {
shutdown() // Do not change to shutdownNow, tests use this method to assert the executor has no more tasks.
while (!awaitTermination(1, TimeUnit.SECONDS)) {
// Try forever. Do not give up, tests use this method to assert the executor has no more tasks.
}
}

View File

@ -43,7 +43,7 @@ class ThreadLocalToggleField<T>(name: String) : ToggleField<T>(name) {
}
/** The named thread has leaked from a previous test. */
class ThreadLeakException : RuntimeException("Leaked thread detected: ${Thread.currentThread().name}")
class ThreadLeakException(valueToString: String) : RuntimeException("Leaked thread '${Thread.currentThread().name}' detected, value was: $valueToString")
/** @param isAGlobalThreadBeingCreated whether a global thread (that should not inherit any value) is being created. */
class InheritableThreadLocalToggleField<T>(name: String,
@ -54,16 +54,12 @@ class InheritableThreadLocalToggleField<T>(name: String,
}
private inner class Holder(value: T) : AtomicReference<T?>(value) {
fun valueOrDeclareLeak() = get() ?: throw ThreadLeakException()
private val valueToString = value.toString() // We never set another non-null value.
fun valueOrDeclareLeak() = get() ?: throw ThreadLeakException(valueToString)
fun childValue(): Holder? {
val e = ThreadLeakException() // Expensive, but so is starting the new thread.
return if (isAGlobalThreadBeingCreated(e.stackTrace)) {
get() ?: log.warn(e.message)
null
} else {
get() ?: log.error(e.message)
this
}
val e = ThreadLeakException(valueToString) // Expensive, but so is starting the new thread.
get() ?: log.warn(e.message)
return if (isAGlobalThreadBeingCreated(e.stackTrace)) null else this
}
}

View File

@ -16,7 +16,7 @@ interface SerializationEnvironment {
val checkpointContext: SerializationContext
}
class SerializationEnvironmentImpl(
open class SerializationEnvironmentImpl(
override val serializationFactory: SerializationFactory,
override val p2pContext: SerializationContext,
rpcServerContext: SerializationContext? = null,

View File

@ -0,0 +1,75 @@
package net.corda.core
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.*
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.nodeapi.User
import net.corda.smoketesting.NodeConfig
import net.corda.smoketesting.NodeProcess
import net.corda.testing.common.internal.ProjectStructure
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.nio.file.Paths
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import java.util.jar.JarFile
import kotlin.streams.toList
class NodeVersioningTest {
private companion object {
val user = User("user1", "test", permissions = setOf("ALL"))
val port = AtomicInteger(15100)
val expectedPlatformVersion = (ProjectStructure.projectRootDir / "constants.properties").read {
val constants = Properties()
constants.load(it)
constants.getProperty("platformVersion").toInt()
}
}
private val factory = NodeProcess.Factory()
private val aliceConfig = NodeConfig(
legalName = CordaX500Name(organisation = "Alice Corp", locality = "Madrid", country = "ES"),
p2pPort = port.andIncrement,
rpcPort = port.andIncrement,
webPort = port.andIncrement,
isNotary = false,
users = listOf(user)
)
@Test
fun `platform version in manifest file`() {
val manifest = JarFile(factory.cordaJar.toFile()).manifest
assertThat(manifest.mainAttributes.getValue("Corda-Platform-Version").toInt()).isEqualTo(expectedPlatformVersion)
}
@Test
fun `platform version from RPC`() {
val cordappsDir = (factory.baseDirectory(aliceConfig) / NodeProcess.CORDAPPS_DIR_NAME).createDirectories()
// Find the jar file for the smoke tests of this module
val selfCordapp = Paths.get("build", "libs").list {
it.filter { "-smokeTests" in it.toString() }.toList().single()
}
selfCordapp.copyToDirectory(cordappsDir)
factory.create(aliceConfig).use { alice ->
alice.connect().use {
val rpc = it.proxy
assertThat(rpc.protocolVersion).isEqualTo(expectedPlatformVersion)
assertThat(rpc.nodeInfo().platformVersion).isEqualTo(expectedPlatformVersion)
assertThat(rpc.startFlow(NodeVersioningTest::GetPlatformVersionFlow).returnValue.getOrThrow()).isEqualTo(expectedPlatformVersion)
}
}
}
@StartableByRPC
class GetPlatformVersionFlow : FlowLogic<Int>() {
@Suspendable
override fun call(): Int = serviceHub.myInfo.platformVersion
}
}

View File

@ -14,6 +14,7 @@ import net.corda.core.utilities.unwrap
import net.corda.nodeapi.User
import net.corda.smoketesting.NodeConfig
import net.corda.smoketesting.NodeProcess
import net.corda.smoketesting.NodeProcess.Companion.CORDAPPS_DIR_NAME
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.nio.file.Paths
@ -22,7 +23,6 @@ import kotlin.streams.toList
class CordappSmokeTest {
private companion object {
private const val CORDAPPS_DIR_NAME = "cordapps"
val user = User("user1", "test", permissions = setOf("ALL"))
val port = AtomicInteger(15100)
}
@ -38,7 +38,6 @@ class CordappSmokeTest {
users = listOf(user)
)
@Test
fun `FlowContent appName returns the filename of the CorDapp jar`() {
val cordappsDir = (factory.baseDirectory(aliceConfig) / CORDAPPS_DIR_NAME).createDirectories()

View File

@ -38,7 +38,7 @@ public class FlowsInJavaTest {
@Test
public void suspendableActionInsideUnwrap() throws Exception {
bobNode.getInternals().registerInitiatedFlow(SendHelloAndThenReceive.class);
bobNode.registerInitiatedFlow(SendHelloAndThenReceive.class);
Future<String> result = startFlow(aliceNode.getServices(), new SendInUnwrapFlow(bob)).getResultFuture();
mockNet.runNetwork();
assertThat(result.get()).isEqualTo("Hello");

View File

@ -8,7 +8,7 @@ import net.corda.core.internal.div
import net.corda.core.serialization.serialize
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.toBase58String
import net.corda.node.utilities.*
import net.corda.nodeapi.internal.crypto.*
import net.corda.testing.kryoSpecific
import net.corda.testing.SerializationEnvironmentRule
import org.junit.Rule

View File

@ -38,24 +38,20 @@ class PartialMerkleTreeTest {
testLedger = ledger {
unverifiedTransaction {
attachments(Cash.PROGRAM_ID)
output(Cash.PROGRAM_ID, "MEGA_CORP cash") {
output(Cash.PROGRAM_ID, "MEGA_CORP cash",
Cash.State(
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
owner = MEGA_CORP
)
}
output(Cash.PROGRAM_ID, "dummy cash 1") {
owner = MEGA_CORP))
output(Cash.PROGRAM_ID, "dummy cash 1",
Cash.State(
amount = 900.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
owner = MINI_CORP
)
}
owner = MINI_CORP))
}
transaction {
attachments(Cash.PROGRAM_ID)
input("MEGA_CORP cash")
output(Cash.PROGRAM_ID, "MEGA_CORP cash".output<Cash.State>().copy(owner = MINI_CORP))
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
timeWindow(TEST_TX_TIME)
this.verifies()
}

View File

@ -3,7 +3,7 @@ package net.corda.core.crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.toTypedArray
import net.corda.core.internal.cert
import net.corda.node.utilities.*
import net.corda.nodeapi.internal.crypto.*
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralSubtree
@ -50,7 +50,7 @@ class X509NameConstraintsTest {
val nameConstraints = NameConstraints(acceptableNames, arrayOf())
val pathValidator = CertPathValidator.getInstance("PKIX")
val certFactory = CertificateFactory.getInstance("X509")
val certFactory = X509CertificateFactory().delegate
assertFailsWith(CertPathValidatorException::class) {
val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank B"), nameConstraints)
@ -85,7 +85,7 @@ class X509NameConstraintsTest {
.map { GeneralSubtree(GeneralName(X500Name(it))) }.toTypedArray()
val nameConstraints = NameConstraints(acceptableNames, arrayOf())
val certFactory = CertificateFactory.getInstance("X509")
val certFactory = X509CertificateFactory().delegate
Crypto.ECDSA_SECP256R1_SHA256
val pathValidator = CertPathValidator.getInstance("PKIX", BouncyCastleProvider.PROVIDER_NAME)

View File

@ -52,10 +52,8 @@ class AttachmentTests {
val bobNode = mockNet.createPartyNode(BOB.name)
val alice = aliceNode.info.singleIdentity()
aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
// Insert an attachment into node zero's store directly.
val id = aliceNode.database.transaction {
aliceNode.attachments.importAttachment(ByteArrayInputStream(fakeAttachment()))
@ -85,10 +83,8 @@ class AttachmentTests {
fun `missing`() {
val aliceNode = mockNet.createPartyNode(ALICE.name)
val bobNode = mockNet.createPartyNode(BOB.name)
aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
// Get node one to fetch a non-existent attachment.
val hash = SecureHash.randomSHA256()
val alice = aliceNode.info.singleIdentity()
@ -108,10 +104,8 @@ class AttachmentTests {
})
val bobNode = mockNet.createNode(MockNodeParameters(legalName = BOB.name))
val alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME)
aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
val attachment = fakeAttachment()
// Insert an attachment into node zero's store directly.
val id = aliceNode.database.transaction {

View File

@ -50,7 +50,7 @@ class CollectSignaturesFlowTests {
private fun registerFlowOnAllNodes(flowClass: KClass<out FlowLogic<*>>) {
listOf(aliceNode, bobNode, charlieNode).forEach {
it.internals.registerInitiatedFlow(flowClass.java)
it.registerInitiatedFlow(flowClass.java)
}
}

View File

@ -133,7 +133,7 @@ class ContractUpgradeFlowTest {
@Test
fun `2 parties contract upgrade using RPC`() {
rpcDriver(initialiseSerialization = false) {
rpcDriver {
// Create dummy contract.
val twoPartyDummyContract = DummyContract.generateInitial(0, notary, alice.ref(1), bob.ref(1))
val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract)

View File

@ -38,14 +38,14 @@ class NoAnswer(private val closure: () -> Unit = {}) : FlowLogic<Unit>() {
* Allows to register a flow of type [R] against an initiating flow of type [I].
*/
inline fun <I : FlowLogic<*>, reified R : FlowLogic<*>> StartedNode<*>.registerInitiatedFlow(initiatingFlowType: KClass<I>, crossinline construct: (session: FlowSession) -> R) {
internals.internalRegisterFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> construct(session) }, R::class.javaObjectType, true)
internalRegisterFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> construct(session) }, R::class.javaObjectType, true)
}
/**
* Allows to register a flow of type [Answer] against an initiating flow of type [I], returning a valure of type [R].
*/
inline fun <I : FlowLogic<*>, reified R : Any> StartedNode<*>.registerAnswer(initiatingFlowType: KClass<I>, value: R) {
internals.internalRegisterFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> Answer(session, value) }, Answer::class.javaObjectType, true)
internalRegisterFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> Answer(session, value) }, Answer::class.javaObjectType, true)
}
/**

View File

@ -4,8 +4,8 @@ import net.corda.core.crypto.entropyToKeyPair
import net.corda.core.internal.read
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.node.utilities.KEYSTORE_TYPE
import net.corda.node.utilities.save
import net.corda.nodeapi.internal.crypto.KEYSTORE_TYPE
import net.corda.nodeapi.internal.crypto.save
import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.getTestPartyAndCertificate
import org.assertj.core.api.Assertions.assertThat

View File

@ -42,8 +42,8 @@ class ResolveTransactionsFlowTest {
notaryNode = mockNet.defaultNotaryNode
megaCorpNode = mockNet.createPartyNode(MEGA_CORP.name)
miniCorpNode = mockNet.createPartyNode(MINI_CORP.name)
megaCorpNode.internals.registerInitiatedFlow(TestResponseFlow::class.java)
miniCorpNode.internals.registerInitiatedFlow(TestResponseFlow::class.java)
megaCorpNode.registerInitiatedFlow(TestResponseFlow::class.java)
miniCorpNode.registerInitiatedFlow(TestResponseFlow::class.java)
notary = mockNet.defaultNotaryIdentity
megaCorp = megaCorpNode.info.singleIdentity()
miniCorp = miniCorpNode.info.singleIdentity()

View File

@ -14,7 +14,6 @@ import org.junit.runners.model.Statement
import org.slf4j.Logger
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
import kotlin.test.assertNull
@ -23,10 +22,7 @@ private fun <T> withSingleThreadExecutor(callable: ExecutorService.() -> T) = Ex
fork {}.getOrThrow() // Start the thread.
callable()
} finally {
shutdown()
while (!awaitTermination(1, TimeUnit.SECONDS)) {
// Do nothing.
}
join()
}
}
@ -134,6 +130,7 @@ class ToggleFieldTest {
assertThatThrownBy { future.getOrThrow() }
.isInstanceOf(ThreadLeakException::class.java)
.hasMessageContaining(threadName)
.hasMessageContaining("hello")
}
}
withSingleThreadExecutor {
@ -141,9 +138,9 @@ class ToggleFieldTest {
}
}
/** We log an error rather than failing-fast as the new thread may be an undetected global. */
/** We log a warning rather than failing-fast as the new thread may be an undetected global. */
@Test
fun `leaked thread propagates holder to non-global thread, with error`() {
fun `leaked thread propagates holder to non-global thread, with warning`() {
val field = inheritableThreadLocalToggleField<String>()
field.set("hello")
withSingleThreadExecutor {
@ -153,17 +150,18 @@ class ToggleFieldTest {
val leakedThreadName = Thread.currentThread().name
verifyNoMoreInteractions(log)
withSingleThreadExecutor {
// If ThreadLeakException is seen in practice, these errors form a trail of where the holder has been:
verify(log).error(argThat { contains(leakedThreadName) })
// If ThreadLeakException is seen in practice, these warnings form a trail of where the holder has been:
verify(log).warn(argThat { contains(leakedThreadName) && contains("hello") })
val newThreadName = fork { Thread.currentThread().name }.getOrThrow()
val future = fork(field::get)
assertThatThrownBy { future.getOrThrow() }
.isInstanceOf(ThreadLeakException::class.java)
.hasMessageContaining(newThreadName)
.hasMessageContaining("hello")
fork {
verifyNoMoreInteractions(log)
withSingleThreadExecutor {
verify(log).error(argThat { contains(newThreadName) })
verify(log).warn(argThat { contains(newThreadName) && contains("hello") })
}
}.getOrThrow()
}
@ -183,7 +181,7 @@ class ToggleFieldTest {
globalThreadCreationMethod {
verifyNoMoreInteractions(log)
withSingleThreadExecutor {
verify(log).warn(argThat { contains(leakedThreadName) })
verify(log).warn(argThat { contains(leakedThreadName) && contains("hello") })
// In practice the new thread is for example a static thread we can't get rid of:
assertNull(fork(field::get).getOrThrow())
}

View File

@ -2,13 +2,13 @@ package net.corda.core.internal.concurrent
import com.nhaarman.mockito_kotlin.*
import net.corda.core.concurrent.CordaFuture
import net.corda.core.internal.join
import net.corda.core.utilities.getOrThrow
import net.corda.testing.rigorousMock
import org.assertj.core.api.Assertions
import org.junit.Test
import org.slf4j.Logger
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@ -108,10 +108,7 @@ class CordaFutureTest {
val throwable = Exception("Boom")
val executor = Executors.newSingleThreadExecutor()
executor.fork { throw throwable }.andForget(log)
executor.shutdown()
while (!executor.awaitTermination(1, TimeUnit.SECONDS)) {
// Do nothing.
}
executor.join()
verify(log).error(any(), same(throwable))
}

View File

@ -15,7 +15,7 @@ import net.corda.core.utilities.unwrap
import net.corda.node.internal.InitiatedFlowFactory
import net.corda.node.internal.StartedNode
import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.node.utilities.currentDBSession
import net.corda.nodeapi.internal.persistence.currentDBSession
import net.corda.testing.ALICE_NAME
import net.corda.testing.BOB_NAME
import net.corda.testing.node.MockNetwork
@ -148,7 +148,7 @@ class AttachmentSerializationTest {
}
private fun launchFlow(clientLogic: ClientLogic, rounds: Int, sendData: Boolean = false) {
server.internals.internalRegisterFlowFactory(
server.internalRegisterFlowFactory(
ClientLogic::class.java,
InitiatedFlowFactory.Core { ServerLogic(it, sendData) },
ServerLogic::class.java,

View File

@ -55,10 +55,10 @@ class TransactionEncumbranceTests {
ledger {
transaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
input(Cash.PROGRAM_ID) { state }
output(Cash.PROGRAM_ID, encumbrance = 1) { stateWithNewOwner }
output(TEST_TIMELOCK_ID, "5pm time-lock") { timeLock }
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
input(Cash.PROGRAM_ID, state)
output(Cash.PROGRAM_ID, encumbrance = 1, contractState = stateWithNewOwner)
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
verifies()
}
}
@ -69,16 +69,16 @@ class TransactionEncumbranceTests {
ledger {
unverifiedTransaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock") { state }
output(TEST_TIMELOCK_ID, "5pm time-lock") { timeLock }
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", state)
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
}
// Un-encumber the output if the time of the transaction is later than the timelock.
transaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
input("state encumbered by 5pm time-lock")
input("5pm time-lock")
output(Cash.PROGRAM_ID) { stateWithNewOwner }
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
output(Cash.PROGRAM_ID, stateWithNewOwner)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
timeWindow(FIVE_PM)
verifies()
}
@ -90,16 +90,16 @@ class TransactionEncumbranceTests {
ledger {
unverifiedTransaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock") { state }
output(TEST_TIMELOCK_ID, "5pm time-lock") { timeLock }
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", state)
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
}
// The time of the transaction is earlier than the time specified in the encumbering timelock.
transaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
input("state encumbered by 5pm time-lock")
input("5pm time-lock")
output(Cash.PROGRAM_ID) { state }
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
output(Cash.PROGRAM_ID, state)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
timeWindow(FOUR_PM)
this `fails with` "the time specified in the time-lock has passed"
}
@ -111,14 +111,14 @@ class TransactionEncumbranceTests {
ledger {
unverifiedTransaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", encumbrance = 1) { state }
output(TEST_TIMELOCK_ID, "5pm time-lock") { timeLock }
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", encumbrance = 1, contractState = state)
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
}
transaction {
attachments(Cash.PROGRAM_ID)
input("state encumbered by 5pm time-lock")
output(Cash.PROGRAM_ID) { stateWithNewOwner }
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
output(Cash.PROGRAM_ID, stateWithNewOwner)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
timeWindow(FIVE_PM)
this `fails with` "Missing required encumbrance 1 in INPUT"
}
@ -130,9 +130,9 @@ class TransactionEncumbranceTests {
ledger {
transaction {
attachments(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { state }
output(Cash.PROGRAM_ID, encumbrance = 0) { stateWithNewOwner }
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
input(Cash.PROGRAM_ID, state)
output(Cash.PROGRAM_ID, encumbrance = 0, contractState = stateWithNewOwner)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
this `fails with` "Missing required encumbrance 0 in OUTPUT"
}
}
@ -143,10 +143,10 @@ class TransactionEncumbranceTests {
ledger {
transaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
input(Cash.PROGRAM_ID) { state }
output(TEST_TIMELOCK_ID, encumbrance = 2) { stateWithNewOwner }
output(TEST_TIMELOCK_ID) { timeLock }
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
input(Cash.PROGRAM_ID, state)
output(TEST_TIMELOCK_ID, encumbrance = 2, contractState = stateWithNewOwner)
output(TEST_TIMELOCK_ID, timeLock)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
this `fails with` "Missing required encumbrance 2 in OUTPUT"
}
}
@ -157,16 +157,16 @@ class TransactionEncumbranceTests {
ledger {
unverifiedTransaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
output(Cash.PROGRAM_ID, "state encumbered by some other state", encumbrance = 1) { state }
output(Cash.PROGRAM_ID, "some other state") { state }
output(TEST_TIMELOCK_ID, "5pm time-lock") { timeLock }
output(Cash.PROGRAM_ID, "state encumbered by some other state", encumbrance = 1, contractState = state)
output(Cash.PROGRAM_ID, "some other state", state)
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
}
transaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
input("state encumbered by some other state")
input("5pm time-lock")
output(Cash.PROGRAM_ID) { stateWithNewOwner }
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
output(Cash.PROGRAM_ID, stateWithNewOwner)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
timeWindow(FIVE_PM)
this `fails with` "Missing required encumbrance 1 in INPUT"
}

View File

@ -6,6 +6,9 @@ from the previous milestone release.
UNRELEASED
----------
* Removed confusing property database.initDatabase, enabling its guarded behaviour with the dev-mode.
In devMode Hibernate will try to create or update database schemas, otherwise it will expect relevant schemas to be present
in the database (pre configured via DDL scripts or equivalent), and validate these are correct.
* ``ConfigUtilities`` now read system properties for a node. This allow to specify data source properties at runtime.

View File

@ -108,24 +108,26 @@ path to the node's base directory.
:notary: Optional configuration object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt
cluster then specify ``raft`` or ``bftSMaRt`` respectively as described below. If a single node notary then omit both.
:validating: Boolean to determine whether the notary is a validating or non-validating one.
:validating: Boolean to determine whether the notary is a validating or non-validating one.
:raft: If part of a distributed Raft cluster specify this config object, with the following settings:
:raft: If part of a distributed Raft cluster specify this config object, with the following settings:
:nodeAddress: The host and port to which to bind the embedded Raft server. Note that the Raft cluster uses a
separate transport layer for communication that does not integrate with ArtemisMQ messaging services.
:nodeAddress: The host and port to which to bind the embedded Raft server. Note that the Raft cluster uses a
separate transport layer for communication that does not integrate with ArtemisMQ messaging services.
:clusterAddresses: List of Raft cluster member addresses used to join the cluster. At least one of the specified
members must be active and be able to communicate with the cluster leader for joining. If empty, a new
cluster will be bootstrapped.
:clusterAddresses: Must list the addresses of all the members in the cluster. At least one of the members must
be active and be able to communicate with the cluster leader for the node to join the cluster. If empty, a
new cluster will be bootstrapped.
:bftSMaRt: If part of a distributed BFT-SMaRt cluster specify this config object, with the following settings:
:bftSMaRt: If part of a distributed BFT-SMaRt cluster specify this config object, with the following settings:
:replicaId: The zero-based index of the current replica. All replicas must specify a unique replica id.
:replicaId: The zero-based index of the current replica. All replicas must specify a unique replica id.
:clusterAddresses: List of all BFT-SMaRt cluster member addresses.
:clusterAddresses: Must list the addresses of all the members in the cluster. At least one of the members must
be active and be able to communicate with the cluster leader for the node to join the cluster. If empty, a
new cluster will be bootstrapped.
:custom: If `true`, will load and install a notary service from a CorDapp. See :doc:`tutorial-custom-notary`.
:custom: If `true`, will load and install a notary service from a CorDapp. See :doc:`tutorial-custom-notary`.
Only one of ``raft``, ``bftSMaRt`` or ``custom`` configuration values may be specified.
@ -136,17 +138,19 @@ path to the node's base directory.
:rpcUsers: A list of users who are authorised to access the RPC system. Each user in the list is a config object with the
following fields:
:username: Username consisting only of word characters (a-z, A-Z, 0-9 and _)
:password: The password
:permissions: A list of permission strings which RPC methods can use to control access
If this field is absent or an empty list then RPC is effectively locked down. Alternatively, if it contains the string
``ALL`` then the user is permitted to use *any* RPC method. This value is intended for administrator users and for developers.
:username: Username consisting only of word characters (a-z, A-Z, 0-9 and _)
:password: The password
:permissions: A list of permissions for starting flows via RPC. To give the user the permission to start the flow
``foo.bar.FlowClass``, add the string ``StartFlow.foo.bar.FlowClass`` to the list. If the list
contains the string ``ALL``, the user can start any flow via RPC. This value is intended for administrator
users and for development.
:devMode: This flag sets the node to run in development mode. On startup, if the keystore ``<workspace>/certificates/sslkeystore.jks``
does not exist, a developer keystore will be used if ``devMode`` is true. The node will exit if ``devMode`` is false
and the keystore does not exist. ``devMode`` also turns on background checking of flow checkpoints to shake out any
bugs in the checkpointing process.
bugs in the checkpointing process. Also, if ``devMode`` is true, Hibernate will try to automatically create the schema required by Corda
or update an existing schema in the SQL database; if ``devMode`` is false, Hibernate will simply validate an existing schema
failing on node start if this schema is either not present or not compatible.
:detectPublicIp: This flag toggles the auto IP detection behaviour, it is enabled by default. On startup the node will
attempt to discover its externally visible IP address first by looking for any public addresses on its network
@ -168,7 +172,7 @@ path to the node's base directory.
:sshd: If provided, node will start internal SSH server which will provide a management shell. It uses the same credentials
and permissions as RPC subsystem. It has one required parameter.
:port: - the port to start SSH server on
:port: The port to start SSH server on
:relay: If provided, the node will attempt to tunnel inbound connections via an external relay. The relay's address will be
advertised to the network map service instead of the provided ``p2pAddress``.

View File

@ -4,8 +4,9 @@ Corda nodes
.. toctree::
:maxdepth: 1
deploying-a-node
generating-a-node
running-a-node
deploying-a-node
corda-configuration-file
clientrpc
shell

View File

@ -118,9 +118,9 @@ Creating the CorDapp JAR
The gradle ``jar`` task included in the CorDapp template build file will automatically build your CorDapp JAR correctly
as long as your dependencies are set correctly.
Note that the hash of the resulting CorDapp JAR is not deterministic, as it depends on variables such as the timestamp
at creation. Nodes running the same CorDapp must therefore ensure they are using the exact same CorDapp jar, and not
different versions of the JAR created from identical sources.
.. warning:: The hash of the generated CorDapp JAR is not deterministic, as it depends on variables such as the
timestamp at creation. Nodes running the same CorDapp must therefore ensure they are using the exact same CorDapp
jar, and not different versions of the JAR created from identical sources.
The filename of the JAR must include a unique identifier to deduplicate it from other releases of the same CorDapp.
This is typically done by appending the version string to the CorDapp's name. This unique identifier should not change
@ -131,7 +131,7 @@ Installing the CorDapp jar
--------------------------
.. note:: Before installing a CorDapp, you must create one or more nodes to install it on. For instructions, please see
:doc:`deploying-a-node`.
:doc:`generating-a-node`.
At runtime, nodes will load any CorDapps present in their ``cordapps`` folder. Therefore in order to install a CorDapp on
a node, the CorDapp JAR must be added to the ``<node_dir>/cordapps/`` folder, where ``node_dir`` is the folder in which

View File

@ -1,141 +1,242 @@
Deploying a node
================
Node structure
--------------
Each Corda node has the following structure:
.. contents::
.. sourcecode:: none
.. note:: These instructions are intended for people who want to deploy a Corda node to a server,
whether they have developed and tested a CorDapp following the instructions in :doc:`generating-a-node`
or are deploying a third-party CorDapp.
.
├── certificates // The node's doorman certificates
├── corda-webserver.jar // The built-in node webserver
├── corda.jar // The core Corda libraries
├── logs // The node logs
├── node.conf // The node's configuration files
├── persistence.mv.db // The node's database
└── cordapps // The CorDapps jars installed on the node
Linux (systemd): Installing and running Corda as a systemd service
------------------------------------------------------------------
We recommend creating systemd services to run a node and the optional webserver. This provides logging and service
handling, and ensures the Corda service is run at boot.
The node is configured by editing its ``node.conf`` file. You install CorDapps on the node by dropping the CorDapp JARs
into the ``cordapps`` folder.
**Prerequisites**:
Node naming
-----------
A node's name must be a valid X500 name that obeys the following additional constraints:
* Oracle Java 8. The supported versions are listed in :doc:`getting-set-up`
* The fields of the name have the following maximum character lengths:
1. Add a system user which will be used to run Corda:
* Common name: 64
* Organisation: 128
* Organisation unit: 64
* Locality: 64
* State: 64
``sudo adduser --system --no-create-home --group corda``
* The country code is a valid ISO 3166-1 two letter code in upper-case
2. Create a directory called ``/opt/corda`` and change its ownership to the user you want to use to run Corda:
* The organisation, locality and country attributes are present
``mkdir /opt/corda; chown corda:corda /opt/corda``
* The organisation field of the name obeys the following constraints:
3. Download the `Corda jar <https://r3.bintray.com/corda/net/corda/corda/>`_
(under ``/VERSION_NUMBER/corda-VERSION_NUMBER.jar``) and place it in ``/opt/corda``
* Has at least two letters
* No leading or trailing whitespace
* No double-spacing
* Upper-case first letter
* Does not contain the words "node" or "server"
* Does not include the characters ',' or '=' or '$' or '"' or '\'' or '\\'
* Is in NFKC normalization form
* Only the latin, common and inherited unicode scripts are supported
3. Create a directory called ``plugins`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively, download one of
our `sample CorDapps <https://www.corda.net/samples/>`_ to the ``plugins`` directory
The deployNodes task
--------------------
The CorDapp template defines a ``deployNodes`` task that allows you to automatically generate and configure a set of
nodes:
4. Save the below as ``/opt/corda/node.conf``. See :doc:`corda-configuration-file` for a description of these options
.. sourcecode:: groovy
.. code-block:: json
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
directory "./build/nodes"
networkMap "O=Controller,L=London,C=GB"
node {
name "O=Controller,L=London,C=GB"
// The notary will offer a validating notary service.
notary = [validating : true]
p2pPort 10002
rpcPort 10003
// No webport property, so no webserver will be created.
h2Port 10004
sshdPort 22
// Includes the corda-finance CorDapp on our node.
cordapps = ["net.corda:corda-finance:$corda_release_version"]
basedir : "/opt/corda"
p2pAddress : "example.com:10002"
rpcAddress : "example.com:10003"
webAddress : "0.0.0.0:10004"
h2port : 11000
emailAddress : "you@example.com"
myLegalName : "O=Bank of Breakfast Tea, L=London, C=GB"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
useHTTPS : false
devMode : false
networkMapService {
address="networkmap.foo.bar.com:10002"
legalName="O=FooBar NetworkMap, L=Dublin, C=IE"
}
rpcUsers=[
{
user=corda
password=portal_password
permissions=[
ALL
]
}
]
5. Make the following changes to ``/opt/corda/node.conf``:
* Change the ``p2pAddress`` and ``rpcAddress`` values to start with your server's hostname or external IP address.
This is the address other nodes or RPC interfaces will use to communicate with your node
* Change the ports if necessary, for example if you are running multiple nodes on one server (see below)
* Enter an email address which will be used as an administrative contact during the registration process. This is
only visible to the permissioning service
* Enter your node's desired legal name. This will be used during the issuance of your certificate and should rarely
change as it should represent the legal identity of your node
* Organization (``O=``) should be a unique and meaningful identifier (e.g. Bank of Breakfast Tea)
* Location (``L=``) is your nearest city
* Country (``C=``) is the `ISO 3166-1 alpha-2 code <https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2>`_
* Change the RPC username and password
6. Create a ``corda.service`` file based on the example below and save it in the ``/etc/systemd/system/`` directory
.. code-block:: shell
[Unit]
Description=Corda Node - Bank of Breakfast Tea
Requires=network.target
[Service]
Type=simple
User=corda
WorkingDirectory=/opt/corda
ExecStart=/usr/bin/java -Xmx2048m -jar /opt/corda/corda.jar
Restart=on-failure
[Install]
WantedBy=multi-user.target
7. Make the following changes to ``corda.service``:
* Make sure the service description is informative - particularly if you plan to run multiple nodes.
* Change the username to the user account you want to use to run Corda. **We recommend that this is not root**
* Set the maximum amount of memory available to the Corda process by changing the ``-Xmx2048m`` parameter
* Make sure the ``corda.service`` file is owned by root with the correct permissions:
* ``sudo chown root:root /etc/systemd/system/corda.service``
* ``sudo chmod 644 /etc/systemd/system/corda.service``
.. note:: The Corda webserver provides a simple interface for interacting with your installed CorDapps in a browser.
Running the webserver is optional.
8. Create a ``corda-webserver.service`` file based on the example below and save it in the ``/etc/systemd/system/``
directory.
.. code-block:: shell
[Unit]
Description=Webserver for Corda Node - Bank of Breakfast Tea
Requires=network.target
[Service]
Type=simple
User=username
WorkingDirectory=/opt/corda
ExecStart=/usr/bin/java -jar /opt/corda/corda-webserver.jar
Restart=on-failure
[Install]
WantedBy=multi-user.target
9. Provision the required certificates to your node. Contact the network permissioning service or see
:doc:`permissioning`
10. You can now start a node and its webserver by running the following ``systemctl`` commands:
* ``sudo systemctl daemon-reload``
* ``sudo systemctl corda start``
* ``sudo systemctl corda-webserver start``
You can run multiple nodes by creating multiple directories and Corda services, modifying the ``node.conf`` and
``service`` files so they are unique.
Windows: Installing and running Corda as a Windows service
----------------------------------------------------------
We recommend running Corda as a Windows service. This provides service handling, ensures the Corda service is run
at boot, and means the Corda service stays running with no users connected to the server.
**Prerequisites**:
* Oracle Java 8. The supported versions are listed in :doc:`getting-set-up`
1. Create a Corda directory and download the Corda jar. Replace ``VERSION_NUMBER`` with the desired version. Here's an
example using PowerShell:
.. code-block:: PowerShell
mkdir C:\Corda
wget http://jcenter.bintray.com/net/corda/corda/VERSION_NUMBER/corda-VERSION_NUMBER.jar -OutFile C:\Corda\corda.jar
2. Create a directory called ``plugins`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively,
download one of our `sample CorDapps <https://www.corda.net/samples/>`_ to the ``plugins`` directory
3. Save the below as ``C:\Corda\node.conf``. See :doc:`corda-configuration-file` for a description of these options
.. code-block:: json
basedir : "C:\\Corda"
p2pAddress : "example.com:10002"
rpcAddress : "example.com:10003"
webAddress : "0.0.0.0:10004"
h2port : 11000
emailAddress: "you@example.com"
myLegalName : "O=Bank of Breakfast Tea, L=London, C=GB"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
extraAdvertisedServiceIds: [ "" ]
useHTTPS : false
devMode : false
networkMapService {
address="networkmap.foo.bar.com:10002"
legalName="O=FooBar NetworkMap, L=Dublin, C=IE"
}
node {
name "O=PartyA,L=London,C=GB"
advertisedServices = []
p2pPort 10005
rpcPort 10006
webPort 10007
h2Port 10008
sshdPort 22
cordapps = ["net.corda:corda-finance:$corda_release_version"]
// Grants user1 all RPC permissions.
rpcUsers = [[ user: "user1", "password": "test", "permissions": ["ALL"]]]
}
node {
name "O=PartyB,L=New York,C=US"
advertisedServices = []
p2pPort 10009
rpcPort 10010
webPort 10011
h2Port 10012
sshdPort 22
cordapps = ["net.corda:corda-finance:$corda_release_version"]
// Grants user1 the ability to start the MyFlow flow.
rpcUsers = [[ user: "user1", "password": "test", "permissions": ["StartFlow.net.corda.flows.MyFlow"]]]
}
}
rpcUsers=[
{
user=corda
password=portal_password
permissions=[
ALL
]
}
]
Running this task will create three nodes in the ``build/nodes`` folder:
4. Make the following changes to ``C:\Corda\node.conf``:
* A ``Controller`` node that:
* Change the ``p2pAddress`` and ``rpcAddress`` values to start with your server's hostname or external IP address.
This is the address other nodes or RPC interfaces will use to communicate with your node
* Change the ports if necessary, for example if you are running multiple nodes on one server (see below)
* Enter an email address which will be used as an administrative contact during the registration process. This is
only visible to the permissioning service
* Enter your node's desired legal name. This will be used during the issuance of your certificate and should rarely
change as it should represent the legal identity of your node
* Serves as the network map
* Offers a validating notary service
* Will not have a webserver (since ``webPort`` is not defined)
* Is running the ``corda-finance`` CorDapp
* Organization (``O=``) should be a unique and meaningful identifier (e.g. Bank of Breakfast Tea)
* Location (``L=``) is your nearest city
* Country (``C=``) is the `ISO 3166-1 alpha-2 code <https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2>`_
* Change the RPC username and password
* ``PartyA`` and ``PartyB`` nodes that:
5. Copy the required Java keystores to the node. See :doc:`permissioning`
* Are pointing at the ``Controller`` as the network map service
* Are not offering any services
* Will have a webserver (since ``webPort`` is defined)
* Are running the ``corda-finance`` CorDapp
* Have an RPC user, ``user1``, that can be used to log into the node via RPC
6. Download the `NSSM service manager <nssm.cc>`_
Additionally, all three nodes will include any CorDapps defined in the project's source folders, even though these
CorDapps are not listed in each node's ``cordapps`` entry. This means that running the ``deployNodes`` task from the
template CorDapp, for example, would automatically build and add the template CorDapp to each node.
7. Unzip ``nssm-2.24\win64\nssm.exe`` to ``C:\Corda``
You can extend ``deployNodes`` to generate additional nodes. The only requirement is that you must specify
a single node to run the network map service, by putting their name in the ``networkMap`` field.
8. Save the following as ``C:\Corda\nssm.bat``:
.. warning:: When adding nodes, make sure that there are no port clashes!
.. code-block:: batch
Running deployNodes
-------------------
To create the nodes defined in our ``deployNodes`` task, we'd run the following command in a terminal window from the
root of the project:
nssm install cordanode1 C:\ProgramData\Oracle\Java\javapath\java.exe
nssm set cordanode1 AppDirectory C:\Corda
nssm set cordanode1 AppParameters "-jar corda.jar -Xmx2048m --config-file=C:\corda\node.conf"
nssm set cordanode1 AppStdout C:\Corda\service.log
nssm set cordanode1 AppStderr C:\Corda\service.log
nssm set cordanode1 Description Corda Node - Bank of Breakfast Tea
sc start cordanode1
* Unix/Mac OSX: ``./gradlew deployNodes``
* Windows: ``gradlew.bat deployNodes``
9. Modify the batch file:
This will create the nodes in the ``build/nodes`` folder.
* If you are installing multiple nodes, use a different service name (``cordanode1``) for each node
* Set the amount of Java heap memory available to this node by modifying the -Xmx argument
* Set an informative description
.. note:: During the build process each node generates a NodeInfo file which is written in its own root directory,
the plug-in proceeds and copies each node NodeInfo to every other node ``additional-node-infos`` directory.
The NodeInfo file contains a node hostname and port, legal name and security certificate.
10. Run the batch file by clicking on it or from a command prompt
There will be a node folder generated for each node you defined, plus a ``runnodes`` shell script (or batch file on
Windows) to run all the nodes at once. If you make any changes to your ``deployNodes`` task, you will need to re-run
the task to see the changes take effect.
11. Run ``services.msc`` and verify that a service called ``cordanode1`` is present and running
You can now run the nodes by following the instructions in :doc:`Running a node <running-a-node>`.
12. Run ``netstat -ano`` and check for the ports you configured in ``node.conf``
13. You may need to open the ports on the Windows firewall
Testing your installation
-------------------------
You can verify Corda is running by connecting to your RPC port from another host, e.g.:
``telnet your-hostname.example.com 10002``
If you receive the message "Escape character is ^]", Corda is running and accessible. Press Ctrl-] and Ctrl-D to exit
telnet.

View File

@ -24,6 +24,7 @@ object CustomVaultQuery {
private companion object {
private val log = contextLogger()
}
fun rebalanceCurrencyReserves(): List<Amount<Currency>> {
val nativeQuery = """
select
@ -44,16 +45,18 @@ object CustomVaultQuery {
"""
log.info("SQL to execute: $nativeQuery")
val session = services.jdbcSession()
val prepStatement = session.prepareStatement(nativeQuery)
val rs = prepStatement.executeQuery()
val topUpLimits: MutableList<Amount<Currency>> = mutableListOf()
while (rs.next()) {
val currencyStr = rs.getString(1)
val amount = rs.getLong(2)
log.info("$currencyStr : $amount")
topUpLimits.add(Amount(amount, Currency.getInstance(currencyStr)))
return session.prepareStatement(nativeQuery).use { prepStatement ->
prepStatement.executeQuery().use { rs ->
val topUpLimits: MutableList<Amount<Currency>> = mutableListOf()
while (rs.next()) {
val currencyStr = rs.getString(1)
val amount = rs.getLong(2)
log.info("$currencyStr : $amount")
topUpLimits.add(Amount(amount, Currency.getInstance(currencyStr)))
}
topUpLimits
}
}
return topUpLimits
}
}
}
@ -69,6 +72,7 @@ object TopupIssuerFlow {
data class TopupRequest(val issueToParty: Party,
val issuerPartyRef: OpaqueBytes,
val notaryParty: Party)
@InitiatingFlow
@StartableByRPC
class TopupIssuanceRequester(val issueToParty: Party,

View File

@ -33,7 +33,7 @@ class CommercialPaperTest {
ledger {
transaction {
attachments(CP_PROGRAM_ID)
input(CP_PROGRAM_ID) { inState }
input(CP_PROGRAM_ID, inState)
verifies()
}
}
@ -46,8 +46,8 @@ class CommercialPaperTest {
val inState = getPaper()
ledger {
transaction {
input(CP_PROGRAM_ID) { inState }
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
input(CP_PROGRAM_ID, inState)
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
attachments(CP_PROGRAM_ID)
verifies()
}
@ -61,8 +61,8 @@ class CommercialPaperTest {
val inState = getPaper()
ledger {
transaction {
input(CP_PROGRAM_ID) { inState }
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
input(CP_PROGRAM_ID, inState)
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
attachments(CP_PROGRAM_ID)
`fails with`("the state is propagated")
}
@ -76,11 +76,11 @@ class CommercialPaperTest {
val inState = getPaper()
ledger {
transaction {
input(CP_PROGRAM_ID) { inState }
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
input(CP_PROGRAM_ID, inState)
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
attachments(CP_PROGRAM_ID)
`fails with`("the state is propagated")
output(CP_PROGRAM_ID, "alice's paper") { inState.withOwner(ALICE) }
output(CP_PROGRAM_ID, "alice's paper", inState.withOwner(ALICE))
verifies()
}
}
@ -92,15 +92,15 @@ class CommercialPaperTest {
fun `simple issuance with tweak`() {
ledger {
transaction {
output(CP_PROGRAM_ID, "paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp.
output(CP_PROGRAM_ID, "paper", getPaper()) // Some CP is issued onto the ledger by MegaCorp.
attachments(CP_PROGRAM_ID)
tweak {
// The wrong pubkey.
command(BIG_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
command(BIG_CORP_PUBKEY, CommercialPaper.Commands.Issue())
timeWindow(TEST_TX_TIME)
`fails with`("output states are issued by a command signer")
}
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue())
timeWindow(TEST_TX_TIME)
verifies()
}
@ -112,15 +112,15 @@ class CommercialPaperTest {
@Test
fun `simple issuance with tweak and top level transaction`() {
transaction {
output(CP_PROGRAM_ID, "paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp.
output(CP_PROGRAM_ID, "paper", getPaper()) // Some CP is issued onto the ledger by MegaCorp.
attachments(CP_PROGRAM_ID)
tweak {
// The wrong pubkey.
command(BIG_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
command(BIG_CORP_PUBKEY, CommercialPaper.Commands.Issue())
timeWindow(TEST_TX_TIME)
`fails with`("output states are issued by a command signer")
}
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue())
timeWindow(TEST_TX_TIME)
verifies()
}
@ -140,8 +140,8 @@ class CommercialPaperTest {
// Some CP is issued onto the ledger by MegaCorp.
transaction("Issuance") {
output(CP_PROGRAM_ID, "paper") { getPaper() }
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
output(CP_PROGRAM_ID, "paper", getPaper())
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue())
attachments(CP_PROGRAM_ID)
timeWindow(TEST_TX_TIME)
verifies()
@ -151,10 +151,10 @@ class CommercialPaperTest {
transaction("Trade") {
input("paper")
input("alice's $900")
output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP }
output(CP_PROGRAM_ID, "alice's paper") { "paper".output<ICommercialPaperState>().withOwner(ALICE) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP)
output(CP_PROGRAM_ID, "alice's paper", "paper".output<ICommercialPaperState>().withOwner(ALICE))
command(ALICE_PUBKEY, Cash.Commands.Move())
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
verifies()
}
}
@ -173,8 +173,8 @@ class CommercialPaperTest {
// Some CP is issued onto the ledger by MegaCorp.
transaction("Issuance") {
output(CP_PROGRAM_ID, "paper") { getPaper() }
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
output(CP_PROGRAM_ID, "paper", getPaper())
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue())
attachments(CP_PROGRAM_ID)
timeWindow(TEST_TX_TIME)
verifies()
@ -183,18 +183,18 @@ class CommercialPaperTest {
transaction("Trade") {
input("paper")
input("alice's $900")
output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP }
output(CP_PROGRAM_ID, "alice's paper") { "paper".output<ICommercialPaperState>().withOwner(ALICE) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP)
output(CP_PROGRAM_ID, "alice's paper", "paper".output<ICommercialPaperState>().withOwner(ALICE))
command(ALICE_PUBKEY, Cash.Commands.Move())
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
verifies()
}
transaction {
input("paper")
// We moved a paper to another pubkey.
output(CP_PROGRAM_ID, "bob's paper") { "paper".output<ICommercialPaperState>().withOwner(BOB) }
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
output(CP_PROGRAM_ID, "bob's paper", "paper".output<ICommercialPaperState>().withOwner(BOB))
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
verifies()
}
@ -215,8 +215,8 @@ class CommercialPaperTest {
// Some CP is issued onto the ledger by MegaCorp.
transaction("Issuance") {
output(CP_PROGRAM_ID, "paper") { getPaper() }
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
output(CP_PROGRAM_ID, "paper", getPaper())
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue())
attachments(CP_PROGRAM_ID)
timeWindow(TEST_TX_TIME)
verifies()
@ -225,10 +225,10 @@ class CommercialPaperTest {
transaction("Trade") {
input("paper")
input("alice's $900")
output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP }
output(CP_PROGRAM_ID, "alice's paper") { "paper".output<ICommercialPaperState>().withOwner(ALICE) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP)
output(CP_PROGRAM_ID, "alice's paper", "paper".output<ICommercialPaperState>().withOwner(ALICE))
command(ALICE_PUBKEY, Cash.Commands.Move())
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
verifies()
}
@ -236,8 +236,8 @@ class CommercialPaperTest {
transaction {
input("paper")
// We moved a paper to another pubkey.
output(CP_PROGRAM_ID, "bob's paper") { "paper".output<ICommercialPaperState>().withOwner(BOB) }
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
output(CP_PROGRAM_ID, "bob's paper", "paper".output<ICommercialPaperState>().withOwner(BOB))
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
verifies()
}
fails()

View File

@ -28,7 +28,7 @@ class CustomVaultQueryTest {
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance", "net.corda.docs"))
nodeA = mockNet.createPartyNode()
nodeB = mockNet.createPartyNode()
nodeA.internals.registerInitiatedFlow(TopupIssuerFlow.TopupIssuer::class.java)
nodeA.registerInitiatedFlow(TopupIssuerFlow.TopupIssuer::class.java)
notary = mockNet.defaultNotaryIdentity
}

View File

@ -27,7 +27,7 @@ class FxTransactionBuildTutorialTest {
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance"))
nodeA = mockNet.createPartyNode()
nodeB = mockNet.createPartyNode()
nodeB.internals.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java)
nodeB.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java)
notary = mockNet.defaultNotaryIdentity
}

View File

@ -35,7 +35,7 @@ class WorkflowTransactionBuildTutorialTest {
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.docs"))
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
val bobNode = mockNet.createPartyNode(BOB_NAME)
aliceNode.internals.registerInitiatedFlow(RecordCompletionFlow::class.java)
aliceNode.registerInitiatedFlow(RecordCompletionFlow::class.java)
aliceServices = aliceNode.services
bobServices = bobNode.services
alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME)

View File

@ -0,0 +1,136 @@
Creating nodes locally
======================
.. contents::
Node structure
--------------
Each Corda node has the following structure:
.. sourcecode:: none
.
├── certificates // The node's certificates
├── corda-webserver.jar // The built-in node webserver
├── corda.jar // The core Corda libraries
├── logs // The node logs
├── node.conf // The node's configuration files
├── persistence.mv.db // The node's database
└── cordapps // The CorDapps jars installed on the node
The node is configured by editing its ``node.conf`` file. You install CorDapps on the node by dropping the CorDapp JARs
into the ``cordapps`` folder.
Node naming
-----------
A node's name must be a valid X.500 name that obeys the following additional constraints:
* The fields of the name have the following maximum character lengths:
* Common name: 64
* Organisation: 128
* Organisation unit: 64
* Locality: 64
* State: 64
* The country code is a valid ISO 3166-1 two letter code in upper-case
* The organisation, locality and country attributes are present
* The organisation field of the name obeys the following constraints:
* Has at least two letters
* No leading or trailing whitespace
* No double-spacing
* Upper-case first letter
* Does not contain the words "node" or "server"
* Does not include the characters ',' or '=' or '$' or '"' or '\'' or '\\'
* Is in NFKC normalization form
* Only the latin, common and inherited unicode scripts are supported
The Cordform task
-----------------
Corda provides a gradle plugin called ``Cordform`` that allows you to automatically generate and configure a set of
nodes. Here is an example ``Cordform`` task called ``deployNodes`` that creates three nodes, defined in the
`Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin/blob/release-V2/build.gradle#L97>`_:
.. sourcecode:: groovy
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
directory "./build/nodes"
networkMap "O=Controller,L=London,C=GB"
node {
name "O=Controller,L=London,C=GB"
// The notary will offer a validating notary service.
notary = [validating : true]
p2pPort 10002
rpcPort 10003
// No webport property, so no webserver will be created.
h2Port 10004
// Includes the corda-finance CorDapp on our node.
cordapps = ["net.corda:corda-finance:$corda_release_version"]
}
node {
name "O=PartyA,L=London,C=GB"
advertisedServices = []
p2pPort 10005
rpcPort 10006
webPort 10007
h2Port 10008
cordapps = ["net.corda:corda-finance:$corda_release_version"]
// Grants user1 all RPC permissions.
rpcUsers = [[ user: "user1", "password": "test", "permissions": ["ALL"]]]
}
node {
name "O=PartyB,L=New York,C=US"
advertisedServices = []
p2pPort 10009
rpcPort 10010
webPort 10011
h2Port 10012
cordapps = ["net.corda:corda-finance:$corda_release_version"]
// Grants user1 the ability to start the MyFlow flow.
rpcUsers = [[ user: "user1", "password": "test", "permissions": ["StartFlow.net.corda.flows.MyFlow"]]]
}
}
Running this task will create three nodes in the ``build/nodes`` folder:
* A ``Controller`` node that:
* Serves as the network map
* Offers a validating notary service
* Will not have a webserver (since ``webPort`` is not defined)
* Is running the ``corda-finance`` CorDapp
* ``PartyA`` and ``PartyB`` nodes that:
* Are pointing at the ``Controller`` as the network map service
* Are not offering any services
* Will have a webserver (since ``webPort`` is defined)
* Are running the ``corda-finance`` CorDapp
* Have an RPC user, ``user1``, that can be used to log into the node via RPC
Additionally, all three nodes will include any CorDapps defined in the project's source folders, even though these
CorDapps are not listed in each node's ``cordapps`` entry. This means that running the ``deployNodes`` task from the
template CorDapp, for example, would automatically build and add the template CorDapp to each node.
You can extend ``deployNodes`` to generate additional nodes. The only requirement is that you must specify
a single node to run the network map service, by putting its name in the ``networkMap`` field.
.. warning:: When adding nodes, make sure that there are no port clashes!
Running deployNodes
-------------------
To create the nodes defined in our ``deployNodes`` task, run the following command in a terminal window from the root
of the project where the ``deployNodes`` task is defined:
* Linux/macOS: ``./gradlew deployNodes``
* Windows: ``gradlew.bat deployNodes``
This will create the nodes in the ``build/nodes`` folder. There will be a node folder generated for each node defined
in the ``deployNodes`` task, plus a ``runnodes`` shell script (or batch file on Windows) to run all the nodes at once
for testing and development purposes. If you make any changes to your CorDapp source or ``deployNodes`` task, you will
need to re-run the task to see the changes take effect.
You can now run the nodes by following the instructions in :doc:`Running a node <running-a-node>`.

View File

@ -1,25 +1,49 @@
Running a node
==============
Running nodes locally
=====================
Starting your node
------------------
After following the steps in :doc:`deploying-a-node`, you should have deployed your node(s) with any chosen CorDapps
already installed. You run each node by navigating to ``<node_dir>`` in a terminal window and running:
.. contents::
.. note:: You should already have generated your node(s) with their CorDapps installed by following the instructions in
:doc:`generating-a-node`.
There are several ways to run a Corda node locally for testing purposes.
Starting all nodes at once
--------------------------
.. note:: ``runnodes`` is a shell script (or batch file on Windows) that is generated by ``deployNodes`` to allow you
to quickly start up all nodes and their webservers. ``runnodes`` should only be used for testing purposes.
Start the nodes with ``runnodes`` by running the following command from the root of the project:
* Linux/macOS: ``build/nodes/runnodes``
* Windows: ``call build\nodes\runnodes.bat``
.. warn:: On macOS, do not click/change focus until all the node terminal windows have opened, or some processes may
fail to start.
Starting an individual Corda node
---------------------------------
Run the node by opening a terminal window in the node's folder and running:
.. code-block:: shell
java -jar corda.jar
.. warning:: If your working directory is not ``<node_dir>`` your cordapps and configuration will not be used.
.. warning:: By default, the node will look for a configuration file called ``node.conf`` and a CorDapps folder called
``cordapps`` in the current working directory. You can override the configuration file and workspace paths on the
command line (e.g. ``./corda.jar --config-file=test.conf --base-directory=/opt/r3corda/nodes/test``).
The configuration file and workspace paths can be overridden on the command line. For example:
Optionally run the node's webserver as well by opening a terminal window in the node's folder and running:
``./corda.jar --config-file=test.conf --base-directory=/opt/r3corda/nodes/test``.
.. code-block:: shell
Otherwise the workspace folder for the node is the current working path.
java -jar corda-webserver.jar
Debugging your node
-------------------
.. warning:: The node webserver is for testing purposes only and will be removed soon.
Starting a node with remote debugging enabled
---------------------------------------------
To enable remote debugging of the node, run the following from the terminal window:
``java -Dcapsule.jvm.args="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005" -jar corda.jar``

View File

@ -137,7 +137,7 @@ which could be represented as ``{ first: foo, second: 123 }``.
.. note:: If your CorDapp is written in Java,
named arguments won't work unless you compiled using the ``-parameters`` argument to javac.
See :doc:`deploying-a-node` for how to specify it via Gradle.
See :doc:`generating-a-node` for how to specify it via Gradle.
The same syntax is also used to specify the parameters for RPCs, accessed via the ``run`` command, like this:

View File

@ -26,7 +26,8 @@ for gradle and IntelliJ, but it's possible this option is not present in your en
"No matching constructor found: - [arg0: int, arg1: Party]: missing parameter arg0"
***********************************************************************************
Your CorDapp is written in Java and you haven't specified the ``-parameters`` compiler argument. See :doc:`deploying-a-node` for how it can be done using Gradle.
Your CorDapp is written in Java and you haven't specified the ``-parameters`` compiler argument. See
:doc:`generating-a-node` for how it can be done using Gradle.
IDEA issues
-----------

View File

@ -176,7 +176,7 @@ There are two ways to run the example CorDapp:
* Via IntelliJ
In both cases, we will deploy a set of test nodes with our CorDapp installed, then run the nodes. You can read more
about how we define the nodes to be deployed :doc:`here <deploying-a-node>`.
about how we define the nodes to be deployed :doc:`here <generating-a-node>`.
Terminal
~~~~~~~~

View File

@ -172,16 +172,14 @@ class Cap {
@Test
fun issue() {
transaction {
output(UNIVERSAL_PROGRAM_ID) { stateInitial }
output(UNIVERSAL_PROGRAM_ID, stateInitial)
timeWindow(TEST_TX_TIME_1)
tweak {
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
this `fails with` "the transaction is signed by all liable parties"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
this.verifies()
}
}
@ -189,44 +187,38 @@ class Cap {
@Test
fun `first fixing`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { stateInitial }
output(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFirst }
input(UNIVERSAL_PROGRAM_ID, stateInitial)
output(UNIVERSAL_PROGRAM_ID, stateAfterFixingFirst)
timeWindow(TEST_TX_TIME_1)
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
this `fails with` "action must be defined"
}
tweak {
// wrong source
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("3M")), 1.0.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("3M")), 1.0.bd))))
this `fails with` "relevant fixing must be included"
}
tweak {
// wrong date
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("3M")), 1.0.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("3M")), 1.0.bd))))
this `fails with` "relevant fixing must be included"
}
tweak {
// wrong tenor
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("9M")), 1.0.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("9M")), 1.0.bd))))
this `fails with` "relevant fixing must be included"
}
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.5.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.5.bd))))
this `fails with` "output state does not reflect fix command"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))))
this.verifies()
}
}
@ -234,19 +226,16 @@ class Cap {
@Test
fun `first execute`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFirst }
output(UNIVERSAL_PROGRAM_ID) { stateAfterExecutionFirst }
output(UNIVERSAL_PROGRAM_ID) { statePaymentFirst }
input(UNIVERSAL_PROGRAM_ID, stateAfterFixingFirst)
output(UNIVERSAL_PROGRAM_ID, stateAfterExecutionFirst)
output(UNIVERSAL_PROGRAM_ID, statePaymentFirst)
timeWindow(TEST_TX_TIME_1)
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
this `fails with` "action must be defined"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("exercise") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("exercise"))
this.verifies()
}
}
@ -254,18 +243,15 @@ class Cap {
@Test
fun `final execute`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFinal }
output(UNIVERSAL_PROGRAM_ID) { statePaymentFinal }
input(UNIVERSAL_PROGRAM_ID, stateAfterFixingFinal)
output(UNIVERSAL_PROGRAM_ID, statePaymentFinal)
timeWindow(TEST_TX_TIME_1)
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
this `fails with` "action must be defined"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("exercise") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("exercise"))
this.verifies()
}
}
@ -273,44 +259,38 @@ class Cap {
@Test
fun `second fixing`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { stateAfterExecutionFirst }
output(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFinal }
input(UNIVERSAL_PROGRAM_ID, stateAfterExecutionFirst)
output(UNIVERSAL_PROGRAM_ID, stateAfterFixingFinal)
timeWindow(TEST_TX_TIME_1)
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
this `fails with` "action must be defined"
}
tweak {
// wrong source
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.0.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.0.bd))))
this `fails with` "relevant fixing must be included"
}
tweak {
// wrong date
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01").plusYears(1), Tenor("3M")), 1.0.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01").plusYears(1), Tenor("3M")), 1.0.bd))))
this `fails with` "relevant fixing must be included"
}
tweak {
// wrong tenor
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("9M")), 1.0.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("9M")), 1.0.bd))))
this `fails with` "relevant fixing must be included"
}
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.5.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.5.bd))))
this `fails with` "output state does not reflect fix command"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.0.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.0.bd))))
this.verifies()
}
}

View File

@ -55,16 +55,14 @@ class Caplet {
@Test
fun issue() {
transaction {
output(UNIVERSAL_PROGRAM_ID) { stateStart }
output(UNIVERSAL_PROGRAM_ID, stateStart)
timeWindow(TEST_TX_TIME_1)
tweak {
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
this `fails with` "the transaction is signed by all liable parties"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
this.verifies()
}
}
@ -72,17 +70,15 @@ class Caplet {
@Test
fun `execute`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { stateFixed }
output(UNIVERSAL_PROGRAM_ID) { stateFinal }
input(UNIVERSAL_PROGRAM_ID, stateFixed)
output(UNIVERSAL_PROGRAM_ID, stateFinal)
timeWindow(TEST_TX_TIME_1)
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
this `fails with` "action must be defined"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("exercise") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("exercise"))
this.verifies()
}
}
@ -90,44 +86,38 @@ class Caplet {
@Test
fun `fixing`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { stateStart }
output(UNIVERSAL_PROGRAM_ID) { stateFixed }
input(UNIVERSAL_PROGRAM_ID, stateStart)
output(UNIVERSAL_PROGRAM_ID, stateFixed)
timeWindow(TEST_TX_TIME_1)
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
this `fails with` "action must be defined"
}
tweak {
// wrong source
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("6M")), 1.0.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("6M")), 1.0.bd))))
this `fails with` "relevant fixing must be included"
}
tweak {
// wrong date
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("6M")), 1.0.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("6M")), 1.0.bd))))
this `fails with` "relevant fixing must be included"
}
tweak {
// wrong tenor
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))))
this `fails with` "relevant fixing must be included"
}
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("6M")), 1.5.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("6M")), 1.5.bd))))
this `fails with` "output state does not reflect fix command"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("6M")), 1.0.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("6M")), 1.0.bd))))
this.verifies()
}
}

View File

@ -52,20 +52,18 @@ class FXFwdTimeOption {
@Test
fun `issue - signature`() {
transaction {
output(UNIVERSAL_PROGRAM_ID) { inState }
output(UNIVERSAL_PROGRAM_ID, inState)
timeWindow(TEST_TX_TIME_1)
tweak {
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
this `fails with` "the transaction is signed by all liable parties"
}
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
this `fails with` "the transaction is signed by all liable parties"
}
command(highStreetBank.owningKey, acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
command(listOf(highStreetBank.owningKey, acmeCorp.owningKey), UniversalContract.Commands.Issue())
this.verifies()
}
}
@ -73,31 +71,28 @@ class FXFwdTimeOption {
@Test
fun `maturity, bank exercise`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { inState }
output(UNIVERSAL_PROGRAM_ID) { outState1 }
output(UNIVERSAL_PROGRAM_ID) { outState2 }
input(UNIVERSAL_PROGRAM_ID, inState)
output(UNIVERSAL_PROGRAM_ID, outState1)
output(UNIVERSAL_PROGRAM_ID, outState2)
timeWindow(TEST_TX_TIME_AFTER_MATURITY)
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
this `fails with` "action must be defined"
}
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("exercise") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("exercise"))
this `fails with` "condition must be met"
}
tweak {
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("exercise") }
command(acmeCorp.owningKey, UniversalContract.Commands.Action("exercise"))
this `fails with` "condition must be met"
}
tweak {
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("expire") }
command(acmeCorp.owningKey, UniversalContract.Commands.Action("expire"))
this `fails with` "condition must be met"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("expire") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("expire"))
this.verifies()
}
}
@ -105,31 +100,28 @@ class FXFwdTimeOption {
@Test
fun `maturity, corp exercise`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { inState }
output(UNIVERSAL_PROGRAM_ID) { outState1 }
output(UNIVERSAL_PROGRAM_ID) { outState2 }
input(UNIVERSAL_PROGRAM_ID, inState)
output(UNIVERSAL_PROGRAM_ID, outState1)
output(UNIVERSAL_PROGRAM_ID, outState2)
timeWindow(TEST_TX_TIME_BEFORE_MATURITY)
tweak {
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("some undefined name") }
command(acmeCorp.owningKey, UniversalContract.Commands.Action("some undefined name"))
this `fails with` "action must be defined"
}
tweak {
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("expire") }
command(acmeCorp.owningKey, UniversalContract.Commands.Action("expire"))
this `fails with` "condition must be met"
}
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("expire") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("expire"))
this `fails with` "condition must be met"
}
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("exercise") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("exercise"))
this `fails with` "condition must be met"
}
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("exercise") }
command(acmeCorp.owningKey, UniversalContract.Commands.Action("exercise"))
this.verifies()
}
}

View File

@ -44,20 +44,18 @@ class FXSwap {
fun `issue - signature`() {
transaction {
output(UNIVERSAL_PROGRAM_ID) { inState }
output(UNIVERSAL_PROGRAM_ID, inState)
timeWindow(TEST_TX_TIME_1)
tweak {
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
this `fails with` "the transaction is signed by all liable parties"
}
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
this `fails with` "the transaction is signed by all liable parties"
}
command(highStreetBank.owningKey, acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
command(listOf(highStreetBank.owningKey, acmeCorp.owningKey), UniversalContract.Commands.Issue())
this.verifies()
}
}
@ -65,18 +63,16 @@ class FXSwap {
@Test
fun `execute`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { inState }
output(UNIVERSAL_PROGRAM_ID) { outState1 }
output(UNIVERSAL_PROGRAM_ID) { outState2 }
input(UNIVERSAL_PROGRAM_ID, inState)
output(UNIVERSAL_PROGRAM_ID, outState1)
output(UNIVERSAL_PROGRAM_ID, outState2)
timeWindow(TEST_TX_TIME_1)
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
this `fails with` "action must be defined"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("execute") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("execute"))
this.verifies()
}
}
@ -84,18 +80,16 @@ class FXSwap {
@Test
fun `execute - reversed order`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { inState }
output(UNIVERSAL_PROGRAM_ID) { outState2 }
output(UNIVERSAL_PROGRAM_ID) { outState1 }
input(UNIVERSAL_PROGRAM_ID, inState)
output(UNIVERSAL_PROGRAM_ID, outState2)
output(UNIVERSAL_PROGRAM_ID, outState1)
timeWindow(TEST_TX_TIME_1)
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
this `fails with` "action must be defined"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("execute") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("execute"))
this.verifies()
}
}
@ -103,12 +97,11 @@ class FXSwap {
@Test
fun `execute - not authorized`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { inState }
output(UNIVERSAL_PROGRAM_ID) { outState1 }
output(UNIVERSAL_PROGRAM_ID) { outState2 }
input(UNIVERSAL_PROGRAM_ID, inState)
output(UNIVERSAL_PROGRAM_ID, outState1)
output(UNIVERSAL_PROGRAM_ID, outState2)
timeWindow(TEST_TX_TIME_1)
command(momAndPop.owningKey) { UniversalContract.Commands.Action("execute") }
command(momAndPop.owningKey, UniversalContract.Commands.Action("execute"))
this `fails with` "condition must be met"
}
}
@ -116,12 +109,11 @@ class FXSwap {
@Test
fun `execute - before maturity`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { inState }
output(UNIVERSAL_PROGRAM_ID) { outState1 }
output(UNIVERSAL_PROGRAM_ID) { outState2 }
input(UNIVERSAL_PROGRAM_ID, inState)
output(UNIVERSAL_PROGRAM_ID, outState1)
output(UNIVERSAL_PROGRAM_ID, outState2)
timeWindow(TEST_TX_TIME_TOO_EARLY)
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") }
command(acmeCorp.owningKey, UniversalContract.Commands.Action("execute"))
this `fails with` "condition must be met"
}
}
@ -129,11 +121,10 @@ class FXSwap {
@Test
fun `execute - outState mismatch 1`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { inState }
output(UNIVERSAL_PROGRAM_ID) { outState1 }
input(UNIVERSAL_PROGRAM_ID, inState)
output(UNIVERSAL_PROGRAM_ID, outState1)
timeWindow(TEST_TX_TIME_1)
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") }
command(acmeCorp.owningKey, UniversalContract.Commands.Action("execute"))
this `fails with` "output state must match action result state"
}
}
@ -141,12 +132,11 @@ class FXSwap {
@Test
fun `execute - outState mismatch 2`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { inState }
output(UNIVERSAL_PROGRAM_ID) { outState1 }
output(UNIVERSAL_PROGRAM_ID) { outStateBad2 }
input(UNIVERSAL_PROGRAM_ID, inState)
output(UNIVERSAL_PROGRAM_ID, outState1)
output(UNIVERSAL_PROGRAM_ID, outStateBad2)
timeWindow(TEST_TX_TIME_1)
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") }
command(acmeCorp.owningKey, UniversalContract.Commands.Action("execute"))
this `fails with` "output states must match action result state"
}
}
@ -154,12 +144,11 @@ class FXSwap {
@Test
fun `execute - outState mismatch 3`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { inState }
output(UNIVERSAL_PROGRAM_ID) { outStateBad1 }
output(UNIVERSAL_PROGRAM_ID) { outState2 }
input(UNIVERSAL_PROGRAM_ID, inState)
output(UNIVERSAL_PROGRAM_ID, outStateBad1)
output(UNIVERSAL_PROGRAM_ID, outState2)
timeWindow(TEST_TX_TIME_1)
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") }
command(acmeCorp.owningKey, UniversalContract.Commands.Action("execute"))
this `fails with` "output states must match action result state"
}
}
@ -167,12 +156,11 @@ class FXSwap {
@Test
fun `execute - outState mismatch 4`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { inState }
output(UNIVERSAL_PROGRAM_ID) { outState1 }
output(UNIVERSAL_PROGRAM_ID) { outStateBad3 }
input(UNIVERSAL_PROGRAM_ID, inState)
output(UNIVERSAL_PROGRAM_ID, outState1)
output(UNIVERSAL_PROGRAM_ID, outStateBad3)
timeWindow(TEST_TX_TIME_1)
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") }
command(acmeCorp.owningKey, UniversalContract.Commands.Action("execute"))
this `fails with` "output states must match action result state"
}
}

View File

@ -134,16 +134,14 @@ class IRS {
@Test
fun issue() {
transaction {
output(UNIVERSAL_PROGRAM_ID) { stateInitial }
output(UNIVERSAL_PROGRAM_ID, stateInitial)
timeWindow(TEST_TX_TIME_1)
tweak {
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
this `fails with` "the transaction is signed by all liable parties"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
this.verifies()
}
}
@ -151,44 +149,38 @@ class IRS {
@Test
fun `first fixing`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { stateInitial }
output(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFirst }
input(UNIVERSAL_PROGRAM_ID, stateInitial)
output(UNIVERSAL_PROGRAM_ID, stateAfterFixingFirst)
timeWindow(TEST_TX_TIME_1)
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
this `fails with` "action must be defined"
}
tweak {
// wrong source
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("3M")), 1.0.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("3M")), 1.0.bd))))
this `fails with` "relevant fixing must be included"
}
tweak {
// wrong date
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("3M")), 1.0.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("3M")), 1.0.bd))))
this `fails with` "relevant fixing must be included"
}
tweak {
// wrong tenor
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("9M")), 1.0.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("9M")), 1.0.bd))))
this `fails with` "relevant fixing must be included"
}
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.5.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.5.bd))))
this `fails with` "output state does not reflect fix command"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) }
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))))
this.verifies()
}
}
@ -196,19 +188,16 @@ class IRS {
@Test
fun `first execute`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFirst }
output(UNIVERSAL_PROGRAM_ID) { stateAfterExecutionFirst }
output(UNIVERSAL_PROGRAM_ID) { statePaymentFirst }
input(UNIVERSAL_PROGRAM_ID, stateAfterFixingFirst)
output(UNIVERSAL_PROGRAM_ID, stateAfterExecutionFirst)
output(UNIVERSAL_PROGRAM_ID, statePaymentFirst)
timeWindow(TEST_TX_TIME_1)
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
this `fails with` "action must be defined"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("pay floating") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("pay floating"))
this.verifies()
}
}

View File

@ -145,16 +145,14 @@ class RollOutTests {
@Test
fun issue() {
transaction {
output(UNIVERSAL_PROGRAM_ID) { stateStart }
output(UNIVERSAL_PROGRAM_ID, stateStart)
timeWindow(TEST_TX_TIME_1)
tweak {
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
this `fails with` "the transaction is signed by all liable parties"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
this.verifies()
}
}
@ -162,18 +160,16 @@ class RollOutTests {
@Test
fun `execute`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { stateStart }
output(UNIVERSAL_PROGRAM_ID) { stateStep1a }
output(UNIVERSAL_PROGRAM_ID) { stateStep1b }
input(UNIVERSAL_PROGRAM_ID, stateStart)
output(UNIVERSAL_PROGRAM_ID, stateStep1a)
output(UNIVERSAL_PROGRAM_ID, stateStep1b)
timeWindow(TEST_TX_TIME_1)
/* tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
this `fails with` "action must be defined"
}*/
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("transfer") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("transfer"))
this.verifies()
}
}

View File

@ -61,16 +61,14 @@ class Swaption {
@Test
fun issue() {
transaction {
output(UNIVERSAL_PROGRAM_ID) { stateInitial }
output(UNIVERSAL_PROGRAM_ID, stateInitial)
timeWindow(TEST_TX_TIME_1)
tweak {
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
this `fails with` "the transaction is signed by all liable parties"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
this.verifies()
}
}

View File

@ -51,15 +51,12 @@ class ZeroCouponBond {
@Test
fun `issue - signature`() {
transaction {
output(UNIVERSAL_PROGRAM_ID) { inState }
output(UNIVERSAL_PROGRAM_ID, inState)
tweak {
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
this `fails with` "the transaction is signed by all liable parties"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
this.verifies()
}
}
@ -67,17 +64,15 @@ class ZeroCouponBond {
@Test
fun `execute`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { inState }
output(UNIVERSAL_PROGRAM_ID) { outState }
input(UNIVERSAL_PROGRAM_ID, inState)
output(UNIVERSAL_PROGRAM_ID, outState)
timeWindow(TEST_TX_TIME_1)
tweak {
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
this `fails with` "action must be defined"
}
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("execute") }
command(highStreetBank.owningKey, UniversalContract.Commands.Action("execute"))
this.verifies()
}
}
@ -85,11 +80,10 @@ class ZeroCouponBond {
@Test
fun `execute - not authorized`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { inState }
output(UNIVERSAL_PROGRAM_ID) { outState }
input(UNIVERSAL_PROGRAM_ID, inState)
output(UNIVERSAL_PROGRAM_ID, outState)
timeWindow(TEST_TX_TIME_1)
command(momAndPop.owningKey) { UniversalContract.Commands.Action("execute") }
command(momAndPop.owningKey, UniversalContract.Commands.Action("execute"))
this `fails with` "condition must be met"
}
}
@ -97,11 +91,10 @@ class ZeroCouponBond {
@Test
fun `execute - outState mismatch`() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { inState }
output(UNIVERSAL_PROGRAM_ID) { outStateWrong }
input(UNIVERSAL_PROGRAM_ID, inState)
output(UNIVERSAL_PROGRAM_ID, outStateWrong)
timeWindow(TEST_TX_TIME_1)
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") }
command(acmeCorp.owningKey, UniversalContract.Commands.Action("execute"))
this `fails with` "output state must match action result state"
}
}
@ -109,29 +102,23 @@ class ZeroCouponBond {
@Test
fun move() {
transaction {
input(UNIVERSAL_PROGRAM_ID) { inState }
input(UNIVERSAL_PROGRAM_ID, inState)
tweak {
output(UNIVERSAL_PROGRAM_ID) { outStateMove }
command(acmeCorp.owningKey) {
UniversalContract.Commands.Move(acmeCorp, momAndPop)
}
output(UNIVERSAL_PROGRAM_ID, outStateMove)
command(acmeCorp.owningKey,
UniversalContract.Commands.Move(acmeCorp, momAndPop))
this `fails with` "the transaction is signed by all liable parties"
}
tweak {
output(UNIVERSAL_PROGRAM_ID) { inState }
command(acmeCorp.owningKey, momAndPop.owningKey, highStreetBank.owningKey) {
UniversalContract.Commands.Move(acmeCorp, momAndPop)
}
output(UNIVERSAL_PROGRAM_ID, inState)
command(listOf(acmeCorp.owningKey, momAndPop.owningKey, highStreetBank.owningKey),
UniversalContract.Commands.Move(acmeCorp, momAndPop))
this `fails with` "output state does not reflect move command"
}
output(UNIVERSAL_PROGRAM_ID) { outStateMove }
command(acmeCorp.owningKey, momAndPop.owningKey, highStreetBank.owningKey) {
UniversalContract.Commands.Move(acmeCorp, momAndPop)
}
output(UNIVERSAL_PROGRAM_ID, outStateMove)
command(listOf(acmeCorp.owningKey, momAndPop.owningKey, highStreetBank.owningKey),
UniversalContract.Commands.Move(acmeCorp, momAndPop))
this.verifies()
}
}

View File

@ -67,13 +67,14 @@ abstract class AbstractCashSelection(private val maxRetries : Int = 8, private v
* with this notary are included.
* @param onlyFromIssuerParties Optional issuer parties to match against.
* @param withIssuerRefs Optional issuer references to match against.
* @return JDBC ResultSet with the matching states that were found. If sufficient funds were found these will be locked,
* @param withResultSet Function that contains the business logic. The JDBC ResultSet with the matching states that were found. If sufficient funds were found these will be locked,
* otherwise what is available is returned unlocked for informational purposes.
* @return The result of the withResultSet function
*/
abstract fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?,
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean
override abstract fun toString() : String
override abstract fun toString(): String
/**
* Query to gather Cash states that are available and retry if they are temporarily unavailable.
@ -122,34 +123,40 @@ abstract class AbstractCashSelection(private val maxRetries : Int = 8, private v
try {
// we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null)
// the softLockReserve update will detect whether we try to lock states locked by others
val rs = executeQuery(connection, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs)
stateAndRefs.clear()
return executeQuery(connection, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs) { rs ->
stateAndRefs.clear()
var totalPennies = 0L
val stateRefs = mutableSetOf<StateRef>()
while (rs.next()) {
val txHash = SecureHash.parse(rs.getString(1))
val index = rs.getInt(2)
val pennies = rs.getLong(3)
totalPennies = rs.getLong(4)
val rowLockId = rs.getString(5)
stateRefs.add(StateRef(txHash, index))
log.trace { "ROW: $rowLockId ($lockId): ${StateRef(txHash, index)} : $pennies ($totalPennies)" }
var totalPennies = 0L
val stateRefs = mutableSetOf<StateRef>()
while (rs.next()) {
val txHash = SecureHash.parse(rs.getString(1))
val index = rs.getInt(2)
val pennies = rs.getLong(3)
totalPennies = rs.getLong(4)
val rowLockId = rs.getString(5)
stateRefs.add(StateRef(txHash, index))
log.trace { "ROW: $rowLockId ($lockId): ${StateRef(txHash, index)} : $pennies ($totalPennies)" }
}
if (stateRefs.isNotEmpty()) {
// TODO: future implementation to retrieve contract states from a Vault BLOB store
stateAndRefs.addAll(services.loadStates(stateRefs) as Collection<StateAndRef<Cash.State>>)
}
val success = stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity
if (success) {
// we should have a minimum number of states to satisfy our selection `amount` criteria
log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs")
// With the current single threaded state machine available states are guaranteed to lock.
// TODO However, we will have to revisit these methods in the future multi-threaded.
services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet())
} else {
log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}")
}
success
}
if (stateRefs.isNotEmpty())
// TODO: future implementation to retrieve contract states from a Vault BLOB store
stateAndRefs.addAll(services.loadStates(stateRefs) as Collection<StateAndRef<Cash.State>>)
if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) {
// we should have a minimum number of states to satisfy our selection `amount` criteria
log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs")
// With the current single threaded state machine available states are guaranteed to lock.
// TODO However, we will have to revisit these methods in the future multi-threaded.
services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet())
return true
}
log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}")
// retry as more states may become available
} catch (e: SQLException) {
log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId]

View File

@ -30,9 +30,8 @@ class CashSelectionH2Impl : AbstractCashSelection() {
// 2) H2 uses session variables to perform this accumulator function:
// http://www.h2database.com/html/functions.html#set
// 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries)
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?,
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet {
connection.createStatement().execute("CALL SET(@t, CAST(0 AS BIGINT));")
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
connection.createStatement().use { it.execute("CALL SET(@t, CAST(0 AS BIGINT));") }
val selectJoin = """
SELECT vs.transaction_id, vs.output_index, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
@ -50,19 +49,22 @@ class CashSelectionH2Impl : AbstractCashSelection() {
" AND ccs.issuer_ref IN (?)" else "")
// Use prepared statement for protection against SQL Injection (http://www.h2database.com/html/advanced.html#sql_injection)
val psSelectJoin = connection.prepareStatement(selectJoin)
var pIndex = 0
psSelectJoin.setString(++pIndex, amount.token.currencyCode)
psSelectJoin.setLong(++pIndex, amount.quantity)
psSelectJoin.setString(++pIndex, lockId.toString())
if (notary != null)
psSelectJoin.setString(++pIndex, notary.name.toString())
if (onlyFromIssuerParties.isNotEmpty())
psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toStringShort() as Any}.toTypedArray() )
if (withIssuerRefs.isNotEmpty())
psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray())
log.debug { psSelectJoin.toString() }
connection.prepareStatement(selectJoin).use { psSelectJoin ->
var pIndex = 0
psSelectJoin.setString(++pIndex, amount.token.currencyCode)
psSelectJoin.setLong(++pIndex, amount.quantity)
psSelectJoin.setString(++pIndex, lockId.toString())
if (notary != null)
psSelectJoin.setString(++pIndex, notary.name.toString())
if (onlyFromIssuerParties.isNotEmpty())
psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toStringShort() as Any }.toTypedArray())
if (withIssuerRefs.isNotEmpty())
psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray())
log.debug { psSelectJoin.toString() }
return psSelectJoin.executeQuery()
psSelectJoin.executeQuery().use { rs ->
return withResultSet(rs)
}
}
}
}

View File

@ -19,7 +19,7 @@ class CashSelectionMySQLImpl : AbstractCashSelection() {
return metadata.driverName == JDBC_DRIVER_NAME
}
override fun executeQuery(statement: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, issuerKeysStr: Set<AbstractParty>, issuerRefsStr: Set<OpaqueBytes>): ResultSet {
override fun executeQuery(statement: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, issuerKeysStr: Set<AbstractParty>, issuerRefsStr: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
TODO("MySQL cash selection not implemented")
}

View File

@ -27,8 +27,7 @@ class CashSelectionPostgreSQLImpl : AbstractCashSelection() {
// 2) The window function accumulated column (`total`) does not include the current row (starts from 0) and cannot
// appear in the WHERE clause, hence restricting row selection and adjusting the returned total in the outer query.
// 3) Currently (version 9.6), FOR UPDATE cannot be specified with window functions
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?,
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet {
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
val selectJoin = """SELECT nested.transaction_id, nested.output_index, nested.pennies,
nested.total+nested.pennies as total_pennies, nested.lock_id
FROM
@ -51,29 +50,32 @@ class CashSelectionPostgreSQLImpl : AbstractCashSelection() {
nested WHERE nested.total < ?
"""
val statement = connection.prepareStatement(selectJoin)
statement.setString(1, amount.token.toString())
statement.setString(2, lockId.toString())
var paramOffset = 0
if (notary != null) {
statement.setString(3, notary.name.toString())
paramOffset += 1
}
if (onlyFromIssuerParties.isNotEmpty()) {
val issuerKeys = connection.createArrayOf("VARCHAR", onlyFromIssuerParties.map
{ it.owningKey.toBase58String() }.toTypedArray())
statement.setArray(3 + paramOffset, issuerKeys)
paramOffset += 1
}
if (withIssuerRefs.isNotEmpty()) {
val issuerRefs = connection.createArrayOf("BYTEA", withIssuerRefs.map
{ it.bytes }.toTypedArray())
statement.setArray(3 + paramOffset, issuerRefs)
paramOffset += 1
}
statement.setLong(3 + paramOffset, amount.quantity)
log.debug { statement.toString() }
connection.prepareStatement(selectJoin).use { statement ->
statement.setString(1, amount.token.toString())
statement.setString(2, lockId.toString())
var paramOffset = 0
if (notary != null) {
statement.setString(3, notary.name.toString())
paramOffset += 1
}
if (onlyFromIssuerParties.isNotEmpty()) {
val issuerKeys = connection.createArrayOf("VARCHAR", onlyFromIssuerParties.map
{ it.owningKey.toBase58String() }.toTypedArray())
statement.setArray(3 + paramOffset, issuerKeys)
paramOffset += 1
}
if (withIssuerRefs.isNotEmpty()) {
val issuerRefs = connection.createArrayOf("BYTEA", withIssuerRefs.map
{ it.bytes }.toTypedArray())
statement.setArray(3 + paramOffset, issuerRefs)
paramOffset += 1
}
statement.setLong(3 + paramOffset, amount.quantity)
log.debug { statement.toString() }
return statement.executeQuery()
statement.executeQuery().use { rs ->
return withResultSet(rs)
}
}
}
}

View File

@ -28,7 +28,7 @@ class CashSelectionSQLServerImpl : AbstractCashSelection() {
override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME"
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?,
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet {
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
val selectJoin = """
WITH row(transaction_id, output_index, pennies, total, lock_id) AS
@ -51,19 +51,22 @@ class CashSelectionSQLServerImpl : AbstractCashSelection() {
FROM row where row.total <= ? + row.pennies"""
// Use prepared statement for protection against SQL Injection
val psSelectJoin = connection.prepareStatement(selectJoin)
var pIndex = 0
psSelectJoin.setString(++pIndex, amount.token.currencyCode)
psSelectJoin.setString(++pIndex, lockId.toString())
if (notary != null)
psSelectJoin.setString(++pIndex, notary.name.toString())
if (onlyFromIssuerParties.isNotEmpty())
psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any}.toTypedArray() )
if (withIssuerRefs.isNotEmpty())
psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray())
psSelectJoin.setLong(++pIndex, amount.quantity)
log.debug(selectJoin)
connection.prepareStatement(selectJoin).use { statement ->
var pIndex = 0
statement.setString(++pIndex, amount.token.currencyCode)
statement.setString(++pIndex, lockId.toString())
if (notary != null)
statement.setString(++pIndex, notary.name.toString())
if (onlyFromIssuerParties.isNotEmpty())
statement.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any }.toTypedArray())
if (withIssuerRefs.isNotEmpty())
statement.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray())
statement.setLong(++pIndex, amount.quantity)
log.debug(selectJoin)
return psSelectJoin.executeQuery()
statement.executeQuery().use { rs ->
return withResultSet(rs)
}
}
}
}

View File

@ -32,34 +32,34 @@ public class CashTestsJava {
tx.input(Cash.PROGRAM_ID, inState);
tx.tweak(tw -> {
tw.output(Cash.PROGRAM_ID, () -> new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), new AnonymousParty(getMINI_CORP_PUBKEY())));
tw.output(Cash.PROGRAM_ID, new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), new AnonymousParty(getMINI_CORP_PUBKEY())));
tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
return tw.failsWith("the amounts balance");
});
tx.tweak(tw -> {
tw.output(Cash.PROGRAM_ID, () -> outState);
tw.output(Cash.PROGRAM_ID, outState);
tw.command(getMEGA_CORP_PUBKEY(), DummyCommandData.INSTANCE);
// Invalid command
return tw.failsWith("required net.corda.finance.contracts.asset.Cash.Commands.Move command");
});
tx.tweak(tw -> {
tw.output(Cash.PROGRAM_ID, () -> outState);
tw.output(Cash.PROGRAM_ID, outState);
tw.command(getMINI_CORP_PUBKEY(), new Cash.Commands.Move());
return tw.failsWith("the owning keys are a subset of the signing keys");
});
tx.tweak(tw -> {
tw.output(Cash.PROGRAM_ID, () -> outState);
tw.output(Cash.PROGRAM_ID, outState);
// issuedBy() can't be directly imported because it conflicts with other identically named functions
// with different overloads (for some reason).
tw.output(Cash.PROGRAM_ID, () -> outState.issuedBy(getMINI_CORP()));
tw.output(Cash.PROGRAM_ID, outState.issuedBy(getMINI_CORP()));
tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
return tw.failsWith("at least one cash input");
});
// Simple reallocation works.
return tx.tweak(tw -> {
tw.output(Cash.PROGRAM_ID, () -> outState);
tw.output(Cash.PROGRAM_ID, outState);
tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
return tw.verifies();
});

View File

@ -13,7 +13,7 @@ import net.corda.finance.DOLLARS
import net.corda.finance.`issued by`
import net.corda.finance.contracts.asset.*
import net.corda.testing.*
import net.corda.testing.contracts.fillWithSomeTestCash
import net.corda.testing.contracts.VaultFiller
import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
import org.junit.Ignore
@ -106,8 +106,8 @@ class CommercialPaperTestsGeneric {
// Some CP is issued onto the ledger by MegaCorp.
transaction("Issuance") {
attachments(CP_PROGRAM_ID, JavaCommercialPaper.JCP_PROGRAM_ID)
output(thisTest.getContract(), "paper") { thisTest.getPaper() }
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
output(thisTest.getContract(), "paper", thisTest.getPaper())
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
timeWindow(TEST_TX_TIME)
this.verifies()
}
@ -118,10 +118,10 @@ class CommercialPaperTestsGeneric {
attachments(Cash.PROGRAM_ID, JavaCommercialPaper.JCP_PROGRAM_ID)
input("paper")
input("alice's $900")
output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP }
output(thisTest.getContract(), "alice's paper") { "paper".output<ICommercialPaperState>().withOwner(ALICE) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
command(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() }
output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP)
output(thisTest.getContract(), "alice's paper", "paper".output<ICommercialPaperState>().withOwner(ALICE))
command(ALICE_PUBKEY, Cash.Commands.Move())
command(MEGA_CORP_PUBKEY, thisTest.getMoveCommand())
this.verifies()
}
@ -133,13 +133,11 @@ class CommercialPaperTestsGeneric {
input("some profits")
fun TransactionDSL<TransactionDSLInterpreter>.outputs(aliceGetsBack: Amount<Issued<Currency>>) {
output(Cash.PROGRAM_ID, "Alice's profit") { aliceGetsBack.STATE ownedBy ALICE }
output(Cash.PROGRAM_ID, "Change") { (someProfits - aliceGetsBack).STATE ownedBy MEGA_CORP }
output(Cash.PROGRAM_ID, "Alice's profit", aliceGetsBack.STATE ownedBy ALICE)
output(Cash.PROGRAM_ID, "Change", (someProfits - aliceGetsBack).STATE ownedBy MEGA_CORP)
}
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
command(ALICE_PUBKEY) { thisTest.getRedeemCommand(DUMMY_NOTARY) }
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
command(ALICE_PUBKEY, thisTest.getRedeemCommand(DUMMY_NOTARY))
tweak {
outputs(700.DOLLARS `issued by` issuer)
timeWindow(TEST_TX_TIME + 8.days)
@ -155,7 +153,7 @@ class CommercialPaperTestsGeneric {
timeWindow(TEST_TX_TIME + 8.days)
tweak {
output(thisTest.getContract()) { "paper".output<ICommercialPaperState>() }
output(thisTest.getContract(), "paper".output<ICommercialPaperState>())
this `fails with` "must be destroyed"
}
@ -169,8 +167,8 @@ class CommercialPaperTestsGeneric {
transaction {
attachment(CP_PROGRAM_ID)
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
output(thisTest.getContract()) { thisTest.getPaper() }
command(MINI_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
output(thisTest.getContract(), thisTest.getPaper())
command(MINI_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
timeWindow(TEST_TX_TIME)
this `fails with` "output states are issued by a command signer"
}
@ -181,8 +179,8 @@ class CommercialPaperTestsGeneric {
transaction {
attachment(CP_PROGRAM_ID)
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
output(thisTest.getContract()) { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) }
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
output(thisTest.getContract(), thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer))
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
timeWindow(TEST_TX_TIME)
this `fails with` "output values sum to more than the inputs"
}
@ -193,8 +191,8 @@ class CommercialPaperTestsGeneric {
transaction {
attachment(CP_PROGRAM_ID)
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
output(thisTest.getContract()) { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) }
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
output(thisTest.getContract(), thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days))
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
timeWindow(TEST_TX_TIME)
this `fails with` "maturity date is not in the past"
}
@ -206,8 +204,8 @@ class CommercialPaperTestsGeneric {
attachment(CP_PROGRAM_ID)
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
input(thisTest.getContract(), thisTest.getPaper())
output(thisTest.getContract()) { thisTest.getPaper() }
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
output(thisTest.getContract(), thisTest.getPaper())
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
timeWindow(TEST_TX_TIME)
this `fails with` "output values sum to more than the inputs"
}
@ -240,7 +238,7 @@ class CommercialPaperTestsGeneric {
aliceVaultService = aliceServices.vaultService
databaseAlice.transaction {
alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, issuerServices, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = DUMMY_CASH_ISSUER)
alicesVault = VaultFiller(aliceServices, DUMMY_NOTARY, DUMMY_NOTARY_KEY, rngFactory = ::Random).fillWithSomeTestCash(9000.DOLLARS, issuerServices, 1, DUMMY_CASH_ISSUER)
aliceVaultService = aliceServices.vaultService
}
@ -250,7 +248,7 @@ class CommercialPaperTestsGeneric {
bigCorpVaultService = bigCorpServices.vaultService
databaseBigCorp.transaction {
bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, issuerServices, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = DUMMY_CASH_ISSUER)
bigCorpVault = VaultFiller(bigCorpServices, DUMMY_NOTARY, DUMMY_NOTARY_KEY, rngFactory = ::Random).fillWithSomeTestCash(13000.DOLLARS, issuerServices, 1, DUMMY_CASH_ISSUER)
bigCorpVaultService = bigCorpServices.vaultService
}

View File

@ -3,7 +3,10 @@ package net.corda.finance.contracts.asset
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.VaultService
import net.corda.core.node.services.queryBy
@ -16,10 +19,10 @@ import net.corda.finance.utils.sumCashBy
import net.corda.finance.utils.sumCashOrNull
import net.corda.finance.utils.sumCashOrZero
import net.corda.node.services.vault.NodeVaultService
import net.corda.node.utilities.CordaPersistence
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.testing.*
import net.corda.testing.contracts.DummyState
import net.corda.testing.contracts.fillWithSomeTestCash
import net.corda.testing.contracts.VaultFiller
import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
import org.junit.After
@ -81,15 +84,12 @@ class CashTests {
}
// Create some cash. Any attempt to spend >$500 will require multiple issuers to be involved.
database.transaction {
ourServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
owner = ourIdentity, issuedBy = MEGA_CORP.ref(1), issuerServices = megaCorpServices)
ourServices.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
owner = ourIdentity, issuedBy = MEGA_CORP.ref(1), issuerServices = megaCorpServices)
ourServices.fillWithSomeTestCash(howMuch = 80.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
owner = ourIdentity, issuedBy = MINI_CORP.ref(1), issuerServices = miniCorpServices)
ourServices.fillWithSomeTestCash(howMuch = 80.SWISS_FRANCS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
owner = ourIdentity, issuedBy = MINI_CORP.ref(1), issuerServices = miniCorpServices)
database.transaction {
val vaultFiller = VaultFiller(ourServices, DUMMY_NOTARY, DUMMY_NOTARY_KEY, rngFactory = ::Random)
vaultFiller.fillWithSomeTestCash(100.DOLLARS, megaCorpServices, 1, MEGA_CORP.ref(1), ourIdentity)
vaultFiller.fillWithSomeTestCash(400.DOLLARS, megaCorpServices, 1, MEGA_CORP.ref(1), ourIdentity)
vaultFiller.fillWithSomeTestCash(80.DOLLARS, miniCorpServices, 1, MINI_CORP.ref(1), ourIdentity)
vaultFiller.fillWithSomeTestCash(80.SWISS_FRANCS, miniCorpServices, 1, MINI_CORP.ref(1), ourIdentity)
}
database.transaction {
vaultStatesUnconsumed = ourServices.vaultService.queryBy<Cash.State>().states
@ -111,34 +111,33 @@ class CashTests {
fun trivial() {
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
input(Cash.PROGRAM_ID, inState)
tweak {
output(Cash.PROGRAM_ID) { outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
output(Cash.PROGRAM_ID, outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer))
command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
tweak {
output(Cash.PROGRAM_ID) { outState }
command(ALICE_PUBKEY) { DummyCommandData }
output(Cash.PROGRAM_ID, outState)
command(ALICE_PUBKEY, DummyCommandData)
// Invalid command
this `fails with` "required net.corda.finance.contracts.asset.Cash.Commands.Move command"
}
tweak {
output(Cash.PROGRAM_ID) { outState }
command(BOB_PUBKEY) { Cash.Commands.Move() }
output(Cash.PROGRAM_ID, outState)
command(BOB_PUBKEY, Cash.Commands.Move())
this `fails with` "the owning keys are a subset of the signing keys"
}
tweak {
output(Cash.PROGRAM_ID) { outState }
output(Cash.PROGRAM_ID) { outState issuedBy MINI_CORP }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
output(Cash.PROGRAM_ID, outState)
output(Cash.PROGRAM_ID, outState issuedBy MINI_CORP)
command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "at least one cash input"
}
// Simple reallocation works.
tweak {
output(Cash.PROGRAM_ID) { outState }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
output(Cash.PROGRAM_ID, outState)
command(ALICE_PUBKEY, Cash.Commands.Move())
this.verifies()
}
}
@ -149,10 +148,9 @@ class CashTests {
// Check we can't "move" money into existence.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { DummyState() }
output(Cash.PROGRAM_ID) { outState }
command(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
input(Cash.PROGRAM_ID, DummyState())
output(Cash.PROGRAM_ID, outState)
command(MINI_CORP_PUBKEY, Cash.Commands.Move())
this `fails with` "there is at least one cash input for this group"
}
}
@ -163,19 +161,17 @@ class CashTests {
// institution is allowed to issue as much cash as they want.
transaction {
attachment(Cash.PROGRAM_ID)
output(Cash.PROGRAM_ID) { outState }
command(ALICE_PUBKEY) { Cash.Commands.Issue() }
output(Cash.PROGRAM_ID, outState)
command(ALICE_PUBKEY, Cash.Commands.Issue())
this `fails with` "output states are issued by a command signer"
}
transaction {
attachment(Cash.PROGRAM_ID)
output(Cash.PROGRAM_ID) {
output(Cash.PROGRAM_ID,
Cash.State(
amount = 1000.DOLLARS `issued by` MINI_CORP.ref(12, 34),
owner = AnonymousParty(ALICE_PUBKEY)
)
}
command(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
owner = AnonymousParty(ALICE_PUBKEY)))
command(MINI_CORP_PUBKEY, Cash.Commands.Issue())
this.verifies()
}
}
@ -211,18 +207,17 @@ class CashTests {
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { issuerInState }
output(Cash.PROGRAM_ID) { inState.copy(amount = inState.amount * 2) }
input(Cash.PROGRAM_ID, issuerInState)
output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2))
// Move fails: not allowed to summon money.
tweak {
command(ALICE_PUBKEY) { Cash.Commands.Move() }
command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
// Issue works.
tweak {
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
this.verifies()
}
}
@ -230,29 +225,29 @@ class CashTests {
// Can't use an issue command to lower the amount.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
output(Cash.PROGRAM_ID) { inState.copy(amount = inState.amount.splitEvenly(2).first()) }
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
input(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount.splitEvenly(2).first()))
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
this `fails with` "output values sum to more than the inputs"
}
// Can't have an issue command that doesn't actually issue money.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
output(Cash.PROGRAM_ID) { inState }
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
input(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, inState)
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
this `fails with` "output values sum to more than the inputs"
}
// Can't have any other commands if we have an issue command (because the issue command overrules them)
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
output(Cash.PROGRAM_ID) { inState.copy(amount = inState.amount * 2) }
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
input(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2))
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
tweak {
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
this `fails with` "there is only a single issue command"
}
this.verifies()
@ -282,26 +277,26 @@ class CashTests {
// Splitting value works.
transaction {
attachment(Cash.PROGRAM_ID)
command(ALICE_PUBKEY) { Cash.Commands.Move() }
command(ALICE_PUBKEY, Cash.Commands.Move())
tweak {
input(Cash.PROGRAM_ID) { inState }
input(Cash.PROGRAM_ID, inState)
val splits4 = inState.amount.splitEvenly(4)
for (i in 0..3) output(Cash.PROGRAM_ID) { inState.copy(amount = splits4[i]) }
for (i in 0..3) output(Cash.PROGRAM_ID, inState.copy(amount = splits4[i]))
this.verifies()
}
// Merging 4 inputs into 2 outputs works.
tweak {
val splits2 = inState.amount.splitEvenly(2)
val splits4 = inState.amount.splitEvenly(4)
for (i in 0..3) input(Cash.PROGRAM_ID) { inState.copy(amount = splits4[i]) }
for (i in 0..1) output(Cash.PROGRAM_ID) { inState.copy(amount = splits2[i]) }
for (i in 0..3) input(Cash.PROGRAM_ID, inState.copy(amount = splits4[i]))
for (i in 0..1) output(Cash.PROGRAM_ID, inState.copy(amount = splits2[i]))
this.verifies()
}
// Merging 2 inputs into 1 works.
tweak {
val splits2 = inState.amount.splitEvenly(2)
for (i in 0..1) input(Cash.PROGRAM_ID) { inState.copy(amount = splits2[i]) }
output(Cash.PROGRAM_ID) { inState }
for (i in 0..1) input(Cash.PROGRAM_ID, inState.copy(amount = splits2[i]))
output(Cash.PROGRAM_ID, inState)
this.verifies()
}
}
@ -311,17 +306,17 @@ class CashTests {
fun zeroSizedValues() {
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
input(Cash.PROGRAM_ID) { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
input(Cash.PROGRAM_ID, inState)
input(Cash.PROGRAM_ID, inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer))
command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "zero sized inputs"
}
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
output(Cash.PROGRAM_ID) { inState }
output(Cash.PROGRAM_ID) { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
input(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer))
command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "zero sized outputs"
}
}
@ -331,58 +326,56 @@ class CashTests {
// Can't change issuer.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
output(Cash.PROGRAM_ID) { outState issuedBy MINI_CORP }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
input(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, outState issuedBy MINI_CORP)
command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
// Can't change deposit reference when splitting.
transaction {
attachment(Cash.PROGRAM_ID)
val splits2 = inState.amount.splitEvenly(2)
input(Cash.PROGRAM_ID) { inState }
for (i in 0..1) output(Cash.PROGRAM_ID) { outState.copy(amount = splits2[i]).editDepositRef(i.toByte()) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
input(Cash.PROGRAM_ID, inState)
for (i in 0..1) output(Cash.PROGRAM_ID, outState.copy(amount = splits2[i]).editDepositRef(i.toByte()))
command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
// Can't mix currencies.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
output(Cash.PROGRAM_ID) { outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer) }
output(Cash.PROGRAM_ID) { outState.copy(amount = 200.POUNDS `issued by` defaultIssuer) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
input(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer))
output(Cash.PROGRAM_ID, outState.copy(amount = 200.POUNDS `issued by` defaultIssuer))
command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
input(Cash.PROGRAM_ID) {
input(Cash.PROGRAM_ID, inState)
input(Cash.PROGRAM_ID,
inState.copy(
amount = 150.POUNDS `issued by` defaultIssuer,
owner = AnonymousParty(BOB_PUBKEY)
)
}
output(Cash.PROGRAM_ID) { outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
owner = AnonymousParty(BOB_PUBKEY)))
output(Cash.PROGRAM_ID, outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer))
command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
// Can't have superfluous input states from different issuers.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
input(Cash.PROGRAM_ID) { inState issuedBy MINI_CORP }
output(Cash.PROGRAM_ID) { outState }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
input(Cash.PROGRAM_ID, inState)
input(Cash.PROGRAM_ID, inState issuedBy MINI_CORP)
output(Cash.PROGRAM_ID, outState)
command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
// Can't combine two different deposits at the same issuer.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
input(Cash.PROGRAM_ID) { inState.editDepositRef(3) }
output(Cash.PROGRAM_ID) { outState.copy(amount = inState.amount * 2).editDepositRef(3) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
input(Cash.PROGRAM_ID, inState)
input(Cash.PROGRAM_ID, inState.editDepositRef(3))
output(Cash.PROGRAM_ID, outState.copy(amount = inState.amount * 2).editDepositRef(3))
command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "for reference [01]"
}
}
@ -392,21 +385,20 @@ class CashTests {
// Single input/output straightforward case.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { issuerInState }
output(Cash.PROGRAM_ID) { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
input(Cash.PROGRAM_ID, issuerInState)
output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)))
tweak {
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer) }
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer))
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
tweak {
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
this `fails with` "required net.corda.finance.contracts.asset.Cash.Commands.Move command"
tweak {
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
this.verifies()
}
}
@ -418,20 +410,15 @@ class CashTests {
// Multi-issuer case.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { issuerInState }
input(Cash.PROGRAM_ID) { issuerInState.copy(owner = MINI_CORP) issuedBy MINI_CORP }
output(Cash.PROGRAM_ID) { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) issuedBy MINI_CORP }
output(Cash.PROGRAM_ID) { issuerInState.copy(owner = MINI_CORP, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { Cash.Commands.Move() }
input(Cash.PROGRAM_ID, issuerInState)
input(Cash.PROGRAM_ID, issuerInState.copy(owner = MINI_CORP) issuedBy MINI_CORP)
output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) issuedBy MINI_CORP)
output(Cash.PROGRAM_ID, issuerInState.copy(owner = MINI_CORP, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)))
command(listOf(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), Cash.Commands.Move())
this `fails with` "the amounts balance"
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
this `fails with` "the amounts balance"
command(MINI_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef)) }
command(MINI_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef)))
this.verifies()
}
}
@ -441,10 +428,10 @@ class CashTests {
// Single input/output straightforward case.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID) { inState }
output(Cash.PROGRAM_ID) { outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
input(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)))
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
command(ALICE_PUBKEY, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
}
@ -454,25 +441,24 @@ class CashTests {
transaction {
attachment(Cash.PROGRAM_ID)
// Gather 2000 dollars from two different issuers.
input(Cash.PROGRAM_ID) { inState }
input(Cash.PROGRAM_ID) { inState issuedBy MINI_CORP }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
input(Cash.PROGRAM_ID, inState)
input(Cash.PROGRAM_ID, inState issuedBy MINI_CORP)
command(ALICE_PUBKEY, Cash.Commands.Move())
// Can't merge them together.
tweak {
output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY), amount = 2000.DOLLARS `issued by` defaultIssuer) }
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY), amount = 2000.DOLLARS `issued by` defaultIssuer))
this `fails with` "the amounts balance"
}
// Missing MiniCorp deposit
tweak {
output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) }
output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) }
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)))
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)))
this `fails with` "the amounts balance"
}
// This works.
output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) }
output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) issuedBy MINI_CORP }
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)))
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)) issuedBy MINI_CORP)
this.verifies()
}
}
@ -483,12 +469,11 @@ class CashTests {
transaction {
attachment(Cash.PROGRAM_ID)
val pounds = Cash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), AnonymousParty(BOB_PUBKEY))
input(Cash.PROGRAM_ID) { inState ownedBy AnonymousParty(ALICE_PUBKEY) }
input(Cash.PROGRAM_ID) { pounds }
output(Cash.PROGRAM_ID) { inState ownedBy AnonymousParty(BOB_PUBKEY) }
output(Cash.PROGRAM_ID) { pounds ownedBy AnonymousParty(ALICE_PUBKEY) }
command(ALICE_PUBKEY, BOB_PUBKEY) { Cash.Commands.Move() }
input(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(ALICE_PUBKEY))
input(Cash.PROGRAM_ID, pounds)
output(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(BOB_PUBKEY))
output(Cash.PROGRAM_ID, pounds ownedBy AnonymousParty(ALICE_PUBKEY))
command(listOf(ALICE_PUBKEY, BOB_PUBKEY), Cash.Commands.Move())
this.verifies()
}
}
@ -792,19 +777,17 @@ class CashTests {
ledger(mockService) {
unverifiedTransaction {
attachment(Cash.PROGRAM_ID)
output(Cash.PROGRAM_ID, "MEGA_CORP cash") {
output(Cash.PROGRAM_ID, "MEGA_CORP cash",
Cash.State(
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
owner = MEGA_CORP
)
}
owner = MEGA_CORP))
}
transaction {
attachment(Cash.PROGRAM_ID)
input("MEGA_CORP cash")
output(Cash.PROGRAM_ID, "MEGA_CORP cash 2", "MEGA_CORP cash".output<Cash.State>().copy(owner = AnonymousParty(ALICE_PUBKEY)))
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
this.verifies()
}
@ -814,7 +797,7 @@ class CashTests {
input("MEGA_CORP cash")
// We send it to another pubkey so that the transaction is not identical to the previous one
output(Cash.PROGRAM_ID, "MEGA_CORP cash 3", "MEGA_CORP cash".output<Cash.State>().copy(owner = ALICE))
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
this.verifies()
}
this.fails()

View File

@ -71,34 +71,33 @@ class ObligationTests {
fun trivial() {
transaction {
attachments(Obligation.PROGRAM_ID)
input(Obligation.PROGRAM_ID) { inState }
input(Obligation.PROGRAM_ID, inState)
tweak {
output(Obligation.PROGRAM_ID) { outState.copy(quantity = 2000.DOLLARS.quantity) }
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
output(Obligation.PROGRAM_ID, outState.copy(quantity = 2000.DOLLARS.quantity))
command(CHARLIE.owningKey, Obligation.Commands.Move())
this `fails with` "the amounts balance"
}
tweak {
output(Obligation.PROGRAM_ID) { outState }
command(CHARLIE.owningKey) { DummyCommandData }
output(Obligation.PROGRAM_ID, outState)
command(CHARLIE.owningKey, DummyCommandData)
// Invalid command
this `fails with` "required net.corda.finance.contracts.asset.Obligation.Commands.Move command"
}
tweak {
output(Obligation.PROGRAM_ID) { outState }
command(BOB_PUBKEY) { Obligation.Commands.Move() }
output(Obligation.PROGRAM_ID, outState)
command(BOB_PUBKEY, Obligation.Commands.Move())
this `fails with` "the owning keys are a subset of the signing keys"
}
tweak {
output(Obligation.PROGRAM_ID) { outState }
output(Obligation.PROGRAM_ID) { outState `issued by` MINI_CORP }
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
output(Obligation.PROGRAM_ID, outState)
output(Obligation.PROGRAM_ID, outState `issued by` MINI_CORP)
command(CHARLIE.owningKey, Obligation.Commands.Move())
this `fails with` "at least one obligation input"
}
// Simple reallocation works.
tweak {
output(Obligation.PROGRAM_ID) { outState }
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
output(Obligation.PROGRAM_ID, outState)
command(CHARLIE.owningKey, Obligation.Commands.Move())
this.verifies()
}
}
@ -109,10 +108,9 @@ class ObligationTests {
// Check we can't "move" debt into existence.
transaction {
attachments(DummyContract.PROGRAM_ID, Obligation.PROGRAM_ID)
input(DummyContract.PROGRAM_ID) { DummyState() }
output(Obligation.PROGRAM_ID) { outState }
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() }
input(DummyContract.PROGRAM_ID, DummyState())
output(Obligation.PROGRAM_ID, outState)
command(MINI_CORP_PUBKEY, Obligation.Commands.Move())
this `fails with` "at least one obligation input"
}
@ -120,21 +118,19 @@ class ObligationTests {
// institution is allowed to issue as much cash as they want.
transaction {
attachments(Obligation.PROGRAM_ID)
output(Obligation.PROGRAM_ID) { outState }
command(CHARLIE.owningKey) { Obligation.Commands.Issue() }
output(Obligation.PROGRAM_ID, outState)
command(CHARLIE.owningKey, Obligation.Commands.Issue())
this `fails with` "output states are issued by a command signer"
}
transaction {
attachments(Obligation.PROGRAM_ID)
output(Obligation.PROGRAM_ID) {
output(Obligation.PROGRAM_ID,
Obligation.State(
obligor = MINI_CORP,
quantity = 1000.DOLLARS.quantity,
beneficiary = CHARLIE,
template = megaCorpDollarSettlement
)
}
command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue() }
template = megaCorpDollarSettlement))
command(MINI_CORP_PUBKEY, Obligation.Commands.Issue())
this.verifies()
}
run {
@ -157,18 +153,17 @@ class ObligationTests {
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
transaction {
attachments(Obligation.PROGRAM_ID)
input(Obligation.PROGRAM_ID) { inState }
output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.amount.quantity * 2) }
input(Obligation.PROGRAM_ID, inState)
output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.amount.quantity * 2))
// Move fails: not allowed to summon money.
tweak {
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
command(CHARLIE.owningKey, Obligation.Commands.Move())
this `fails with` "the amounts balance"
}
// Issue works.
tweak {
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
command(MEGA_CORP_PUBKEY, Obligation.Commands.Issue())
this.verifies()
}
}
@ -176,29 +171,29 @@ class ObligationTests {
// Can't use an issue command to lower the amount.
transaction {
attachments(Obligation.PROGRAM_ID)
input(Obligation.PROGRAM_ID) { inState }
output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.amount.quantity / 2) }
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
input(Obligation.PROGRAM_ID, inState)
output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.amount.quantity / 2))
command(MEGA_CORP_PUBKEY, Obligation.Commands.Issue())
this `fails with` "output values sum to more than the inputs"
}
// Can't have an issue command that doesn't actually issue money.
transaction {
attachments(Obligation.PROGRAM_ID)
input(Obligation.PROGRAM_ID) { inState }
output(Obligation.PROGRAM_ID) { inState }
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
input(Obligation.PROGRAM_ID, inState)
output(Obligation.PROGRAM_ID, inState)
command(MEGA_CORP_PUBKEY, Obligation.Commands.Issue())
this `fails with` ""
}
// Can't have any other commands if we have an issue command (because the issue command overrules them).
transaction {
attachments(Obligation.PROGRAM_ID)
input(Obligation.PROGRAM_ID) { inState }
output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.amount.quantity * 2) }
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
input(Obligation.PROGRAM_ID, inState)
output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.amount.quantity * 2))
command(MEGA_CORP_PUBKEY, Obligation.Commands.Issue())
tweak {
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
command(MEGA_CORP_PUBKEY, Obligation.Commands.Issue())
this `fails with` "there is only a single issue command"
}
this.verifies()
@ -352,7 +347,7 @@ class ObligationTests {
input("Alice's $1,000,000 obligation to Bob")
input("Bob's $1,000,000 obligation to Alice")
// Note we can sign with either key here
command(ALICE_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
command(ALICE_PUBKEY, Obligation.Commands.Net(NetType.CLOSE_OUT))
timeWindow(TEST_TX_TIME)
this.verifies()
}
@ -368,8 +363,8 @@ class ObligationTests {
input("Alice's $1,000,000 obligation to Bob")
input("Bob's $1,000,000 obligation to Alice")
input("MegaCorp's $1,000,000 obligation to Bob")
output(Obligation.PROGRAM_ID, "change") { oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, BOB) }
command(BOB_PUBKEY, MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
output(Obligation.PROGRAM_ID, "change", oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, BOB))
command(listOf(BOB_PUBKEY, MEGA_CORP_PUBKEY), Obligation.Commands.Net(NetType.CLOSE_OUT))
timeWindow(TEST_TX_TIME)
this.verifies()
}
@ -383,8 +378,8 @@ class ObligationTests {
attachments(Obligation.PROGRAM_ID)
input("Alice's $1,000,000 obligation to Bob")
input("Bob's $1,000,000 obligation to Alice")
output(Obligation.PROGRAM_ID, "change") { (oneMillionDollars.splitEvenly(2).first()).OBLIGATION between Pair(ALICE, BOB) }
command(BOB_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
output(Obligation.PROGRAM_ID, "change", oneMillionDollars.splitEvenly(2).first().OBLIGATION between Pair(ALICE, BOB))
command(BOB_PUBKEY, Obligation.Commands.Net(NetType.CLOSE_OUT))
timeWindow(TEST_TX_TIME)
this `fails with` "amounts owed on input and output must match"
}
@ -397,7 +392,7 @@ class ObligationTests {
attachments(Obligation.PROGRAM_ID)
input("Alice's $1,000,000 obligation to Bob")
input("Bob's $1,000,000 obligation to Alice")
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
command(MEGA_CORP_PUBKEY, Obligation.Commands.Net(NetType.CLOSE_OUT))
timeWindow(TEST_TX_TIME)
this `fails with` "any involved party has signed"
}
@ -413,7 +408,7 @@ class ObligationTests {
attachments(Obligation.PROGRAM_ID)
input("Alice's $1,000,000 obligation to Bob")
input("Bob's $1,000,000 obligation to Alice")
command(ALICE_PUBKEY, BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
command(listOf(ALICE_PUBKEY, BOB_PUBKEY), Obligation.Commands.Net(NetType.PAYMENT))
timeWindow(TEST_TX_TIME)
this.verifies()
}
@ -428,7 +423,7 @@ class ObligationTests {
attachments(Obligation.PROGRAM_ID)
input("Alice's $1,000,000 obligation to Bob")
input("Bob's $1,000,000 obligation to Alice")
command(BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
command(BOB_PUBKEY, Obligation.Commands.Net(NetType.PAYMENT))
timeWindow(TEST_TX_TIME)
this `fails with` "all involved parties have signed"
}
@ -441,8 +436,8 @@ class ObligationTests {
attachments(Obligation.PROGRAM_ID)
input("Bob's $1,000,000 obligation to Alice")
input("MegaCorp's $1,000,000 obligation to Bob")
output(Obligation.PROGRAM_ID, "MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, ALICE) }
command(ALICE_PUBKEY, BOB_PUBKEY, MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
output(Obligation.PROGRAM_ID, "MegaCorp's $1,000,000 obligation to Alice", oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, ALICE))
command(listOf(ALICE_PUBKEY, BOB_PUBKEY, MEGA_CORP_PUBKEY), Obligation.Commands.Net(NetType.PAYMENT))
timeWindow(TEST_TX_TIME)
this.verifies()
}
@ -456,8 +451,8 @@ class ObligationTests {
attachments(Obligation.PROGRAM_ID)
input("Bob's $1,000,000 obligation to Alice")
input("MegaCorp's $1,000,000 obligation to Bob")
output(Obligation.PROGRAM_ID, "MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, ALICE) }
command(ALICE_PUBKEY, BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
output(Obligation.PROGRAM_ID, "MegaCorp's $1,000,000 obligation to Alice", oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, ALICE))
command(listOf(ALICE_PUBKEY, BOB_PUBKEY), Obligation.Commands.Net(NetType.PAYMENT))
timeWindow(TEST_TX_TIME)
this `fails with` "all involved parties have signed"
}
@ -473,9 +468,9 @@ class ObligationTests {
attachments(Obligation.PROGRAM_ID)
input("Alice's $1,000,000 obligation to Bob")
input("Alice's $1,000,000")
output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB }
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) }
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) }
output(Obligation.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB)
command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)))
command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java))
attachment(attachment(cashContractBytes.inputStream()))
this.verifies()
}
@ -488,10 +483,10 @@ class ObligationTests {
attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID)
input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB))
input(Cash.PROGRAM_ID, 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE)
output(Obligation.PROGRAM_ID, "Alice's $500,000 obligation to Bob") { halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB) }
output(Obligation.PROGRAM_ID, "Bob's $500,000") { 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB }
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) }
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) }
output(Obligation.PROGRAM_ID, "Alice's $500,000 obligation to Bob", halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB))
output(Obligation.PROGRAM_ID, "Bob's $500,000", 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB)
command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)))
command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java))
attachment(attachment(cashContractBytes.inputStream()))
this.verifies()
}
@ -504,9 +499,9 @@ class ObligationTests {
attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID)
input(Obligation.PROGRAM_ID, defaultedObligation) // Alice's defaulted $1,000,000 obligation to Bob
input(Cash.PROGRAM_ID, 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE)
output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB }
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) }
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) }
output(Obligation.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB)
command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)))
command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java))
this `fails with` "all inputs are in the normal state"
}
}
@ -518,9 +513,9 @@ class ObligationTests {
attachments(Obligation.PROGRAM_ID)
input("Alice's $1,000,000 obligation to Bob")
input("Alice's $1,000,000")
output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB }
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) }
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) }
output(Obligation.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB)
command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)))
command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java))
attachment(attachment(cashContractBytes.inputStream()))
this `fails with` "amount in settle command"
}
@ -546,9 +541,9 @@ class ObligationTests {
attachments(Obligation.PROGRAM_ID)
input("Alice's 1 FCOJ obligation to Bob")
input("Alice's 1 FCOJ")
output(Obligation.PROGRAM_ID, "Bob's 1 FCOJ") { CommodityContract.State(oneUnitFcoj, BOB) }
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneUnitFcoj.quantity, oneUnitFcojObligation.amount.token)) }
command(ALICE_PUBKEY) { CommodityContract.Commands.Move(Obligation::class.java) }
output(Obligation.PROGRAM_ID, "Bob's 1 FCOJ", CommodityContract.State(oneUnitFcoj, BOB))
command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneUnitFcoj.quantity, oneUnitFcojObligation.amount.token)))
command(ALICE_PUBKEY, CommodityContract.Commands.Move(Obligation::class.java))
attachment(attachment(commodityContractBytes.inputStream()))
verifies()
}
@ -563,8 +558,8 @@ class ObligationTests {
transaction("Settlement") {
attachments(Obligation.PROGRAM_ID)
input("Alice's $1,000,000 obligation to Bob")
output(Obligation.PROGRAM_ID, "Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)).copy(lifecycle = Lifecycle.DEFAULTED) }
command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED) }
output(Obligation.PROGRAM_ID, "Alice's defaulted $1,000,000 obligation to Bob", (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)).copy(lifecycle = Lifecycle.DEFAULTED))
command(BOB_PUBKEY, Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED))
this `fails with` "there is a time-window from the authority"
}
}
@ -575,8 +570,8 @@ class ObligationTests {
transaction {
attachments(Obligation.PROGRAM_ID)
input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` futureTestTime)
output(Obligation.PROGRAM_ID, "Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` futureTestTime).copy(lifecycle = Lifecycle.DEFAULTED) }
command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED) }
output(Obligation.PROGRAM_ID, "Alice's defaulted $1,000,000 obligation to Bob", (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` futureTestTime).copy(lifecycle = Lifecycle.DEFAULTED))
command(BOB_PUBKEY, Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED))
timeWindow(TEST_TX_TIME)
this `fails with` "the due date has passed"
}
@ -586,8 +581,8 @@ class ObligationTests {
transaction {
attachments(Obligation.PROGRAM_ID)
input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` pastTestTime)
output(Obligation.PROGRAM_ID, "Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` pastTestTime).copy(lifecycle = Lifecycle.DEFAULTED) }
command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED) }
output(Obligation.PROGRAM_ID, "Alice's defaulted $1,000,000 obligation to Bob", (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` pastTestTime).copy(lifecycle = Lifecycle.DEFAULTED))
command(BOB_PUBKEY, Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED))
timeWindow(TEST_TX_TIME)
this.verifies()
}
@ -600,24 +595,24 @@ class ObligationTests {
// Splitting value works.
transaction {
attachments(Obligation.PROGRAM_ID)
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
command(CHARLIE.owningKey, Obligation.Commands.Move())
tweak {
input(Obligation.PROGRAM_ID) { inState }
repeat(4) { output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.quantity / 4) } }
input(Obligation.PROGRAM_ID, inState)
repeat(4) { output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.quantity / 4)) }
this.verifies()
}
// Merging 4 inputs into 2 outputs works.
tweak {
repeat(4) { input(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.quantity / 4) } }
output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.quantity / 2) }
output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.quantity / 2) }
repeat(4) { input(Obligation.PROGRAM_ID, inState.copy(quantity = inState.quantity / 4)) }
output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.quantity / 2))
output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.quantity / 2))
this.verifies()
}
// Merging 2 inputs into 1 works.
tweak {
input(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.quantity / 2) }
input(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.quantity / 2) }
output(Obligation.PROGRAM_ID) { inState }
input(Obligation.PROGRAM_ID, inState.copy(quantity = inState.quantity / 2))
input(Obligation.PROGRAM_ID, inState.copy(quantity = inState.quantity / 2))
output(Obligation.PROGRAM_ID, inState)
this.verifies()
}
}
@ -627,18 +622,16 @@ class ObligationTests {
fun zeroSizedValues() {
transaction {
attachments(Obligation.PROGRAM_ID)
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
command(CHARLIE.owningKey, Obligation.Commands.Move())
tweak {
input(Obligation.PROGRAM_ID) { inState }
input(Obligation.PROGRAM_ID) { inState.copy(quantity = 0L) }
input(Obligation.PROGRAM_ID, inState)
input(Obligation.PROGRAM_ID, inState.copy(quantity = 0L))
this `fails with` "zero sized inputs"
}
tweak {
input(Obligation.PROGRAM_ID) { inState }
output(Obligation.PROGRAM_ID) { inState }
output(Obligation.PROGRAM_ID) { inState.copy(quantity = 0L) }
input(Obligation.PROGRAM_ID, inState)
output(Obligation.PROGRAM_ID, inState)
output(Obligation.PROGRAM_ID, inState.copy(quantity = 0L))
this `fails with` "zero sized outputs"
}
}
@ -649,41 +642,39 @@ class ObligationTests {
// Can't change issuer.
transaction {
attachments(Obligation.PROGRAM_ID)
input(Obligation.PROGRAM_ID) { inState }
output(Obligation.PROGRAM_ID) { outState `issued by` MINI_CORP }
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() }
input(Obligation.PROGRAM_ID, inState)
output(Obligation.PROGRAM_ID, outState `issued by` MINI_CORP)
command(MINI_CORP_PUBKEY, Obligation.Commands.Move())
this `fails with` "the amounts balance"
}
// Can't mix currencies.
transaction {
attachments(Obligation.PROGRAM_ID)
input(Obligation.PROGRAM_ID) { inState }
output(Obligation.PROGRAM_ID) { outState.copy(quantity = 80000, template = megaCorpDollarSettlement) }
output(Obligation.PROGRAM_ID) { outState.copy(quantity = 20000, template = megaCorpPoundSettlement) }
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() }
input(Obligation.PROGRAM_ID, inState)
output(Obligation.PROGRAM_ID, outState.copy(quantity = 80000, template = megaCorpDollarSettlement))
output(Obligation.PROGRAM_ID, outState.copy(quantity = 20000, template = megaCorpPoundSettlement))
command(MINI_CORP_PUBKEY, Obligation.Commands.Move())
this `fails with` "the amounts balance"
}
transaction {
attachments(Obligation.PROGRAM_ID)
input(Obligation.PROGRAM_ID) { inState }
input(Obligation.PROGRAM_ID) {
input(Obligation.PROGRAM_ID, inState)
input(Obligation.PROGRAM_ID,
inState.copy(
quantity = 15000,
template = megaCorpPoundSettlement,
beneficiary = AnonymousParty(BOB_PUBKEY)
)
}
output(Obligation.PROGRAM_ID) { outState.copy(quantity = 115000) }
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() }
beneficiary = AnonymousParty(BOB_PUBKEY)))
output(Obligation.PROGRAM_ID, outState.copy(quantity = 115000))
command(MINI_CORP_PUBKEY, Obligation.Commands.Move())
this `fails with` "the amounts balance"
}
// Can't have superfluous input states from different issuers.
transaction {
attachments(Obligation.PROGRAM_ID)
input(Obligation.PROGRAM_ID) { inState }
input(Obligation.PROGRAM_ID) { inState `issued by` MINI_CORP }
output(Obligation.PROGRAM_ID) { outState }
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
input(Obligation.PROGRAM_ID, inState)
input(Obligation.PROGRAM_ID, inState `issued by` MINI_CORP)
output(Obligation.PROGRAM_ID, outState)
command(CHARLIE.owningKey, Obligation.Commands.Move())
this `fails with` "the amounts balance"
}
}
@ -693,21 +684,20 @@ class ObligationTests {
// Single input/output straightforward case.
transaction {
attachments(Obligation.PROGRAM_ID)
input(Obligation.PROGRAM_ID) { inState }
output(Obligation.PROGRAM_ID) { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) }
input(Obligation.PROGRAM_ID, inState)
output(Obligation.PROGRAM_ID, outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity))
tweak {
command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(100.DOLLARS.quantity, inState.amount.token)) }
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
command(CHARLIE.owningKey, Obligation.Commands.Exit(Amount(100.DOLLARS.quantity, inState.amount.token)))
command(CHARLIE.owningKey, Obligation.Commands.Move())
this `fails with` "the amounts balance"
}
tweak {
command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token)) }
command(CHARLIE.owningKey, Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token)))
this `fails with` "required net.corda.finance.contracts.asset.Obligation.Commands.Move command"
tweak {
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
command(CHARLIE.owningKey, Obligation.Commands.Move())
this.verifies()
}
}
@ -720,21 +710,15 @@ class ObligationTests {
// Multi-product case.
transaction {
attachments(Obligation.PROGRAM_ID)
input(Obligation.PROGRAM_ID) { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedPounds)) }
input(Obligation.PROGRAM_ID) { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedDollars)) }
output(Obligation.PROGRAM_ID) { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedPounds), quantity = inState.quantity - 200.POUNDS.quantity) }
output(Obligation.PROGRAM_ID) { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedDollars), quantity = inState.quantity - 200.DOLLARS.quantity) }
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
input(Obligation.PROGRAM_ID, inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedPounds)))
input(Obligation.PROGRAM_ID, inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedDollars)))
output(Obligation.PROGRAM_ID, inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedPounds), quantity = inState.quantity - 200.POUNDS.quantity))
output(Obligation.PROGRAM_ID, inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedDollars), quantity = inState.quantity - 200.DOLLARS.quantity))
command(CHARLIE.owningKey, Obligation.Commands.Move())
this `fails with` "the amounts balance"
command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token.copy(product = megaCorpDollarSettlement))) }
command(CHARLIE.owningKey, Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token.copy(product = megaCorpDollarSettlement))))
this `fails with` "the amounts balance"
command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(200.POUNDS.quantity, inState.amount.token.copy(product = megaCorpPoundSettlement))) }
command(CHARLIE.owningKey, Obligation.Commands.Exit(Amount(200.POUNDS.quantity, inState.amount.token.copy(product = megaCorpPoundSettlement))))
this.verifies()
}
}
@ -745,27 +729,26 @@ class ObligationTests {
attachments(Obligation.PROGRAM_ID)
// Gather 2000 dollars from two different issuers.
input(Obligation.PROGRAM_ID) { inState }
input(Obligation.PROGRAM_ID) { inState `issued by` MINI_CORP }
input(Obligation.PROGRAM_ID, inState)
input(Obligation.PROGRAM_ID, inState `issued by` MINI_CORP)
// Can't merge them together.
tweak {
output(Obligation.PROGRAM_ID) { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY), quantity = 200000L) }
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
output(Obligation.PROGRAM_ID, inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY), quantity = 200000L))
command(CHARLIE.owningKey, Obligation.Commands.Move())
this `fails with` "the amounts balance"
}
// Missing MiniCorp deposit
tweak {
output(Obligation.PROGRAM_ID) { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) }
output(Obligation.PROGRAM_ID) { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) }
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
output(Obligation.PROGRAM_ID, inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)))
output(Obligation.PROGRAM_ID, inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)))
command(CHARLIE.owningKey, Obligation.Commands.Move())
this `fails with` "the amounts balance"
}
// This works.
output(Obligation.PROGRAM_ID) { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) }
output(Obligation.PROGRAM_ID) { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) `issued by` MINI_CORP }
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
output(Obligation.PROGRAM_ID, inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)))
output(Obligation.PROGRAM_ID, inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) `issued by` MINI_CORP)
command(CHARLIE.owningKey, Obligation.Commands.Move())
this.verifies()
}
}
@ -776,12 +759,11 @@ class ObligationTests {
transaction {
attachments(Obligation.PROGRAM_ID)
val pounds = Obligation.State(Lifecycle.NORMAL, MINI_CORP, megaCorpPoundSettlement, 658.POUNDS.quantity, AnonymousParty(BOB_PUBKEY))
input(Obligation.PROGRAM_ID) { inState `owned by` CHARLIE }
input(Obligation.PROGRAM_ID) { pounds }
output(Obligation.PROGRAM_ID) { inState `owned by` AnonymousParty(BOB_PUBKEY) }
output(Obligation.PROGRAM_ID) { pounds `owned by` CHARLIE }
command(CHARLIE.owningKey, BOB_PUBKEY) { Obligation.Commands.Move() }
input(Obligation.PROGRAM_ID, inState `owned by` CHARLIE)
input(Obligation.PROGRAM_ID, pounds)
output(Obligation.PROGRAM_ID, inState `owned by` AnonymousParty(BOB_PUBKEY))
output(Obligation.PROGRAM_ID, pounds `owned by` CHARLIE)
command(listOf(CHARLIE.owningKey, BOB_PUBKEY), Obligation.Commands.Move())
this.verifies()
}
}

View File

@ -10,6 +10,7 @@ buildscript {
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
ext.bouncycastle_version = constants.getProperty("bouncycastleVersion")
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
ext.jsr305_version = constants.getProperty("jsr305Version")
ext.kotlin_version = constants.getProperty("kotlinVersion")
repositories {

View File

@ -11,6 +11,9 @@ version gradle_plugins_version
group 'net.corda.plugins'
dependencies {
// JSR 305: Nullability annotations
compile "com.google.code.findbugs:jsr305:$jsr305_version"
// TypeSafe Config: for simple and human friendly config files.
compile "com.typesafe:config:$typesafe_config_version"

View File

@ -1,24 +1,40 @@
package net.corda.cordform;
import javax.annotation.Nonnull;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public abstract class CordformDefinition {
public final Path driverDirectory;
public final ArrayList<Consumer<? super CordformNode>> nodeConfigurers = new ArrayList<>();
private Path nodesDirectory = Paths.get("build", "nodes");
private final List<Consumer<CordformNode>> nodeConfigurers = new ArrayList<>();
private final List<String> cordappPackages = new ArrayList<>();
public CordformDefinition(Path driverDirectory) {
this.driverDirectory = driverDirectory;
public Path getNodesDirectory() {
return nodesDirectory;
}
public void addNode(Consumer<? super CordformNode> configurer) {
public void setNodesDirectory(Path nodesDirectory) {
this.nodesDirectory = nodesDirectory;
}
public List<Consumer<CordformNode>> getNodeConfigurers() {
return nodeConfigurers;
}
public void addNode(Consumer<CordformNode> configurer) {
nodeConfigurers.add(configurer);
}
public List<String> getCordappPackages() {
return cordappPackages;
}
/**
* Make arbitrary changes to the node directories before they are started.
* @param context Lookup of node directory by node name.
*/
public abstract void setup(CordformContext context);
public abstract void setup(@Nonnull CordformContext context);
}

View File

@ -2,6 +2,9 @@ package net.corda.cordform;
import static java.util.Collections.emptyList;
import com.typesafe.config.*;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -54,7 +57,7 @@ public class CordformNode implements NodeDefinition {
*/
public void name(String name) {
this.name = name;
config = config.withValue("myLegalName", ConfigValueFactory.fromAnyRef(name));
setValue("myLegalName", name);
}
/**
@ -62,6 +65,7 @@ public class CordformNode implements NodeDefinition {
*
* @return This node's P2P address.
*/
@Nonnull
public String getP2pAddress() {
return config.getString("p2pAddress");
}
@ -71,8 +75,8 @@ public class CordformNode implements NodeDefinition {
*
* @param p2pPort The Artemis messaging queue port.
*/
public void p2pPort(Integer p2pPort) {
config = config.withValue("p2pAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + p2pPort));
public void p2pPort(int p2pPort) {
p2pAddress(DEFAULT_HOST + ':' + p2pPort);
}
/**
@ -81,7 +85,15 @@ public class CordformNode implements NodeDefinition {
* @param p2pAddress The Artemis messaging queue host and port.
*/
public void p2pAddress(String p2pAddress) {
config = config.withValue("p2pAddress", ConfigValueFactory.fromAnyRef(p2pAddress));
setValue("p2pAddress", p2pAddress);
}
/**
* Returns the RPC address for this node, or null if one hasn't been specified.
*/
@Nullable
public String getRpcAddress() {
return getOptionalString("rpcAddress");
}
/**
@ -89,8 +101,8 @@ public class CordformNode implements NodeDefinition {
*
* @param rpcPort The Artemis RPC queue port.
*/
public void rpcPort(Integer rpcPort) {
config = config.withValue("rpcAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + rpcPort));
public void rpcPort(int rpcPort) {
rpcAddress(DEFAULT_HOST + ':' + rpcPort);
}
/**
@ -99,7 +111,31 @@ public class CordformNode implements NodeDefinition {
* @param rpcAddress The Artemis RPC queue host and port.
*/
public void rpcAddress(String rpcAddress) {
config = config.withValue("rpcAddress", ConfigValueFactory.fromAnyRef(rpcAddress));
setValue("rpcAddress", rpcAddress);
}
/**
* Returns the address of the web server that will connect to the node, or null if one hasn't been specified.
*/
@Nullable
public String getWebAddress() {
return getOptionalString("webAddress");
}
/**
* Configure a webserver to connect to the node via RPC. This port will specify the port it will listen on. The node
* must have an RPC address configured.
*/
public void webPort(int webPort) {
webAddress(DEFAULT_HOST + ':' + webPort);
}
/**
* Configure a webserver to connect to the node via RPC. This address will specify the port it will listen on. The node
* must have an RPC address configured.
*/
public void webAddress(String webAddress) {
setValue("webAddress", webAddress);
}
/**
@ -108,6 +144,14 @@ public class CordformNode implements NodeDefinition {
* @param configFile The file path.
*/
public void configFile(String configFile) {
config = config.withValue("configFile", ConfigValueFactory.fromAnyRef(configFile));
setValue("configFile", configFile);
}
private String getOptionalString(String path) {
return config.hasPath(path) ? config.getString(path) : null;
}
private void setValue(String path, Object value) {
config = config.withValue(path, ConfigValueFactory.fromAnyRef(value));
}
}

View File

@ -11,10 +11,10 @@ import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.net.URLClassLoader
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.TimeUnit
import java.util.jar.JarInputStream
/**
* Creates nodes based on the configuration of this task in the gradle configuration DSL.
@ -23,12 +23,16 @@ import java.util.concurrent.TimeUnit
*/
@Suppress("unused")
open class Cordform : DefaultTask() {
private companion object {
private val defaultDirectory: Path = Paths.get("build", "nodes")
}
/**
* Optionally the name of a CordformDefinition subclass to which all configuration will be delegated.
*/
@Suppress("MemberVisibilityCanPrivate")
var definitionClass: String? = null
private var directory = Paths.get("build", "nodes")
private var directory = defaultDirectory
private val nodes = mutableListOf<Node>()
/**
@ -116,7 +120,6 @@ open class Cordform : DefaultTask() {
/**
* This task action will create and install the nodes based on the node configurations added.
*/
@Suppress("unused")
@TaskAction
fun build() {
project.logger.info("Running Cordform task")
@ -129,10 +132,18 @@ open class Cordform : DefaultTask() {
private fun initializeConfiguration() {
if (definitionClass != null) {
val cd = loadCordformDefinition()
// If the user has specified their own directory (even if it's the same default path) then let them know
// it's not used and should just rely on the one in CordformDefinition
require(directory === defaultDirectory) {
"'directory' cannot be used when 'definitionClass' is specified. Use CordformDefinition.nodesDirectory instead."
}
directory = cd.nodesDirectory
val cordapps = cd.getMatchingCordapps()
cd.nodeConfigurers.forEach {
val node = node { }
it.accept(node)
node.rootDir(directory)
node.installCordapps(cordapps)
}
cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) }
} else {
@ -142,6 +153,30 @@ open class Cordform : DefaultTask() {
}
}
private fun CordformDefinition.getMatchingCordapps(): List<File> {
val cordappJars = project.configuration("cordapp").files
return cordappPackages.map { `package` ->
val cordappsWithPackage = cordappJars.filter { it.containsPackage(`package`) }
when (cordappsWithPackage.size) {
0 -> throw IllegalArgumentException("There are no cordapp dependencies containing the package $`package`")
1 -> cordappsWithPackage[0]
else -> throw IllegalArgumentException("More than one cordapp dependency contains the package $`package`: $cordappsWithPackage")
}
}
}
private fun File.containsPackage(`package`: String): Boolean {
JarInputStream(inputStream()).use {
while (true) {
val name = it.nextJarEntry?.name ?: break
if (name.endsWith(".class") && name.replace('/', '.').startsWith(`package`)) {
return true
}
}
return false
}
}
private fun generateAndInstallNodeInfos() {
generateNodeInfos()
installNodeInfos()
@ -149,7 +184,7 @@ open class Cordform : DefaultTask() {
private fun generateNodeInfos() {
project.logger.info("Generating node infos")
var nodeProcesses = buildNodeProcesses()
val nodeProcesses = buildNodeProcesses()
try {
validateNodeProcessess(nodeProcesses)
} finally {
@ -177,7 +212,7 @@ open class Cordform : DefaultTask() {
private fun buildNodeProcess(node: Node): Pair<Node, Process> {
node.makeLogDirectory()
var process = ProcessBuilder(generateNodeInfoCommand())
val process = ProcessBuilder(generateNodeInfoCommand())
.directory(node.fullPath().toFile())
.redirectErrorStream(true)
// InheritIO causes hangs on windows due the gradle buffer also not being flushed.
@ -224,6 +259,8 @@ open class Cordform : DefaultTask() {
}
}
}
private fun Node.logFile(): Path = this.logDirectory().resolve("generate-info.log")
private fun ProcessBuilder.addEnvironment(key: String, value: String) = this.apply { environment().put(key, value) }
}

View File

@ -3,7 +3,6 @@ package net.corda.plugins
import com.typesafe.config.*
import net.corda.cordform.CordformNode
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.RDN
import org.bouncycastle.asn1.x500.style.BCStyle
import org.gradle.api.Project
import java.io.File
@ -39,6 +38,7 @@ class Node(private val project: Project) : CordformNode() {
private val releaseVersion = project.rootProject.ext<String>("corda_release_version")
internal lateinit var nodeDir: File
private set
/**
* Sets whether this node will use HTTPS communication.
@ -60,26 +60,6 @@ class Node(private val project: Project) : CordformNode() {
config = config.withValue("useTestClock", ConfigValueFactory.fromAnyRef(useTestClock))
}
/**
* Set the HTTP web server port for this node. Will use localhost as the address.
*
* @param webPort The web port number for this node.
*/
fun webPort(webPort: Int) {
config = config.withValue("webAddress",
ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$webPort"))
}
/**
* Set the HTTP web server address and port for this node.
*
* @param webAddress The web address for this node.
*/
fun webAddress(webAddress: String) {
config = config.withValue("webAddress",
ConfigValueFactory.fromAnyRef(webAddress))
}
/**
* Set the network map address for this node.
*
@ -104,7 +84,6 @@ class Node(private val project: Project) : CordformNode() {
config = config.withValue("sshd.port", ConfigValueFactory.fromAnyRef(sshdPort))
}
internal fun build() {
configureProperties()
installCordaJar()
@ -118,19 +97,15 @@ class Node(private val project: Project) : CordformNode() {
}
internal fun rootDir(rootDir: Path) {
if(name == null) {
if (name == null) {
project.logger.error("Node has a null name - cannot create node")
throw IllegalStateException("Node has a null name - cannot create node")
}
val dirName = try {
val o = X500Name(name).getRDNs(BCStyle.O)
if (o.size > 0) {
o.first().first.value.toString()
} else {
name
}
} catch(_ : IllegalArgumentException) {
if (o.isNotEmpty()) o.first().first.value.toString() else name
} catch (_ : IllegalArgumentException) {
// Can't parse as an X500 name, use the full string
name
}
@ -192,9 +167,8 @@ class Node(private val project: Project) : CordformNode() {
/**
* Installs other cordapps to this node's cordapps directory.
*/
private fun installCordapps() {
internal fun installCordapps(cordapps: Collection<File> = getCordappList()) {
val cordappsDir = File(nodeDir, "cordapps")
val cordapps = getCordappList()
project.copy {
it.apply {
from(cordapps)
@ -280,7 +254,7 @@ class Node(private val project: Project) : CordformNode() {
throw RuntimeException("No Corda Webserver JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-webserver-$releaseVersion.jar\"")
} else {
val jar = maybeJar.singleFile
assert(jar.isFile)
require(jar.isFile)
return jar
}
}

View File

@ -1,6 +1,7 @@
#Mon Nov 13 08:47:49 GMT 2017
#Sat Nov 25 22:21:50 GMT 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
# Gradle >= 4.3.1 required for Enterprise only experimental:intellij-plugin
distributionUrl=https\://services.gradle.org/distributions/gradle-4.3.1-all.zip

View File

@ -3,7 +3,7 @@ apply plugin: 'net.corda.plugins.quasar-utils'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
description 'Corda node Artemis API'
description 'Corda node API'
dependencies {
compile project(":core")

View File

@ -1,4 +1,4 @@
package net.corda.node.utilities
package net.corda.nodeapi.internal.crypto
import net.corda.core.crypto.SignatureScheme
import org.bouncycastle.asn1.x509.AlgorithmIdentifier

View File

@ -1,9 +1,8 @@
@file:JvmName("KeyStoreUtilities")
package net.corda.node.utilities
package net.corda.nodeapi.internal.crypto
import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.*
import org.bouncycastle.cert.X509CertificateHolder
import java.io.IOException
@ -11,9 +10,7 @@ import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Path
import java.security.*
import java.security.cert.CertPath
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
const val KEYSTORE_TYPE = "JKS"
@ -169,44 +166,3 @@ fun KeyStore.getSupportedKey(alias: String, keyPassword: String): PrivateKey {
val key = getKey(alias, keyPass) as PrivateKey
return Crypto.toSupportedPrivateKey(key)
}
class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) {
private val keyStore = storePath.read { loadKeyStore(it, storePassword) }
private fun createCertificate(serviceName: CordaX500Name, pubKey: PublicKey): CertPath {
val clientCertPath = keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
// Assume key password = store password.
val clientCA = certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
// Create new keys and store in keystore.
val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, pubKey)
val certPath = CertificateFactory.getInstance("X509").generateCertPath(listOf(cert.cert) + clientCertPath)
require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" }
// TODO: X509Utilities.validateCertificateChain()
return certPath
}
fun signAndSaveNewKeyPair(serviceName: CordaX500Name, privateKeyAlias: String, keyPair: KeyPair) {
val certPath = createCertificate(serviceName, keyPair.public)
// Assume key password = store password.
keyStore.addOrReplaceKey(privateKeyAlias, keyPair.private, storePassword.toCharArray(), certPath.certificates.toTypedArray())
keyStore.save(storePath, storePassword)
}
fun savePublicKey(serviceName: CordaX500Name, pubKeyAlias: String, pubKey: PublicKey) {
val certPath = createCertificate(serviceName, pubKey)
// Assume key password = store password.
keyStore.addOrReplaceCertificate(pubKeyAlias, certPath.certificates.first())
keyStore.save(storePath, storePassword)
}
// Delegate methods to keystore. Sadly keystore doesn't have an interface.
fun containsAlias(alias: String) = keyStore.containsAlias(alias)
fun getX509Certificate(alias: String) = keyStore.getX509Certificate(alias)
fun getCertificateChain(alias: String): Array<out Certificate> = keyStore.getCertificateChain(alias)
fun getCertificate(alias: String): Certificate = keyStore.getCertificate(alias)
fun certificateAndKeyPair(alias: String): CertificateAndKeyPair = keyStore.getCertificateAndKeyPair(alias, storePassword)
}

View File

@ -0,0 +1,52 @@
package net.corda.nodeapi.internal.crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.cert
import net.corda.core.internal.read
import java.nio.file.Path
import java.security.KeyPair
import java.security.PublicKey
import java.security.cert.CertPath
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) {
private val keyStore = storePath.read { loadKeyStore(it, storePassword) }
private fun createCertificate(serviceName: CordaX500Name, pubKey: PublicKey): CertPath {
val clientCertPath = keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
// Assume key password = store password.
val clientCA = certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
// Create new keys and store in keystore.
val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, pubKey)
val certPath = X509CertificateFactory().delegate.generateCertPath(listOf(cert.cert) + clientCertPath)
require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" }
// TODO: X509Utilities.validateCertificateChain()
return certPath
}
fun signAndSaveNewKeyPair(serviceName: CordaX500Name, privateKeyAlias: String, keyPair: KeyPair) {
val certPath = createCertificate(serviceName, keyPair.public)
// Assume key password = store password.
keyStore.addOrReplaceKey(privateKeyAlias, keyPair.private, storePassword.toCharArray(), certPath.certificates.toTypedArray())
keyStore.save(storePath, storePassword)
}
fun savePublicKey(serviceName: CordaX500Name, pubKeyAlias: String, pubKey: PublicKey) {
val certPath = createCertificate(serviceName, pubKey)
// Assume key password = store password.
keyStore.addOrReplaceCertificate(pubKeyAlias, certPath.certificates.first())
keyStore.save(storePath, storePassword)
}
// Delegate methods to keystore. Sadly keystore doesn't have an interface.
fun containsAlias(alias: String) = keyStore.containsAlias(alias)
fun getX509Certificate(alias: String) = keyStore.getX509Certificate(alias)
fun getCertificateChain(alias: String): Array<out Certificate> = keyStore.getCertificateChain(alias)
fun getCertificate(alias: String): Certificate = keyStore.getCertificate(alias)
fun certificateAndKeyPair(alias: String): CertificateAndKeyPair = keyStore.getCertificateAndKeyPair(alias, storePassword)
}

View File

@ -1,9 +1,11 @@
package net.corda.node.utilities
package net.corda.nodeapi.internal.crypto
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignatureScheme
import net.corda.core.crypto.random63BitValue
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.read
import net.corda.core.internal.write
import net.corda.core.internal.x500Name
import net.corda.core.utilities.days
import net.corda.core.utilities.millis
@ -25,10 +27,10 @@ import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder
import org.bouncycastle.util.io.pem.PemReader
import java.io.FileReader
import java.io.FileWriter
import java.io.InputStream
import java.math.BigInteger
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyPair
import java.security.PublicKey
@ -52,6 +54,7 @@ object X509Utilities {
const val CORDA_CLIENT_CA_CN = "Corda Client CA Certificate"
private val DEFAULT_VALIDITY_WINDOW = Pair(0.millis, 3650.days)
/**
* Helper function to return the latest out of an instant and an optional date.
*/
@ -89,7 +92,9 @@ object X509Utilities {
* Create a de novo root self-signed X509 v3 CA cert.
*/
@JvmStatic
fun createSelfSignedCACertificate(subject: CordaX500Name, keyPair: KeyPair, validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW): X509CertificateHolder {
fun createSelfSignedCACertificate(subject: CordaX500Name,
keyPair: KeyPair,
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW): X509CertificateHolder {
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second)
return createCertificate(CertificateType.ROOT_CA, subject.x500Name, keyPair, subject.x500Name, keyPair.public, window)
}
@ -114,8 +119,9 @@ object X509Utilities {
subject: CordaX500Name,
subjectPublicKey: PublicKey,
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW,
nameConstraints: NameConstraints? = null): X509CertificateHolder
= createCertificate(certificateType, issuerCertificate, issuerKeyPair, subject.x500Name, subjectPublicKey, validityWindow, nameConstraints)
nameConstraints: NameConstraints? = null): X509CertificateHolder {
return createCertificate(certificateType, issuerCertificate, issuerKeyPair, subject.x500Name, subjectPublicKey, validityWindow, nameConstraints)
}
/**
* Create a X509 v3 certificate for use as a CA or for TLS. This does not require a [CordaX500Name] because the
@ -145,10 +151,9 @@ object X509Utilities {
@Throws(CertPathValidatorException::class)
fun validateCertificateChain(trustedRoot: X509Certificate, vararg certificates: Certificate) {
require(certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" }
val certFactory = CertificateFactory.getInstance("X509")
val params = PKIXParameters(setOf(TrustAnchor(trustedRoot, null)))
params.isRevocationEnabled = false
val certPath = certFactory.generateCertPath(certificates.toList())
val certPath = X509CertificateFactory().delegate.generateCertPath(certificates.toList())
val pathValidator = CertPathValidator.getInstance("PKIX")
pathValidator.validate(certPath, params)
}
@ -156,30 +161,29 @@ object X509Utilities {
/**
* Helper method to store a .pem/.cer format file copy of a certificate if required for import into a PC/Mac, or for inspection.
* @param x509Certificate certificate to save.
* @param filename Target filename.
* @param file Target file.
*/
@JvmStatic
fun saveCertificateAsPEMFile(x509Certificate: X509CertificateHolder, filename: Path) {
FileWriter(filename.toFile()).use {
JcaPEMWriter(it).use {
it.writeObject(x509Certificate)
}
fun saveCertificateAsPEMFile(x509Certificate: X509CertificateHolder, file: Path) {
JcaPEMWriter(file.toFile().writer()).use {
it.writeObject(x509Certificate)
}
}
/**
* Helper method to load back a .pem/.cer format file copy of a certificate.
* @param filename Source filename.
* @param file Source file.
* @return The X509Certificate that was encoded in the file.
*/
@JvmStatic
fun loadCertificateFromPEMFile(filename: Path): X509CertificateHolder {
val reader = PemReader(FileReader(filename.toFile()))
val pemObject = reader.readPemObject()
val cert = X509CertificateHolder(pemObject.content)
return cert.apply {
isValidOn(Date())
fun loadCertificateFromPEMFile(file: Path): X509CertificateHolder {
val cert = file.read {
val reader = PemReader(it.reader())
val pemObject = reader.readPemObject()
X509CertificateHolder(pemObject.content)
}
cert.isValidOn(Date())
return cert
}
/**
@ -243,13 +247,13 @@ object X509Utilities {
* @param validityWindow the time period the certificate is valid for.
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
*/
internal fun createCertificate(certificateType: CertificateType,
issuer: X500Name,
issuerSigner: ContentSigner,
subject: CordaX500Name,
subjectPublicKey: PublicKey,
validityWindow: Pair<Date, Date>,
nameConstraints: NameConstraints? = null): X509CertificateHolder {
fun createCertificate(certificateType: CertificateType,
issuer: X500Name,
issuerSigner: ContentSigner,
subject: CordaX500Name,
subjectPublicKey: PublicKey,
validityWindow: Pair<Date, Date>,
nameConstraints: NameConstraints? = null): X509CertificateHolder {
val builder = createCertificate(certificateType, issuer, subject.x500Name, subjectPublicKey, validityWindow, nameConstraints)
return builder.build(issuerSigner).apply {
require(isValidOn(Date()))
@ -266,11 +270,13 @@ object X509Utilities {
* @param validityWindow the time period the certificate is valid for.
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
*/
internal fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerKeyPair: KeyPair,
subject: X500Name, subjectPublicKey: PublicKey,
validityWindow: Pair<Date, Date>,
nameConstraints: NameConstraints? = null): X509CertificateHolder {
fun createCertificate(certificateType: CertificateType,
issuer: X500Name,
issuerKeyPair: KeyPair,
subject: X500Name,
subjectPublicKey: PublicKey,
validityWindow: Pair<Date, Date>,
nameConstraints: NameConstraints? = null): X509CertificateHolder {
val signatureScheme = Crypto.findSignatureScheme(issuerKeyPair.private)
val provider = Crypto.findProvider(signatureScheme.providerName)
val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints)
@ -285,28 +291,71 @@ object X509Utilities {
/**
* Create certificate signing request using provided information.
*/
internal fun createCertificateSigningRequest(subject: CordaX500Name, email: String, keyPair: KeyPair, signatureScheme: SignatureScheme): PKCS10CertificationRequest {
private fun createCertificateSigningRequest(subject: CordaX500Name,
email: String,
keyPair: KeyPair,
signatureScheme: SignatureScheme): PKCS10CertificationRequest {
val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, Crypto.findProvider(signatureScheme.providerName))
return JcaPKCS10CertificationRequestBuilder(subject.x500Name, keyPair.public).addAttribute(BCStyle.E, DERUTF8String(email)).build(signer)
}
fun createCertificateSigningRequest(subject: CordaX500Name, email: String, keyPair: KeyPair) = createCertificateSigningRequest(subject, email, keyPair, DEFAULT_TLS_SIGNATURE_SCHEME)
fun createCertificateSigningRequest(subject: CordaX500Name, email: String, keyPair: KeyPair): PKCS10CertificationRequest {
return createCertificateSigningRequest(subject, email, keyPair, DEFAULT_TLS_SIGNATURE_SCHEME)
}
}
class CertificateStream(val input: InputStream) {
private val certificateFactory = CertificateFactory.getInstance("X.509")
fun nextCertificate(): X509Certificate = certificateFactory.generateCertificate(input) as X509Certificate
/**
* Wraps a [CertificateFactory] to remove boilerplate. It's unclear whether [CertificateFactory] is threadsafe so best
* so assume this class is not.
*/
class X509CertificateFactory {
val delegate: CertificateFactory = CertificateFactory.getInstance("X.509")
fun generateCertificate(input: InputStream): X509Certificate {
return delegate.generateCertificate(input) as X509Certificate
}
}
enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurposeId, val isCA: Boolean) {
ROOT_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true),
INTERMEDIATE_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true),
CLIENT_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true),
TLS(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.keyAgreement), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = false),
ROOT_CA(
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign),
KeyPurposeId.id_kp_serverAuth,
KeyPurposeId.id_kp_clientAuth,
KeyPurposeId.anyExtendedKeyUsage,
isCA = true
),
INTERMEDIATE_CA(
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign),
KeyPurposeId.id_kp_serverAuth,
KeyPurposeId.id_kp_clientAuth,
KeyPurposeId.anyExtendedKeyUsage,
isCA = true
),
CLIENT_CA(
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign),
KeyPurposeId.id_kp_serverAuth,
KeyPurposeId.id_kp_clientAuth,
KeyPurposeId.anyExtendedKeyUsage,
isCA = true
),
TLS(
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.keyAgreement),
KeyPurposeId.id_kp_serverAuth,
KeyPurposeId.id_kp_clientAuth,
KeyPurposeId.anyExtendedKeyUsage,
isCA = false
),
// TODO: Identity certs should have only limited depth (i.e. 1) CA signing capability, with tight name constraints
IDENTITY(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true)
IDENTITY(
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign),
KeyPurposeId.id_kp_serverAuth,
KeyPurposeId.id_kp_clientAuth,
KeyPurposeId.anyExtendedKeyUsage,
isCA = true
)
}
data class CertificateAndKeyPair(val certificate: X509CertificateHolder, val keyPair: KeyPair)

View File

@ -1,63 +1,80 @@
package net.corda.node.utilities
package net.corda.nodeapi.internal.persistence
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import net.corda.core.node.services.IdentityService
import net.corda.node.services.api.SchemaService
import net.corda.node.services.config.DatabaseConfig
import net.corda.node.services.persistence.HibernateConfiguration
import net.corda.node.services.schema.NodeSchemaService
import net.corda.core.schemas.MappedSchema
import rx.Observable
import rx.Subscriber
import rx.subjects.UnicastSubject
import java.io.Closeable
import java.sql.Connection
import java.sql.SQLException
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
import javax.persistence.AttributeConverter
import javax.sql.DataSource
/**
* Table prefix for all tables owned by the node module.
*/
const val NODE_DATABASE_PREFIX = "node_"
//HikariDataSource implements Closeable which allows CordaPersistence to be Closeable
// This class forms part of the node config and so any changes to it must be handled with care
data class DatabaseConfig(
val initialiseSchema: Boolean = true,
val serverNameTablePrefix: String = "",
val transactionIsolationLevel: TransactionIsolationLevel = TransactionIsolationLevel.REPEATABLE_READ,
val schema: String? = null
)
// This class forms part of the node config and so any changes to it must be handled with care
enum class TransactionIsolationLevel {
NONE,
READ_UNCOMMITTED,
READ_COMMITTED,
REPEATABLE_READ,
SERIALIZABLE;
/**
* The JDBC constant value of the same name but prefixed with TRANSACTION_ defined in [java.sql.Connection].
*/
val jdbcValue: Int = java.sql.Connection::class.java.getField("TRANSACTION_$name").get(null) as Int
}
class CordaPersistence(
val dataSource: HikariDataSource,
private val schemaService: SchemaService,
private val identityService: IdentityService,
databaseConfig: DatabaseConfig
val dataSource: DataSource,
databaseConfig: DatabaseConfig,
schemas: Set<MappedSchema>,
attributeConverters: Collection<AttributeConverter<*, *>> = emptySet()
) : Closeable {
val transactionIsolationLevel = databaseConfig.transactionIsolationLevel.jdbcValue
val defaultIsolationLevel = databaseConfig.transactionIsolationLevel
val hibernateConfig: HibernateConfiguration by lazy {
transaction {
HibernateConfiguration(schemaService, databaseConfig, identityService)
HibernateConfiguration(schemas, databaseConfig, attributeConverters)
}
}
val entityManagerFactory get() = hibernateConfig.sessionFactoryForRegisteredSchemas
companion object {
fun connect(dataSource: HikariDataSource, schemaService: SchemaService, identityService: IdentityService, databaseConfig: DatabaseConfig): CordaPersistence {
return CordaPersistence(dataSource, schemaService, identityService, databaseConfig).apply {
DatabaseTransactionManager(this)
init {
DatabaseTransactionManager(this)
// Check not in read-only mode.
transaction {
dataSource.connection.use {
check(!it.metaData.isReadOnly) { "Database should not be readonly." }
}
}
}
/**
* Creates an instance of [DatabaseTransaction], with the given isolation level.
* @param isolationLevel isolation level for the transaction. If not specified the default (i.e. provided at the creation time) is used.
* Creates an instance of [DatabaseTransaction], with the given transaction isolation level.
*/
fun createTransaction(isolationLevel: Int): DatabaseTransaction {
fun createTransaction(isolationLevel: TransactionIsolationLevel): DatabaseTransaction {
// We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases.
DatabaseTransactionManager.dataSource = this
return DatabaseTransactionManager.currentOrNew(isolationLevel)
}
/**
* Creates an instance of [DatabaseTransaction], with the transaction isolation level specified at the creation time.
* Creates an instance of [DatabaseTransaction], with the default transaction isolation level.
*/
fun createTransaction(): DatabaseTransaction = createTransaction(transactionIsolationLevel)
fun createTransaction(): DatabaseTransaction = createTransaction(defaultIsolationLevel)
fun createSession(): Connection {
// We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases.
@ -71,7 +88,7 @@ class CordaPersistence(
* @param isolationLevel isolation level for the transaction.
* @param statement to be executed in the scope of this transaction.
*/
fun <T> transaction(isolationLevel: Int, statement: DatabaseTransaction.() -> T): T {
fun <T> transaction(isolationLevel: TransactionIsolationLevel, statement: DatabaseTransaction.() -> T): T {
DatabaseTransactionManager.dataSource = this
return transaction(isolationLevel, 3, statement)
}
@ -80,22 +97,21 @@ class CordaPersistence(
* Executes given statement in the scope of transaction with the transaction level specified at the creation time.
* @param statement to be executed in the scope of this transaction.
*/
fun <T> transaction(statement: DatabaseTransaction.() -> T): T = transaction(transactionIsolationLevel, statement)
fun <T> transaction(statement: DatabaseTransaction.() -> T): T = transaction(defaultIsolationLevel, statement)
private fun <T> transaction(transactionIsolation: Int, repetitionAttempts: Int, statement: DatabaseTransaction.() -> T): T {
private fun <T> transaction(isolationLevel: TransactionIsolationLevel, repetitionAttempts: Int, statement: DatabaseTransaction.() -> T): T {
val outer = DatabaseTransactionManager.currentOrNull()
return if (outer != null) {
outer.statement()
} else {
inTopLevelTransaction(transactionIsolation, repetitionAttempts, statement)
inTopLevelTransaction(isolationLevel, repetitionAttempts, statement)
}
}
private fun <T> inTopLevelTransaction(transactionIsolation: Int, repetitionAttempts: Int, statement: DatabaseTransaction.() -> T): T {
private fun <T> inTopLevelTransaction(isolationLevel: TransactionIsolationLevel, repetitionAttempts: Int, statement: DatabaseTransaction.() -> T): T {
var repetitions = 0
while (true) {
val transaction = DatabaseTransactionManager.currentOrNew(transactionIsolation)
val transaction = DatabaseTransactionManager.currentOrNew(isolationLevel)
try {
val answer = transaction.statement()
transaction.commit()
@ -116,23 +132,11 @@ class CordaPersistence(
}
override fun close() {
dataSource.close()
// DataSource doesn't implement AutoCloseable so we just have to hope that the implementation does so that we can close it
(dataSource as? AutoCloseable)?.close()
}
}
fun configureDatabase(dataSourceProperties: Properties, databaseConfig: DatabaseConfig, identityService: IdentityService, schemaService: SchemaService = NodeSchemaService(null)): CordaPersistence {
val config = HikariConfig(dataSourceProperties)
val dataSource = HikariDataSource(config)
val persistence = CordaPersistence.connect(dataSource, schemaService, identityService, databaseConfig)
// Check not in read-only mode.
persistence.transaction {
persistence.dataSource.connection.use {
check(!it.metaData.isReadOnly) { "Database should not be readonly." }
}
}
return persistence
}
/**
* Buffer observations until after the current database transaction has been closed. Observations are never
* dropped, simply delayed.
@ -144,7 +148,7 @@ fun configureDatabase(dataSourceProperties: Properties, databaseConfig: Database
*/
fun <T : Any> rx.Observer<T>.bufferUntilDatabaseCommit(): rx.Observer<T> {
val currentTxId = DatabaseTransactionManager.transactionId
val databaseTxBoundary: Observable<DatabaseTransactionManager.Boundary> = DatabaseTransactionManager.transactionBoundaries.filter { it.txId == currentTxId }.first()
val databaseTxBoundary: Observable<DatabaseTransactionManager.Boundary> = DatabaseTransactionManager.transactionBoundaries.first { it.txId == currentTxId }
val subject = UnicastSubject.create<T>()
subject.delaySubscription(databaseTxBoundary).subscribe(this)
databaseTxBoundary.doOnCompleted { subject.onCompleted() }
@ -183,14 +187,9 @@ private class DatabaseTransactionWrappingSubscriber<U>(val db: CordaPersistence?
// A subscriber that wraps another but does not pass on observations to it.
private class NoOpSubscriber<U>(t: Subscriber<in U>) : Subscriber<U>(t) {
override fun onCompleted() {
}
override fun onError(e: Throwable?) {
}
override fun onNext(s: U) {
}
override fun onCompleted() {}
override fun onError(e: Throwable?) {}
override fun onNext(s: U) {}
}
/**

View File

@ -0,0 +1,61 @@
package net.corda.nodeapi.internal.persistence
import org.hibernate.Session
import org.hibernate.Transaction
import rx.subjects.Subject
import java.sql.Connection
import java.util.*
class DatabaseTransaction(
isolation: Int,
private val threadLocal: ThreadLocal<DatabaseTransaction>,
private val transactionBoundaries: Subject<DatabaseTransactionManager.Boundary, DatabaseTransactionManager.Boundary>,
val cordaPersistence: CordaPersistence
) {
val id: UUID = UUID.randomUUID()
val connection: Connection by lazy(LazyThreadSafetyMode.NONE) {
cordaPersistence.dataSource.connection.apply {
autoCommit = false
transactionIsolation = isolation
}
}
private val sessionDelegate = lazy {
val session = cordaPersistence.entityManagerFactory.withOptions().connection(connection).openSession()
hibernateTransaction = session.beginTransaction()
session
}
val session: Session by sessionDelegate
private lateinit var hibernateTransaction: Transaction
private val outerTransaction: DatabaseTransaction? = threadLocal.get()
fun commit() {
if (sessionDelegate.isInitialized()) {
hibernateTransaction.commit()
}
connection.commit()
}
fun rollback() {
if (sessionDelegate.isInitialized() && session.isOpen) {
session.clear()
}
if (!connection.isClosed) {
connection.rollback()
}
}
fun close() {
if (sessionDelegate.isInitialized() && session.isOpen) {
session.close()
}
connection.close()
threadLocal.set(outerTransaction)
if (outerTransaction == null) {
transactionBoundaries.onNext(DatabaseTransactionManager.Boundary(id))
}
}
}

View File

@ -1,68 +1,14 @@
package net.corda.node.utilities
package net.corda.nodeapi.internal.persistence
import co.paralleluniverse.strands.Strand
import org.hibernate.Session
import org.hibernate.Transaction
import rx.subjects.PublishSubject
import rx.subjects.Subject
import java.sql.Connection
import java.util.*
import java.util.concurrent.ConcurrentHashMap
class DatabaseTransaction(isolation: Int, val threadLocal: ThreadLocal<DatabaseTransaction>,
val transactionBoundaries: Subject<DatabaseTransactionManager.Boundary, DatabaseTransactionManager.Boundary>,
val cordaPersistence: CordaPersistence) {
fun currentDBSession(): Session = DatabaseTransactionManager.current().session
val id: UUID = UUID.randomUUID()
val connection: Connection by lazy(LazyThreadSafetyMode.NONE) {
cordaPersistence.dataSource.connection
.apply {
autoCommit = false
transactionIsolation = isolation
}
}
private val sessionDelegate = lazy {
val session = cordaPersistence.entityManagerFactory.withOptions().connection(connection).openSession()
hibernateTransaction = session.beginTransaction()
session
}
val session: Session by sessionDelegate
private lateinit var hibernateTransaction: Transaction
private val outerTransaction: DatabaseTransaction? = threadLocal.get()
fun commit() {
if (sessionDelegate.isInitialized()) {
hibernateTransaction.commit()
}
connection.commit()
}
fun rollback() {
if (sessionDelegate.isInitialized() && session.isOpen) {
session.clear()
}
if (!connection.isClosed) {
connection.rollback()
}
}
fun close() {
if (sessionDelegate.isInitialized() && session.isOpen) {
session.close()
}
connection.close()
threadLocal.set(outerTransaction)
if (outerTransaction == null) {
transactionBoundaries.onNext(DatabaseTransactionManager.Boundary(id))
}
}
}
fun currentDBSession() = DatabaseTransactionManager.current().session
class DatabaseTransactionManager(initDataSource: CordaPersistence) {
companion object {
private val threadLocalDb = ThreadLocal<CordaPersistence>()
@ -95,11 +41,15 @@ class DatabaseTransactionManager(initDataSource: CordaPersistence) {
fun currentOrNull(): DatabaseTransaction? = manager.currentOrNull()
fun currentOrNew(isolation: Int = dataSource.transactionIsolationLevel) = currentOrNull() ?: manager.newTransaction(isolation)
fun currentOrNew(isolation: TransactionIsolationLevel = dataSource.defaultIsolationLevel): DatabaseTransaction {
return currentOrNull() ?: manager.newTransaction(isolation.jdbcValue)
}
fun current(): DatabaseTransaction = currentOrNull() ?: error("No transaction in context.")
fun newTransaction(isolation: Int = dataSource.transactionIsolationLevel) = manager.newTransaction(isolation)
fun newTransaction(isolation: TransactionIsolationLevel = dataSource.defaultIsolationLevel): DatabaseTransaction {
return manager.newTransaction(isolation.jdbcValue)
}
}
data class Boundary(val txId: UUID)

View File

@ -1,13 +1,9 @@
package net.corda.node.services.persistence
package net.corda.nodeapi.internal.persistence
import net.corda.core.internal.castIfPossible
import net.corda.core.node.services.IdentityService
import net.corda.core.schemas.MappedSchema
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.toHexString
import net.corda.node.services.api.SchemaService
import net.corda.node.services.config.DatabaseConfig
import net.corda.node.utilities.DatabaseTransactionManager
import org.hibernate.SessionFactory
import org.hibernate.boot.MetadataSources
import org.hibernate.boot.model.naming.Identifier
@ -18,14 +14,18 @@ import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment
import org.hibernate.service.UnknownUnwrapTypeException
import org.hibernate.type.AbstractSingleColumnStandardBasicType
import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry
import org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor
import org.hibernate.type.descriptor.sql.BlobTypeDescriptor
import org.hibernate.type.descriptor.sql.VarbinaryTypeDescriptor
import java.sql.Connection
import java.util.concurrent.ConcurrentHashMap
import javax.persistence.AttributeConverter
class HibernateConfiguration(val schemaService: SchemaService, private val databaseConfig: DatabaseConfig, private val identityService: IdentityService) {
class HibernateConfiguration(
schemas: Set<MappedSchema>,
private val databaseConfig: DatabaseConfig,
private val attributeConverters: Collection<AttributeConverter<*, *>>
) {
companion object {
private val logger = contextLogger()
}
@ -33,13 +33,8 @@ class HibernateConfiguration(val schemaService: SchemaService, private val datab
// TODO: make this a guava cache or similar to limit ability for this to grow forever.
private val sessionFactories = ConcurrentHashMap<Set<MappedSchema>, SessionFactory>()
val sessionFactoryForRegisteredSchemas = schemaService.schemaOptions.keys.let {
val sessionFactoryForRegisteredSchemas = schemas.let {
logger.info("Init HibernateConfiguration for schemas: $it")
// Register the AbstractPartyDescriptor so Hibernate doesn't warn when encountering AbstractParty. Unfortunately
// Hibernate warns about not being able to find a descriptor if we don't provide one, but won't use it by default
// so we end up providing both descriptor and converter. We should re-examine this in later versions to see if
// either Hibernate can be convinced to stop warning, use the descriptor by default, or something else.
JavaTypeDescriptorRegistry.INSTANCE.addDescriptor(AbstractPartyDescriptor(identityService))
sessionFactoryForSchemas(it)
}
@ -54,7 +49,7 @@ class HibernateConfiguration(val schemaService: SchemaService, private val datab
// necessarily remain and would likely be replaced by something like Liquibase. For now it is very convenient though.
// TODO: replace auto schema generation as it isn't intended for production use, according to Hibernate docs.
val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", NodeDatabaseConnectionProvider::class.java.name)
.setProperty("hibernate.hbm2ddl.auto", if (databaseConfig.initDatabase) "update" else "validate")
.setProperty("hibernate.hbm2ddl.auto", if (databaseConfig.initialiseSchema) "update" else "validate")
.setProperty("hibernate.format_sql", "true")
.setProperty("hibernate.connection.isolation", databaseConfig.transactionIsolationLevel.jdbcValue.toString())
@ -83,7 +78,7 @@ class HibernateConfiguration(val schemaService: SchemaService, private val datab
}
})
// register custom converters
applyAttributeConverter(AbstractPartyToX500NameAsStringConverter(identityService))
attributeConverters.forEach { applyAttributeConverter(it) }
// Register a tweaked version of `org.hibernate.type.MaterializedBlobType` that truncates logged messages.
// to avoid OOM when large blobs might get logged.
applyBasicType(CordaMaterializedBlobType, CordaMaterializedBlobType.name)

View File

@ -26,5 +26,8 @@ class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer<Any> {
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = (obj as? Binary)?.array ?: obj
override fun readObject(
obj: Any,
schemas: SerializationSchemas,
input: DeserializationInput): Any = (obj as? Binary)?.array ?: obj
}

View File

@ -35,5 +35,5 @@ interface AMQPSerializer<out T> {
/**
* Read the given object from the input. The envelope is provided in case the schema is required.
*/
fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T
fun readObject(obj: Any, schema: SerializationSchemas, input: DeserializationInput): T
}

View File

@ -56,9 +56,9 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory)
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any {
if (obj is List<*>) {
return obj.map { input.readObjectOrNull(it, schema, elementType) }.toArrayOfType(elementType)
return obj.map { input.readObjectOrNull(it, schemas, elementType) }.toArrayOfType(elementType)
} else throw NotSerializableException("Expected a List but found $obj")
}

View File

@ -77,8 +77,8 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) {
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) {
// TODO: Can we verify the entries in the list?
concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, schema, declaredType.actualTypeArguments[0]) })
concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, schemas, declaredType.actualTypeArguments[0]) })
}
}

View File

@ -67,8 +67,8 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T> {
superClassSerializer.writeDescribedObject(obj, data, type, output)
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T {
return superClassSerializer.readObject(obj, schema, input)
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): T {
return superClassSerializer.readObject(obj, schemas, input)
}
}
@ -133,8 +133,8 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T> {
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T {
val proxy: P = uncheckedCast(proxySerializer.readObject(obj, schema, input))
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): T {
val proxy: P = uncheckedCast(proxySerializer.readObject(obj, schemas, input))
return fromProxy(proxy)
}
}
@ -166,7 +166,7 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T> {
data.putString(unmaker(obj))
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T {
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): T {
val proxy = obj as String
return maker(proxy)
}

View File

@ -97,21 +97,21 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
@Throws(NotSerializableException::class)
fun <T : Any> deserialize(bytes: ByteSequence, clazz: Class<T>): T = des {
val envelope = getEnvelope(bytes)
clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz))
clazz.cast(readObjectOrNull(envelope.obj, SerializationSchemas(envelope.schema, envelope.transformsSchema), clazz))
}
@Throws(NotSerializableException::class)
fun <T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>, clazz: Class<T>): ObjectAndEnvelope<T> = des {
val envelope = getEnvelope(bytes)
// Now pick out the obj and schema from the envelope.
ObjectAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)), envelope)
ObjectAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, SerializationSchemas(envelope.schema, envelope.transformsSchema), clazz)), envelope)
}
internal fun readObjectOrNull(obj: Any?, schema: Schema, type: Type): Any? {
internal fun readObjectOrNull(obj: Any?, schema: SerializationSchemas, type: Type): Any? {
return if (obj == null) null else readObject(obj, schema, type)
}
internal fun readObject(obj: Any, schema: Schema, type: Type): Any =
internal fun readObject(obj: Any, schemas: SerializationSchemas, type: Type): Any =
if (obj is DescribedType && ReferencedObject.DESCRIPTOR == obj.descriptor) {
// It must be a reference to an instance that has already been read, cheaply and quickly returning it by reference.
val objectIndex = (obj.described as UnsignedInteger).toInt()
@ -127,11 +127,11 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
val objectRead = when (obj) {
is DescribedType -> {
// Look up serializer in factory by descriptor
val serializer = serializerFactory.get(obj.descriptor, schema)
val serializer = serializerFactory.get(obj.descriptor, schemas)
if (SerializerFactory.AnyType != type && serializer.type != type && with(serializer.type) { !isSubClassOf(type) && !materiallyEquivalentTo(type) })
throw NotSerializableException("Described type with descriptor ${obj.descriptor} was " +
"expected to be of type $type but was ${serializer.type}")
serializer.readObject(obj.described, schema, this)
serializer.readObject(obj.described, schemas, this)
}
is Binary -> obj.array
else -> obj // this will be the case for primitive types like [boolean] et al.

View File

@ -27,7 +27,7 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Seria
output.writeTypeNotations(typeNotation)
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any {
val enumName = (obj as List<*>)[0] as String
val enumOrd = obj[1] as Int
val fromOrd = type.asClass()!!.enumConstants[enumOrd] as Enum<*>?

View File

@ -32,8 +32,8 @@ class EvolutionSerializer(
* @param property object to read the actual property value
*/
data class OldParam(val type: Type, val idx: Int, val property: PropertySerializer) {
fun readProperty(paramValues: List<*>, schema: Schema, input: DeserializationInput) =
property.readProperty(paramValues[idx], schema, input)
fun readProperty(paramValues: List<*>, schemas: SerializationSchemas, input: DeserializationInput) =
property.readProperty(paramValues[idx], schemas, input)
}
companion object {
@ -121,10 +121,10 @@ class EvolutionSerializer(
*
* TODO: Object references
*/
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any {
if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj")
return construct(readers.map { it?.readProperty(obj, schema, input) })
return construct(readers.map { it?.readProperty(obj, schemas, input) })
}
}

View File

@ -88,15 +88,15 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) {
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) {
// TODO: General generics question. Do we need to validate that entries in Maps and Collections match the generic type? Is it a security hole?
val entries: Iterable<Pair<Any?, Any?>> = (obj as Map<*, *>).map { readEntry(schema, input, it) }
val entries: Iterable<Pair<Any?, Any?>> = (obj as Map<*, *>).map { readEntry(schemas, input, it) }
concreteBuilder(entries.toMap())
}
private fun readEntry(schema: Schema, input: DeserializationInput, entry: Map.Entry<Any?, Any?>) =
input.readObjectOrNull(entry.key, schema, declaredType.actualTypeArguments[0]) to
input.readObjectOrNull(entry.value, schema, declaredType.actualTypeArguments[1])
private fun readEntry(schemas: SerializationSchemas, input: DeserializationInput, entry: Map.Entry<Any?, Any?>) =
input.readObjectOrNull(entry.key, schemas, declaredType.actualTypeArguments[0]) to
input.readObjectOrNull(entry.value, schemas, declaredType.actualTypeArguments[1])
// Cannot use * as a bound for EnumMap and EnumSet since * is not an enum. So, we use a sample enum instead.
// We don't actually care about the type, we just need to make the compiler happier.

View File

@ -55,10 +55,15 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ clazz.typeName }) {
override fun readObject(
obj: Any,
schemas: SerializationSchemas,
input: DeserializationInput): Any = ifThrowsAppend({ clazz.typeName }) {
if (obj is List<*>) {
if (obj.size > propertySerializers.size) throw NotSerializableException("Too many properties in described type $typeName")
val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schema, input) }
if (obj.size > propertySerializers.size) {
throw NotSerializableException("Too many properties in described type $typeName")
}
val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schemas, input) }
construct(params)
} else throw NotSerializableException("Body of described type is unexpected $obj")
}

View File

@ -14,7 +14,7 @@ import kotlin.reflect.jvm.javaGetter
sealed class PropertySerializer(val name: String, val readMethod: Method?, val resolvedType: Type) {
abstract fun writeClassInfo(output: SerializationOutput)
abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput)
abstract fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any?
abstract fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any?
val type: String = generateType()
val requires: List<String> = generateRequires()
@ -91,8 +91,8 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
}
}
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? = ifThrowsAppend({ nameForDebug }) {
input.readObjectOrNull(obj, schema, resolvedType)
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any? = ifThrowsAppend({ nameForDebug }) {
input.readObjectOrNull(obj, schemas, resolvedType)
}
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) = ifThrowsAppend({ nameForDebug }) {
@ -108,7 +108,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
class AMQPPrimitivePropertySerializer(name: String, readMethod: Method?, resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) {
override fun writeClassInfo(output: SerializationOutput) {}
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? {
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any? {
return if (obj is Binary) obj.array else obj
}
@ -131,7 +131,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
PropertySerializer(name, readMethod, Character::class.java) {
override fun writeClassInfo(output: SerializationOutput) {}
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? {
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any? {
return if (obj == null) null else (obj as Short).toChar()
}

View File

@ -4,7 +4,9 @@ import com.google.common.primitives.Primitives
import com.google.common.reflect.TypeResolver
import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.ClassWhitelist
import net.corda.nodeapi.internal.serialization.carpenter.*
import net.corda.nodeapi.internal.serialization.carpenter.CarpenterMetaSchema
import net.corda.nodeapi.internal.serialization.carpenter.ClassCarpenter
import net.corda.nodeapi.internal.serialization.carpenter.MetaCarpenter
import org.apache.qpid.proton.amqp.*
import java.io.NotSerializableException
import java.lang.reflect.*
@ -13,7 +15,8 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import javax.annotation.concurrent.ThreadSafe
data class FactorySchemaAndDescriptor(val schema: Schema, val typeDescriptor: Any)
data class SerializationSchemas(val schema: Schema, val transforms: TransformsSchema)
data class FactorySchemaAndDescriptor(val schemas: SerializationSchemas, val typeDescriptor: Any)
/**
* Factory of serializers designed to be shared across threads and invocations.
@ -40,7 +43,10 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
val classloader: ClassLoader
get() = classCarpenter.classloader
private fun getEvolutionSerializer(typeNotation: TypeNotation, newSerializer: AMQPSerializer<Any>): AMQPSerializer<Any> {
private fun getEvolutionSerializer(
typeNotation: TypeNotation,
newSerializer: AMQPSerializer<Any>,
transforms: TransformsSchema): AMQPSerializer<Any> {
return serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
when (typeNotation) {
is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, this)
@ -168,7 +174,7 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
* contained in the [Schema].
*/
@Throws(NotSerializableException::class)
fun get(typeDescriptor: Any, schema: Schema): AMQPSerializer<Any> {
fun get(typeDescriptor: Any, schema: SerializationSchemas): AMQPSerializer<Any> {
return serializersByDescriptor[typeDescriptor] ?: {
processSchema(FactorySchemaAndDescriptor(schema, typeDescriptor))
serializersByDescriptor[typeDescriptor] ?: throw NotSerializableException(
@ -194,9 +200,9 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
* Iterate over an AMQP schema, for each type ascertain weather it's on ClassPath of [classloader] amd
* if not use the [ClassCarpenter] to generate a class to use in it's place
*/
private fun processSchema(schema: FactorySchemaAndDescriptor, sentinel: Boolean = false) {
private fun processSchema(schemaAndDescriptor: FactorySchemaAndDescriptor, sentinel: Boolean = false) {
val metaSchema = CarpenterMetaSchema.newInstance()
for (typeNotation in schema.schema.types) {
for (typeNotation in schemaAndDescriptor.schemas.schema.types) {
try {
val serialiser = processSchemaEntry(typeNotation)
@ -204,7 +210,7 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
// doesn't match that of the serialised object then we are dealing with different
// instance of the class, as such we need to build an EvolutionSerialiser
if (serialiser.typeDescriptor != typeNotation.descriptor.name) {
getEvolutionSerializer(typeNotation, serialiser)
getEvolutionSerializer(typeNotation, serialiser, schemaAndDescriptor.schemas.transforms)
}
} catch (e: ClassNotFoundException) {
if (sentinel) throw e
@ -215,7 +221,7 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
if (metaSchema.isNotEmpty()) {
val mc = MetaCarpenter(metaSchema, classCarpenter)
mc.build()
processSchema(schema, true)
processSchema(schemaAndDescriptor, true)
}
}

View File

@ -28,7 +28,7 @@ class SingletonSerializer(override val type: Class<*>, val singleton: Any, facto
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any {
return singleton
}
}

View File

@ -85,6 +85,9 @@ class UnknownTransform : Transform() {
override val name: String get() = typeName
}
/**
* Used by the unit testing framework
*/
class UnknownTestTransform(val a: Int, val b: Int, val c: Int) : Transform() {
companion object : DescribedTypeConstructor<UnknownTestTransform> {
val typeName = "UnknownTest"

View File

@ -34,8 +34,8 @@ object InputStreamSerializer : CustomSerializer.Implements<InputStream>(InputStr
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): InputStream {
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): InputStream {
val bits = input.readObject(obj, schemas, ByteArray::class.java) as ByteArray
return ByteArrayInputStream(bits)
}
}

View File

@ -20,8 +20,8 @@ object PrivateKeySerializer : CustomSerializer.Implements<PrivateKey>(PrivateKey
output.writeObject(obj.encoded, data, clazz)
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): PrivateKey {
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): PrivateKey {
val bits = input.readObject(obj, schemas, ByteArray::class.java) as ByteArray
return Crypto.decodePrivateKey(bits)
}
}

View File

@ -17,8 +17,8 @@ object PublicKeySerializer : CustomSerializer.Implements<PublicKey>(PublicKey::c
output.writeObject(obj.encoded, data, clazz)
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): PublicKey {
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): PublicKey {
val bits = input.readObject(obj, schemas, ByteArray::class.java) as ByteArray
return Crypto.decodePublicKey(bits)
}
}

View File

@ -1,9 +1,9 @@
package net.corda.nodeapi.internal.serialization.amqp.custom
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.nodeapi.internal.serialization.amqp.*
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
object X509CertificateSerializer : CustomSerializer.Implements<X509Certificate>(X509Certificate::class.java) {
@ -20,8 +20,8 @@ object X509CertificateSerializer : CustomSerializer.Implements<X509Certificate>(
output.writeObject(obj.encoded, data, clazz)
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): X509Certificate {
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
return CertificateFactory.getInstance("X.509").generateCertificate(bits.inputStream()) as X509Certificate
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): X509Certificate {
val bits = input.readObject(obj, schemas, ByteArray::class.java) as ByteArray
return X509CertificateFactory().generateCertificate(bits.inputStream())
}
}

View File

@ -22,6 +22,7 @@ import net.corda.core.serialization.SerializedBytes
import net.corda.core.toFuture
import net.corda.core.toObservable
import net.corda.core.transactions.*
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.core.utilities.SgxSupport
import net.corda.nodeapi.internal.serialization.CordaClassResolver
import net.corda.nodeapi.internal.serialization.serializationContextKey
@ -490,8 +491,7 @@ object CertPathSerializer : Serializer<CertPath>() {
@ThreadSafe
object X509CertificateSerializer : Serializer<X509Certificate>() {
override fun read(kryo: Kryo, input: Input, type: Class<X509Certificate>): X509Certificate {
val factory = CertificateFactory.getInstance("X.509")
return factory.generateCertificate(input.readBytesWithLength().inputStream()) as X509Certificate
return X509CertificateFactory().generateCertificate(input.readBytesWithLength().inputStream())
}
override fun write(kryo: Kryo, output: Output, obj: X509Certificate) {

View File

@ -1,4 +1,4 @@
package net.corda.node.utilities
package net.corda.nodeapi.internal.crypto
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512
@ -14,9 +14,9 @@ import net.corda.core.serialization.serialize
import net.corda.node.serialization.KryoServerSerializationScheme
import net.corda.node.services.config.createKeystoreForCordaNode
import net.corda.nodeapi.internal.serialization.AllWhitelist
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
import net.corda.nodeapi.internal.serialization.SerializationContextImpl
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
import net.corda.testing.ALICE
import net.corda.testing.BOB
import net.corda.testing.BOB_PUBKEY
@ -41,7 +41,6 @@ import java.security.PrivateKey
import java.security.SecureRandom
import java.security.cert.CertPath
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.*
import java.util.stream.Stream
@ -49,7 +48,6 @@ import javax.net.ssl.*
import kotlin.concurrent.thread
import kotlin.test.*
class X509UtilitiesTest {
@Rule
@JvmField
@ -360,10 +358,16 @@ class X509UtilitiesTest {
trustStorePassword: String
): KeyStore {
val rootCAKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", organisation = "R3CEV", locality = "London", country = "GB"), rootCAKey)
val baseName = CordaX500Name(organisation = "R3CEV", locality = "London", country = "GB")
val rootCACert = X509Utilities.createSelfSignedCACertificate(baseName.copy(commonName = "Corda Node Root CA"), rootCAKey)
val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, CordaX500Name(commonName = "Corda Node Intermediate CA", organisation = "R3CEV", locality = "London", country = "GB"), intermediateCAKeyPair.public)
val intermediateCACert = X509Utilities.createCertificate(
CertificateType.INTERMEDIATE_CA,
rootCACert,
rootCAKey,
baseName.copy(commonName = "Corda Node Intermediate CA"),
intermediateCAKeyPair.public)
val keyPass = keyPassword.toCharArray()
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
@ -426,11 +430,10 @@ class X509UtilitiesTest {
emptyMap(),
true,
SerializationContext.UseCase.P2P)
val certFactory = CertificateFactory.getInstance("X509")
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCACert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootCAKey)
val certificate = X509Utilities.createCertificate(CertificateType.TLS, rootCACert, rootCAKey, BOB.name.x500Name, BOB_PUBKEY)
val expected = certFactory.generateCertPath(listOf(certificate.cert, rootCACert.cert))
val expected = X509CertificateFactory().delegate.generateCertPath(listOf(certificate.cert, rootCACert.cert))
val serialized = expected.serialize(factory, context).bytes
val actual: CertPath = serialized.deserialize(factory, context)
assertEquals(expected, actual)

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