mirror of
https://github.com/corda/corda.git
synced 2024-12-28 16:58:55 +00:00
Merge Open Source to Enterprise (pull request #147)
This commit is contained in:
commit
63fca3e7c9
@ -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>
|
|
@ -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
137
CONTRIBUTORS.md
Normal 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)
|
@ -6,8 +6,7 @@ buildscript {
|
|||||||
// Our version: bump this on release.
|
// Our version: bump this on release.
|
||||||
ext.corda_release_version = "0.16-SNAPSHOT"
|
ext.corda_release_version = "0.16-SNAPSHOT"
|
||||||
// Increment this on any release that changes public APIs anywhere in the Corda platform
|
// 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 = constants.getProperty("platformVersion")
|
||||||
ext.corda_platform_version = 1
|
|
||||||
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
|
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
|
||||||
|
|
||||||
// Dependency versions. Can run 'gradle dependencyUpdates' to find new versions of things.
|
// Dependency versions. Can run 'gradle dependencyUpdates' to find new versions of things.
|
||||||
@ -49,6 +48,7 @@ buildscript {
|
|||||||
ext.commons_collections_version = '4.1'
|
ext.commons_collections_version = '4.1'
|
||||||
ext.beanutils_version = '1.9.3'
|
ext.beanutils_version = '1.9.3'
|
||||||
ext.crash_version = 'faba68332800f21278c5b600bf14ad55cef5989e'
|
ext.crash_version = 'faba68332800f21278c5b600bf14ad55cef5989e'
|
||||||
|
ext.jsr305_version = constants.getProperty("jsr305Version")
|
||||||
ext.spring_jdbc_version ='5.0.0.RELEASE'
|
ext.spring_jdbc_version ='5.0.0.RELEASE'
|
||||||
|
|
||||||
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
|
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
|
||||||
|
@ -12,12 +12,15 @@ import net.corda.core.serialization.serialize
|
|||||||
import net.corda.core.utilities.*
|
import net.corda.core.utilities.*
|
||||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||||
import net.corda.nodeapi.RPCApi
|
import net.corda.nodeapi.RPCApi
|
||||||
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
import net.corda.testing.IntegrationTest
|
import net.corda.testing.IntegrationTest
|
||||||
import net.corda.testing.driver.poll
|
import net.corda.testing.driver.poll
|
||||||
import net.corda.testing.internal.*
|
import net.corda.testing.internal.*
|
||||||
import org.apache.activemq.artemis.api.core.SimpleString
|
import org.apache.activemq.artemis.api.core.SimpleString
|
||||||
|
import org.junit.After
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
@ -27,6 +30,14 @@ import java.util.concurrent.*
|
|||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
class RPCStabilityTests : IntegrationTest() {
|
class RPCStabilityTests : IntegrationTest() {
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val testSerialization = SerializationEnvironmentRule(true)
|
||||||
|
private val pool = Executors.newFixedThreadPool(10, testThreadFactory())
|
||||||
|
@After
|
||||||
|
fun shutdown() {
|
||||||
|
pool.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
object DummyOps : RPCOps {
|
object DummyOps : RPCOps {
|
||||||
override val protocolVersion = 0
|
override val protocolVersion = 0
|
||||||
@ -198,9 +209,9 @@ class RPCStabilityTests : IntegrationTest() {
|
|||||||
val proxy = startRpcClient<LeakObservableOps>(server.get().broker.hostAndPort!!).get()
|
val proxy = startRpcClient<LeakObservableOps>(server.get().broker.hostAndPort!!).get()
|
||||||
// Leak many observables
|
// Leak many observables
|
||||||
val N = 200
|
val N = 200
|
||||||
(1..N).toList().parallelStream().forEach {
|
(1..N).map {
|
||||||
proxy.leakObservable()
|
pool.fork { proxy.leakObservable(); Unit }
|
||||||
}
|
}.transpose().getOrThrow()
|
||||||
// In a loop force GC and check whether the server is notified
|
// In a loop force GC and check whether the server is notified
|
||||||
while (true) {
|
while (true) {
|
||||||
System.gc()
|
System.gc()
|
||||||
@ -232,7 +243,7 @@ class RPCStabilityTests : IntegrationTest() {
|
|||||||
assertEquals("pong", client.ping())
|
assertEquals("pong", client.ping())
|
||||||
serverFollower.shutdown()
|
serverFollower.shutdown()
|
||||||
startRpcServer<ReconnectOps>(ops = ops, customPort = serverPort).getOrThrow()
|
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))
|
assertEquals("pong", pingFuture.getOrThrow(10.seconds))
|
||||||
clientFollower.shutdown() // Driver would do this after the new server, causing hang.
|
clientFollower.shutdown() // Driver would do this after the new server, causing hang.
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ public class StandaloneCordaRPCJavaClientTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void copyFinanceCordapp() {
|
private void copyFinanceCordapp() {
|
||||||
Path cordappsDir = (factory.baseDirectory(notaryConfig).resolve("cordapps"));
|
Path cordappsDir = (factory.baseDirectory(notaryConfig).resolve(NodeProcess.CORDAPPS_DIR_NAME));
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(cordappsDir);
|
Files.createDirectories(cordappsDir);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
|
@ -86,7 +86,7 @@ class StandaloneCordaRPClientTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun copyFinanceCordapp() {
|
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
|
// Find the finance jar file for the smoke tests of this module
|
||||||
val financeJar = Paths.get("build", "resources", "smokeTest").list {
|
val financeJar = Paths.get("build", "resources", "smokeTest").list {
|
||||||
it.filter { "corda-finance" in it.toString() }.toList().single()
|
it.filter { "corda-finance" in it.toString() }.toList().single()
|
||||||
|
@ -6,14 +6,20 @@ import net.corda.core.internal.concurrent.map
|
|||||||
import net.corda.core.messaging.RPCOps
|
import net.corda.core.messaging.RPCOps
|
||||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||||
import net.corda.nodeapi.User
|
import net.corda.nodeapi.User
|
||||||
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
import net.corda.testing.internal.RPCDriverExposedDSLInterface
|
import net.corda.testing.internal.RPCDriverExposedDSLInterface
|
||||||
import net.corda.testing.internal.rpcTestUser
|
import net.corda.testing.internal.rpcTestUser
|
||||||
import net.corda.testing.internal.startInVmRpcClient
|
import net.corda.testing.internal.startInVmRpcClient
|
||||||
import net.corda.testing.internal.startRpcClient
|
import net.corda.testing.internal.startRpcClient
|
||||||
import org.apache.activemq.artemis.api.core.client.ClientSession
|
import org.apache.activemq.artemis.api.core.client.ClientSession
|
||||||
|
import org.junit.Rule
|
||||||
import org.junit.runners.Parameterized
|
import org.junit.runners.Parameterized
|
||||||
|
|
||||||
open class AbstractRPCTest {
|
open class AbstractRPCTest {
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val testSerialization = SerializationEnvironmentRule(true)
|
||||||
|
|
||||||
enum class RPCTestMode {
|
enum class RPCTestMode {
|
||||||
InVm,
|
InVm,
|
||||||
Netty
|
Netty
|
||||||
|
@ -5,19 +5,22 @@ import net.corda.core.messaging.RPCOps
|
|||||||
import net.corda.core.utilities.millis
|
import net.corda.core.utilities.millis
|
||||||
import net.corda.core.crypto.random63BitValue
|
import net.corda.core.crypto.random63BitValue
|
||||||
import net.corda.core.internal.concurrent.fork
|
import net.corda.core.internal.concurrent.fork
|
||||||
|
import net.corda.core.internal.concurrent.transpose
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||||
import net.corda.testing.internal.RPCDriverExposedDSLInterface
|
import net.corda.testing.internal.RPCDriverExposedDSLInterface
|
||||||
import net.corda.testing.internal.rpcDriver
|
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.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.Parameterized
|
import org.junit.runners.Parameterized
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.UnicastSubject
|
import rx.subjects.UnicastSubject
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.*
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import java.util.concurrent.ForkJoinPool
|
|
||||||
|
|
||||||
@RunWith(Parameterized::class)
|
@RunWith(Parameterized::class)
|
||||||
class RPCConcurrencyTests : AbstractRPCTest() {
|
class RPCConcurrencyTests : AbstractRPCTest() {
|
||||||
@ -36,7 +39,7 @@ class RPCConcurrencyTests : AbstractRPCTest() {
|
|||||||
fun getParallelObservableTree(depth: Int, branchingFactor: Int): ObservableRose<Int>
|
fun getParallelObservableTree(depth: Int, branchingFactor: Int): ObservableRose<Int>
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestOpsImpl : TestOps {
|
class TestOpsImpl(private val pool: Executor) : TestOps {
|
||||||
private val latches = ConcurrentHashMap<Long, CountDownLatch>()
|
private val latches = ConcurrentHashMap<Long, CountDownLatch>()
|
||||||
override val protocolVersion = 0
|
override val protocolVersion = 0
|
||||||
|
|
||||||
@ -68,24 +71,22 @@ class RPCConcurrencyTests : AbstractRPCTest() {
|
|||||||
val branches = if (depth == 0) {
|
val branches = if (depth == 0) {
|
||||||
Observable.empty<ObservableRose<Int>>()
|
Observable.empty<ObservableRose<Int>>()
|
||||||
} else {
|
} else {
|
||||||
val publish = UnicastSubject.create<ObservableRose<Int>>()
|
UnicastSubject.create<ObservableRose<Int>>().also { publish ->
|
||||||
ForkJoinPool.commonPool().fork {
|
(1..branchingFactor).map {
|
||||||
(1..branchingFactor).toList().parallelStream().forEach {
|
pool.fork { publish.onNext(getParallelObservableTree(depth - 1, branchingFactor)) }
|
||||||
publish.onNext(getParallelObservableTree(depth - 1, branchingFactor))
|
}.transpose().then {
|
||||||
|
it.getOrThrow()
|
||||||
|
publish.onCompleted()
|
||||||
}
|
}
|
||||||
publish.onCompleted()
|
|
||||||
}
|
}
|
||||||
publish
|
|
||||||
}
|
}
|
||||||
return ObservableRose(depth, branches)
|
return ObservableRose(depth, branches)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var testOpsImpl: TestOpsImpl
|
|
||||||
private fun RPCDriverExposedDSLInterface.testProxy(): TestProxy<TestOps> {
|
private fun RPCDriverExposedDSLInterface.testProxy(): TestProxy<TestOps> {
|
||||||
testOpsImpl = TestOpsImpl()
|
|
||||||
return testProxy<TestOps>(
|
return testProxy<TestOps>(
|
||||||
testOpsImpl,
|
TestOpsImpl(pool),
|
||||||
clientConfiguration = RPCClientConfiguration.default.copy(
|
clientConfiguration = RPCClientConfiguration.default.copy(
|
||||||
reapInterval = 100.millis,
|
reapInterval = 100.millis,
|
||||||
cacheConcurrencyLevel = 16
|
cacheConcurrencyLevel = 16
|
||||||
@ -96,6 +97,12 @@ class RPCConcurrencyTests : AbstractRPCTest() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val pool = Executors.newFixedThreadPool(10, testThreadFactory())
|
||||||
|
@After
|
||||||
|
fun shutdown() {
|
||||||
|
pool.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `call multiple RPCs in parallel`() {
|
fun `call multiple RPCs in parallel`() {
|
||||||
rpcDriver {
|
rpcDriver {
|
||||||
@ -103,19 +110,17 @@ class RPCConcurrencyTests : AbstractRPCTest() {
|
|||||||
val numberOfBlockedCalls = 2
|
val numberOfBlockedCalls = 2
|
||||||
val numberOfDownsRequired = 100
|
val numberOfDownsRequired = 100
|
||||||
val id = proxy.ops.newLatch(numberOfDownsRequired)
|
val id = proxy.ops.newLatch(numberOfDownsRequired)
|
||||||
val done = CountDownLatch(numberOfBlockedCalls)
|
|
||||||
// Start a couple of blocking RPC calls
|
// Start a couple of blocking RPC calls
|
||||||
(1..numberOfBlockedCalls).forEach {
|
val done = (1..numberOfBlockedCalls).map {
|
||||||
ForkJoinPool.commonPool().fork {
|
pool.fork {
|
||||||
proxy.ops.waitLatch(id)
|
proxy.ops.waitLatch(id)
|
||||||
done.countDown()
|
|
||||||
}
|
}
|
||||||
}
|
}.transpose()
|
||||||
// Down the latch that the others are waiting for concurrently
|
// Down the latch that the others are waiting for concurrently
|
||||||
(1..numberOfDownsRequired).toList().parallelStream().forEach {
|
(1..numberOfDownsRequired).map {
|
||||||
proxy.ops.downLatch(id)
|
pool.fork { proxy.ops.downLatch(id) }
|
||||||
}
|
}.transpose().getOrThrow()
|
||||||
done.await()
|
done.getOrThrow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +151,7 @@ class RPCConcurrencyTests : AbstractRPCTest() {
|
|||||||
fun ObservableRose<Int>.subscribeToAll() {
|
fun ObservableRose<Int>.subscribeToAll() {
|
||||||
remainingLatch.countDown()
|
remainingLatch.countDown()
|
||||||
this.branches.subscribe { tree ->
|
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" }
|
require(it in depthsSeen) { "Got ${tree.value} before $it" }
|
||||||
}
|
}
|
||||||
depthsSeen.add(tree.value)
|
depthsSeen.add(tree.value)
|
||||||
@ -165,11 +170,11 @@ class RPCConcurrencyTests : AbstractRPCTest() {
|
|||||||
val treeDepth = 2
|
val treeDepth = 2
|
||||||
val treeBranchingFactor = 10
|
val treeBranchingFactor = 10
|
||||||
val remainingLatch = CountDownLatch((intPower(treeBranchingFactor, treeDepth + 1) - 1) / (treeBranchingFactor - 1))
|
val remainingLatch = CountDownLatch((intPower(treeBranchingFactor, treeDepth + 1) - 1) / (treeBranchingFactor - 1))
|
||||||
val depthsSeen = Collections.synchronizedSet(HashSet<Int>())
|
val depthsSeen = ConcurrentHashSet<Int>()
|
||||||
fun ObservableRose<Int>.subscribeToAll() {
|
fun ObservableRose<Int>.subscribeToAll() {
|
||||||
remainingLatch.countDown()
|
remainingLatch.countDown()
|
||||||
branches.subscribe { tree ->
|
branches.subscribe { tree ->
|
||||||
(tree.value + 1..treeDepth - 1).forEach {
|
(tree.value + 1 until treeDepth).forEach {
|
||||||
require(it in depthsSeen) { "Got ${tree.value} before $it" }
|
require(it in depthsSeen) { "Got ${tree.value} before $it" }
|
||||||
}
|
}
|
||||||
depthsSeen.add(tree.value)
|
depthsSeen.add(tree.value)
|
||||||
|
@ -5,12 +5,18 @@ import net.corda.core.concurrent.CordaFuture
|
|||||||
import net.corda.core.internal.concurrent.openFuture
|
import net.corda.core.internal.concurrent.openFuture
|
||||||
import net.corda.core.messaging.*
|
import net.corda.core.messaging.*
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
import net.corda.testing.internal.rpcDriver
|
import net.corda.testing.internal.rpcDriver
|
||||||
import net.corda.testing.internal.startRpcClient
|
import net.corda.testing.internal.startRpcClient
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class RPCFailureTests {
|
class RPCFailureTests {
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val testSerialization = SerializationEnvironmentRule(true)
|
||||||
|
|
||||||
class Unserializable
|
class Unserializable
|
||||||
interface Ops : RPCOps {
|
interface Ops : RPCOps {
|
||||||
fun getUnserializable(): Unserializable
|
fun getUnserializable(): Unserializable
|
||||||
|
@ -49,8 +49,7 @@ class IdentitySyncFlowTests {
|
|||||||
val alice: Party = aliceNode.info.singleIdentity()
|
val alice: Party = aliceNode.info.singleIdentity()
|
||||||
val bob: Party = bobNode.info.singleIdentity()
|
val bob: Party = bobNode.info.singleIdentity()
|
||||||
val notary = mockNet.defaultNotaryIdentity
|
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
|
// Alice issues then pays some cash to a new confidential identity that Bob doesn't know about
|
||||||
val anonymous = true
|
val anonymous = true
|
||||||
val ref = OpaqueBytes.of(0x01)
|
val ref = OpaqueBytes.of(0x01)
|
||||||
@ -80,8 +79,7 @@ class IdentitySyncFlowTests {
|
|||||||
val bob: Party = bobNode.info.singleIdentity()
|
val bob: Party = bobNode.info.singleIdentity()
|
||||||
val charlie: Party = charlieNode.info.singleIdentity()
|
val charlie: Party = charlieNode.info.singleIdentity()
|
||||||
val notary = mockNet.defaultNotaryIdentity
|
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
|
// Charlie issues then pays some cash to a new confidential identity
|
||||||
val anonymous = true
|
val anonymous = true
|
||||||
val ref = OpaqueBytes.of(0x01)
|
val ref = OpaqueBytes.of(0x01)
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
gradlePluginsVersion=2.0.9
|
gradlePluginsVersion=3.0.0
|
||||||
kotlinVersion=1.1.60
|
kotlinVersion=1.1.60
|
||||||
|
platformVersion=1
|
||||||
guavaVersion=21.0
|
guavaVersion=21.0
|
||||||
bouncycastleVersion=1.57
|
bouncycastleVersion=1.57
|
||||||
typesafeConfigVersion=1.3.1
|
typesafeConfigVersion=1.3.1
|
||||||
|
jsr305Version=3.0.2
|
||||||
|
@ -78,7 +78,7 @@ dependencies {
|
|||||||
compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8"
|
compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8"
|
||||||
|
|
||||||
// Thread safety annotations
|
// 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)
|
// Log4J: logging framework (ONLY explicitly referenced by net.corda.core.utilities.Logging.kt)
|
||||||
compile "org.apache.logging.log4j:log4j-core:${log4j_version}"
|
compile "org.apache.logging.log4j:log4j-core:${log4j_version}"
|
||||||
|
@ -31,6 +31,8 @@ import java.time.Duration
|
|||||||
import java.time.temporal.Temporal
|
import java.time.temporal.Temporal
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.Spliterator.*
|
import java.util.Spliterator.*
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.stream.IntStream
|
import java.util.stream.IntStream
|
||||||
import java.util.stream.Stream
|
import java.util.stream.Stream
|
||||||
import java.util.stream.StreamSupport
|
import java.util.stream.StreamSupport
|
||||||
@ -307,3 +309,10 @@ fun TransactionBuilder.toLedgerTransaction(services: ServiceHub, serializationCo
|
|||||||
val KClass<*>.packageName: String get() = java.`package`.name
|
val KClass<*>.packageName: String get() = java.`package`.name
|
||||||
|
|
||||||
fun URL.openHttpConnection(): HttpURLConnection = openConnection() as HttpURLConnection
|
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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -43,7 +43,7 @@ class ThreadLocalToggleField<T>(name: String) : ToggleField<T>(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The named thread has leaked from a previous test. */
|
/** 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. */
|
/** @param isAGlobalThreadBeingCreated whether a global thread (that should not inherit any value) is being created. */
|
||||||
class InheritableThreadLocalToggleField<T>(name: String,
|
class InheritableThreadLocalToggleField<T>(name: String,
|
||||||
@ -54,16 +54,12 @@ class InheritableThreadLocalToggleField<T>(name: String,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private inner class Holder(value: T) : AtomicReference<T?>(value) {
|
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? {
|
fun childValue(): Holder? {
|
||||||
val e = ThreadLeakException() // Expensive, but so is starting the new thread.
|
val e = ThreadLeakException(valueToString) // Expensive, but so is starting the new thread.
|
||||||
return if (isAGlobalThreadBeingCreated(e.stackTrace)) {
|
get() ?: log.warn(e.message)
|
||||||
get() ?: log.warn(e.message)
|
return if (isAGlobalThreadBeingCreated(e.stackTrace)) null else this
|
||||||
null
|
|
||||||
} else {
|
|
||||||
get() ?: log.error(e.message)
|
|
||||||
this
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ interface SerializationEnvironment {
|
|||||||
val checkpointContext: SerializationContext
|
val checkpointContext: SerializationContext
|
||||||
}
|
}
|
||||||
|
|
||||||
class SerializationEnvironmentImpl(
|
open class SerializationEnvironmentImpl(
|
||||||
override val serializationFactory: SerializationFactory,
|
override val serializationFactory: SerializationFactory,
|
||||||
override val p2pContext: SerializationContext,
|
override val p2pContext: SerializationContext,
|
||||||
rpcServerContext: SerializationContext? = null,
|
rpcServerContext: SerializationContext? = null,
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ import net.corda.core.utilities.unwrap
|
|||||||
import net.corda.nodeapi.User
|
import net.corda.nodeapi.User
|
||||||
import net.corda.smoketesting.NodeConfig
|
import net.corda.smoketesting.NodeConfig
|
||||||
import net.corda.smoketesting.NodeProcess
|
import net.corda.smoketesting.NodeProcess
|
||||||
|
import net.corda.smoketesting.NodeProcess.Companion.CORDAPPS_DIR_NAME
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
@ -22,7 +23,6 @@ import kotlin.streams.toList
|
|||||||
|
|
||||||
class CordappSmokeTest {
|
class CordappSmokeTest {
|
||||||
private companion object {
|
private companion object {
|
||||||
private const val CORDAPPS_DIR_NAME = "cordapps"
|
|
||||||
val user = User("user1", "test", permissions = setOf("ALL"))
|
val user = User("user1", "test", permissions = setOf("ALL"))
|
||||||
val port = AtomicInteger(15100)
|
val port = AtomicInteger(15100)
|
||||||
}
|
}
|
||||||
@ -38,7 +38,6 @@ class CordappSmokeTest {
|
|||||||
users = listOf(user)
|
users = listOf(user)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `FlowContent appName returns the filename of the CorDapp jar`() {
|
fun `FlowContent appName returns the filename of the CorDapp jar`() {
|
||||||
val cordappsDir = (factory.baseDirectory(aliceConfig) / CORDAPPS_DIR_NAME).createDirectories()
|
val cordappsDir = (factory.baseDirectory(aliceConfig) / CORDAPPS_DIR_NAME).createDirectories()
|
||||||
|
@ -38,7 +38,7 @@ public class FlowsInJavaTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void suspendableActionInsideUnwrap() throws Exception {
|
public void suspendableActionInsideUnwrap() throws Exception {
|
||||||
bobNode.getInternals().registerInitiatedFlow(SendHelloAndThenReceive.class);
|
bobNode.registerInitiatedFlow(SendHelloAndThenReceive.class);
|
||||||
Future<String> result = startFlow(aliceNode.getServices(), new SendInUnwrapFlow(bob)).getResultFuture();
|
Future<String> result = startFlow(aliceNode.getServices(), new SendInUnwrapFlow(bob)).getResultFuture();
|
||||||
mockNet.runNetwork();
|
mockNet.runNetwork();
|
||||||
assertThat(result.get()).isEqualTo("Hello");
|
assertThat(result.get()).isEqualTo("Hello");
|
||||||
|
@ -8,7 +8,7 @@ import net.corda.core.internal.div
|
|||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.toBase58String
|
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.kryoSpecific
|
||||||
import net.corda.testing.SerializationEnvironmentRule
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
@ -38,24 +38,20 @@ class PartialMerkleTreeTest {
|
|||||||
testLedger = ledger {
|
testLedger = ledger {
|
||||||
unverifiedTransaction {
|
unverifiedTransaction {
|
||||||
attachments(Cash.PROGRAM_ID)
|
attachments(Cash.PROGRAM_ID)
|
||||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash") {
|
output(Cash.PROGRAM_ID, "MEGA_CORP cash",
|
||||||
Cash.State(
|
Cash.State(
|
||||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||||
owner = MEGA_CORP
|
owner = MEGA_CORP))
|
||||||
)
|
output(Cash.PROGRAM_ID, "dummy cash 1",
|
||||||
}
|
|
||||||
output(Cash.PROGRAM_ID, "dummy cash 1") {
|
|
||||||
Cash.State(
|
Cash.State(
|
||||||
amount = 900.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
amount = 900.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||||
owner = MINI_CORP
|
owner = MINI_CORP))
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Cash.PROGRAM_ID)
|
attachments(Cash.PROGRAM_ID)
|
||||||
input("MEGA_CORP cash")
|
input("MEGA_CORP cash")
|
||||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash".output<Cash.State>().copy(owner = MINI_CORP))
|
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)
|
timeWindow(TEST_TX_TIME)
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ package net.corda.core.crypto
|
|||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.toTypedArray
|
import net.corda.core.internal.toTypedArray
|
||||||
import net.corda.core.internal.cert
|
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.x500.X500Name
|
||||||
import org.bouncycastle.asn1.x509.GeneralName
|
import org.bouncycastle.asn1.x509.GeneralName
|
||||||
import org.bouncycastle.asn1.x509.GeneralSubtree
|
import org.bouncycastle.asn1.x509.GeneralSubtree
|
||||||
@ -50,7 +50,7 @@ class X509NameConstraintsTest {
|
|||||||
|
|
||||||
val nameConstraints = NameConstraints(acceptableNames, arrayOf())
|
val nameConstraints = NameConstraints(acceptableNames, arrayOf())
|
||||||
val pathValidator = CertPathValidator.getInstance("PKIX")
|
val pathValidator = CertPathValidator.getInstance("PKIX")
|
||||||
val certFactory = CertificateFactory.getInstance("X509")
|
val certFactory = X509CertificateFactory().delegate
|
||||||
|
|
||||||
assertFailsWith(CertPathValidatorException::class) {
|
assertFailsWith(CertPathValidatorException::class) {
|
||||||
val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank B"), nameConstraints)
|
val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank B"), nameConstraints)
|
||||||
@ -85,7 +85,7 @@ class X509NameConstraintsTest {
|
|||||||
.map { GeneralSubtree(GeneralName(X500Name(it))) }.toTypedArray()
|
.map { GeneralSubtree(GeneralName(X500Name(it))) }.toTypedArray()
|
||||||
|
|
||||||
val nameConstraints = NameConstraints(acceptableNames, arrayOf())
|
val nameConstraints = NameConstraints(acceptableNames, arrayOf())
|
||||||
val certFactory = CertificateFactory.getInstance("X509")
|
val certFactory = X509CertificateFactory().delegate
|
||||||
Crypto.ECDSA_SECP256R1_SHA256
|
Crypto.ECDSA_SECP256R1_SHA256
|
||||||
val pathValidator = CertPathValidator.getInstance("PKIX", BouncyCastleProvider.PROVIDER_NAME)
|
val pathValidator = CertPathValidator.getInstance("PKIX", BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
|
||||||
|
@ -52,10 +52,8 @@ class AttachmentTests {
|
|||||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||||
|
|
||||||
val alice = aliceNode.info.singleIdentity()
|
val alice = aliceNode.info.singleIdentity()
|
||||||
|
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||||
aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||||
bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
|
||||||
|
|
||||||
// Insert an attachment into node zero's store directly.
|
// Insert an attachment into node zero's store directly.
|
||||||
val id = aliceNode.database.transaction {
|
val id = aliceNode.database.transaction {
|
||||||
aliceNode.attachments.importAttachment(ByteArrayInputStream(fakeAttachment()))
|
aliceNode.attachments.importAttachment(ByteArrayInputStream(fakeAttachment()))
|
||||||
@ -85,10 +83,8 @@ class AttachmentTests {
|
|||||||
fun `missing`() {
|
fun `missing`() {
|
||||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||||
|
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||||
aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||||
bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
|
||||||
|
|
||||||
// Get node one to fetch a non-existent attachment.
|
// Get node one to fetch a non-existent attachment.
|
||||||
val hash = SecureHash.randomSHA256()
|
val hash = SecureHash.randomSHA256()
|
||||||
val alice = aliceNode.info.singleIdentity()
|
val alice = aliceNode.info.singleIdentity()
|
||||||
@ -108,10 +104,8 @@ class AttachmentTests {
|
|||||||
})
|
})
|
||||||
val bobNode = mockNet.createNode(MockNodeParameters(legalName = BOB.name))
|
val bobNode = mockNet.createNode(MockNodeParameters(legalName = BOB.name))
|
||||||
val alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME)
|
val alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME)
|
||||||
|
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||||
aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||||
bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
|
||||||
|
|
||||||
val attachment = fakeAttachment()
|
val attachment = fakeAttachment()
|
||||||
// Insert an attachment into node zero's store directly.
|
// Insert an attachment into node zero's store directly.
|
||||||
val id = aliceNode.database.transaction {
|
val id = aliceNode.database.transaction {
|
||||||
|
@ -50,7 +50,7 @@ class CollectSignaturesFlowTests {
|
|||||||
|
|
||||||
private fun registerFlowOnAllNodes(flowClass: KClass<out FlowLogic<*>>) {
|
private fun registerFlowOnAllNodes(flowClass: KClass<out FlowLogic<*>>) {
|
||||||
listOf(aliceNode, bobNode, charlieNode).forEach {
|
listOf(aliceNode, bobNode, charlieNode).forEach {
|
||||||
it.internals.registerInitiatedFlow(flowClass.java)
|
it.registerInitiatedFlow(flowClass.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +133,7 @@ class ContractUpgradeFlowTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `2 parties contract upgrade using RPC`() {
|
fun `2 parties contract upgrade using RPC`() {
|
||||||
rpcDriver(initialiseSerialization = false) {
|
rpcDriver {
|
||||||
// Create dummy contract.
|
// Create dummy contract.
|
||||||
val twoPartyDummyContract = DummyContract.generateInitial(0, notary, alice.ref(1), bob.ref(1))
|
val twoPartyDummyContract = DummyContract.generateInitial(0, notary, alice.ref(1), bob.ref(1))
|
||||||
val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract)
|
val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract)
|
||||||
|
@ -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].
|
* 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) {
|
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].
|
* 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) {
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4,8 +4,8 @@ import net.corda.core.crypto.entropyToKeyPair
|
|||||||
import net.corda.core.internal.read
|
import net.corda.core.internal.read
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.node.utilities.KEYSTORE_TYPE
|
import net.corda.nodeapi.internal.crypto.KEYSTORE_TYPE
|
||||||
import net.corda.node.utilities.save
|
import net.corda.nodeapi.internal.crypto.save
|
||||||
import net.corda.testing.SerializationEnvironmentRule
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
import net.corda.testing.getTestPartyAndCertificate
|
import net.corda.testing.getTestPartyAndCertificate
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
@ -42,8 +42,8 @@ class ResolveTransactionsFlowTest {
|
|||||||
notaryNode = mockNet.defaultNotaryNode
|
notaryNode = mockNet.defaultNotaryNode
|
||||||
megaCorpNode = mockNet.createPartyNode(MEGA_CORP.name)
|
megaCorpNode = mockNet.createPartyNode(MEGA_CORP.name)
|
||||||
miniCorpNode = mockNet.createPartyNode(MINI_CORP.name)
|
miniCorpNode = mockNet.createPartyNode(MINI_CORP.name)
|
||||||
megaCorpNode.internals.registerInitiatedFlow(TestResponseFlow::class.java)
|
megaCorpNode.registerInitiatedFlow(TestResponseFlow::class.java)
|
||||||
miniCorpNode.internals.registerInitiatedFlow(TestResponseFlow::class.java)
|
miniCorpNode.registerInitiatedFlow(TestResponseFlow::class.java)
|
||||||
notary = mockNet.defaultNotaryIdentity
|
notary = mockNet.defaultNotaryIdentity
|
||||||
megaCorp = megaCorpNode.info.singleIdentity()
|
megaCorp = megaCorpNode.info.singleIdentity()
|
||||||
miniCorp = miniCorpNode.info.singleIdentity()
|
miniCorp = miniCorpNode.info.singleIdentity()
|
||||||
|
@ -14,7 +14,6 @@ import org.junit.runners.model.Statement
|
|||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertNull
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
@ -23,10 +22,7 @@ private fun <T> withSingleThreadExecutor(callable: ExecutorService.() -> T) = Ex
|
|||||||
fork {}.getOrThrow() // Start the thread.
|
fork {}.getOrThrow() // Start the thread.
|
||||||
callable()
|
callable()
|
||||||
} finally {
|
} finally {
|
||||||
shutdown()
|
join()
|
||||||
while (!awaitTermination(1, TimeUnit.SECONDS)) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +130,7 @@ class ToggleFieldTest {
|
|||||||
assertThatThrownBy { future.getOrThrow() }
|
assertThatThrownBy { future.getOrThrow() }
|
||||||
.isInstanceOf(ThreadLeakException::class.java)
|
.isInstanceOf(ThreadLeakException::class.java)
|
||||||
.hasMessageContaining(threadName)
|
.hasMessageContaining(threadName)
|
||||||
|
.hasMessageContaining("hello")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
withSingleThreadExecutor {
|
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
|
@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>()
|
val field = inheritableThreadLocalToggleField<String>()
|
||||||
field.set("hello")
|
field.set("hello")
|
||||||
withSingleThreadExecutor {
|
withSingleThreadExecutor {
|
||||||
@ -153,17 +150,18 @@ class ToggleFieldTest {
|
|||||||
val leakedThreadName = Thread.currentThread().name
|
val leakedThreadName = Thread.currentThread().name
|
||||||
verifyNoMoreInteractions(log)
|
verifyNoMoreInteractions(log)
|
||||||
withSingleThreadExecutor {
|
withSingleThreadExecutor {
|
||||||
// If ThreadLeakException is seen in practice, these errors form a trail of where the holder has been:
|
// If ThreadLeakException is seen in practice, these warnings form a trail of where the holder has been:
|
||||||
verify(log).error(argThat { contains(leakedThreadName) })
|
verify(log).warn(argThat { contains(leakedThreadName) && contains("hello") })
|
||||||
val newThreadName = fork { Thread.currentThread().name }.getOrThrow()
|
val newThreadName = fork { Thread.currentThread().name }.getOrThrow()
|
||||||
val future = fork(field::get)
|
val future = fork(field::get)
|
||||||
assertThatThrownBy { future.getOrThrow() }
|
assertThatThrownBy { future.getOrThrow() }
|
||||||
.isInstanceOf(ThreadLeakException::class.java)
|
.isInstanceOf(ThreadLeakException::class.java)
|
||||||
.hasMessageContaining(newThreadName)
|
.hasMessageContaining(newThreadName)
|
||||||
|
.hasMessageContaining("hello")
|
||||||
fork {
|
fork {
|
||||||
verifyNoMoreInteractions(log)
|
verifyNoMoreInteractions(log)
|
||||||
withSingleThreadExecutor {
|
withSingleThreadExecutor {
|
||||||
verify(log).error(argThat { contains(newThreadName) })
|
verify(log).warn(argThat { contains(newThreadName) && contains("hello") })
|
||||||
}
|
}
|
||||||
}.getOrThrow()
|
}.getOrThrow()
|
||||||
}
|
}
|
||||||
@ -183,7 +181,7 @@ class ToggleFieldTest {
|
|||||||
globalThreadCreationMethod {
|
globalThreadCreationMethod {
|
||||||
verifyNoMoreInteractions(log)
|
verifyNoMoreInteractions(log)
|
||||||
withSingleThreadExecutor {
|
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:
|
// In practice the new thread is for example a static thread we can't get rid of:
|
||||||
assertNull(fork(field::get).getOrThrow())
|
assertNull(fork(field::get).getOrThrow())
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,13 @@ package net.corda.core.internal.concurrent
|
|||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.*
|
import com.nhaarman.mockito_kotlin.*
|
||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
|
import net.corda.core.internal.join
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.testing.rigorousMock
|
import net.corda.testing.rigorousMock
|
||||||
import org.assertj.core.api.Assertions
|
import org.assertj.core.api.Assertions
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
@ -108,10 +108,7 @@ class CordaFutureTest {
|
|||||||
val throwable = Exception("Boom")
|
val throwable = Exception("Boom")
|
||||||
val executor = Executors.newSingleThreadExecutor()
|
val executor = Executors.newSingleThreadExecutor()
|
||||||
executor.fork { throw throwable }.andForget(log)
|
executor.fork { throw throwable }.andForget(log)
|
||||||
executor.shutdown()
|
executor.join()
|
||||||
while (!executor.awaitTermination(1, TimeUnit.SECONDS)) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
verify(log).error(any(), same(throwable))
|
verify(log).error(any(), same(throwable))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ import net.corda.core.utilities.unwrap
|
|||||||
import net.corda.node.internal.InitiatedFlowFactory
|
import net.corda.node.internal.InitiatedFlowFactory
|
||||||
import net.corda.node.internal.StartedNode
|
import net.corda.node.internal.StartedNode
|
||||||
import net.corda.node.services.persistence.NodeAttachmentService
|
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.ALICE_NAME
|
||||||
import net.corda.testing.BOB_NAME
|
import net.corda.testing.BOB_NAME
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
||||||
@ -148,7 +148,7 @@ class AttachmentSerializationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun launchFlow(clientLogic: ClientLogic, rounds: Int, sendData: Boolean = false) {
|
private fun launchFlow(clientLogic: ClientLogic, rounds: Int, sendData: Boolean = false) {
|
||||||
server.internals.internalRegisterFlowFactory(
|
server.internalRegisterFlowFactory(
|
||||||
ClientLogic::class.java,
|
ClientLogic::class.java,
|
||||||
InitiatedFlowFactory.Core { ServerLogic(it, sendData) },
|
InitiatedFlowFactory.Core { ServerLogic(it, sendData) },
|
||||||
ServerLogic::class.java,
|
ServerLogic::class.java,
|
||||||
|
@ -55,10 +55,10 @@ class TransactionEncumbranceTests {
|
|||||||
ledger {
|
ledger {
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||||
input(Cash.PROGRAM_ID) { state }
|
input(Cash.PROGRAM_ID, state)
|
||||||
output(Cash.PROGRAM_ID, encumbrance = 1) { stateWithNewOwner }
|
output(Cash.PROGRAM_ID, encumbrance = 1, contractState = stateWithNewOwner)
|
||||||
output(TEST_TIMELOCK_ID, "5pm time-lock") { timeLock }
|
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
|
||||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
command(MEGA_CORP.owningKey, Cash.Commands.Move())
|
||||||
verifies()
|
verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,16 +69,16 @@ class TransactionEncumbranceTests {
|
|||||||
ledger {
|
ledger {
|
||||||
unverifiedTransaction {
|
unverifiedTransaction {
|
||||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||||
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock") { state }
|
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", state)
|
||||||
output(TEST_TIMELOCK_ID, "5pm time-lock") { timeLock }
|
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
|
||||||
}
|
}
|
||||||
// Un-encumber the output if the time of the transaction is later than the timelock.
|
// Un-encumber the output if the time of the transaction is later than the timelock.
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||||
input("state encumbered by 5pm time-lock")
|
input("state encumbered by 5pm time-lock")
|
||||||
input("5pm time-lock")
|
input("5pm time-lock")
|
||||||
output(Cash.PROGRAM_ID) { stateWithNewOwner }
|
output(Cash.PROGRAM_ID, stateWithNewOwner)
|
||||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
command(MEGA_CORP.owningKey, Cash.Commands.Move())
|
||||||
timeWindow(FIVE_PM)
|
timeWindow(FIVE_PM)
|
||||||
verifies()
|
verifies()
|
||||||
}
|
}
|
||||||
@ -90,16 +90,16 @@ class TransactionEncumbranceTests {
|
|||||||
ledger {
|
ledger {
|
||||||
unverifiedTransaction {
|
unverifiedTransaction {
|
||||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||||
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock") { state }
|
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", state)
|
||||||
output(TEST_TIMELOCK_ID, "5pm time-lock") { timeLock }
|
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
|
||||||
}
|
}
|
||||||
// The time of the transaction is earlier than the time specified in the encumbering timelock.
|
// The time of the transaction is earlier than the time specified in the encumbering timelock.
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||||
input("state encumbered by 5pm time-lock")
|
input("state encumbered by 5pm time-lock")
|
||||||
input("5pm time-lock")
|
input("5pm time-lock")
|
||||||
output(Cash.PROGRAM_ID) { state }
|
output(Cash.PROGRAM_ID, state)
|
||||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
command(MEGA_CORP.owningKey, Cash.Commands.Move())
|
||||||
timeWindow(FOUR_PM)
|
timeWindow(FOUR_PM)
|
||||||
this `fails with` "the time specified in the time-lock has passed"
|
this `fails with` "the time specified in the time-lock has passed"
|
||||||
}
|
}
|
||||||
@ -111,14 +111,14 @@ class TransactionEncumbranceTests {
|
|||||||
ledger {
|
ledger {
|
||||||
unverifiedTransaction {
|
unverifiedTransaction {
|
||||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||||
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", encumbrance = 1) { state }
|
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", encumbrance = 1, contractState = state)
|
||||||
output(TEST_TIMELOCK_ID, "5pm time-lock") { timeLock }
|
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Cash.PROGRAM_ID)
|
attachments(Cash.PROGRAM_ID)
|
||||||
input("state encumbered by 5pm time-lock")
|
input("state encumbered by 5pm time-lock")
|
||||||
output(Cash.PROGRAM_ID) { stateWithNewOwner }
|
output(Cash.PROGRAM_ID, stateWithNewOwner)
|
||||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
command(MEGA_CORP.owningKey, Cash.Commands.Move())
|
||||||
timeWindow(FIVE_PM)
|
timeWindow(FIVE_PM)
|
||||||
this `fails with` "Missing required encumbrance 1 in INPUT"
|
this `fails with` "Missing required encumbrance 1 in INPUT"
|
||||||
}
|
}
|
||||||
@ -130,9 +130,9 @@ class TransactionEncumbranceTests {
|
|||||||
ledger {
|
ledger {
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Cash.PROGRAM_ID)
|
attachments(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { state }
|
input(Cash.PROGRAM_ID, state)
|
||||||
output(Cash.PROGRAM_ID, encumbrance = 0) { stateWithNewOwner }
|
output(Cash.PROGRAM_ID, encumbrance = 0, contractState = stateWithNewOwner)
|
||||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
command(MEGA_CORP.owningKey, Cash.Commands.Move())
|
||||||
this `fails with` "Missing required encumbrance 0 in OUTPUT"
|
this `fails with` "Missing required encumbrance 0 in OUTPUT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,10 +143,10 @@ class TransactionEncumbranceTests {
|
|||||||
ledger {
|
ledger {
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||||
input(Cash.PROGRAM_ID) { state }
|
input(Cash.PROGRAM_ID, state)
|
||||||
output(TEST_TIMELOCK_ID, encumbrance = 2) { stateWithNewOwner }
|
output(TEST_TIMELOCK_ID, encumbrance = 2, contractState = stateWithNewOwner)
|
||||||
output(TEST_TIMELOCK_ID) { timeLock }
|
output(TEST_TIMELOCK_ID, timeLock)
|
||||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
command(MEGA_CORP.owningKey, Cash.Commands.Move())
|
||||||
this `fails with` "Missing required encumbrance 2 in OUTPUT"
|
this `fails with` "Missing required encumbrance 2 in OUTPUT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,16 +157,16 @@ class TransactionEncumbranceTests {
|
|||||||
ledger {
|
ledger {
|
||||||
unverifiedTransaction {
|
unverifiedTransaction {
|
||||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||||
output(Cash.PROGRAM_ID, "state encumbered by some other state", encumbrance = 1) { state }
|
output(Cash.PROGRAM_ID, "state encumbered by some other state", encumbrance = 1, contractState = state)
|
||||||
output(Cash.PROGRAM_ID, "some other state") { state }
|
output(Cash.PROGRAM_ID, "some other state", state)
|
||||||
output(TEST_TIMELOCK_ID, "5pm time-lock") { timeLock }
|
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||||
input("state encumbered by some other state")
|
input("state encumbered by some other state")
|
||||||
input("5pm time-lock")
|
input("5pm time-lock")
|
||||||
output(Cash.PROGRAM_ID) { stateWithNewOwner }
|
output(Cash.PROGRAM_ID, stateWithNewOwner)
|
||||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
command(MEGA_CORP.owningKey, Cash.Commands.Move())
|
||||||
timeWindow(FIVE_PM)
|
timeWindow(FIVE_PM)
|
||||||
this `fails with` "Missing required encumbrance 1 in INPUT"
|
this `fails with` "Missing required encumbrance 1 in INPUT"
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,9 @@ from the previous milestone release.
|
|||||||
|
|
||||||
UNRELEASED
|
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.
|
* ``ConfigUtilities`` now read system properties for a node. This allow to specify data source properties at runtime.
|
||||||
|
|
||||||
|
@ -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
|
: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.
|
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
|
: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.
|
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
|
:clusterAddresses: Must list the addresses of all the members in the cluster. At least one of the members must
|
||||||
members must be active and be able to communicate with the cluster leader for joining. If empty, a new
|
be active and be able to communicate with the cluster leader for the node to join the cluster. If empty, a
|
||||||
cluster will be bootstrapped.
|
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.
|
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
|
: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:
|
following fields:
|
||||||
|
|
||||||
:username: Username consisting only of word characters (a-z, A-Z, 0-9 and _)
|
:username: Username consisting only of word characters (a-z, A-Z, 0-9 and _)
|
||||||
:password: The password
|
:password: The password
|
||||||
:permissions: A list of permission strings which RPC methods can use to control access
|
: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
|
||||||
If this field is absent or an empty list then RPC is effectively locked down. Alternatively, if it contains the string
|
contains the string ``ALL``, the user can start any flow via RPC. This value is intended for administrator
|
||||||
``ALL`` then the user is permitted to use *any* RPC method. This value is intended for administrator users and for developers.
|
users and for development.
|
||||||
|
|
||||||
:devMode: This flag sets the node to run in development mode. On startup, if the keystore ``<workspace>/certificates/sslkeystore.jks``
|
: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
|
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
|
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
|
: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
|
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
|
: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.
|
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
|
: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``.
|
advertised to the network map service instead of the provided ``p2pAddress``.
|
||||||
|
@ -4,8 +4,9 @@ Corda nodes
|
|||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
deploying-a-node
|
generating-a-node
|
||||||
running-a-node
|
running-a-node
|
||||||
|
deploying-a-node
|
||||||
corda-configuration-file
|
corda-configuration-file
|
||||||
clientrpc
|
clientrpc
|
||||||
shell
|
shell
|
||||||
|
@ -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
|
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.
|
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
|
.. warning:: The hash of the generated CorDapp JAR is not deterministic, as it depends on variables such as the
|
||||||
at creation. Nodes running the same CorDapp must therefore ensure they are using the exact same CorDapp jar, and not
|
timestamp at creation. Nodes running the same CorDapp must therefore ensure they are using the exact same CorDapp
|
||||||
different versions of the JAR created from identical sources.
|
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.
|
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
|
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
|
.. 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
|
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
|
a node, the CorDapp JAR must be added to the ``<node_dir>/cordapps/`` folder, where ``node_dir`` is the folder in which
|
||||||
|
@ -1,141 +1,242 @@
|
|||||||
Deploying a node
|
Deploying a node
|
||||||
================
|
================
|
||||||
|
|
||||||
Node structure
|
.. contents::
|
||||||
--------------
|
|
||||||
Each Corda node has the following structure:
|
|
||||||
|
|
||||||
.. 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.
|
||||||
|
|
||||||
.
|
Linux (systemd): Installing and running Corda as a systemd service
|
||||||
├── certificates // The node's doorman certificates
|
------------------------------------------------------------------
|
||||||
├── corda-webserver.jar // The built-in node webserver
|
We recommend creating systemd services to run a node and the optional webserver. This provides logging and service
|
||||||
├── corda.jar // The core Corda libraries
|
handling, and ensures the Corda service is run at boot.
|
||||||
├── 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
|
**Prerequisites**:
|
||||||
into the ``cordapps`` folder.
|
|
||||||
|
|
||||||
Node naming
|
* Oracle Java 8. The supported versions are listed in :doc:`getting-set-up`
|
||||||
-----------
|
|
||||||
A node's name must be a valid X500 name that obeys the following additional constraints:
|
|
||||||
|
|
||||||
* 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
|
``sudo adduser --system --no-create-home --group corda``
|
||||||
* Organisation: 128
|
|
||||||
* Organisation unit: 64
|
|
||||||
* Locality: 64
|
|
||||||
* State: 64
|
|
||||||
|
|
||||||
* 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
|
3. Create a directory called ``plugins`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively, download one of
|
||||||
* No leading or trailing whitespace
|
our `sample CorDapps <https://www.corda.net/samples/>`_ to the ``plugins`` directory
|
||||||
* 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 deployNodes task
|
4. Save the below as ``/opt/corda/node.conf``. See :doc:`corda-configuration-file` for a description of these options
|
||||||
--------------------
|
|
||||||
The CorDapp template defines a ``deployNodes`` task that allows you to automatically generate and configure a set of
|
|
||||||
nodes:
|
|
||||||
|
|
||||||
.. sourcecode:: groovy
|
.. code-block:: json
|
||||||
|
|
||||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
basedir : "/opt/corda"
|
||||||
directory "./build/nodes"
|
p2pAddress : "example.com:10002"
|
||||||
networkMap "O=Controller,L=London,C=GB"
|
rpcAddress : "example.com:10003"
|
||||||
node {
|
webAddress : "0.0.0.0:10004"
|
||||||
name "O=Controller,L=London,C=GB"
|
h2port : 11000
|
||||||
// The notary will offer a validating notary service.
|
emailAddress : "you@example.com"
|
||||||
notary = [validating : true]
|
myLegalName : "O=Bank of Breakfast Tea, L=London, C=GB"
|
||||||
p2pPort 10002
|
keyStorePassword : "cordacadevpass"
|
||||||
rpcPort 10003
|
trustStorePassword : "trustpass"
|
||||||
// No webport property, so no webserver will be created.
|
useHTTPS : false
|
||||||
h2Port 10004
|
devMode : false
|
||||||
sshdPort 22
|
networkMapService {
|
||||||
// Includes the corda-finance CorDapp on our node.
|
address="networkmap.foo.bar.com:10002"
|
||||||
cordapps = ["net.corda:corda-finance:$corda_release_version"]
|
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 {
|
rpcUsers=[
|
||||||
name "O=PartyA,L=London,C=GB"
|
{
|
||||||
advertisedServices = []
|
user=corda
|
||||||
p2pPort 10005
|
password=portal_password
|
||||||
rpcPort 10006
|
permissions=[
|
||||||
webPort 10007
|
ALL
|
||||||
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"]]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
* Organization (``O=``) should be a unique and meaningful identifier (e.g. Bank of Breakfast Tea)
|
||||||
* Offers a validating notary service
|
* Location (``L=``) is your nearest city
|
||||||
* Will not have a webserver (since ``webPort`` is not defined)
|
* Country (``C=``) is the `ISO 3166-1 alpha-2 code <https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2>`_
|
||||||
* Is running the ``corda-finance`` CorDapp
|
* 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
|
6. Download the `NSSM service manager <nssm.cc>`_
|
||||||
* 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
|
7. Unzip ``nssm-2.24\win64\nssm.exe`` to ``C:\Corda``
|
||||||
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
|
8. Save the following as ``C:\Corda\nssm.bat``:
|
||||||
a single node to run the network map service, by putting their name in the ``networkMap`` field.
|
|
||||||
|
|
||||||
.. warning:: When adding nodes, make sure that there are no port clashes!
|
.. code-block:: batch
|
||||||
|
|
||||||
Running deployNodes
|
nssm install cordanode1 C:\ProgramData\Oracle\Java\javapath\java.exe
|
||||||
-------------------
|
nssm set cordanode1 AppDirectory C:\Corda
|
||||||
To create the nodes defined in our ``deployNodes`` task, we'd run the following command in a terminal window from the
|
nssm set cordanode1 AppParameters "-jar corda.jar -Xmx2048m --config-file=C:\corda\node.conf"
|
||||||
root of the project:
|
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``
|
9. Modify the batch file:
|
||||||
* Windows: ``gradlew.bat deployNodes``
|
|
||||||
|
|
||||||
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,
|
10. Run the batch file by clicking on it or from a command prompt
|
||||||
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.
|
|
||||||
|
|
||||||
There will be a node folder generated for each node you defined, plus a ``runnodes`` shell script (or batch file on
|
11. Run ``services.msc`` and verify that a service called ``cordanode1`` is present and running
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
@ -24,6 +24,7 @@ object CustomVaultQuery {
|
|||||||
private companion object {
|
private companion object {
|
||||||
private val log = contextLogger()
|
private val log = contextLogger()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rebalanceCurrencyReserves(): List<Amount<Currency>> {
|
fun rebalanceCurrencyReserves(): List<Amount<Currency>> {
|
||||||
val nativeQuery = """
|
val nativeQuery = """
|
||||||
select
|
select
|
||||||
@ -44,16 +45,18 @@ object CustomVaultQuery {
|
|||||||
"""
|
"""
|
||||||
log.info("SQL to execute: $nativeQuery")
|
log.info("SQL to execute: $nativeQuery")
|
||||||
val session = services.jdbcSession()
|
val session = services.jdbcSession()
|
||||||
val prepStatement = session.prepareStatement(nativeQuery)
|
return session.prepareStatement(nativeQuery).use { prepStatement ->
|
||||||
val rs = prepStatement.executeQuery()
|
prepStatement.executeQuery().use { rs ->
|
||||||
val topUpLimits: MutableList<Amount<Currency>> = mutableListOf()
|
val topUpLimits: MutableList<Amount<Currency>> = mutableListOf()
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
val currencyStr = rs.getString(1)
|
val currencyStr = rs.getString(1)
|
||||||
val amount = rs.getLong(2)
|
val amount = rs.getLong(2)
|
||||||
log.info("$currencyStr : $amount")
|
log.info("$currencyStr : $amount")
|
||||||
topUpLimits.add(Amount(amount, Currency.getInstance(currencyStr)))
|
topUpLimits.add(Amount(amount, Currency.getInstance(currencyStr)))
|
||||||
|
}
|
||||||
|
topUpLimits
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return topUpLimits
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,6 +72,7 @@ object TopupIssuerFlow {
|
|||||||
data class TopupRequest(val issueToParty: Party,
|
data class TopupRequest(val issueToParty: Party,
|
||||||
val issuerPartyRef: OpaqueBytes,
|
val issuerPartyRef: OpaqueBytes,
|
||||||
val notaryParty: Party)
|
val notaryParty: Party)
|
||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class TopupIssuanceRequester(val issueToParty: Party,
|
class TopupIssuanceRequester(val issueToParty: Party,
|
||||||
|
@ -33,7 +33,7 @@ class CommercialPaperTest {
|
|||||||
ledger {
|
ledger {
|
||||||
transaction {
|
transaction {
|
||||||
attachments(CP_PROGRAM_ID)
|
attachments(CP_PROGRAM_ID)
|
||||||
input(CP_PROGRAM_ID) { inState }
|
input(CP_PROGRAM_ID, inState)
|
||||||
verifies()
|
verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,8 +46,8 @@ class CommercialPaperTest {
|
|||||||
val inState = getPaper()
|
val inState = getPaper()
|
||||||
ledger {
|
ledger {
|
||||||
transaction {
|
transaction {
|
||||||
input(CP_PROGRAM_ID) { inState }
|
input(CP_PROGRAM_ID, inState)
|
||||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
|
||||||
attachments(CP_PROGRAM_ID)
|
attachments(CP_PROGRAM_ID)
|
||||||
verifies()
|
verifies()
|
||||||
}
|
}
|
||||||
@ -61,8 +61,8 @@ class CommercialPaperTest {
|
|||||||
val inState = getPaper()
|
val inState = getPaper()
|
||||||
ledger {
|
ledger {
|
||||||
transaction {
|
transaction {
|
||||||
input(CP_PROGRAM_ID) { inState }
|
input(CP_PROGRAM_ID, inState)
|
||||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
|
||||||
attachments(CP_PROGRAM_ID)
|
attachments(CP_PROGRAM_ID)
|
||||||
`fails with`("the state is propagated")
|
`fails with`("the state is propagated")
|
||||||
}
|
}
|
||||||
@ -76,11 +76,11 @@ class CommercialPaperTest {
|
|||||||
val inState = getPaper()
|
val inState = getPaper()
|
||||||
ledger {
|
ledger {
|
||||||
transaction {
|
transaction {
|
||||||
input(CP_PROGRAM_ID) { inState }
|
input(CP_PROGRAM_ID, inState)
|
||||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
|
||||||
attachments(CP_PROGRAM_ID)
|
attachments(CP_PROGRAM_ID)
|
||||||
`fails with`("the state is propagated")
|
`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()
|
verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,15 +92,15 @@ class CommercialPaperTest {
|
|||||||
fun `simple issuance with tweak`() {
|
fun `simple issuance with tweak`() {
|
||||||
ledger {
|
ledger {
|
||||||
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)
|
attachments(CP_PROGRAM_ID)
|
||||||
tweak {
|
tweak {
|
||||||
// The wrong pubkey.
|
// The wrong pubkey.
|
||||||
command(BIG_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
command(BIG_CORP_PUBKEY, CommercialPaper.Commands.Issue())
|
||||||
timeWindow(TEST_TX_TIME)
|
timeWindow(TEST_TX_TIME)
|
||||||
`fails with`("output states are issued by a command signer")
|
`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)
|
timeWindow(TEST_TX_TIME)
|
||||||
verifies()
|
verifies()
|
||||||
}
|
}
|
||||||
@ -112,15 +112,15 @@ class CommercialPaperTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `simple issuance with tweak and top level transaction`() {
|
fun `simple issuance with tweak and top level transaction`() {
|
||||||
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)
|
attachments(CP_PROGRAM_ID)
|
||||||
tweak {
|
tweak {
|
||||||
// The wrong pubkey.
|
// The wrong pubkey.
|
||||||
command(BIG_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
command(BIG_CORP_PUBKEY, CommercialPaper.Commands.Issue())
|
||||||
timeWindow(TEST_TX_TIME)
|
timeWindow(TEST_TX_TIME)
|
||||||
`fails with`("output states are issued by a command signer")
|
`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)
|
timeWindow(TEST_TX_TIME)
|
||||||
verifies()
|
verifies()
|
||||||
}
|
}
|
||||||
@ -140,8 +140,8 @@ class CommercialPaperTest {
|
|||||||
|
|
||||||
// Some CP is issued onto the ledger by MegaCorp.
|
// Some CP is issued onto the ledger by MegaCorp.
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
output(CP_PROGRAM_ID, "paper") { getPaper() }
|
output(CP_PROGRAM_ID, "paper", getPaper())
|
||||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue())
|
||||||
attachments(CP_PROGRAM_ID)
|
attachments(CP_PROGRAM_ID)
|
||||||
timeWindow(TEST_TX_TIME)
|
timeWindow(TEST_TX_TIME)
|
||||||
verifies()
|
verifies()
|
||||||
@ -151,10 +151,10 @@ class CommercialPaperTest {
|
|||||||
transaction("Trade") {
|
transaction("Trade") {
|
||||||
input("paper")
|
input("paper")
|
||||||
input("alice's $900")
|
input("alice's $900")
|
||||||
output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP }
|
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) }
|
output(CP_PROGRAM_ID, "alice's paper", "paper".output<ICommercialPaperState>().withOwner(ALICE))
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
|
||||||
verifies()
|
verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -173,8 +173,8 @@ class CommercialPaperTest {
|
|||||||
|
|
||||||
// Some CP is issued onto the ledger by MegaCorp.
|
// Some CP is issued onto the ledger by MegaCorp.
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
output(CP_PROGRAM_ID, "paper") { getPaper() }
|
output(CP_PROGRAM_ID, "paper", getPaper())
|
||||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue())
|
||||||
attachments(CP_PROGRAM_ID)
|
attachments(CP_PROGRAM_ID)
|
||||||
timeWindow(TEST_TX_TIME)
|
timeWindow(TEST_TX_TIME)
|
||||||
verifies()
|
verifies()
|
||||||
@ -183,18 +183,18 @@ class CommercialPaperTest {
|
|||||||
transaction("Trade") {
|
transaction("Trade") {
|
||||||
input("paper")
|
input("paper")
|
||||||
input("alice's $900")
|
input("alice's $900")
|
||||||
output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP }
|
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) }
|
output(CP_PROGRAM_ID, "alice's paper", "paper".output<ICommercialPaperState>().withOwner(ALICE))
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
|
||||||
verifies()
|
verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
input("paper")
|
input("paper")
|
||||||
// We moved a paper to another pubkey.
|
// We moved a paper to another pubkey.
|
||||||
output(CP_PROGRAM_ID, "bob's paper") { "paper".output<ICommercialPaperState>().withOwner(BOB) }
|
output(CP_PROGRAM_ID, "bob's paper", "paper".output<ICommercialPaperState>().withOwner(BOB))
|
||||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
|
||||||
verifies()
|
verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,8 +215,8 @@ class CommercialPaperTest {
|
|||||||
|
|
||||||
// Some CP is issued onto the ledger by MegaCorp.
|
// Some CP is issued onto the ledger by MegaCorp.
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
output(CP_PROGRAM_ID, "paper") { getPaper() }
|
output(CP_PROGRAM_ID, "paper", getPaper())
|
||||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue())
|
||||||
attachments(CP_PROGRAM_ID)
|
attachments(CP_PROGRAM_ID)
|
||||||
timeWindow(TEST_TX_TIME)
|
timeWindow(TEST_TX_TIME)
|
||||||
verifies()
|
verifies()
|
||||||
@ -225,10 +225,10 @@ class CommercialPaperTest {
|
|||||||
transaction("Trade") {
|
transaction("Trade") {
|
||||||
input("paper")
|
input("paper")
|
||||||
input("alice's $900")
|
input("alice's $900")
|
||||||
output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP }
|
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) }
|
output(CP_PROGRAM_ID, "alice's paper", "paper".output<ICommercialPaperState>().withOwner(ALICE))
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
|
||||||
verifies()
|
verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,8 +236,8 @@ class CommercialPaperTest {
|
|||||||
transaction {
|
transaction {
|
||||||
input("paper")
|
input("paper")
|
||||||
// We moved a paper to another pubkey.
|
// We moved a paper to another pubkey.
|
||||||
output(CP_PROGRAM_ID, "bob's paper") { "paper".output<ICommercialPaperState>().withOwner(BOB) }
|
output(CP_PROGRAM_ID, "bob's paper", "paper".output<ICommercialPaperState>().withOwner(BOB))
|
||||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
|
||||||
verifies()
|
verifies()
|
||||||
}
|
}
|
||||||
fails()
|
fails()
|
||||||
|
@ -28,7 +28,7 @@ class CustomVaultQueryTest {
|
|||||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance", "net.corda.docs"))
|
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance", "net.corda.docs"))
|
||||||
nodeA = mockNet.createPartyNode()
|
nodeA = mockNet.createPartyNode()
|
||||||
nodeB = mockNet.createPartyNode()
|
nodeB = mockNet.createPartyNode()
|
||||||
nodeA.internals.registerInitiatedFlow(TopupIssuerFlow.TopupIssuer::class.java)
|
nodeA.registerInitiatedFlow(TopupIssuerFlow.TopupIssuer::class.java)
|
||||||
notary = mockNet.defaultNotaryIdentity
|
notary = mockNet.defaultNotaryIdentity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class FxTransactionBuildTutorialTest {
|
|||||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance"))
|
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance"))
|
||||||
nodeA = mockNet.createPartyNode()
|
nodeA = mockNet.createPartyNode()
|
||||||
nodeB = mockNet.createPartyNode()
|
nodeB = mockNet.createPartyNode()
|
||||||
nodeB.internals.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java)
|
nodeB.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java)
|
||||||
notary = mockNet.defaultNotaryIdentity
|
notary = mockNet.defaultNotaryIdentity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ class WorkflowTransactionBuildTutorialTest {
|
|||||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.docs"))
|
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.docs"))
|
||||||
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||||
val bobNode = mockNet.createPartyNode(BOB_NAME)
|
val bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||||
aliceNode.internals.registerInitiatedFlow(RecordCompletionFlow::class.java)
|
aliceNode.registerInitiatedFlow(RecordCompletionFlow::class.java)
|
||||||
aliceServices = aliceNode.services
|
aliceServices = aliceNode.services
|
||||||
bobServices = bobNode.services
|
bobServices = bobNode.services
|
||||||
alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME)
|
alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME)
|
||||||
|
136
docs/source/generating-a-node.rst
Normal file
136
docs/source/generating-a-node.rst
Normal 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>`.
|
@ -1,25 +1,49 @@
|
|||||||
Running a node
|
Running nodes locally
|
||||||
==============
|
=====================
|
||||||
|
|
||||||
Starting your node
|
.. contents::
|
||||||
------------------
|
|
||||||
After following the steps in :doc:`deploying-a-node`, you should have deployed your node(s) with any chosen CorDapps
|
.. note:: You should already have generated your node(s) with their CorDapps installed by following the instructions in
|
||||||
already installed. You run each node by navigating to ``<node_dir>`` in a terminal window and running:
|
: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
|
.. code-block:: shell
|
||||||
|
|
||||||
java -jar corda.jar
|
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:
|
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``
|
``java -Dcapsule.jvm.args="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005" -jar corda.jar``
|
||||||
|
@ -137,7 +137,7 @@ which could be represented as ``{ first: foo, second: 123 }``.
|
|||||||
|
|
||||||
.. note:: If your CorDapp is written in Java,
|
.. note:: If your CorDapp is written in Java,
|
||||||
named arguments won't work unless you compiled using the ``-parameters`` argument to javac.
|
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:
|
The same syntax is also used to specify the parameters for RPCs, accessed via the ``run`` command, like this:
|
||||||
|
|
||||||
|
@ -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"
|
"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
|
IDEA issues
|
||||||
-----------
|
-----------
|
||||||
|
@ -176,7 +176,7 @@ There are two ways to run the example CorDapp:
|
|||||||
* Via IntelliJ
|
* 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
|
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
|
Terminal
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
@ -172,16 +172,14 @@ class Cap {
|
|||||||
@Test
|
@Test
|
||||||
fun issue() {
|
fun issue() {
|
||||||
transaction {
|
transaction {
|
||||||
output(UNIVERSAL_PROGRAM_ID) { stateInitial }
|
output(UNIVERSAL_PROGRAM_ID, stateInitial)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
|
||||||
this `fails with` "the transaction is signed by all liable parties"
|
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()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -189,44 +187,38 @@ class Cap {
|
|||||||
@Test
|
@Test
|
||||||
fun `first fixing`() {
|
fun `first fixing`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { stateInitial }
|
input(UNIVERSAL_PROGRAM_ID, stateInitial)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFirst }
|
output(UNIVERSAL_PROGRAM_ID, stateAfterFixingFirst)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
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"
|
this `fails with` "action must be defined"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
// wrong source
|
// 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"
|
this `fails with` "relevant fixing must be included"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
// wrong date
|
// 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"
|
this `fails with` "relevant fixing must be included"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
// wrong tenor
|
// 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"
|
this `fails with` "relevant fixing must be included"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
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"
|
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()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,19 +226,16 @@ class Cap {
|
|||||||
@Test
|
@Test
|
||||||
fun `first execute`() {
|
fun `first execute`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFirst }
|
input(UNIVERSAL_PROGRAM_ID, stateAfterFixingFirst)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { stateAfterExecutionFirst }
|
output(UNIVERSAL_PROGRAM_ID, stateAfterExecutionFirst)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { statePaymentFirst }
|
output(UNIVERSAL_PROGRAM_ID, statePaymentFirst)
|
||||||
|
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
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"
|
this `fails with` "action must be defined"
|
||||||
}
|
}
|
||||||
|
command(highStreetBank.owningKey, UniversalContract.Commands.Action("exercise"))
|
||||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("exercise") }
|
|
||||||
|
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -254,18 +243,15 @@ class Cap {
|
|||||||
@Test
|
@Test
|
||||||
fun `final execute`() {
|
fun `final execute`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFinal }
|
input(UNIVERSAL_PROGRAM_ID, stateAfterFixingFinal)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { statePaymentFinal }
|
output(UNIVERSAL_PROGRAM_ID, statePaymentFinal)
|
||||||
|
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
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"
|
this `fails with` "action must be defined"
|
||||||
}
|
}
|
||||||
|
command(highStreetBank.owningKey, UniversalContract.Commands.Action("exercise"))
|
||||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("exercise") }
|
|
||||||
|
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -273,44 +259,38 @@ class Cap {
|
|||||||
@Test
|
@Test
|
||||||
fun `second fixing`() {
|
fun `second fixing`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { stateAfterExecutionFirst }
|
input(UNIVERSAL_PROGRAM_ID, stateAfterExecutionFirst)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFinal }
|
output(UNIVERSAL_PROGRAM_ID, stateAfterFixingFinal)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
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"
|
this `fails with` "action must be defined"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
// wrong source
|
// 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"
|
this `fails with` "relevant fixing must be included"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
// wrong date
|
// 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"
|
this `fails with` "relevant fixing must be included"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
// wrong tenor
|
// 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"
|
this `fails with` "relevant fixing must be included"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
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"
|
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()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,16 +55,14 @@ class Caplet {
|
|||||||
@Test
|
@Test
|
||||||
fun issue() {
|
fun issue() {
|
||||||
transaction {
|
transaction {
|
||||||
output(UNIVERSAL_PROGRAM_ID) { stateStart }
|
output(UNIVERSAL_PROGRAM_ID, stateStart)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
|
||||||
this `fails with` "the transaction is signed by all liable parties"
|
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()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,17 +70,15 @@ class Caplet {
|
|||||||
@Test
|
@Test
|
||||||
fun `execute`() {
|
fun `execute`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { stateFixed }
|
input(UNIVERSAL_PROGRAM_ID, stateFixed)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { stateFinal }
|
output(UNIVERSAL_PROGRAM_ID, stateFinal)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
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"
|
this `fails with` "action must be defined"
|
||||||
}
|
}
|
||||||
|
command(highStreetBank.owningKey, UniversalContract.Commands.Action("exercise"))
|
||||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("exercise") }
|
|
||||||
|
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,44 +86,38 @@ class Caplet {
|
|||||||
@Test
|
@Test
|
||||||
fun `fixing`() {
|
fun `fixing`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { stateStart }
|
input(UNIVERSAL_PROGRAM_ID, stateStart)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { stateFixed }
|
output(UNIVERSAL_PROGRAM_ID, stateFixed)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
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"
|
this `fails with` "action must be defined"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
// wrong source
|
// 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"
|
this `fails with` "relevant fixing must be included"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
// wrong date
|
// 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"
|
this `fails with` "relevant fixing must be included"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
// wrong tenor
|
// 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"
|
this `fails with` "relevant fixing must be included"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
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"
|
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()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,20 +52,18 @@ class FXFwdTimeOption {
|
|||||||
@Test
|
@Test
|
||||||
fun `issue - signature`() {
|
fun `issue - signature`() {
|
||||||
transaction {
|
transaction {
|
||||||
output(UNIVERSAL_PROGRAM_ID) { inState }
|
output(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
|
||||||
this `fails with` "the transaction is signed by all liable parties"
|
this `fails with` "the transaction is signed by all liable parties"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
|
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
|
||||||
this `fails with` "the transaction is signed by all liable parties"
|
this `fails with` "the transaction is signed by all liable parties"
|
||||||
}
|
}
|
||||||
|
command(listOf(highStreetBank.owningKey, acmeCorp.owningKey), UniversalContract.Commands.Issue())
|
||||||
command(highStreetBank.owningKey, acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
|
||||||
|
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,31 +71,28 @@ class FXFwdTimeOption {
|
|||||||
@Test
|
@Test
|
||||||
fun `maturity, bank exercise`() {
|
fun `maturity, bank exercise`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState2 }
|
output(UNIVERSAL_PROGRAM_ID, outState2)
|
||||||
|
|
||||||
timeWindow(TEST_TX_TIME_AFTER_MATURITY)
|
timeWindow(TEST_TX_TIME_AFTER_MATURITY)
|
||||||
|
|
||||||
tweak {
|
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"
|
this `fails with` "action must be defined"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("exercise") }
|
command(highStreetBank.owningKey, UniversalContract.Commands.Action("exercise"))
|
||||||
this `fails with` "condition must be met"
|
this `fails with` "condition must be met"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("exercise") }
|
command(acmeCorp.owningKey, UniversalContract.Commands.Action("exercise"))
|
||||||
this `fails with` "condition must be met"
|
this `fails with` "condition must be met"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("expire") }
|
command(acmeCorp.owningKey, UniversalContract.Commands.Action("expire"))
|
||||||
this `fails with` "condition must be met"
|
this `fails with` "condition must be met"
|
||||||
}
|
}
|
||||||
|
command(highStreetBank.owningKey, UniversalContract.Commands.Action("expire"))
|
||||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("expire") }
|
|
||||||
|
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -105,31 +100,28 @@ class FXFwdTimeOption {
|
|||||||
@Test
|
@Test
|
||||||
fun `maturity, corp exercise`() {
|
fun `maturity, corp exercise`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState2 }
|
output(UNIVERSAL_PROGRAM_ID, outState2)
|
||||||
|
|
||||||
timeWindow(TEST_TX_TIME_BEFORE_MATURITY)
|
timeWindow(TEST_TX_TIME_BEFORE_MATURITY)
|
||||||
|
|
||||||
tweak {
|
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"
|
this `fails with` "action must be defined"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("expire") }
|
command(acmeCorp.owningKey, UniversalContract.Commands.Action("expire"))
|
||||||
this `fails with` "condition must be met"
|
this `fails with` "condition must be met"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("expire") }
|
command(highStreetBank.owningKey, UniversalContract.Commands.Action("expire"))
|
||||||
this `fails with` "condition must be met"
|
this `fails with` "condition must be met"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("exercise") }
|
command(highStreetBank.owningKey, UniversalContract.Commands.Action("exercise"))
|
||||||
this `fails with` "condition must be met"
|
this `fails with` "condition must be met"
|
||||||
}
|
}
|
||||||
|
command(acmeCorp.owningKey, UniversalContract.Commands.Action("exercise"))
|
||||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("exercise") }
|
|
||||||
|
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,20 +44,18 @@ class FXSwap {
|
|||||||
fun `issue - signature`() {
|
fun `issue - signature`() {
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
output(UNIVERSAL_PROGRAM_ID) { inState }
|
output(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
|
||||||
this `fails with` "the transaction is signed by all liable parties"
|
this `fails with` "the transaction is signed by all liable parties"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
|
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
|
||||||
this `fails with` "the transaction is signed by all liable parties"
|
this `fails with` "the transaction is signed by all liable parties"
|
||||||
}
|
}
|
||||||
|
command(listOf(highStreetBank.owningKey, acmeCorp.owningKey), UniversalContract.Commands.Issue())
|
||||||
command(highStreetBank.owningKey, acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
|
||||||
|
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,18 +63,16 @@ class FXSwap {
|
|||||||
@Test
|
@Test
|
||||||
fun `execute`() {
|
fun `execute`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState2 }
|
output(UNIVERSAL_PROGRAM_ID, outState2)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
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"
|
this `fails with` "action must be defined"
|
||||||
}
|
}
|
||||||
|
command(highStreetBank.owningKey, UniversalContract.Commands.Action("execute"))
|
||||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("execute") }
|
|
||||||
|
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,18 +80,16 @@ class FXSwap {
|
|||||||
@Test
|
@Test
|
||||||
fun `execute - reversed order`() {
|
fun `execute - reversed order`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState2 }
|
output(UNIVERSAL_PROGRAM_ID, outState2)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
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"
|
this `fails with` "action must be defined"
|
||||||
}
|
}
|
||||||
|
command(highStreetBank.owningKey, UniversalContract.Commands.Action("execute"))
|
||||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("execute") }
|
|
||||||
|
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,12 +97,11 @@ class FXSwap {
|
|||||||
@Test
|
@Test
|
||||||
fun `execute - not authorized`() {
|
fun `execute - not authorized`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState2 }
|
output(UNIVERSAL_PROGRAM_ID, outState2)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
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"
|
this `fails with` "condition must be met"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -116,12 +109,11 @@ class FXSwap {
|
|||||||
@Test
|
@Test
|
||||||
fun `execute - before maturity`() {
|
fun `execute - before maturity`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState2 }
|
output(UNIVERSAL_PROGRAM_ID, outState2)
|
||||||
timeWindow(TEST_TX_TIME_TOO_EARLY)
|
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"
|
this `fails with` "condition must be met"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,11 +121,10 @@ class FXSwap {
|
|||||||
@Test
|
@Test
|
||||||
fun `execute - outState mismatch 1`() {
|
fun `execute - outState mismatch 1`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
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"
|
this `fails with` "output state must match action result state"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,12 +132,11 @@ class FXSwap {
|
|||||||
@Test
|
@Test
|
||||||
fun `execute - outState mismatch 2`() {
|
fun `execute - outState mismatch 2`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outStateBad2 }
|
output(UNIVERSAL_PROGRAM_ID, outStateBad2)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
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"
|
this `fails with` "output states must match action result state"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,12 +144,11 @@ class FXSwap {
|
|||||||
@Test
|
@Test
|
||||||
fun `execute - outState mismatch 3`() {
|
fun `execute - outState mismatch 3`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outStateBad1 }
|
output(UNIVERSAL_PROGRAM_ID, outStateBad1)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState2 }
|
output(UNIVERSAL_PROGRAM_ID, outState2)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
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"
|
this `fails with` "output states must match action result state"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,12 +156,11 @@ class FXSwap {
|
|||||||
@Test
|
@Test
|
||||||
fun `execute - outState mismatch 4`() {
|
fun `execute - outState mismatch 4`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outStateBad3 }
|
output(UNIVERSAL_PROGRAM_ID, outStateBad3)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
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"
|
this `fails with` "output states must match action result state"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,16 +134,14 @@ class IRS {
|
|||||||
@Test
|
@Test
|
||||||
fun issue() {
|
fun issue() {
|
||||||
transaction {
|
transaction {
|
||||||
output(UNIVERSAL_PROGRAM_ID) { stateInitial }
|
output(UNIVERSAL_PROGRAM_ID, stateInitial)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
|
||||||
this `fails with` "the transaction is signed by all liable parties"
|
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()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -151,44 +149,38 @@ class IRS {
|
|||||||
@Test
|
@Test
|
||||||
fun `first fixing`() {
|
fun `first fixing`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { stateInitial }
|
input(UNIVERSAL_PROGRAM_ID, stateInitial)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFirst }
|
output(UNIVERSAL_PROGRAM_ID, stateAfterFixingFirst)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
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"
|
this `fails with` "action must be defined"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
// wrong source
|
// 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"
|
this `fails with` "relevant fixing must be included"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
// wrong date
|
// 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"
|
this `fails with` "relevant fixing must be included"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
// wrong tenor
|
// 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"
|
this `fails with` "relevant fixing must be included"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
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"
|
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()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -196,19 +188,16 @@ class IRS {
|
|||||||
@Test
|
@Test
|
||||||
fun `first execute`() {
|
fun `first execute`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFirst }
|
input(UNIVERSAL_PROGRAM_ID, stateAfterFixingFirst)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { stateAfterExecutionFirst }
|
output(UNIVERSAL_PROGRAM_ID, stateAfterExecutionFirst)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { statePaymentFirst }
|
output(UNIVERSAL_PROGRAM_ID, statePaymentFirst)
|
||||||
|
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
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"
|
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()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,16 +145,14 @@ class RollOutTests {
|
|||||||
@Test
|
@Test
|
||||||
fun issue() {
|
fun issue() {
|
||||||
transaction {
|
transaction {
|
||||||
output(UNIVERSAL_PROGRAM_ID) { stateStart }
|
output(UNIVERSAL_PROGRAM_ID, stateStart)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
|
||||||
this `fails with` "the transaction is signed by all liable parties"
|
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()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,18 +160,16 @@ class RollOutTests {
|
|||||||
@Test
|
@Test
|
||||||
fun `execute`() {
|
fun `execute`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { stateStart }
|
input(UNIVERSAL_PROGRAM_ID, stateStart)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { stateStep1a }
|
output(UNIVERSAL_PROGRAM_ID, stateStep1a)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { stateStep1b }
|
output(UNIVERSAL_PROGRAM_ID, stateStep1b)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
/* tweak {
|
/* 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"
|
this `fails with` "action must be defined"
|
||||||
}*/
|
}*/
|
||||||
|
command(highStreetBank.owningKey, UniversalContract.Commands.Action("transfer"))
|
||||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("transfer") }
|
|
||||||
|
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,16 +61,14 @@ class Swaption {
|
|||||||
@Test
|
@Test
|
||||||
fun issue() {
|
fun issue() {
|
||||||
transaction {
|
transaction {
|
||||||
output(UNIVERSAL_PROGRAM_ID) { stateInitial }
|
output(UNIVERSAL_PROGRAM_ID, stateInitial)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
|
||||||
this `fails with` "the transaction is signed by all liable parties"
|
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()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,15 +51,12 @@ class ZeroCouponBond {
|
|||||||
@Test
|
@Test
|
||||||
fun `issue - signature`() {
|
fun `issue - signature`() {
|
||||||
transaction {
|
transaction {
|
||||||
output(UNIVERSAL_PROGRAM_ID) { inState }
|
output(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
|
||||||
this `fails with` "the transaction is signed by all liable parties"
|
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()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,17 +64,15 @@ class ZeroCouponBond {
|
|||||||
@Test
|
@Test
|
||||||
fun `execute`() {
|
fun `execute`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState }
|
output(UNIVERSAL_PROGRAM_ID, outState)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
timeWindow(TEST_TX_TIME_1)
|
||||||
|
|
||||||
tweak {
|
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"
|
this `fails with` "action must be defined"
|
||||||
}
|
}
|
||||||
|
command(highStreetBank.owningKey, UniversalContract.Commands.Action("execute"))
|
||||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("execute") }
|
|
||||||
|
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,11 +80,10 @@ class ZeroCouponBond {
|
|||||||
@Test
|
@Test
|
||||||
fun `execute - not authorized`() {
|
fun `execute - not authorized`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outState }
|
output(UNIVERSAL_PROGRAM_ID, outState)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
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"
|
this `fails with` "condition must be met"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,11 +91,10 @@ class ZeroCouponBond {
|
|||||||
@Test
|
@Test
|
||||||
fun `execute - outState mismatch`() {
|
fun `execute - outState mismatch`() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outStateWrong }
|
output(UNIVERSAL_PROGRAM_ID, outStateWrong)
|
||||||
timeWindow(TEST_TX_TIME_1)
|
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"
|
this `fails with` "output state must match action result state"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,29 +102,23 @@ class ZeroCouponBond {
|
|||||||
@Test
|
@Test
|
||||||
fun move() {
|
fun move() {
|
||||||
transaction {
|
transaction {
|
||||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outStateMove }
|
output(UNIVERSAL_PROGRAM_ID, outStateMove)
|
||||||
command(acmeCorp.owningKey) {
|
command(acmeCorp.owningKey,
|
||||||
UniversalContract.Commands.Move(acmeCorp, momAndPop)
|
UniversalContract.Commands.Move(acmeCorp, momAndPop))
|
||||||
}
|
|
||||||
this `fails with` "the transaction is signed by all liable parties"
|
this `fails with` "the transaction is signed by all liable parties"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
output(UNIVERSAL_PROGRAM_ID) { inState }
|
output(UNIVERSAL_PROGRAM_ID, inState)
|
||||||
command(acmeCorp.owningKey, momAndPop.owningKey, highStreetBank.owningKey) {
|
command(listOf(acmeCorp.owningKey, momAndPop.owningKey, highStreetBank.owningKey),
|
||||||
UniversalContract.Commands.Move(acmeCorp, momAndPop)
|
UniversalContract.Commands.Move(acmeCorp, momAndPop))
|
||||||
}
|
|
||||||
this `fails with` "output state does not reflect move command"
|
this `fails with` "output state does not reflect move command"
|
||||||
}
|
}
|
||||||
|
output(UNIVERSAL_PROGRAM_ID, outStateMove)
|
||||||
output(UNIVERSAL_PROGRAM_ID) { outStateMove }
|
command(listOf(acmeCorp.owningKey, momAndPop.owningKey, highStreetBank.owningKey),
|
||||||
|
UniversalContract.Commands.Move(acmeCorp, momAndPop))
|
||||||
command(acmeCorp.owningKey, momAndPop.owningKey, highStreetBank.owningKey) {
|
|
||||||
UniversalContract.Commands.Move(acmeCorp, momAndPop)
|
|
||||||
}
|
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,13 +67,14 @@ abstract class AbstractCashSelection(private val maxRetries : Int = 8, private v
|
|||||||
* with this notary are included.
|
* with this notary are included.
|
||||||
* @param onlyFromIssuerParties Optional issuer parties to match against.
|
* @param onlyFromIssuerParties Optional issuer parties to match against.
|
||||||
* @param withIssuerRefs Optional issuer references 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.
|
* 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?,
|
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.
|
* 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 {
|
try {
|
||||||
// we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null)
|
// 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
|
// the softLockReserve update will detect whether we try to lock states locked by others
|
||||||
val rs = executeQuery(connection, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs)
|
return executeQuery(connection, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs) { rs ->
|
||||||
stateAndRefs.clear()
|
stateAndRefs.clear()
|
||||||
|
|
||||||
var totalPennies = 0L
|
var totalPennies = 0L
|
||||||
val stateRefs = mutableSetOf<StateRef>()
|
val stateRefs = mutableSetOf<StateRef>()
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
val txHash = SecureHash.parse(rs.getString(1))
|
val txHash = SecureHash.parse(rs.getString(1))
|
||||||
val index = rs.getInt(2)
|
val index = rs.getInt(2)
|
||||||
val pennies = rs.getLong(3)
|
val pennies = rs.getLong(3)
|
||||||
totalPennies = rs.getLong(4)
|
totalPennies = rs.getLong(4)
|
||||||
val rowLockId = rs.getString(5)
|
val rowLockId = rs.getString(5)
|
||||||
stateRefs.add(StateRef(txHash, index))
|
stateRefs.add(StateRef(txHash, index))
|
||||||
log.trace { "ROW: $rowLockId ($lockId): ${StateRef(txHash, index)} : $pennies ($totalPennies)" }
|
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
|
// retry as more states may become available
|
||||||
} catch (e: SQLException) {
|
} catch (e: SQLException) {
|
||||||
log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId]
|
log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId]
|
||||||
|
@ -30,9 +30,8 @@ class CashSelectionH2Impl : AbstractCashSelection() {
|
|||||||
// 2) H2 uses session variables to perform this accumulator function:
|
// 2) H2 uses session variables to perform this accumulator function:
|
||||||
// http://www.h2database.com/html/functions.html#set
|
// 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)
|
// 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?,
|
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
|
||||||
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet {
|
connection.createStatement().use { it.execute("CALL SET(@t, CAST(0 AS BIGINT));") }
|
||||||
connection.createStatement().execute("CALL SET(@t, CAST(0 AS BIGINT));")
|
|
||||||
|
|
||||||
val selectJoin = """
|
val selectJoin = """
|
||||||
SELECT vs.transaction_id, vs.output_index, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
|
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 "")
|
" AND ccs.issuer_ref IN (?)" else "")
|
||||||
|
|
||||||
// Use prepared statement for protection against SQL Injection (http://www.h2database.com/html/advanced.html#sql_injection)
|
// Use prepared statement for protection against SQL Injection (http://www.h2database.com/html/advanced.html#sql_injection)
|
||||||
val psSelectJoin = connection.prepareStatement(selectJoin)
|
connection.prepareStatement(selectJoin).use { psSelectJoin ->
|
||||||
var pIndex = 0
|
var pIndex = 0
|
||||||
psSelectJoin.setString(++pIndex, amount.token.currencyCode)
|
psSelectJoin.setString(++pIndex, amount.token.currencyCode)
|
||||||
psSelectJoin.setLong(++pIndex, amount.quantity)
|
psSelectJoin.setLong(++pIndex, amount.quantity)
|
||||||
psSelectJoin.setString(++pIndex, lockId.toString())
|
psSelectJoin.setString(++pIndex, lockId.toString())
|
||||||
if (notary != null)
|
if (notary != null)
|
||||||
psSelectJoin.setString(++pIndex, notary.name.toString())
|
psSelectJoin.setString(++pIndex, notary.name.toString())
|
||||||
if (onlyFromIssuerParties.isNotEmpty())
|
if (onlyFromIssuerParties.isNotEmpty())
|
||||||
psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toStringShort() as Any}.toTypedArray() )
|
psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toStringShort() as Any }.toTypedArray())
|
||||||
if (withIssuerRefs.isNotEmpty())
|
if (withIssuerRefs.isNotEmpty())
|
||||||
psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray())
|
psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray())
|
||||||
log.debug { psSelectJoin.toString() }
|
log.debug { psSelectJoin.toString() }
|
||||||
|
|
||||||
return psSelectJoin.executeQuery()
|
psSelectJoin.executeQuery().use { rs ->
|
||||||
|
return withResultSet(rs)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -19,7 +19,7 @@ class CashSelectionMySQLImpl : AbstractCashSelection() {
|
|||||||
return metadata.driverName == JDBC_DRIVER_NAME
|
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")
|
TODO("MySQL cash selection not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
// 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.
|
// 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
|
// 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?,
|
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
|
||||||
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet {
|
|
||||||
val selectJoin = """SELECT nested.transaction_id, nested.output_index, nested.pennies,
|
val selectJoin = """SELECT nested.transaction_id, nested.output_index, nested.pennies,
|
||||||
nested.total+nested.pennies as total_pennies, nested.lock_id
|
nested.total+nested.pennies as total_pennies, nested.lock_id
|
||||||
FROM
|
FROM
|
||||||
@ -51,29 +50,32 @@ class CashSelectionPostgreSQLImpl : AbstractCashSelection() {
|
|||||||
nested WHERE nested.total < ?
|
nested WHERE nested.total < ?
|
||||||
"""
|
"""
|
||||||
|
|
||||||
val statement = connection.prepareStatement(selectJoin)
|
connection.prepareStatement(selectJoin).use { statement ->
|
||||||
statement.setString(1, amount.token.toString())
|
statement.setString(1, amount.token.toString())
|
||||||
statement.setString(2, lockId.toString())
|
statement.setString(2, lockId.toString())
|
||||||
var paramOffset = 0
|
var paramOffset = 0
|
||||||
if (notary != null) {
|
if (notary != null) {
|
||||||
statement.setString(3, notary.name.toString())
|
statement.setString(3, notary.name.toString())
|
||||||
paramOffset += 1
|
paramOffset += 1
|
||||||
}
|
}
|
||||||
if (onlyFromIssuerParties.isNotEmpty()) {
|
if (onlyFromIssuerParties.isNotEmpty()) {
|
||||||
val issuerKeys = connection.createArrayOf("VARCHAR", onlyFromIssuerParties.map
|
val issuerKeys = connection.createArrayOf("VARCHAR", onlyFromIssuerParties.map
|
||||||
{ it.owningKey.toBase58String() }.toTypedArray())
|
{ it.owningKey.toBase58String() }.toTypedArray())
|
||||||
statement.setArray(3 + paramOffset, issuerKeys)
|
statement.setArray(3 + paramOffset, issuerKeys)
|
||||||
paramOffset += 1
|
paramOffset += 1
|
||||||
}
|
}
|
||||||
if (withIssuerRefs.isNotEmpty()) {
|
if (withIssuerRefs.isNotEmpty()) {
|
||||||
val issuerRefs = connection.createArrayOf("BYTEA", withIssuerRefs.map
|
val issuerRefs = connection.createArrayOf("BYTEA", withIssuerRefs.map
|
||||||
{ it.bytes }.toTypedArray())
|
{ it.bytes }.toTypedArray())
|
||||||
statement.setArray(3 + paramOffset, issuerRefs)
|
statement.setArray(3 + paramOffset, issuerRefs)
|
||||||
paramOffset += 1
|
paramOffset += 1
|
||||||
}
|
}
|
||||||
statement.setLong(3 + paramOffset, amount.quantity)
|
statement.setLong(3 + paramOffset, amount.quantity)
|
||||||
log.debug { statement.toString() }
|
log.debug { statement.toString() }
|
||||||
|
|
||||||
return statement.executeQuery()
|
statement.executeQuery().use { rs ->
|
||||||
|
return withResultSet(rs)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ class CashSelectionSQLServerImpl : AbstractCashSelection() {
|
|||||||
override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME"
|
override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME"
|
||||||
|
|
||||||
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?,
|
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 = """
|
val selectJoin = """
|
||||||
WITH row(transaction_id, output_index, pennies, total, lock_id) AS
|
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"""
|
FROM row where row.total <= ? + row.pennies"""
|
||||||
|
|
||||||
// Use prepared statement for protection against SQL Injection
|
// Use prepared statement for protection against SQL Injection
|
||||||
val psSelectJoin = connection.prepareStatement(selectJoin)
|
connection.prepareStatement(selectJoin).use { statement ->
|
||||||
var pIndex = 0
|
var pIndex = 0
|
||||||
psSelectJoin.setString(++pIndex, amount.token.currencyCode)
|
statement.setString(++pIndex, amount.token.currencyCode)
|
||||||
psSelectJoin.setString(++pIndex, lockId.toString())
|
statement.setString(++pIndex, lockId.toString())
|
||||||
if (notary != null)
|
if (notary != null)
|
||||||
psSelectJoin.setString(++pIndex, notary.name.toString())
|
statement.setString(++pIndex, notary.name.toString())
|
||||||
if (onlyFromIssuerParties.isNotEmpty())
|
if (onlyFromIssuerParties.isNotEmpty())
|
||||||
psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any}.toTypedArray() )
|
statement.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any }.toTypedArray())
|
||||||
if (withIssuerRefs.isNotEmpty())
|
if (withIssuerRefs.isNotEmpty())
|
||||||
psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray())
|
statement.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray())
|
||||||
psSelectJoin.setLong(++pIndex, amount.quantity)
|
statement.setLong(++pIndex, amount.quantity)
|
||||||
log.debug(selectJoin)
|
log.debug(selectJoin)
|
||||||
|
|
||||||
return psSelectJoin.executeQuery()
|
statement.executeQuery().use { rs ->
|
||||||
|
return withResultSet(rs)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -32,34 +32,34 @@ public class CashTestsJava {
|
|||||||
tx.input(Cash.PROGRAM_ID, inState);
|
tx.input(Cash.PROGRAM_ID, inState);
|
||||||
|
|
||||||
tx.tweak(tw -> {
|
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());
|
tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
|
||||||
return tw.failsWith("the amounts balance");
|
return tw.failsWith("the amounts balance");
|
||||||
});
|
});
|
||||||
|
|
||||||
tx.tweak(tw -> {
|
tx.tweak(tw -> {
|
||||||
tw.output(Cash.PROGRAM_ID, () -> outState);
|
tw.output(Cash.PROGRAM_ID, outState);
|
||||||
tw.command(getMEGA_CORP_PUBKEY(), DummyCommandData.INSTANCE);
|
tw.command(getMEGA_CORP_PUBKEY(), DummyCommandData.INSTANCE);
|
||||||
// Invalid command
|
// Invalid command
|
||||||
return tw.failsWith("required net.corda.finance.contracts.asset.Cash.Commands.Move command");
|
return tw.failsWith("required net.corda.finance.contracts.asset.Cash.Commands.Move command");
|
||||||
});
|
});
|
||||||
tx.tweak(tw -> {
|
tx.tweak(tw -> {
|
||||||
tw.output(Cash.PROGRAM_ID, () -> outState);
|
tw.output(Cash.PROGRAM_ID, outState);
|
||||||
tw.command(getMINI_CORP_PUBKEY(), new Cash.Commands.Move());
|
tw.command(getMINI_CORP_PUBKEY(), new Cash.Commands.Move());
|
||||||
return tw.failsWith("the owning keys are a subset of the signing keys");
|
return tw.failsWith("the owning keys are a subset of the signing keys");
|
||||||
});
|
});
|
||||||
tx.tweak(tw -> {
|
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
|
// issuedBy() can't be directly imported because it conflicts with other identically named functions
|
||||||
// with different overloads (for some reason).
|
// 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());
|
tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
|
||||||
return tw.failsWith("at least one cash input");
|
return tw.failsWith("at least one cash input");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simple reallocation works.
|
// Simple reallocation works.
|
||||||
return tx.tweak(tw -> {
|
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());
|
tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
|
||||||
return tw.verifies();
|
return tw.verifies();
|
||||||
});
|
});
|
||||||
|
@ -13,7 +13,7 @@ import net.corda.finance.DOLLARS
|
|||||||
import net.corda.finance.`issued by`
|
import net.corda.finance.`issued by`
|
||||||
import net.corda.finance.contracts.asset.*
|
import net.corda.finance.contracts.asset.*
|
||||||
import net.corda.testing.*
|
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
|
||||||
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
|
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
@ -106,8 +106,8 @@ class CommercialPaperTestsGeneric {
|
|||||||
// Some CP is issued onto the ledger by MegaCorp.
|
// Some CP is issued onto the ledger by MegaCorp.
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
attachments(CP_PROGRAM_ID, JavaCommercialPaper.JCP_PROGRAM_ID)
|
attachments(CP_PROGRAM_ID, JavaCommercialPaper.JCP_PROGRAM_ID)
|
||||||
output(thisTest.getContract(), "paper") { thisTest.getPaper() }
|
output(thisTest.getContract(), "paper", thisTest.getPaper())
|
||||||
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
|
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
|
||||||
timeWindow(TEST_TX_TIME)
|
timeWindow(TEST_TX_TIME)
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
@ -118,10 +118,10 @@ class CommercialPaperTestsGeneric {
|
|||||||
attachments(Cash.PROGRAM_ID, JavaCommercialPaper.JCP_PROGRAM_ID)
|
attachments(Cash.PROGRAM_ID, JavaCommercialPaper.JCP_PROGRAM_ID)
|
||||||
input("paper")
|
input("paper")
|
||||||
input("alice's $900")
|
input("alice's $900")
|
||||||
output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP }
|
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) }
|
output(thisTest.getContract(), "alice's paper", "paper".output<ICommercialPaperState>().withOwner(ALICE))
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
command(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() }
|
command(MEGA_CORP_PUBKEY, thisTest.getMoveCommand())
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,13 +133,11 @@ class CommercialPaperTestsGeneric {
|
|||||||
input("some profits")
|
input("some profits")
|
||||||
|
|
||||||
fun TransactionDSL<TransactionDSLInterpreter>.outputs(aliceGetsBack: Amount<Issued<Currency>>) {
|
fun TransactionDSL<TransactionDSLInterpreter>.outputs(aliceGetsBack: Amount<Issued<Currency>>) {
|
||||||
output(Cash.PROGRAM_ID, "Alice's profit") { aliceGetsBack.STATE ownedBy ALICE }
|
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, "Change", (someProfits - aliceGetsBack).STATE ownedBy MEGA_CORP)
|
||||||
}
|
}
|
||||||
|
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, thisTest.getRedeemCommand(DUMMY_NOTARY))
|
||||||
command(ALICE_PUBKEY) { thisTest.getRedeemCommand(DUMMY_NOTARY) }
|
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
outputs(700.DOLLARS `issued by` issuer)
|
outputs(700.DOLLARS `issued by` issuer)
|
||||||
timeWindow(TEST_TX_TIME + 8.days)
|
timeWindow(TEST_TX_TIME + 8.days)
|
||||||
@ -155,7 +153,7 @@ class CommercialPaperTestsGeneric {
|
|||||||
timeWindow(TEST_TX_TIME + 8.days)
|
timeWindow(TEST_TX_TIME + 8.days)
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
output(thisTest.getContract()) { "paper".output<ICommercialPaperState>() }
|
output(thisTest.getContract(), "paper".output<ICommercialPaperState>())
|
||||||
this `fails with` "must be destroyed"
|
this `fails with` "must be destroyed"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,8 +167,8 @@ class CommercialPaperTestsGeneric {
|
|||||||
transaction {
|
transaction {
|
||||||
attachment(CP_PROGRAM_ID)
|
attachment(CP_PROGRAM_ID)
|
||||||
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
|
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
|
||||||
output(thisTest.getContract()) { thisTest.getPaper() }
|
output(thisTest.getContract(), thisTest.getPaper())
|
||||||
command(MINI_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
|
command(MINI_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
|
||||||
timeWindow(TEST_TX_TIME)
|
timeWindow(TEST_TX_TIME)
|
||||||
this `fails with` "output states are issued by a command signer"
|
this `fails with` "output states are issued by a command signer"
|
||||||
}
|
}
|
||||||
@ -181,8 +179,8 @@ class CommercialPaperTestsGeneric {
|
|||||||
transaction {
|
transaction {
|
||||||
attachment(CP_PROGRAM_ID)
|
attachment(CP_PROGRAM_ID)
|
||||||
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
|
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
|
||||||
output(thisTest.getContract()) { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) }
|
output(thisTest.getContract(), thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer))
|
||||||
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
|
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
|
||||||
timeWindow(TEST_TX_TIME)
|
timeWindow(TEST_TX_TIME)
|
||||||
this `fails with` "output values sum to more than the inputs"
|
this `fails with` "output values sum to more than the inputs"
|
||||||
}
|
}
|
||||||
@ -193,8 +191,8 @@ class CommercialPaperTestsGeneric {
|
|||||||
transaction {
|
transaction {
|
||||||
attachment(CP_PROGRAM_ID)
|
attachment(CP_PROGRAM_ID)
|
||||||
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
|
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
|
||||||
output(thisTest.getContract()) { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) }
|
output(thisTest.getContract(), thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days))
|
||||||
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
|
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
|
||||||
timeWindow(TEST_TX_TIME)
|
timeWindow(TEST_TX_TIME)
|
||||||
this `fails with` "maturity date is not in the past"
|
this `fails with` "maturity date is not in the past"
|
||||||
}
|
}
|
||||||
@ -206,8 +204,8 @@ class CommercialPaperTestsGeneric {
|
|||||||
attachment(CP_PROGRAM_ID)
|
attachment(CP_PROGRAM_ID)
|
||||||
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
|
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
|
||||||
input(thisTest.getContract(), thisTest.getPaper())
|
input(thisTest.getContract(), thisTest.getPaper())
|
||||||
output(thisTest.getContract()) { thisTest.getPaper() }
|
output(thisTest.getContract(), thisTest.getPaper())
|
||||||
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
|
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
|
||||||
timeWindow(TEST_TX_TIME)
|
timeWindow(TEST_TX_TIME)
|
||||||
this `fails with` "output values sum to more than the inputs"
|
this `fails with` "output values sum to more than the inputs"
|
||||||
}
|
}
|
||||||
@ -240,7 +238,7 @@ class CommercialPaperTestsGeneric {
|
|||||||
aliceVaultService = aliceServices.vaultService
|
aliceVaultService = aliceServices.vaultService
|
||||||
|
|
||||||
databaseAlice.transaction {
|
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
|
aliceVaultService = aliceServices.vaultService
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,7 +248,7 @@ class CommercialPaperTestsGeneric {
|
|||||||
bigCorpVaultService = bigCorpServices.vaultService
|
bigCorpVaultService = bigCorpServices.vaultService
|
||||||
|
|
||||||
databaseBigCorp.transaction {
|
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
|
bigCorpVaultService = bigCorpServices.vaultService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,10 @@ package net.corda.finance.contracts.asset
|
|||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.generateKeyPair
|
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.ServiceHub
|
||||||
import net.corda.core.node.services.VaultService
|
import net.corda.core.node.services.VaultService
|
||||||
import net.corda.core.node.services.queryBy
|
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.sumCashOrNull
|
||||||
import net.corda.finance.utils.sumCashOrZero
|
import net.corda.finance.utils.sumCashOrZero
|
||||||
import net.corda.node.services.vault.NodeVaultService
|
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.*
|
||||||
import net.corda.testing.contracts.DummyState
|
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
|
||||||
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
|
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
|
||||||
import org.junit.After
|
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.
|
// Create some cash. Any attempt to spend >$500 will require multiple issuers to be involved.
|
||||||
database.transaction {
|
database.transaction {
|
||||||
ourServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
|
val vaultFiller = VaultFiller(ourServices, DUMMY_NOTARY, DUMMY_NOTARY_KEY, rngFactory = ::Random)
|
||||||
owner = ourIdentity, issuedBy = MEGA_CORP.ref(1), issuerServices = megaCorpServices)
|
vaultFiller.fillWithSomeTestCash(100.DOLLARS, megaCorpServices, 1, MEGA_CORP.ref(1), ourIdentity)
|
||||||
ourServices.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
|
vaultFiller.fillWithSomeTestCash(400.DOLLARS, megaCorpServices, 1, MEGA_CORP.ref(1), ourIdentity)
|
||||||
owner = ourIdentity, issuedBy = MEGA_CORP.ref(1), issuerServices = megaCorpServices)
|
vaultFiller.fillWithSomeTestCash(80.DOLLARS, miniCorpServices, 1, MINI_CORP.ref(1), ourIdentity)
|
||||||
ourServices.fillWithSomeTestCash(howMuch = 80.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
|
vaultFiller.fillWithSomeTestCash(80.SWISS_FRANCS, miniCorpServices, 1, MINI_CORP.ref(1), ourIdentity)
|
||||||
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 {
|
database.transaction {
|
||||||
vaultStatesUnconsumed = ourServices.vaultService.queryBy<Cash.State>().states
|
vaultStatesUnconsumed = ourServices.vaultService.queryBy<Cash.State>().states
|
||||||
@ -111,34 +111,33 @@ class CashTests {
|
|||||||
fun trivial() {
|
fun trivial() {
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { inState }
|
input(Cash.PROGRAM_ID, inState)
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
output(Cash.PROGRAM_ID) { outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer) }
|
output(Cash.PROGRAM_ID, outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer))
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
output(Cash.PROGRAM_ID) { outState }
|
output(Cash.PROGRAM_ID, outState)
|
||||||
command(ALICE_PUBKEY) { DummyCommandData }
|
command(ALICE_PUBKEY, DummyCommandData)
|
||||||
// Invalid command
|
// Invalid command
|
||||||
this `fails with` "required net.corda.finance.contracts.asset.Cash.Commands.Move command"
|
this `fails with` "required net.corda.finance.contracts.asset.Cash.Commands.Move command"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
output(Cash.PROGRAM_ID) { outState }
|
output(Cash.PROGRAM_ID, outState)
|
||||||
command(BOB_PUBKEY) { Cash.Commands.Move() }
|
command(BOB_PUBKEY, Cash.Commands.Move())
|
||||||
this `fails with` "the owning keys are a subset of the signing keys"
|
this `fails with` "the owning keys are a subset of the signing keys"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
output(Cash.PROGRAM_ID) { outState }
|
output(Cash.PROGRAM_ID, outState)
|
||||||
output(Cash.PROGRAM_ID) { outState issuedBy MINI_CORP }
|
output(Cash.PROGRAM_ID, outState issuedBy MINI_CORP)
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
this `fails with` "at least one cash input"
|
this `fails with` "at least one cash input"
|
||||||
}
|
}
|
||||||
// Simple reallocation works.
|
// Simple reallocation works.
|
||||||
tweak {
|
tweak {
|
||||||
output(Cash.PROGRAM_ID) { outState }
|
output(Cash.PROGRAM_ID, outState)
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,10 +148,9 @@ class CashTests {
|
|||||||
// Check we can't "move" money into existence.
|
// Check we can't "move" money into existence.
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { DummyState() }
|
input(Cash.PROGRAM_ID, DummyState())
|
||||||
output(Cash.PROGRAM_ID) { outState }
|
output(Cash.PROGRAM_ID, outState)
|
||||||
command(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
command(MINI_CORP_PUBKEY, Cash.Commands.Move())
|
||||||
|
|
||||||
this `fails with` "there is at least one cash input for this group"
|
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.
|
// institution is allowed to issue as much cash as they want.
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
output(Cash.PROGRAM_ID) { outState }
|
output(Cash.PROGRAM_ID, outState)
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Issue() }
|
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||||
this `fails with` "output states are issued by a command signer"
|
this `fails with` "output states are issued by a command signer"
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
output(Cash.PROGRAM_ID) {
|
output(Cash.PROGRAM_ID,
|
||||||
Cash.State(
|
Cash.State(
|
||||||
amount = 1000.DOLLARS `issued by` MINI_CORP.ref(12, 34),
|
amount = 1000.DOLLARS `issued by` MINI_CORP.ref(12, 34),
|
||||||
owner = AnonymousParty(ALICE_PUBKEY)
|
owner = AnonymousParty(ALICE_PUBKEY)))
|
||||||
)
|
command(MINI_CORP_PUBKEY, Cash.Commands.Issue())
|
||||||
}
|
|
||||||
command(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
|
||||||
this.verifies()
|
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.
|
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { issuerInState }
|
input(Cash.PROGRAM_ID, issuerInState)
|
||||||
output(Cash.PROGRAM_ID) { inState.copy(amount = inState.amount * 2) }
|
output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2))
|
||||||
|
|
||||||
// Move fails: not allowed to summon money.
|
// Move fails: not allowed to summon money.
|
||||||
tweak {
|
tweak {
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue works.
|
// Issue works.
|
||||||
tweak {
|
tweak {
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,29 +225,29 @@ class CashTests {
|
|||||||
// Can't use an issue command to lower the amount.
|
// Can't use an issue command to lower the amount.
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { inState }
|
input(Cash.PROGRAM_ID, inState)
|
||||||
output(Cash.PROGRAM_ID) { inState.copy(amount = inState.amount.splitEvenly(2).first()) }
|
output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount.splitEvenly(2).first()))
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
|
||||||
this `fails with` "output values sum to more than the inputs"
|
this `fails with` "output values sum to more than the inputs"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Can't have an issue command that doesn't actually issue money.
|
// Can't have an issue command that doesn't actually issue money.
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { inState }
|
input(Cash.PROGRAM_ID, inState)
|
||||||
output(Cash.PROGRAM_ID) { inState }
|
output(Cash.PROGRAM_ID, inState)
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
|
||||||
this `fails with` "output values sum to more than the inputs"
|
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)
|
// Can't have any other commands if we have an issue command (because the issue command overrules them)
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { inState }
|
input(Cash.PROGRAM_ID, inState)
|
||||||
output(Cash.PROGRAM_ID) { inState.copy(amount = inState.amount * 2) }
|
output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2))
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
|
||||||
tweak {
|
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 `fails with` "there is only a single issue command"
|
||||||
}
|
}
|
||||||
this.verifies()
|
this.verifies()
|
||||||
@ -282,26 +277,26 @@ class CashTests {
|
|||||||
// Splitting value works.
|
// Splitting value works.
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
tweak {
|
tweak {
|
||||||
input(Cash.PROGRAM_ID) { inState }
|
input(Cash.PROGRAM_ID, inState)
|
||||||
val splits4 = inState.amount.splitEvenly(4)
|
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()
|
this.verifies()
|
||||||
}
|
}
|
||||||
// Merging 4 inputs into 2 outputs works.
|
// Merging 4 inputs into 2 outputs works.
|
||||||
tweak {
|
tweak {
|
||||||
val splits2 = inState.amount.splitEvenly(2)
|
val splits2 = inState.amount.splitEvenly(2)
|
||||||
val splits4 = inState.amount.splitEvenly(4)
|
val splits4 = inState.amount.splitEvenly(4)
|
||||||
for (i in 0..3) input(Cash.PROGRAM_ID) { inState.copy(amount = splits4[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]) }
|
for (i in 0..1) output(Cash.PROGRAM_ID, inState.copy(amount = splits2[i]))
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
// Merging 2 inputs into 1 works.
|
// Merging 2 inputs into 1 works.
|
||||||
tweak {
|
tweak {
|
||||||
val splits2 = inState.amount.splitEvenly(2)
|
val splits2 = inState.amount.splitEvenly(2)
|
||||||
for (i in 0..1) input(Cash.PROGRAM_ID) { inState.copy(amount = splits2[i]) }
|
for (i in 0..1) input(Cash.PROGRAM_ID, inState.copy(amount = splits2[i]))
|
||||||
output(Cash.PROGRAM_ID) { inState }
|
output(Cash.PROGRAM_ID, inState)
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -311,17 +306,17 @@ class CashTests {
|
|||||||
fun zeroSizedValues() {
|
fun zeroSizedValues() {
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { inState }
|
input(Cash.PROGRAM_ID, inState)
|
||||||
input(Cash.PROGRAM_ID) { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
|
input(Cash.PROGRAM_ID, inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer))
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
this `fails with` "zero sized inputs"
|
this `fails with` "zero sized inputs"
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { inState }
|
input(Cash.PROGRAM_ID, inState)
|
||||||
output(Cash.PROGRAM_ID) { inState }
|
output(Cash.PROGRAM_ID, inState)
|
||||||
output(Cash.PROGRAM_ID) { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
|
output(Cash.PROGRAM_ID, inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer))
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
this `fails with` "zero sized outputs"
|
this `fails with` "zero sized outputs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -331,58 +326,56 @@ class CashTests {
|
|||||||
// Can't change issuer.
|
// Can't change issuer.
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { inState }
|
input(Cash.PROGRAM_ID, inState)
|
||||||
output(Cash.PROGRAM_ID) { outState issuedBy MINI_CORP }
|
output(Cash.PROGRAM_ID, outState issuedBy MINI_CORP)
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
// Can't change deposit reference when splitting.
|
// Can't change deposit reference when splitting.
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
val splits2 = inState.amount.splitEvenly(2)
|
val splits2 = inState.amount.splitEvenly(2)
|
||||||
input(Cash.PROGRAM_ID) { inState }
|
input(Cash.PROGRAM_ID, inState)
|
||||||
for (i in 0..1) output(Cash.PROGRAM_ID) { outState.copy(amount = splits2[i]).editDepositRef(i.toByte()) }
|
for (i in 0..1) output(Cash.PROGRAM_ID, outState.copy(amount = splits2[i]).editDepositRef(i.toByte()))
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
// Can't mix currencies.
|
// Can't mix currencies.
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { inState }
|
input(Cash.PROGRAM_ID, inState)
|
||||||
output(Cash.PROGRAM_ID) { outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer) }
|
output(Cash.PROGRAM_ID, outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer))
|
||||||
output(Cash.PROGRAM_ID) { outState.copy(amount = 200.POUNDS `issued by` defaultIssuer) }
|
output(Cash.PROGRAM_ID, outState.copy(amount = 200.POUNDS `issued by` defaultIssuer))
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { inState }
|
input(Cash.PROGRAM_ID, inState)
|
||||||
input(Cash.PROGRAM_ID) {
|
input(Cash.PROGRAM_ID,
|
||||||
inState.copy(
|
inState.copy(
|
||||||
amount = 150.POUNDS `issued by` defaultIssuer,
|
amount = 150.POUNDS `issued by` defaultIssuer,
|
||||||
owner = AnonymousParty(BOB_PUBKEY)
|
owner = AnonymousParty(BOB_PUBKEY)))
|
||||||
)
|
output(Cash.PROGRAM_ID, outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer))
|
||||||
}
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
output(Cash.PROGRAM_ID) { outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer) }
|
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
// Can't have superfluous input states from different issuers.
|
// Can't have superfluous input states from different issuers.
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { inState }
|
input(Cash.PROGRAM_ID, inState)
|
||||||
input(Cash.PROGRAM_ID) { inState issuedBy MINI_CORP }
|
input(Cash.PROGRAM_ID, inState issuedBy MINI_CORP)
|
||||||
output(Cash.PROGRAM_ID) { outState }
|
output(Cash.PROGRAM_ID, outState)
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
// Can't combine two different deposits at the same issuer.
|
// Can't combine two different deposits at the same issuer.
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { inState }
|
input(Cash.PROGRAM_ID, inState)
|
||||||
input(Cash.PROGRAM_ID) { inState.editDepositRef(3) }
|
input(Cash.PROGRAM_ID, inState.editDepositRef(3))
|
||||||
output(Cash.PROGRAM_ID) { outState.copy(amount = inState.amount * 2).editDepositRef(3) }
|
output(Cash.PROGRAM_ID, outState.copy(amount = inState.amount * 2).editDepositRef(3))
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
this `fails with` "for reference [01]"
|
this `fails with` "for reference [01]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -392,21 +385,20 @@ class CashTests {
|
|||||||
// Single input/output straightforward case.
|
// Single input/output straightforward case.
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { issuerInState }
|
input(Cash.PROGRAM_ID, issuerInState)
|
||||||
output(Cash.PROGRAM_ID) { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
|
output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)))
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer) }
|
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer))
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
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"
|
this `fails with` "required net.corda.finance.contracts.asset.Cash.Commands.Move command"
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -418,20 +410,15 @@ class CashTests {
|
|||||||
// Multi-issuer case.
|
// Multi-issuer case.
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { issuerInState }
|
input(Cash.PROGRAM_ID, issuerInState)
|
||||||
input(Cash.PROGRAM_ID) { issuerInState.copy(owner = MINI_CORP) issuedBy MINI_CORP }
|
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(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)))
|
||||||
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())
|
||||||
|
|
||||||
command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
|
||||||
|
|
||||||
this `fails with` "the amounts balance"
|
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"
|
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()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -441,10 +428,10 @@ class CashTests {
|
|||||||
// Single input/output straightforward case.
|
// Single input/output straightforward case.
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input(Cash.PROGRAM_ID) { inState }
|
input(Cash.PROGRAM_ID, inState)
|
||||||
output(Cash.PROGRAM_ID) { outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
|
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(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -454,25 +441,24 @@ class CashTests {
|
|||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
// Gather 2000 dollars from two different issuers.
|
// Gather 2000 dollars from two different issuers.
|
||||||
input(Cash.PROGRAM_ID) { inState }
|
input(Cash.PROGRAM_ID, inState)
|
||||||
input(Cash.PROGRAM_ID) { inState issuedBy MINI_CORP }
|
input(Cash.PROGRAM_ID, inState issuedBy MINI_CORP)
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||||
|
|
||||||
// Can't merge them together.
|
// Can't merge them together.
|
||||||
tweak {
|
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"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
// Missing MiniCorp deposit
|
// Missing MiniCorp deposit
|
||||||
tweak {
|
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 `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
|
|
||||||
// This works.
|
// This works.
|
||||||
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)) issuedBy MINI_CORP }
|
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)) issuedBy MINI_CORP)
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -483,12 +469,11 @@ class CashTests {
|
|||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
val pounds = Cash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), AnonymousParty(BOB_PUBKEY))
|
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, inState ownedBy AnonymousParty(ALICE_PUBKEY))
|
||||||
input(Cash.PROGRAM_ID) { pounds }
|
input(Cash.PROGRAM_ID, pounds)
|
||||||
output(Cash.PROGRAM_ID) { inState ownedBy AnonymousParty(BOB_PUBKEY) }
|
output(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(BOB_PUBKEY))
|
||||||
output(Cash.PROGRAM_ID) { pounds ownedBy AnonymousParty(ALICE_PUBKEY) }
|
output(Cash.PROGRAM_ID, pounds ownedBy AnonymousParty(ALICE_PUBKEY))
|
||||||
command(ALICE_PUBKEY, BOB_PUBKEY) { Cash.Commands.Move() }
|
command(listOf(ALICE_PUBKEY, BOB_PUBKEY), Cash.Commands.Move())
|
||||||
|
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -792,19 +777,17 @@ class CashTests {
|
|||||||
ledger(mockService) {
|
ledger(mockService) {
|
||||||
unverifiedTransaction {
|
unverifiedTransaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash") {
|
output(Cash.PROGRAM_ID, "MEGA_CORP cash",
|
||||||
Cash.State(
|
Cash.State(
|
||||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||||
owner = MEGA_CORP
|
owner = MEGA_CORP))
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
attachment(Cash.PROGRAM_ID)
|
attachment(Cash.PROGRAM_ID)
|
||||||
input("MEGA_CORP cash")
|
input("MEGA_CORP cash")
|
||||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash 2", "MEGA_CORP cash".output<Cash.State>().copy(owner = AnonymousParty(ALICE_PUBKEY)))
|
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()
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -814,7 +797,7 @@ class CashTests {
|
|||||||
input("MEGA_CORP cash")
|
input("MEGA_CORP cash")
|
||||||
// We send it to another pubkey so that the transaction is not identical to the previous one
|
// 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))
|
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.verifies()
|
||||||
}
|
}
|
||||||
this.fails()
|
this.fails()
|
||||||
|
@ -71,34 +71,33 @@ class ObligationTests {
|
|||||||
fun trivial() {
|
fun trivial() {
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input(Obligation.PROGRAM_ID) { inState }
|
input(Obligation.PROGRAM_ID, inState)
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
output(Obligation.PROGRAM_ID) { outState.copy(quantity = 2000.DOLLARS.quantity) }
|
output(Obligation.PROGRAM_ID, outState.copy(quantity = 2000.DOLLARS.quantity))
|
||||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
output(Obligation.PROGRAM_ID) { outState }
|
output(Obligation.PROGRAM_ID, outState)
|
||||||
command(CHARLIE.owningKey) { DummyCommandData }
|
command(CHARLIE.owningKey, DummyCommandData)
|
||||||
// Invalid command
|
// Invalid command
|
||||||
this `fails with` "required net.corda.finance.contracts.asset.Obligation.Commands.Move command"
|
this `fails with` "required net.corda.finance.contracts.asset.Obligation.Commands.Move command"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
output(Obligation.PROGRAM_ID) { outState }
|
output(Obligation.PROGRAM_ID, outState)
|
||||||
command(BOB_PUBKEY) { Obligation.Commands.Move() }
|
command(BOB_PUBKEY, Obligation.Commands.Move())
|
||||||
this `fails with` "the owning keys are a subset of the signing keys"
|
this `fails with` "the owning keys are a subset of the signing keys"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
output(Obligation.PROGRAM_ID) { outState }
|
output(Obligation.PROGRAM_ID, outState)
|
||||||
output(Obligation.PROGRAM_ID) { outState `issued by` MINI_CORP }
|
output(Obligation.PROGRAM_ID, outState `issued by` MINI_CORP)
|
||||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||||
this `fails with` "at least one obligation input"
|
this `fails with` "at least one obligation input"
|
||||||
}
|
}
|
||||||
// Simple reallocation works.
|
// Simple reallocation works.
|
||||||
tweak {
|
tweak {
|
||||||
output(Obligation.PROGRAM_ID) { outState }
|
output(Obligation.PROGRAM_ID, outState)
|
||||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,10 +108,9 @@ class ObligationTests {
|
|||||||
// Check we can't "move" debt into existence.
|
// Check we can't "move" debt into existence.
|
||||||
transaction {
|
transaction {
|
||||||
attachments(DummyContract.PROGRAM_ID, Obligation.PROGRAM_ID)
|
attachments(DummyContract.PROGRAM_ID, Obligation.PROGRAM_ID)
|
||||||
input(DummyContract.PROGRAM_ID) { DummyState() }
|
input(DummyContract.PROGRAM_ID, DummyState())
|
||||||
output(Obligation.PROGRAM_ID) { outState }
|
output(Obligation.PROGRAM_ID, outState)
|
||||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() }
|
command(MINI_CORP_PUBKEY, Obligation.Commands.Move())
|
||||||
|
|
||||||
this `fails with` "at least one obligation input"
|
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.
|
// institution is allowed to issue as much cash as they want.
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
output(Obligation.PROGRAM_ID) { outState }
|
output(Obligation.PROGRAM_ID, outState)
|
||||||
command(CHARLIE.owningKey) { Obligation.Commands.Issue() }
|
command(CHARLIE.owningKey, Obligation.Commands.Issue())
|
||||||
this `fails with` "output states are issued by a command signer"
|
this `fails with` "output states are issued by a command signer"
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
output(Obligation.PROGRAM_ID) {
|
output(Obligation.PROGRAM_ID,
|
||||||
Obligation.State(
|
Obligation.State(
|
||||||
obligor = MINI_CORP,
|
obligor = MINI_CORP,
|
||||||
quantity = 1000.DOLLARS.quantity,
|
quantity = 1000.DOLLARS.quantity,
|
||||||
beneficiary = CHARLIE,
|
beneficiary = CHARLIE,
|
||||||
template = megaCorpDollarSettlement
|
template = megaCorpDollarSettlement))
|
||||||
)
|
command(MINI_CORP_PUBKEY, Obligation.Commands.Issue())
|
||||||
}
|
|
||||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
run {
|
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.
|
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input(Obligation.PROGRAM_ID) { inState }
|
input(Obligation.PROGRAM_ID, inState)
|
||||||
output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.amount.quantity * 2) }
|
output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.amount.quantity * 2))
|
||||||
|
|
||||||
// Move fails: not allowed to summon money.
|
// Move fails: not allowed to summon money.
|
||||||
tweak {
|
tweak {
|
||||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue works.
|
// Issue works.
|
||||||
tweak {
|
tweak {
|
||||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY, Obligation.Commands.Issue())
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,29 +171,29 @@ class ObligationTests {
|
|||||||
// Can't use an issue command to lower the amount.
|
// Can't use an issue command to lower the amount.
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input(Obligation.PROGRAM_ID) { inState }
|
input(Obligation.PROGRAM_ID, inState)
|
||||||
output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.amount.quantity / 2) }
|
output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.amount.quantity / 2))
|
||||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY, Obligation.Commands.Issue())
|
||||||
this `fails with` "output values sum to more than the inputs"
|
this `fails with` "output values sum to more than the inputs"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Can't have an issue command that doesn't actually issue money.
|
// Can't have an issue command that doesn't actually issue money.
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input(Obligation.PROGRAM_ID) { inState }
|
input(Obligation.PROGRAM_ID, inState)
|
||||||
output(Obligation.PROGRAM_ID) { inState }
|
output(Obligation.PROGRAM_ID, inState)
|
||||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY, Obligation.Commands.Issue())
|
||||||
this `fails with` ""
|
this `fails with` ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Can't have any other commands if we have an issue command (because the issue command overrules them).
|
// Can't have any other commands if we have an issue command (because the issue command overrules them).
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input(Obligation.PROGRAM_ID) { inState }
|
input(Obligation.PROGRAM_ID, inState)
|
||||||
output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.amount.quantity * 2) }
|
output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.amount.quantity * 2))
|
||||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY, Obligation.Commands.Issue())
|
||||||
tweak {
|
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 `fails with` "there is only a single issue command"
|
||||||
}
|
}
|
||||||
this.verifies()
|
this.verifies()
|
||||||
@ -352,7 +347,7 @@ class ObligationTests {
|
|||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
input("Bob's $1,000,000 obligation to Alice")
|
||||||
// Note we can sign with either key here
|
// 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)
|
timeWindow(TEST_TX_TIME)
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
@ -368,8 +363,8 @@ class ObligationTests {
|
|||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
input("Bob's $1,000,000 obligation to Alice")
|
||||||
input("MegaCorp's $1,000,000 obligation to Bob")
|
input("MegaCorp's $1,000,000 obligation to Bob")
|
||||||
output(Obligation.PROGRAM_ID, "change") { oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, 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) }
|
command(listOf(BOB_PUBKEY, MEGA_CORP_PUBKEY), Obligation.Commands.Net(NetType.CLOSE_OUT))
|
||||||
timeWindow(TEST_TX_TIME)
|
timeWindow(TEST_TX_TIME)
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
@ -383,8 +378,8 @@ class ObligationTests {
|
|||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
input("Bob's $1,000,000 obligation to Alice")
|
||||||
output(Obligation.PROGRAM_ID, "change") { (oneMillionDollars.splitEvenly(2).first()).OBLIGATION between Pair(ALICE, BOB) }
|
output(Obligation.PROGRAM_ID, "change", oneMillionDollars.splitEvenly(2).first().OBLIGATION between Pair(ALICE, BOB))
|
||||||
command(BOB_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
|
command(BOB_PUBKEY, Obligation.Commands.Net(NetType.CLOSE_OUT))
|
||||||
timeWindow(TEST_TX_TIME)
|
timeWindow(TEST_TX_TIME)
|
||||||
this `fails with` "amounts owed on input and output must match"
|
this `fails with` "amounts owed on input and output must match"
|
||||||
}
|
}
|
||||||
@ -397,7 +392,7 @@ class ObligationTests {
|
|||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
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)
|
timeWindow(TEST_TX_TIME)
|
||||||
this `fails with` "any involved party has signed"
|
this `fails with` "any involved party has signed"
|
||||||
}
|
}
|
||||||
@ -413,7 +408,7 @@ class ObligationTests {
|
|||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
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)
|
timeWindow(TEST_TX_TIME)
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
@ -428,7 +423,7 @@ class ObligationTests {
|
|||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
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)
|
timeWindow(TEST_TX_TIME)
|
||||||
this `fails with` "all involved parties have signed"
|
this `fails with` "all involved parties have signed"
|
||||||
}
|
}
|
||||||
@ -441,8 +436,8 @@ class ObligationTests {
|
|||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
input("Bob's $1,000,000 obligation to Alice")
|
||||||
input("MegaCorp's $1,000,000 obligation to Bob")
|
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) }
|
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) }
|
command(listOf(ALICE_PUBKEY, BOB_PUBKEY, MEGA_CORP_PUBKEY), Obligation.Commands.Net(NetType.PAYMENT))
|
||||||
timeWindow(TEST_TX_TIME)
|
timeWindow(TEST_TX_TIME)
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
@ -456,8 +451,8 @@ class ObligationTests {
|
|||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
input("Bob's $1,000,000 obligation to Alice")
|
||||||
input("MegaCorp's $1,000,000 obligation to Bob")
|
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) }
|
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) }
|
command(listOf(ALICE_PUBKEY, BOB_PUBKEY), Obligation.Commands.Net(NetType.PAYMENT))
|
||||||
timeWindow(TEST_TX_TIME)
|
timeWindow(TEST_TX_TIME)
|
||||||
this `fails with` "all involved parties have signed"
|
this `fails with` "all involved parties have signed"
|
||||||
}
|
}
|
||||||
@ -473,9 +468,9 @@ class ObligationTests {
|
|||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Alice's $1,000,000")
|
input("Alice's $1,000,000")
|
||||||
output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB }
|
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, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)))
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) }
|
command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java))
|
||||||
attachment(attachment(cashContractBytes.inputStream()))
|
attachment(attachment(cashContractBytes.inputStream()))
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
@ -488,10 +483,10 @@ class ObligationTests {
|
|||||||
attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID)
|
||||||
input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB))
|
input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB))
|
||||||
input(Cash.PROGRAM_ID, 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE)
|
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, "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 }
|
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, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)))
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) }
|
command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java))
|
||||||
attachment(attachment(cashContractBytes.inputStream()))
|
attachment(attachment(cashContractBytes.inputStream()))
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
@ -504,9 +499,9 @@ class ObligationTests {
|
|||||||
attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID)
|
||||||
input(Obligation.PROGRAM_ID, defaultedObligation) // Alice's defaulted $1,000,000 obligation to Bob
|
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)
|
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 }
|
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, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)))
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) }
|
command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java))
|
||||||
this `fails with` "all inputs are in the normal state"
|
this `fails with` "all inputs are in the normal state"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -518,9 +513,9 @@ class ObligationTests {
|
|||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Alice's $1,000,000")
|
input("Alice's $1,000,000")
|
||||||
output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB }
|
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, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)))
|
||||||
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) }
|
command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java))
|
||||||
attachment(attachment(cashContractBytes.inputStream()))
|
attachment(attachment(cashContractBytes.inputStream()))
|
||||||
this `fails with` "amount in settle command"
|
this `fails with` "amount in settle command"
|
||||||
}
|
}
|
||||||
@ -546,9 +541,9 @@ class ObligationTests {
|
|||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input("Alice's 1 FCOJ obligation to Bob")
|
input("Alice's 1 FCOJ obligation to Bob")
|
||||||
input("Alice's 1 FCOJ")
|
input("Alice's 1 FCOJ")
|
||||||
output(Obligation.PROGRAM_ID, "Bob's 1 FCOJ") { CommodityContract.State(oneUnitFcoj, BOB) }
|
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, Obligation.Commands.Settle(Amount(oneUnitFcoj.quantity, oneUnitFcojObligation.amount.token)))
|
||||||
command(ALICE_PUBKEY) { CommodityContract.Commands.Move(Obligation::class.java) }
|
command(ALICE_PUBKEY, CommodityContract.Commands.Move(Obligation::class.java))
|
||||||
attachment(attachment(commodityContractBytes.inputStream()))
|
attachment(attachment(commodityContractBytes.inputStream()))
|
||||||
verifies()
|
verifies()
|
||||||
}
|
}
|
||||||
@ -563,8 +558,8 @@ class ObligationTests {
|
|||||||
transaction("Settlement") {
|
transaction("Settlement") {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
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) }
|
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) }
|
command(BOB_PUBKEY, Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED))
|
||||||
this `fails with` "there is a time-window from the authority"
|
this `fails with` "there is a time-window from the authority"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -575,8 +570,8 @@ class ObligationTests {
|
|||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` futureTestTime)
|
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) }
|
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) }
|
command(BOB_PUBKEY, Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED))
|
||||||
timeWindow(TEST_TX_TIME)
|
timeWindow(TEST_TX_TIME)
|
||||||
this `fails with` "the due date has passed"
|
this `fails with` "the due date has passed"
|
||||||
}
|
}
|
||||||
@ -586,8 +581,8 @@ class ObligationTests {
|
|||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` pastTestTime)
|
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) }
|
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) }
|
command(BOB_PUBKEY, Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED))
|
||||||
timeWindow(TEST_TX_TIME)
|
timeWindow(TEST_TX_TIME)
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
@ -600,24 +595,24 @@ class ObligationTests {
|
|||||||
// Splitting value works.
|
// Splitting value works.
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||||
tweak {
|
tweak {
|
||||||
input(Obligation.PROGRAM_ID) { inState }
|
input(Obligation.PROGRAM_ID, inState)
|
||||||
repeat(4) { output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.quantity / 4) } }
|
repeat(4) { output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.quantity / 4)) }
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
// Merging 4 inputs into 2 outputs works.
|
// Merging 4 inputs into 2 outputs works.
|
||||||
tweak {
|
tweak {
|
||||||
repeat(4) { input(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.quantity / 4) } }
|
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))
|
||||||
output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.quantity / 2) }
|
output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.quantity / 2))
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
// Merging 2 inputs into 1 works.
|
// Merging 2 inputs into 1 works.
|
||||||
tweak {
|
tweak {
|
||||||
input(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.quantity / 2) }
|
input(Obligation.PROGRAM_ID, inState.copy(quantity = inState.quantity / 2))
|
||||||
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 }
|
output(Obligation.PROGRAM_ID, inState)
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -627,18 +622,16 @@ class ObligationTests {
|
|||||||
fun zeroSizedValues() {
|
fun zeroSizedValues() {
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||||
tweak {
|
tweak {
|
||||||
input(Obligation.PROGRAM_ID) { inState }
|
input(Obligation.PROGRAM_ID, inState)
|
||||||
input(Obligation.PROGRAM_ID) { inState.copy(quantity = 0L) }
|
input(Obligation.PROGRAM_ID, inState.copy(quantity = 0L))
|
||||||
|
|
||||||
this `fails with` "zero sized inputs"
|
this `fails with` "zero sized inputs"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
input(Obligation.PROGRAM_ID) { inState }
|
input(Obligation.PROGRAM_ID, inState)
|
||||||
output(Obligation.PROGRAM_ID) { inState }
|
output(Obligation.PROGRAM_ID, inState)
|
||||||
output(Obligation.PROGRAM_ID) { inState.copy(quantity = 0L) }
|
output(Obligation.PROGRAM_ID, inState.copy(quantity = 0L))
|
||||||
|
|
||||||
this `fails with` "zero sized outputs"
|
this `fails with` "zero sized outputs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -649,41 +642,39 @@ class ObligationTests {
|
|||||||
// Can't change issuer.
|
// Can't change issuer.
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input(Obligation.PROGRAM_ID) { inState }
|
input(Obligation.PROGRAM_ID, inState)
|
||||||
output(Obligation.PROGRAM_ID) { outState `issued by` MINI_CORP }
|
output(Obligation.PROGRAM_ID, outState `issued by` MINI_CORP)
|
||||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() }
|
command(MINI_CORP_PUBKEY, Obligation.Commands.Move())
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
// Can't mix currencies.
|
// Can't mix currencies.
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input(Obligation.PROGRAM_ID) { inState }
|
input(Obligation.PROGRAM_ID, inState)
|
||||||
output(Obligation.PROGRAM_ID) { outState.copy(quantity = 80000, template = megaCorpDollarSettlement) }
|
output(Obligation.PROGRAM_ID, outState.copy(quantity = 80000, template = megaCorpDollarSettlement))
|
||||||
output(Obligation.PROGRAM_ID) { outState.copy(quantity = 20000, template = megaCorpPoundSettlement) }
|
output(Obligation.PROGRAM_ID, outState.copy(quantity = 20000, template = megaCorpPoundSettlement))
|
||||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() }
|
command(MINI_CORP_PUBKEY, Obligation.Commands.Move())
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input(Obligation.PROGRAM_ID) { inState }
|
input(Obligation.PROGRAM_ID, inState)
|
||||||
input(Obligation.PROGRAM_ID) {
|
input(Obligation.PROGRAM_ID,
|
||||||
inState.copy(
|
inState.copy(
|
||||||
quantity = 15000,
|
quantity = 15000,
|
||||||
template = megaCorpPoundSettlement,
|
template = megaCorpPoundSettlement,
|
||||||
beneficiary = AnonymousParty(BOB_PUBKEY)
|
beneficiary = AnonymousParty(BOB_PUBKEY)))
|
||||||
)
|
output(Obligation.PROGRAM_ID, outState.copy(quantity = 115000))
|
||||||
}
|
command(MINI_CORP_PUBKEY, Obligation.Commands.Move())
|
||||||
output(Obligation.PROGRAM_ID) { outState.copy(quantity = 115000) }
|
|
||||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() }
|
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
// Can't have superfluous input states from different issuers.
|
// Can't have superfluous input states from different issuers.
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input(Obligation.PROGRAM_ID) { inState }
|
input(Obligation.PROGRAM_ID, inState)
|
||||||
input(Obligation.PROGRAM_ID) { inState `issued by` MINI_CORP }
|
input(Obligation.PROGRAM_ID, inState `issued by` MINI_CORP)
|
||||||
output(Obligation.PROGRAM_ID) { outState }
|
output(Obligation.PROGRAM_ID, outState)
|
||||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -693,21 +684,20 @@ class ObligationTests {
|
|||||||
// Single input/output straightforward case.
|
// Single input/output straightforward case.
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
input(Obligation.PROGRAM_ID) { inState }
|
input(Obligation.PROGRAM_ID, inState)
|
||||||
output(Obligation.PROGRAM_ID) { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) }
|
output(Obligation.PROGRAM_ID, outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity))
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(100.DOLLARS.quantity, inState.amount.token)) }
|
command(CHARLIE.owningKey, Obligation.Commands.Exit(Amount(100.DOLLARS.quantity, inState.amount.token)))
|
||||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
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"
|
this `fails with` "required net.corda.finance.contracts.asset.Obligation.Commands.Move command"
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -720,21 +710,15 @@ class ObligationTests {
|
|||||||
// Multi-product case.
|
// Multi-product case.
|
||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
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 = megaIssuedPounds)) }
|
input(Obligation.PROGRAM_ID, inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedDollars)))
|
||||||
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))
|
||||||
output(Obligation.PROGRAM_ID) { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedPounds), quantity = inState.quantity - 200.POUNDS.quantity) }
|
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||||
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"
|
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"
|
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()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -745,27 +729,26 @@ class ObligationTests {
|
|||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
|
|
||||||
// Gather 2000 dollars from two different issuers.
|
// Gather 2000 dollars from two different issuers.
|
||||||
input(Obligation.PROGRAM_ID) { inState }
|
input(Obligation.PROGRAM_ID, inState)
|
||||||
input(Obligation.PROGRAM_ID) { inState `issued by` MINI_CORP }
|
input(Obligation.PROGRAM_ID, inState `issued by` MINI_CORP)
|
||||||
|
|
||||||
// Can't merge them together.
|
// Can't merge them together.
|
||||||
tweak {
|
tweak {
|
||||||
output(Obligation.PROGRAM_ID) { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY), quantity = 200000L) }
|
output(Obligation.PROGRAM_ID, inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY), quantity = 200000L))
|
||||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
// Missing MiniCorp deposit
|
// Missing MiniCorp deposit
|
||||||
tweak {
|
tweak {
|
||||||
output(Obligation.PROGRAM_ID) { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) }
|
output(Obligation.PROGRAM_ID, inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)))
|
||||||
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() }
|
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
|
|
||||||
// This works.
|
// This works.
|
||||||
output(Obligation.PROGRAM_ID) { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) }
|
output(Obligation.PROGRAM_ID, inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)))
|
||||||
output(Obligation.PROGRAM_ID) { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) `issued by` MINI_CORP }
|
output(Obligation.PROGRAM_ID, inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) `issued by` MINI_CORP)
|
||||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -776,12 +759,11 @@ class ObligationTests {
|
|||||||
transaction {
|
transaction {
|
||||||
attachments(Obligation.PROGRAM_ID)
|
attachments(Obligation.PROGRAM_ID)
|
||||||
val pounds = Obligation.State(Lifecycle.NORMAL, MINI_CORP, megaCorpPoundSettlement, 658.POUNDS.quantity, AnonymousParty(BOB_PUBKEY))
|
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, inState `owned by` CHARLIE)
|
||||||
input(Obligation.PROGRAM_ID) { pounds }
|
input(Obligation.PROGRAM_ID, pounds)
|
||||||
output(Obligation.PROGRAM_ID) { inState `owned by` AnonymousParty(BOB_PUBKEY) }
|
output(Obligation.PROGRAM_ID, inState `owned by` AnonymousParty(BOB_PUBKEY))
|
||||||
output(Obligation.PROGRAM_ID) { pounds `owned by` CHARLIE }
|
output(Obligation.PROGRAM_ID, pounds `owned by` CHARLIE)
|
||||||
command(CHARLIE.owningKey, BOB_PUBKEY) { Obligation.Commands.Move() }
|
command(listOf(CHARLIE.owningKey, BOB_PUBKEY), Obligation.Commands.Move())
|
||||||
|
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ buildscript {
|
|||||||
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
|
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
|
||||||
ext.bouncycastle_version = constants.getProperty("bouncycastleVersion")
|
ext.bouncycastle_version = constants.getProperty("bouncycastleVersion")
|
||||||
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
|
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
|
||||||
|
ext.jsr305_version = constants.getProperty("jsr305Version")
|
||||||
ext.kotlin_version = constants.getProperty("kotlinVersion")
|
ext.kotlin_version = constants.getProperty("kotlinVersion")
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -11,6 +11,9 @@ version gradle_plugins_version
|
|||||||
group 'net.corda.plugins'
|
group 'net.corda.plugins'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// JSR 305: Nullability annotations
|
||||||
|
compile "com.google.code.findbugs:jsr305:$jsr305_version"
|
||||||
|
|
||||||
// TypeSafe Config: for simple and human friendly config files.
|
// TypeSafe Config: for simple and human friendly config files.
|
||||||
compile "com.typesafe:config:$typesafe_config_version"
|
compile "com.typesafe:config:$typesafe_config_version"
|
||||||
|
|
||||||
|
@ -1,24 +1,40 @@
|
|||||||
package net.corda.cordform;
|
package net.corda.cordform;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public abstract class CordformDefinition {
|
public abstract class CordformDefinition {
|
||||||
public final Path driverDirectory;
|
private Path nodesDirectory = Paths.get("build", "nodes");
|
||||||
public final ArrayList<Consumer<? super CordformNode>> nodeConfigurers = new ArrayList<>();
|
private final List<Consumer<CordformNode>> nodeConfigurers = new ArrayList<>();
|
||||||
|
private final List<String> cordappPackages = new ArrayList<>();
|
||||||
|
|
||||||
public CordformDefinition(Path driverDirectory) {
|
public Path getNodesDirectory() {
|
||||||
this.driverDirectory = driverDirectory;
|
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);
|
nodeConfigurers.add(configurer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getCordappPackages() {
|
||||||
|
return cordappPackages;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make arbitrary changes to the node directories before they are started.
|
* Make arbitrary changes to the node directories before they are started.
|
||||||
* @param context Lookup of node directory by node name.
|
* @param context Lookup of node directory by node name.
|
||||||
*/
|
*/
|
||||||
public abstract void setup(CordformContext context);
|
public abstract void setup(@Nonnull CordformContext context);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,9 @@ package net.corda.cordform;
|
|||||||
|
|
||||||
import static java.util.Collections.emptyList;
|
import static java.util.Collections.emptyList;
|
||||||
import com.typesafe.config.*;
|
import com.typesafe.config.*;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -54,7 +57,7 @@ public class CordformNode implements NodeDefinition {
|
|||||||
*/
|
*/
|
||||||
public void name(String name) {
|
public void name(String name) {
|
||||||
this.name = 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.
|
* @return This node's P2P address.
|
||||||
*/
|
*/
|
||||||
|
@Nonnull
|
||||||
public String getP2pAddress() {
|
public String getP2pAddress() {
|
||||||
return config.getString("p2pAddress");
|
return config.getString("p2pAddress");
|
||||||
}
|
}
|
||||||
@ -71,8 +75,8 @@ public class CordformNode implements NodeDefinition {
|
|||||||
*
|
*
|
||||||
* @param p2pPort The Artemis messaging queue port.
|
* @param p2pPort The Artemis messaging queue port.
|
||||||
*/
|
*/
|
||||||
public void p2pPort(Integer p2pPort) {
|
public void p2pPort(int p2pPort) {
|
||||||
config = config.withValue("p2pAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + p2pPort));
|
p2pAddress(DEFAULT_HOST + ':' + p2pPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,7 +85,15 @@ public class CordformNode implements NodeDefinition {
|
|||||||
* @param p2pAddress The Artemis messaging queue host and port.
|
* @param p2pAddress The Artemis messaging queue host and port.
|
||||||
*/
|
*/
|
||||||
public void p2pAddress(String p2pAddress) {
|
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.
|
* @param rpcPort The Artemis RPC queue port.
|
||||||
*/
|
*/
|
||||||
public void rpcPort(Integer rpcPort) {
|
public void rpcPort(int rpcPort) {
|
||||||
config = config.withValue("rpcAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + rpcPort));
|
rpcAddress(DEFAULT_HOST + ':' + rpcPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -99,7 +111,31 @@ public class CordformNode implements NodeDefinition {
|
|||||||
* @param rpcAddress The Artemis RPC queue host and port.
|
* @param rpcAddress The Artemis RPC queue host and port.
|
||||||
*/
|
*/
|
||||||
public void rpcAddress(String rpcAddress) {
|
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.
|
* @param configFile The file path.
|
||||||
*/
|
*/
|
||||||
public void configFile(String configFile) {
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,10 @@ import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
|
|||||||
import org.gradle.api.tasks.TaskAction
|
import org.gradle.api.tasks.TaskAction
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.jar.JarInputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates nodes based on the configuration of this task in the gradle configuration DSL.
|
* 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")
|
@Suppress("unused")
|
||||||
open class Cordform : DefaultTask() {
|
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.
|
* Optionally the name of a CordformDefinition subclass to which all configuration will be delegated.
|
||||||
*/
|
*/
|
||||||
@Suppress("MemberVisibilityCanPrivate")
|
@Suppress("MemberVisibilityCanPrivate")
|
||||||
var definitionClass: String? = null
|
var definitionClass: String? = null
|
||||||
private var directory = Paths.get("build", "nodes")
|
private var directory = defaultDirectory
|
||||||
private val nodes = mutableListOf<Node>()
|
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.
|
* This task action will create and install the nodes based on the node configurations added.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
|
||||||
@TaskAction
|
@TaskAction
|
||||||
fun build() {
|
fun build() {
|
||||||
project.logger.info("Running Cordform task")
|
project.logger.info("Running Cordform task")
|
||||||
@ -129,10 +132,18 @@ open class Cordform : DefaultTask() {
|
|||||||
private fun initializeConfiguration() {
|
private fun initializeConfiguration() {
|
||||||
if (definitionClass != null) {
|
if (definitionClass != null) {
|
||||||
val cd = loadCordformDefinition()
|
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 {
|
cd.nodeConfigurers.forEach {
|
||||||
val node = node { }
|
val node = node { }
|
||||||
it.accept(node)
|
it.accept(node)
|
||||||
node.rootDir(directory)
|
node.rootDir(directory)
|
||||||
|
node.installCordapps(cordapps)
|
||||||
}
|
}
|
||||||
cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) }
|
cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) }
|
||||||
} else {
|
} 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() {
|
private fun generateAndInstallNodeInfos() {
|
||||||
generateNodeInfos()
|
generateNodeInfos()
|
||||||
installNodeInfos()
|
installNodeInfos()
|
||||||
@ -149,7 +184,7 @@ open class Cordform : DefaultTask() {
|
|||||||
|
|
||||||
private fun generateNodeInfos() {
|
private fun generateNodeInfos() {
|
||||||
project.logger.info("Generating node infos")
|
project.logger.info("Generating node infos")
|
||||||
var nodeProcesses = buildNodeProcesses()
|
val nodeProcesses = buildNodeProcesses()
|
||||||
try {
|
try {
|
||||||
validateNodeProcessess(nodeProcesses)
|
validateNodeProcessess(nodeProcesses)
|
||||||
} finally {
|
} finally {
|
||||||
@ -177,7 +212,7 @@ open class Cordform : DefaultTask() {
|
|||||||
|
|
||||||
private fun buildNodeProcess(node: Node): Pair<Node, Process> {
|
private fun buildNodeProcess(node: Node): Pair<Node, Process> {
|
||||||
node.makeLogDirectory()
|
node.makeLogDirectory()
|
||||||
var process = ProcessBuilder(generateNodeInfoCommand())
|
val process = ProcessBuilder(generateNodeInfoCommand())
|
||||||
.directory(node.fullPath().toFile())
|
.directory(node.fullPath().toFile())
|
||||||
.redirectErrorStream(true)
|
.redirectErrorStream(true)
|
||||||
// InheritIO causes hangs on windows due the gradle buffer also not being flushed.
|
// 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 Node.logFile(): Path = this.logDirectory().resolve("generate-info.log")
|
||||||
|
|
||||||
private fun ProcessBuilder.addEnvironment(key: String, value: String) = this.apply { environment().put(key, value) }
|
private fun ProcessBuilder.addEnvironment(key: String, value: String) = this.apply { environment().put(key, value) }
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package net.corda.plugins
|
|||||||
import com.typesafe.config.*
|
import com.typesafe.config.*
|
||||||
import net.corda.cordform.CordformNode
|
import net.corda.cordform.CordformNode
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
import org.bouncycastle.asn1.x500.RDN
|
|
||||||
import org.bouncycastle.asn1.x500.style.BCStyle
|
import org.bouncycastle.asn1.x500.style.BCStyle
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
import java.io.File
|
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")
|
private val releaseVersion = project.rootProject.ext<String>("corda_release_version")
|
||||||
internal lateinit var nodeDir: File
|
internal lateinit var nodeDir: File
|
||||||
|
private set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets whether this node will use HTTPS communication.
|
* 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))
|
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.
|
* 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))
|
config = config.withValue("sshd.port", ConfigValueFactory.fromAnyRef(sshdPort))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal fun build() {
|
internal fun build() {
|
||||||
configureProperties()
|
configureProperties()
|
||||||
installCordaJar()
|
installCordaJar()
|
||||||
@ -118,19 +97,15 @@ class Node(private val project: Project) : CordformNode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal fun rootDir(rootDir: Path) {
|
internal fun rootDir(rootDir: Path) {
|
||||||
if(name == null) {
|
if (name == null) {
|
||||||
project.logger.error("Node has a null name - cannot create node")
|
project.logger.error("Node has a null name - cannot create node")
|
||||||
throw IllegalStateException("Node has a null name - cannot create node")
|
throw IllegalStateException("Node has a null name - cannot create node")
|
||||||
}
|
}
|
||||||
|
|
||||||
val dirName = try {
|
val dirName = try {
|
||||||
val o = X500Name(name).getRDNs(BCStyle.O)
|
val o = X500Name(name).getRDNs(BCStyle.O)
|
||||||
if (o.size > 0) {
|
if (o.isNotEmpty()) o.first().first.value.toString() else name
|
||||||
o.first().first.value.toString()
|
} catch (_ : IllegalArgumentException) {
|
||||||
} else {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
} catch(_ : IllegalArgumentException) {
|
|
||||||
// Can't parse as an X500 name, use the full string
|
// Can't parse as an X500 name, use the full string
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
@ -192,9 +167,8 @@ class Node(private val project: Project) : CordformNode() {
|
|||||||
/**
|
/**
|
||||||
* Installs other cordapps to this node's cordapps directory.
|
* 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 cordappsDir = File(nodeDir, "cordapps")
|
||||||
val cordapps = getCordappList()
|
|
||||||
project.copy {
|
project.copy {
|
||||||
it.apply {
|
it.apply {
|
||||||
from(cordapps)
|
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\"")
|
throw RuntimeException("No Corda Webserver JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-webserver-$releaseVersion.jar\"")
|
||||||
} else {
|
} else {
|
||||||
val jar = maybeJar.singleFile
|
val jar = maybeJar.singleFile
|
||||||
assert(jar.isFile)
|
require(jar.isFile)
|
||||||
return jar
|
return jar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,7 @@
|
|||||||
#Mon Nov 13 08:47:49 GMT 2017
|
#Sat Nov 25 22:21:50 GMT 2017
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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
|
distributionUrl=https\://services.gradle.org/distributions/gradle-4.3.1-all.zip
|
||||||
|
@ -3,7 +3,7 @@ apply plugin: 'net.corda.plugins.quasar-utils'
|
|||||||
apply plugin: 'net.corda.plugins.publish-utils'
|
apply plugin: 'net.corda.plugins.publish-utils'
|
||||||
apply plugin: 'com.jfrog.artifactory'
|
apply plugin: 'com.jfrog.artifactory'
|
||||||
|
|
||||||
description 'Corda node Artemis API'
|
description 'Corda node API'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(":core")
|
compile project(":core")
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.node.utilities
|
package net.corda.nodeapi.internal.crypto
|
||||||
|
|
||||||
import net.corda.core.crypto.SignatureScheme
|
import net.corda.core.crypto.SignatureScheme
|
||||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
|
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
|
@ -1,9 +1,8 @@
|
|||||||
@file:JvmName("KeyStoreUtilities")
|
@file:JvmName("KeyStoreUtilities")
|
||||||
|
|
||||||
package net.corda.node.utilities
|
package net.corda.nodeapi.internal.crypto
|
||||||
|
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.identity.CordaX500Name
|
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.*
|
||||||
import org.bouncycastle.cert.X509CertificateHolder
|
import org.bouncycastle.cert.X509CertificateHolder
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -11,9 +10,7 @@ import java.io.InputStream
|
|||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.security.*
|
import java.security.*
|
||||||
import java.security.cert.CertPath
|
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
import java.security.cert.CertificateFactory
|
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
|
|
||||||
const val KEYSTORE_TYPE = "JKS"
|
const val KEYSTORE_TYPE = "JKS"
|
||||||
@ -169,44 +166,3 @@ fun KeyStore.getSupportedKey(alias: String, keyPassword: String): PrivateKey {
|
|||||||
val key = getKey(alias, keyPass) as PrivateKey
|
val key = getKey(alias, keyPass) as PrivateKey
|
||||||
return Crypto.toSupportedPrivateKey(key)
|
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)
|
|
||||||
}
|
|
@ -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)
|
||||||
|
}
|
@ -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.Crypto
|
||||||
import net.corda.core.crypto.SignatureScheme
|
import net.corda.core.crypto.SignatureScheme
|
||||||
import net.corda.core.crypto.random63BitValue
|
import net.corda.core.crypto.random63BitValue
|
||||||
import net.corda.core.identity.CordaX500Name
|
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.internal.x500Name
|
||||||
import net.corda.core.utilities.days
|
import net.corda.core.utilities.days
|
||||||
import net.corda.core.utilities.millis
|
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.PKCS10CertificationRequest
|
||||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder
|
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder
|
||||||
import org.bouncycastle.util.io.pem.PemReader
|
import org.bouncycastle.util.io.pem.PemReader
|
||||||
import java.io.FileReader
|
|
||||||
import java.io.FileWriter
|
import java.io.FileWriter
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -52,6 +54,7 @@ object X509Utilities {
|
|||||||
const val CORDA_CLIENT_CA_CN = "Corda Client CA Certificate"
|
const val CORDA_CLIENT_CA_CN = "Corda Client CA Certificate"
|
||||||
|
|
||||||
private val DEFAULT_VALIDITY_WINDOW = Pair(0.millis, 3650.days)
|
private val DEFAULT_VALIDITY_WINDOW = Pair(0.millis, 3650.days)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to return the latest out of an instant and an optional date.
|
* 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.
|
* Create a de novo root self-signed X509 v3 CA cert.
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@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)
|
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second)
|
||||||
return createCertificate(CertificateType.ROOT_CA, subject.x500Name, keyPair, subject.x500Name, keyPair.public, window)
|
return createCertificate(CertificateType.ROOT_CA, subject.x500Name, keyPair, subject.x500Name, keyPair.public, window)
|
||||||
}
|
}
|
||||||
@ -114,8 +119,9 @@ object X509Utilities {
|
|||||||
subject: CordaX500Name,
|
subject: CordaX500Name,
|
||||||
subjectPublicKey: PublicKey,
|
subjectPublicKey: PublicKey,
|
||||||
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW,
|
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW,
|
||||||
nameConstraints: NameConstraints? = null): X509CertificateHolder
|
nameConstraints: NameConstraints? = null): X509CertificateHolder {
|
||||||
= createCertificate(certificateType, issuerCertificate, issuerKeyPair, subject.x500Name, subjectPublicKey, validityWindow, nameConstraints)
|
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
|
* 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)
|
@Throws(CertPathValidatorException::class)
|
||||||
fun validateCertificateChain(trustedRoot: X509Certificate, vararg certificates: Certificate) {
|
fun validateCertificateChain(trustedRoot: X509Certificate, vararg certificates: Certificate) {
|
||||||
require(certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" }
|
require(certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" }
|
||||||
val certFactory = CertificateFactory.getInstance("X509")
|
|
||||||
val params = PKIXParameters(setOf(TrustAnchor(trustedRoot, null)))
|
val params = PKIXParameters(setOf(TrustAnchor(trustedRoot, null)))
|
||||||
params.isRevocationEnabled = false
|
params.isRevocationEnabled = false
|
||||||
val certPath = certFactory.generateCertPath(certificates.toList())
|
val certPath = X509CertificateFactory().delegate.generateCertPath(certificates.toList())
|
||||||
val pathValidator = CertPathValidator.getInstance("PKIX")
|
val pathValidator = CertPathValidator.getInstance("PKIX")
|
||||||
pathValidator.validate(certPath, params)
|
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.
|
* 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 x509Certificate certificate to save.
|
||||||
* @param filename Target filename.
|
* @param file Target file.
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun saveCertificateAsPEMFile(x509Certificate: X509CertificateHolder, filename: Path) {
|
fun saveCertificateAsPEMFile(x509Certificate: X509CertificateHolder, file: Path) {
|
||||||
FileWriter(filename.toFile()).use {
|
JcaPEMWriter(file.toFile().writer()).use {
|
||||||
JcaPEMWriter(it).use {
|
it.writeObject(x509Certificate)
|
||||||
it.writeObject(x509Certificate)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to load back a .pem/.cer format file copy of a certificate.
|
* 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.
|
* @return The X509Certificate that was encoded in the file.
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun loadCertificateFromPEMFile(filename: Path): X509CertificateHolder {
|
fun loadCertificateFromPEMFile(file: Path): X509CertificateHolder {
|
||||||
val reader = PemReader(FileReader(filename.toFile()))
|
val cert = file.read {
|
||||||
val pemObject = reader.readPemObject()
|
val reader = PemReader(it.reader())
|
||||||
val cert = X509CertificateHolder(pemObject.content)
|
val pemObject = reader.readPemObject()
|
||||||
return cert.apply {
|
X509CertificateHolder(pemObject.content)
|
||||||
isValidOn(Date())
|
|
||||||
}
|
}
|
||||||
|
cert.isValidOn(Date())
|
||||||
|
return cert
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -243,13 +247,13 @@ object X509Utilities {
|
|||||||
* @param validityWindow the time period the certificate is valid for.
|
* @param validityWindow the time period the certificate is valid for.
|
||||||
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
|
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
|
||||||
*/
|
*/
|
||||||
internal fun createCertificate(certificateType: CertificateType,
|
fun createCertificate(certificateType: CertificateType,
|
||||||
issuer: X500Name,
|
issuer: X500Name,
|
||||||
issuerSigner: ContentSigner,
|
issuerSigner: ContentSigner,
|
||||||
subject: CordaX500Name,
|
subject: CordaX500Name,
|
||||||
subjectPublicKey: PublicKey,
|
subjectPublicKey: PublicKey,
|
||||||
validityWindow: Pair<Date, Date>,
|
validityWindow: Pair<Date, Date>,
|
||||||
nameConstraints: NameConstraints? = null): X509CertificateHolder {
|
nameConstraints: NameConstraints? = null): X509CertificateHolder {
|
||||||
val builder = createCertificate(certificateType, issuer, subject.x500Name, subjectPublicKey, validityWindow, nameConstraints)
|
val builder = createCertificate(certificateType, issuer, subject.x500Name, subjectPublicKey, validityWindow, nameConstraints)
|
||||||
return builder.build(issuerSigner).apply {
|
return builder.build(issuerSigner).apply {
|
||||||
require(isValidOn(Date()))
|
require(isValidOn(Date()))
|
||||||
@ -266,11 +270,13 @@ object X509Utilities {
|
|||||||
* @param validityWindow the time period the certificate is valid for.
|
* @param validityWindow the time period the certificate is valid for.
|
||||||
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
|
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
|
||||||
*/
|
*/
|
||||||
internal fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerKeyPair: KeyPair,
|
fun createCertificate(certificateType: CertificateType,
|
||||||
subject: X500Name, subjectPublicKey: PublicKey,
|
issuer: X500Name,
|
||||||
validityWindow: Pair<Date, Date>,
|
issuerKeyPair: KeyPair,
|
||||||
nameConstraints: NameConstraints? = null): X509CertificateHolder {
|
subject: X500Name,
|
||||||
|
subjectPublicKey: PublicKey,
|
||||||
|
validityWindow: Pair<Date, Date>,
|
||||||
|
nameConstraints: NameConstraints? = null): X509CertificateHolder {
|
||||||
val signatureScheme = Crypto.findSignatureScheme(issuerKeyPair.private)
|
val signatureScheme = Crypto.findSignatureScheme(issuerKeyPair.private)
|
||||||
val provider = Crypto.findProvider(signatureScheme.providerName)
|
val provider = Crypto.findProvider(signatureScheme.providerName)
|
||||||
val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints)
|
val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints)
|
||||||
@ -285,28 +291,71 @@ object X509Utilities {
|
|||||||
/**
|
/**
|
||||||
* Create certificate signing request using provided information.
|
* 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))
|
val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, Crypto.findProvider(signatureScheme.providerName))
|
||||||
return JcaPKCS10CertificationRequestBuilder(subject.x500Name, keyPair.public).addAttribute(BCStyle.E, DERUTF8String(email)).build(signer)
|
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) {
|
* Wraps a [CertificateFactory] to remove boilerplate. It's unclear whether [CertificateFactory] is threadsafe so best
|
||||||
private val certificateFactory = CertificateFactory.getInstance("X.509")
|
* so assume this class is not.
|
||||||
|
*/
|
||||||
fun nextCertificate(): X509Certificate = certificateFactory.generateCertificate(input) as X509Certificate
|
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) {
|
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),
|
ROOT_CA(
|
||||||
INTERMEDIATE_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true),
|
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign),
|
||||||
CLIENT_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true),
|
KeyPurposeId.id_kp_serverAuth,
|
||||||
TLS(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.keyAgreement), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = false),
|
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
|
// 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)
|
data class CertificateAndKeyPair(val certificate: X509CertificateHolder, val keyPair: KeyPair)
|
@ -1,63 +1,80 @@
|
|||||||
package net.corda.node.utilities
|
package net.corda.nodeapi.internal.persistence
|
||||||
|
|
||||||
import com.zaxxer.hikari.HikariConfig
|
import net.corda.core.schemas.MappedSchema
|
||||||
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 rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscriber
|
import rx.Subscriber
|
||||||
import rx.subjects.UnicastSubject
|
import rx.subjects.UnicastSubject
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
import java.sql.SQLException
|
import java.sql.SQLException
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
import javax.persistence.AttributeConverter
|
||||||
|
import javax.sql.DataSource
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table prefix for all tables owned by the node module.
|
* Table prefix for all tables owned by the node module.
|
||||||
*/
|
*/
|
||||||
const val NODE_DATABASE_PREFIX = "node_"
|
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(
|
class CordaPersistence(
|
||||||
val dataSource: HikariDataSource,
|
val dataSource: DataSource,
|
||||||
private val schemaService: SchemaService,
|
databaseConfig: DatabaseConfig,
|
||||||
private val identityService: IdentityService,
|
schemas: Set<MappedSchema>,
|
||||||
databaseConfig: DatabaseConfig
|
attributeConverters: Collection<AttributeConverter<*, *>> = emptySet()
|
||||||
) : Closeable {
|
) : Closeable {
|
||||||
val transactionIsolationLevel = databaseConfig.transactionIsolationLevel.jdbcValue
|
val defaultIsolationLevel = databaseConfig.transactionIsolationLevel
|
||||||
val hibernateConfig: HibernateConfiguration by lazy {
|
val hibernateConfig: HibernateConfiguration by lazy {
|
||||||
transaction {
|
transaction {
|
||||||
HibernateConfiguration(schemaService, databaseConfig, identityService)
|
HibernateConfiguration(schemas, databaseConfig, attributeConverters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val entityManagerFactory get() = hibernateConfig.sessionFactoryForRegisteredSchemas
|
val entityManagerFactory get() = hibernateConfig.sessionFactoryForRegisteredSchemas
|
||||||
|
|
||||||
companion object {
|
init {
|
||||||
fun connect(dataSource: HikariDataSource, schemaService: SchemaService, identityService: IdentityService, databaseConfig: DatabaseConfig): CordaPersistence {
|
DatabaseTransactionManager(this)
|
||||||
return CordaPersistence(dataSource, schemaService, identityService, databaseConfig).apply {
|
// Check not in read-only mode.
|
||||||
DatabaseTransactionManager(this)
|
transaction {
|
||||||
|
dataSource.connection.use {
|
||||||
|
check(!it.metaData.isReadOnly) { "Database should not be readonly." }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of [DatabaseTransaction], with the given isolation level.
|
* Creates an instance of [DatabaseTransaction], with the given transaction isolation level.
|
||||||
* @param isolationLevel isolation level for the transaction. If not specified the default (i.e. provided at the creation time) is used.
|
|
||||||
*/
|
*/
|
||||||
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.
|
// We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases.
|
||||||
DatabaseTransactionManager.dataSource = this
|
DatabaseTransactionManager.dataSource = this
|
||||||
return DatabaseTransactionManager.currentOrNew(isolationLevel)
|
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 {
|
fun createSession(): Connection {
|
||||||
// We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases.
|
// 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 isolationLevel isolation level for the transaction.
|
||||||
* @param statement to be executed in the scope of this 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
|
DatabaseTransactionManager.dataSource = this
|
||||||
return transaction(isolationLevel, 3, statement)
|
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.
|
* 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.
|
* @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()
|
val outer = DatabaseTransactionManager.currentOrNull()
|
||||||
|
|
||||||
return if (outer != null) {
|
return if (outer != null) {
|
||||||
outer.statement()
|
outer.statement()
|
||||||
} else {
|
} 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
|
var repetitions = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
val transaction = DatabaseTransactionManager.currentOrNew(transactionIsolation)
|
val transaction = DatabaseTransactionManager.currentOrNew(isolationLevel)
|
||||||
try {
|
try {
|
||||||
val answer = transaction.statement()
|
val answer = transaction.statement()
|
||||||
transaction.commit()
|
transaction.commit()
|
||||||
@ -116,23 +132,11 @@ class CordaPersistence(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
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
|
* Buffer observations until after the current database transaction has been closed. Observations are never
|
||||||
* dropped, simply delayed.
|
* dropped, simply delayed.
|
||||||
@ -144,7 +148,7 @@ fun configureDatabase(dataSourceProperties: Properties, databaseConfig: Database
|
|||||||
*/
|
*/
|
||||||
fun <T : Any> rx.Observer<T>.bufferUntilDatabaseCommit(): rx.Observer<T> {
|
fun <T : Any> rx.Observer<T>.bufferUntilDatabaseCommit(): rx.Observer<T> {
|
||||||
val currentTxId = DatabaseTransactionManager.transactionId
|
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>()
|
val subject = UnicastSubject.create<T>()
|
||||||
subject.delaySubscription(databaseTxBoundary).subscribe(this)
|
subject.delaySubscription(databaseTxBoundary).subscribe(this)
|
||||||
databaseTxBoundary.doOnCompleted { subject.onCompleted() }
|
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.
|
// A subscriber that wraps another but does not pass on observations to it.
|
||||||
private class NoOpSubscriber<U>(t: Subscriber<in U>) : Subscriber<U>(t) {
|
private class NoOpSubscriber<U>(t: Subscriber<in U>) : Subscriber<U>(t) {
|
||||||
override fun onCompleted() {
|
override fun onCompleted() {}
|
||||||
}
|
override fun onError(e: Throwable?) {}
|
||||||
|
override fun onNext(s: U) {}
|
||||||
override fun onError(e: Throwable?) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNext(s: U) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,68 +1,14 @@
|
|||||||
package net.corda.node.utilities
|
package net.corda.nodeapi.internal.persistence
|
||||||
|
|
||||||
import co.paralleluniverse.strands.Strand
|
import co.paralleluniverse.strands.Strand
|
||||||
import org.hibernate.Session
|
import org.hibernate.Session
|
||||||
import org.hibernate.Transaction
|
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import rx.subjects.Subject
|
import rx.subjects.Subject
|
||||||
import java.sql.Connection
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class DatabaseTransaction(isolation: Int, val threadLocal: ThreadLocal<DatabaseTransaction>,
|
fun currentDBSession(): Session = DatabaseTransactionManager.current().session
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun currentDBSession() = DatabaseTransactionManager.current().session
|
|
||||||
class DatabaseTransactionManager(initDataSource: CordaPersistence) {
|
class DatabaseTransactionManager(initDataSource: CordaPersistence) {
|
||||||
companion object {
|
companion object {
|
||||||
private val threadLocalDb = ThreadLocal<CordaPersistence>()
|
private val threadLocalDb = ThreadLocal<CordaPersistence>()
|
||||||
@ -95,11 +41,15 @@ class DatabaseTransactionManager(initDataSource: CordaPersistence) {
|
|||||||
|
|
||||||
fun currentOrNull(): DatabaseTransaction? = manager.currentOrNull()
|
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 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)
|
data class Boundary(val txId: UUID)
|
@ -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.internal.castIfPossible
|
||||||
import net.corda.core.node.services.IdentityService
|
|
||||||
import net.corda.core.schemas.MappedSchema
|
import net.corda.core.schemas.MappedSchema
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.toHexString
|
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.SessionFactory
|
||||||
import org.hibernate.boot.MetadataSources
|
import org.hibernate.boot.MetadataSources
|
||||||
import org.hibernate.boot.model.naming.Identifier
|
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.engine.jdbc.env.spi.JdbcEnvironment
|
||||||
import org.hibernate.service.UnknownUnwrapTypeException
|
import org.hibernate.service.UnknownUnwrapTypeException
|
||||||
import org.hibernate.type.AbstractSingleColumnStandardBasicType
|
import org.hibernate.type.AbstractSingleColumnStandardBasicType
|
||||||
import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry
|
|
||||||
import org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor
|
import org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor
|
||||||
import org.hibernate.type.descriptor.sql.BlobTypeDescriptor
|
import org.hibernate.type.descriptor.sql.BlobTypeDescriptor
|
||||||
import org.hibernate.type.descriptor.sql.VarbinaryTypeDescriptor
|
import org.hibernate.type.descriptor.sql.VarbinaryTypeDescriptor
|
||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
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 {
|
companion object {
|
||||||
private val logger = contextLogger()
|
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.
|
// TODO: make this a guava cache or similar to limit ability for this to grow forever.
|
||||||
private val sessionFactories = ConcurrentHashMap<Set<MappedSchema>, SessionFactory>()
|
private val sessionFactories = ConcurrentHashMap<Set<MappedSchema>, SessionFactory>()
|
||||||
|
|
||||||
val sessionFactoryForRegisteredSchemas = schemaService.schemaOptions.keys.let {
|
val sessionFactoryForRegisteredSchemas = schemas.let {
|
||||||
logger.info("Init HibernateConfiguration for schemas: $it")
|
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)
|
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.
|
// 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.
|
// 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)
|
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.format_sql", "true")
|
||||||
.setProperty("hibernate.connection.isolation", databaseConfig.transactionIsolationLevel.jdbcValue.toString())
|
.setProperty("hibernate.connection.isolation", databaseConfig.transactionIsolationLevel.jdbcValue.toString())
|
||||||
|
|
||||||
@ -83,7 +78,7 @@ class HibernateConfiguration(val schemaService: SchemaService, private val datab
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
// register custom converters
|
// register custom converters
|
||||||
applyAttributeConverter(AbstractPartyToX500NameAsStringConverter(identityService))
|
attributeConverters.forEach { applyAttributeConverter(it) }
|
||||||
// Register a tweaked version of `org.hibernate.type.MaterializedBlobType` that truncates logged messages.
|
// Register a tweaked version of `org.hibernate.type.MaterializedBlobType` that truncates logged messages.
|
||||||
// to avoid OOM when large blobs might get logged.
|
// to avoid OOM when large blobs might get logged.
|
||||||
applyBasicType(CordaMaterializedBlobType, CordaMaterializedBlobType.name)
|
applyBasicType(CordaMaterializedBlobType, CordaMaterializedBlobType.name)
|
@ -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
|
||||||
}
|
}
|
@ -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.
|
* 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
|
||||||
}
|
}
|
@ -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<*>) {
|
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")
|
} else throw NotSerializableException("Expected a List but found $obj")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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?
|
// 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]) })
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -67,8 +67,8 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T> {
|
|||||||
superClassSerializer.writeDescribedObject(obj, data, type, output)
|
superClassSerializer.writeDescribedObject(obj, data, type, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T {
|
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): T {
|
||||||
return superClassSerializer.readObject(obj, schema, input)
|
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 {
|
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): T {
|
||||||
val proxy: P = uncheckedCast(proxySerializer.readObject(obj, schema, input))
|
val proxy: P = uncheckedCast(proxySerializer.readObject(obj, schemas, input))
|
||||||
return fromProxy(proxy)
|
return fromProxy(proxy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -166,7 +166,7 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T> {
|
|||||||
data.putString(unmaker(obj))
|
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
|
val proxy = obj as String
|
||||||
return maker(proxy)
|
return maker(proxy)
|
||||||
}
|
}
|
||||||
|
@ -97,21 +97,21 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
|
|||||||
@Throws(NotSerializableException::class)
|
@Throws(NotSerializableException::class)
|
||||||
fun <T : Any> deserialize(bytes: ByteSequence, clazz: Class<T>): T = des {
|
fun <T : Any> deserialize(bytes: ByteSequence, clazz: Class<T>): T = des {
|
||||||
val envelope = getEnvelope(bytes)
|
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)
|
@Throws(NotSerializableException::class)
|
||||||
fun <T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>, clazz: Class<T>): ObjectAndEnvelope<T> = des {
|
fun <T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>, clazz: Class<T>): ObjectAndEnvelope<T> = des {
|
||||||
val envelope = getEnvelope(bytes)
|
val envelope = getEnvelope(bytes)
|
||||||
// Now pick out the obj and schema from the envelope.
|
// 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)
|
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) {
|
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.
|
// 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()
|
val objectIndex = (obj.described as UnsignedInteger).toInt()
|
||||||
@ -127,11 +127,11 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
|
|||||||
val objectRead = when (obj) {
|
val objectRead = when (obj) {
|
||||||
is DescribedType -> {
|
is DescribedType -> {
|
||||||
// Look up serializer in factory by descriptor
|
// 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) })
|
if (SerializerFactory.AnyType != type && serializer.type != type && with(serializer.type) { !isSubClassOf(type) && !materiallyEquivalentTo(type) })
|
||||||
throw NotSerializableException("Described type with descriptor ${obj.descriptor} was " +
|
throw NotSerializableException("Described type with descriptor ${obj.descriptor} was " +
|
||||||
"expected to be of type $type but was ${serializer.type}")
|
"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
|
is Binary -> obj.array
|
||||||
else -> obj // this will be the case for primitive types like [boolean] et al.
|
else -> obj // this will be the case for primitive types like [boolean] et al.
|
||||||
|
@ -27,7 +27,7 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Seria
|
|||||||
output.writeTypeNotations(typeNotation)
|
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 enumName = (obj as List<*>)[0] as String
|
||||||
val enumOrd = obj[1] as Int
|
val enumOrd = obj[1] as Int
|
||||||
val fromOrd = type.asClass()!!.enumConstants[enumOrd] as Enum<*>?
|
val fromOrd = type.asClass()!!.enumConstants[enumOrd] as Enum<*>?
|
||||||
|
@ -32,8 +32,8 @@ class EvolutionSerializer(
|
|||||||
* @param property object to read the actual property value
|
* @param property object to read the actual property value
|
||||||
*/
|
*/
|
||||||
data class OldParam(val type: Type, val idx: Int, val property: PropertySerializer) {
|
data class OldParam(val type: Type, val idx: Int, val property: PropertySerializer) {
|
||||||
fun readProperty(paramValues: List<*>, schema: Schema, input: DeserializationInput) =
|
fun readProperty(paramValues: List<*>, schemas: SerializationSchemas, input: DeserializationInput) =
|
||||||
property.readProperty(paramValues[idx], schema, input)
|
property.readProperty(paramValues[idx], schemas, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -121,10 +121,10 @@ class EvolutionSerializer(
|
|||||||
*
|
*
|
||||||
* TODO: Object references
|
* 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")
|
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) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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?
|
// 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())
|
concreteBuilder(entries.toMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readEntry(schema: Schema, input: DeserializationInput, entry: Map.Entry<Any?, Any?>) =
|
private fun readEntry(schemas: SerializationSchemas, input: DeserializationInput, entry: Map.Entry<Any?, Any?>) =
|
||||||
input.readObjectOrNull(entry.key, schema, declaredType.actualTypeArguments[0]) to
|
input.readObjectOrNull(entry.key, schemas, declaredType.actualTypeArguments[0]) to
|
||||||
input.readObjectOrNull(entry.value, schema, declaredType.actualTypeArguments[1])
|
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.
|
// 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.
|
// We don't actually care about the type, we just need to make the compiler happier.
|
||||||
|
@ -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 is List<*>) {
|
||||||
if (obj.size > propertySerializers.size) throw NotSerializableException("Too many properties in described type $typeName")
|
if (obj.size > propertySerializers.size) {
|
||||||
val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schema, input) }
|
throw NotSerializableException("Too many properties in described type $typeName")
|
||||||
|
}
|
||||||
|
val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schemas, input) }
|
||||||
construct(params)
|
construct(params)
|
||||||
} else throw NotSerializableException("Body of described type is unexpected $obj")
|
} else throw NotSerializableException("Body of described type is unexpected $obj")
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import kotlin.reflect.jvm.javaGetter
|
|||||||
sealed class PropertySerializer(val name: String, val readMethod: Method?, val resolvedType: Type) {
|
sealed class PropertySerializer(val name: String, val readMethod: Method?, val resolvedType: Type) {
|
||||||
abstract fun writeClassInfo(output: SerializationOutput)
|
abstract fun writeClassInfo(output: SerializationOutput)
|
||||||
abstract fun writeProperty(obj: Any?, data: Data, 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 type: String = generateType()
|
||||||
val requires: List<String> = generateRequires()
|
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 }) {
|
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any? = ifThrowsAppend({ nameForDebug }) {
|
||||||
input.readObjectOrNull(obj, schema, resolvedType)
|
input.readObjectOrNull(obj, schemas, resolvedType)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) = ifThrowsAppend({ nameForDebug }) {
|
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) {
|
class AMQPPrimitivePropertySerializer(name: String, readMethod: Method?, resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) {
|
||||||
override fun writeClassInfo(output: SerializationOutput) {}
|
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
|
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) {
|
PropertySerializer(name, readMethod, Character::class.java) {
|
||||||
override fun writeClassInfo(output: SerializationOutput) {}
|
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()
|
return if (obj == null) null else (obj as Short).toChar()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,9 @@ import com.google.common.primitives.Primitives
|
|||||||
import com.google.common.reflect.TypeResolver
|
import com.google.common.reflect.TypeResolver
|
||||||
import net.corda.core.internal.uncheckedCast
|
import net.corda.core.internal.uncheckedCast
|
||||||
import net.corda.core.serialization.ClassWhitelist
|
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 org.apache.qpid.proton.amqp.*
|
||||||
import java.io.NotSerializableException
|
import java.io.NotSerializableException
|
||||||
import java.lang.reflect.*
|
import java.lang.reflect.*
|
||||||
@ -13,7 +15,8 @@ import java.util.concurrent.ConcurrentHashMap
|
|||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import javax.annotation.concurrent.ThreadSafe
|
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.
|
* 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
|
val classloader: ClassLoader
|
||||||
get() = classCarpenter.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!!) {
|
return serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
|
||||||
when (typeNotation) {
|
when (typeNotation) {
|
||||||
is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, this)
|
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].
|
* contained in the [Schema].
|
||||||
*/
|
*/
|
||||||
@Throws(NotSerializableException::class)
|
@Throws(NotSerializableException::class)
|
||||||
fun get(typeDescriptor: Any, schema: Schema): AMQPSerializer<Any> {
|
fun get(typeDescriptor: Any, schema: SerializationSchemas): AMQPSerializer<Any> {
|
||||||
return serializersByDescriptor[typeDescriptor] ?: {
|
return serializersByDescriptor[typeDescriptor] ?: {
|
||||||
processSchema(FactorySchemaAndDescriptor(schema, typeDescriptor))
|
processSchema(FactorySchemaAndDescriptor(schema, typeDescriptor))
|
||||||
serializersByDescriptor[typeDescriptor] ?: throw NotSerializableException(
|
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
|
* 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
|
* 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()
|
val metaSchema = CarpenterMetaSchema.newInstance()
|
||||||
for (typeNotation in schema.schema.types) {
|
for (typeNotation in schemaAndDescriptor.schemas.schema.types) {
|
||||||
try {
|
try {
|
||||||
val serialiser = processSchemaEntry(typeNotation)
|
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
|
// 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
|
// instance of the class, as such we need to build an EvolutionSerialiser
|
||||||
if (serialiser.typeDescriptor != typeNotation.descriptor.name) {
|
if (serialiser.typeDescriptor != typeNotation.descriptor.name) {
|
||||||
getEvolutionSerializer(typeNotation, serialiser)
|
getEvolutionSerializer(typeNotation, serialiser, schemaAndDescriptor.schemas.transforms)
|
||||||
}
|
}
|
||||||
} catch (e: ClassNotFoundException) {
|
} catch (e: ClassNotFoundException) {
|
||||||
if (sentinel) throw e
|
if (sentinel) throw e
|
||||||
@ -215,7 +221,7 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
|||||||
if (metaSchema.isNotEmpty()) {
|
if (metaSchema.isNotEmpty()) {
|
||||||
val mc = MetaCarpenter(metaSchema, classCarpenter)
|
val mc = MetaCarpenter(metaSchema, classCarpenter)
|
||||||
mc.build()
|
mc.build()
|
||||||
processSchema(schema, true)
|
processSchema(schemaAndDescriptor, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
return singleton
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -85,6 +85,9 @@ class UnknownTransform : Transform() {
|
|||||||
override val name: String get() = typeName
|
override val name: String get() = typeName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by the unit testing framework
|
||||||
|
*/
|
||||||
class UnknownTestTransform(val a: Int, val b: Int, val c: Int) : Transform() {
|
class UnknownTestTransform(val a: Int, val b: Int, val c: Int) : Transform() {
|
||||||
companion object : DescribedTypeConstructor<UnknownTestTransform> {
|
companion object : DescribedTypeConstructor<UnknownTestTransform> {
|
||||||
val typeName = "UnknownTest"
|
val typeName = "UnknownTest"
|
||||||
|
@ -34,8 +34,8 @@ object InputStreamSerializer : CustomSerializer.Implements<InputStream>(InputStr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): InputStream {
|
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): InputStream {
|
||||||
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
|
val bits = input.readObject(obj, schemas, ByteArray::class.java) as ByteArray
|
||||||
return ByteArrayInputStream(bits)
|
return ByteArrayInputStream(bits)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -20,8 +20,8 @@ object PrivateKeySerializer : CustomSerializer.Implements<PrivateKey>(PrivateKey
|
|||||||
output.writeObject(obj.encoded, data, clazz)
|
output.writeObject(obj.encoded, data, clazz)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): PrivateKey {
|
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): PrivateKey {
|
||||||
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
|
val bits = input.readObject(obj, schemas, ByteArray::class.java) as ByteArray
|
||||||
return Crypto.decodePrivateKey(bits)
|
return Crypto.decodePrivateKey(bits)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -17,8 +17,8 @@ object PublicKeySerializer : CustomSerializer.Implements<PublicKey>(PublicKey::c
|
|||||||
output.writeObject(obj.encoded, data, clazz)
|
output.writeObject(obj.encoded, data, clazz)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): PublicKey {
|
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): PublicKey {
|
||||||
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
|
val bits = input.readObject(obj, schemas, ByteArray::class.java) as ByteArray
|
||||||
return Crypto.decodePublicKey(bits)
|
return Crypto.decodePublicKey(bits)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,9 +1,9 @@
|
|||||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||||
|
|
||||||
|
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||||
import net.corda.nodeapi.internal.serialization.amqp.*
|
import net.corda.nodeapi.internal.serialization.amqp.*
|
||||||
import org.apache.qpid.proton.codec.Data
|
import org.apache.qpid.proton.codec.Data
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
import java.security.cert.CertificateFactory
|
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
|
|
||||||
object X509CertificateSerializer : CustomSerializer.Implements<X509Certificate>(X509Certificate::class.java) {
|
object X509CertificateSerializer : CustomSerializer.Implements<X509Certificate>(X509Certificate::class.java) {
|
||||||
@ -20,8 +20,8 @@ object X509CertificateSerializer : CustomSerializer.Implements<X509Certificate>(
|
|||||||
output.writeObject(obj.encoded, data, clazz)
|
output.writeObject(obj.encoded, data, clazz)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): X509Certificate {
|
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): X509Certificate {
|
||||||
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
|
val bits = input.readObject(obj, schemas, ByteArray::class.java) as ByteArray
|
||||||
return CertificateFactory.getInstance("X.509").generateCertificate(bits.inputStream()) as X509Certificate
|
return X509CertificateFactory().generateCertificate(bits.inputStream())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import net.corda.core.serialization.SerializedBytes
|
|||||||
import net.corda.core.toFuture
|
import net.corda.core.toFuture
|
||||||
import net.corda.core.toObservable
|
import net.corda.core.toObservable
|
||||||
import net.corda.core.transactions.*
|
import net.corda.core.transactions.*
|
||||||
|
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||||
import net.corda.core.utilities.SgxSupport
|
import net.corda.core.utilities.SgxSupport
|
||||||
import net.corda.nodeapi.internal.serialization.CordaClassResolver
|
import net.corda.nodeapi.internal.serialization.CordaClassResolver
|
||||||
import net.corda.nodeapi.internal.serialization.serializationContextKey
|
import net.corda.nodeapi.internal.serialization.serializationContextKey
|
||||||
@ -490,8 +491,7 @@ object CertPathSerializer : Serializer<CertPath>() {
|
|||||||
@ThreadSafe
|
@ThreadSafe
|
||||||
object X509CertificateSerializer : Serializer<X509Certificate>() {
|
object X509CertificateSerializer : Serializer<X509Certificate>() {
|
||||||
override fun read(kryo: Kryo, input: Input, type: Class<X509Certificate>): X509Certificate {
|
override fun read(kryo: Kryo, input: Input, type: Class<X509Certificate>): X509Certificate {
|
||||||
val factory = CertificateFactory.getInstance("X.509")
|
return X509CertificateFactory().generateCertificate(input.readBytesWithLength().inputStream())
|
||||||
return factory.generateCertificate(input.readBytesWithLength().inputStream()) as X509Certificate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun write(kryo: Kryo, output: Output, obj: X509Certificate) {
|
override fun write(kryo: Kryo, output: Output, obj: X509Certificate) {
|
||||||
|
@ -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
|
||||||
import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512
|
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.serialization.KryoServerSerializationScheme
|
||||||
import net.corda.node.services.config.createKeystoreForCordaNode
|
import net.corda.node.services.config.createKeystoreForCordaNode
|
||||||
import net.corda.nodeapi.internal.serialization.AllWhitelist
|
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.SerializationContextImpl
|
||||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
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.ALICE
|
||||||
import net.corda.testing.BOB
|
import net.corda.testing.BOB
|
||||||
import net.corda.testing.BOB_PUBKEY
|
import net.corda.testing.BOB_PUBKEY
|
||||||
@ -41,7 +41,6 @@ import java.security.PrivateKey
|
|||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.security.cert.CertPath
|
import java.security.cert.CertPath
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
import java.security.cert.CertificateFactory
|
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.stream.Stream
|
import java.util.stream.Stream
|
||||||
@ -49,7 +48,6 @@ import javax.net.ssl.*
|
|||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.test.*
|
import kotlin.test.*
|
||||||
|
|
||||||
|
|
||||||
class X509UtilitiesTest {
|
class X509UtilitiesTest {
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
@ -360,10 +358,16 @@ class X509UtilitiesTest {
|
|||||||
trustStorePassword: String
|
trustStorePassword: String
|
||||||
): KeyStore {
|
): KeyStore {
|
||||||
val rootCAKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
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 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 keyPass = keyPassword.toCharArray()
|
||||||
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
|
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
|
||||||
@ -426,11 +430,10 @@ class X509UtilitiesTest {
|
|||||||
emptyMap(),
|
emptyMap(),
|
||||||
true,
|
true,
|
||||||
SerializationContext.UseCase.P2P)
|
SerializationContext.UseCase.P2P)
|
||||||
val certFactory = CertificateFactory.getInstance("X509")
|
|
||||||
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||||
val rootCACert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootCAKey)
|
val rootCACert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootCAKey)
|
||||||
val certificate = X509Utilities.createCertificate(CertificateType.TLS, rootCACert, rootCAKey, BOB.name.x500Name, BOB_PUBKEY)
|
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 serialized = expected.serialize(factory, context).bytes
|
||||||
val actual: CertPath = serialized.deserialize(factory, context)
|
val actual: CertPath = serialized.deserialize(factory, context)
|
||||||
assertEquals(expected, actual)
|
assertEquals(expected, actual)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user