mirror of
https://github.com/corda/corda.git
synced 2024-12-28 00:38: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.
|
||||
ext.corda_release_version = "0.16-SNAPSHOT"
|
||||
// Increment this on any release that changes public APIs anywhere in the Corda platform
|
||||
// TODO This is going to be difficult until we have a clear separation throughout the code of what is public and what is internal
|
||||
ext.corda_platform_version = 1
|
||||
ext.corda_platform_version = constants.getProperty("platformVersion")
|
||||
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
|
||||
|
||||
// Dependency versions. Can run 'gradle dependencyUpdates' to find new versions of things.
|
||||
@ -49,6 +48,7 @@ buildscript {
|
||||
ext.commons_collections_version = '4.1'
|
||||
ext.beanutils_version = '1.9.3'
|
||||
ext.crash_version = 'faba68332800f21278c5b600bf14ad55cef5989e'
|
||||
ext.jsr305_version = constants.getProperty("jsr305Version")
|
||||
ext.spring_jdbc_version ='5.0.0.RELEASE'
|
||||
|
||||
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
|
||||
|
@ -12,12 +12,15 @@ import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import net.corda.testing.IntegrationTest
|
||||
import net.corda.testing.driver.poll
|
||||
import net.corda.testing.internal.*
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
@ -27,6 +30,14 @@ import java.util.concurrent.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class RPCStabilityTests : IntegrationTest() {
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule(true)
|
||||
private val pool = Executors.newFixedThreadPool(10, testThreadFactory())
|
||||
@After
|
||||
fun shutdown() {
|
||||
pool.shutdown()
|
||||
}
|
||||
|
||||
object DummyOps : RPCOps {
|
||||
override val protocolVersion = 0
|
||||
@ -198,9 +209,9 @@ class RPCStabilityTests : IntegrationTest() {
|
||||
val proxy = startRpcClient<LeakObservableOps>(server.get().broker.hostAndPort!!).get()
|
||||
// Leak many observables
|
||||
val N = 200
|
||||
(1..N).toList().parallelStream().forEach {
|
||||
proxy.leakObservable()
|
||||
}
|
||||
(1..N).map {
|
||||
pool.fork { proxy.leakObservable(); Unit }
|
||||
}.transpose().getOrThrow()
|
||||
// In a loop force GC and check whether the server is notified
|
||||
while (true) {
|
||||
System.gc()
|
||||
@ -232,7 +243,7 @@ class RPCStabilityTests : IntegrationTest() {
|
||||
assertEquals("pong", client.ping())
|
||||
serverFollower.shutdown()
|
||||
startRpcServer<ReconnectOps>(ops = ops, customPort = serverPort).getOrThrow()
|
||||
val pingFuture = ForkJoinPool.commonPool().fork(client::ping)
|
||||
val pingFuture = pool.fork(client::ping)
|
||||
assertEquals("pong", pingFuture.getOrThrow(10.seconds))
|
||||
clientFollower.shutdown() // Driver would do this after the new server, causing hang.
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
}
|
||||
|
||||
private void copyFinanceCordapp() {
|
||||
Path cordappsDir = (factory.baseDirectory(notaryConfig).resolve("cordapps"));
|
||||
Path cordappsDir = (factory.baseDirectory(notaryConfig).resolve(NodeProcess.CORDAPPS_DIR_NAME));
|
||||
try {
|
||||
Files.createDirectories(cordappsDir);
|
||||
} catch (IOException ex) {
|
||||
|
@ -86,7 +86,7 @@ class StandaloneCordaRPClientTest {
|
||||
}
|
||||
|
||||
private fun copyFinanceCordapp() {
|
||||
val cordappsDir = (factory.baseDirectory(notaryConfig) / "cordapps").createDirectories()
|
||||
val cordappsDir = (factory.baseDirectory(notaryConfig) / NodeProcess.CORDAPPS_DIR_NAME).createDirectories()
|
||||
// Find the finance jar file for the smoke tests of this module
|
||||
val financeJar = Paths.get("build", "resources", "smokeTest").list {
|
||||
it.filter { "corda-finance" in it.toString() }.toList().single()
|
||||
|
@ -6,14 +6,20 @@ import net.corda.core.internal.concurrent.map
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import net.corda.testing.internal.RPCDriverExposedDSLInterface
|
||||
import net.corda.testing.internal.rpcTestUser
|
||||
import net.corda.testing.internal.startInVmRpcClient
|
||||
import net.corda.testing.internal.startRpcClient
|
||||
import org.apache.activemq.artemis.api.core.client.ClientSession
|
||||
import org.junit.Rule
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
open class AbstractRPCTest {
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule(true)
|
||||
|
||||
enum class RPCTestMode {
|
||||
InVm,
|
||||
Netty
|
||||
|
@ -5,19 +5,22 @@ import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.utilities.millis
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.internal.concurrent.fork
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||
import net.corda.testing.internal.RPCDriverExposedDSLInterface
|
||||
import net.corda.testing.internal.rpcDriver
|
||||
import net.corda.testing.internal.testThreadFactory
|
||||
import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet
|
||||
import org.junit.After
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import rx.Observable
|
||||
import rx.subjects.UnicastSubject
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
import java.util.concurrent.*
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class RPCConcurrencyTests : AbstractRPCTest() {
|
||||
@ -36,7 +39,7 @@ class RPCConcurrencyTests : AbstractRPCTest() {
|
||||
fun getParallelObservableTree(depth: Int, branchingFactor: Int): ObservableRose<Int>
|
||||
}
|
||||
|
||||
class TestOpsImpl : TestOps {
|
||||
class TestOpsImpl(private val pool: Executor) : TestOps {
|
||||
private val latches = ConcurrentHashMap<Long, CountDownLatch>()
|
||||
override val protocolVersion = 0
|
||||
|
||||
@ -68,24 +71,22 @@ class RPCConcurrencyTests : AbstractRPCTest() {
|
||||
val branches = if (depth == 0) {
|
||||
Observable.empty<ObservableRose<Int>>()
|
||||
} else {
|
||||
val publish = UnicastSubject.create<ObservableRose<Int>>()
|
||||
ForkJoinPool.commonPool().fork {
|
||||
(1..branchingFactor).toList().parallelStream().forEach {
|
||||
publish.onNext(getParallelObservableTree(depth - 1, branchingFactor))
|
||||
UnicastSubject.create<ObservableRose<Int>>().also { publish ->
|
||||
(1..branchingFactor).map {
|
||||
pool.fork { publish.onNext(getParallelObservableTree(depth - 1, branchingFactor)) }
|
||||
}.transpose().then {
|
||||
it.getOrThrow()
|
||||
publish.onCompleted()
|
||||
}
|
||||
publish.onCompleted()
|
||||
}
|
||||
publish
|
||||
}
|
||||
return ObservableRose(depth, branches)
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var testOpsImpl: TestOpsImpl
|
||||
private fun RPCDriverExposedDSLInterface.testProxy(): TestProxy<TestOps> {
|
||||
testOpsImpl = TestOpsImpl()
|
||||
return testProxy<TestOps>(
|
||||
testOpsImpl,
|
||||
TestOpsImpl(pool),
|
||||
clientConfiguration = RPCClientConfiguration.default.copy(
|
||||
reapInterval = 100.millis,
|
||||
cacheConcurrencyLevel = 16
|
||||
@ -96,6 +97,12 @@ class RPCConcurrencyTests : AbstractRPCTest() {
|
||||
)
|
||||
}
|
||||
|
||||
private val pool = Executors.newFixedThreadPool(10, testThreadFactory())
|
||||
@After
|
||||
fun shutdown() {
|
||||
pool.shutdown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `call multiple RPCs in parallel`() {
|
||||
rpcDriver {
|
||||
@ -103,19 +110,17 @@ class RPCConcurrencyTests : AbstractRPCTest() {
|
||||
val numberOfBlockedCalls = 2
|
||||
val numberOfDownsRequired = 100
|
||||
val id = proxy.ops.newLatch(numberOfDownsRequired)
|
||||
val done = CountDownLatch(numberOfBlockedCalls)
|
||||
// Start a couple of blocking RPC calls
|
||||
(1..numberOfBlockedCalls).forEach {
|
||||
ForkJoinPool.commonPool().fork {
|
||||
val done = (1..numberOfBlockedCalls).map {
|
||||
pool.fork {
|
||||
proxy.ops.waitLatch(id)
|
||||
done.countDown()
|
||||
}
|
||||
}
|
||||
}.transpose()
|
||||
// Down the latch that the others are waiting for concurrently
|
||||
(1..numberOfDownsRequired).toList().parallelStream().forEach {
|
||||
proxy.ops.downLatch(id)
|
||||
}
|
||||
done.await()
|
||||
(1..numberOfDownsRequired).map {
|
||||
pool.fork { proxy.ops.downLatch(id) }
|
||||
}.transpose().getOrThrow()
|
||||
done.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,7 +151,7 @@ class RPCConcurrencyTests : AbstractRPCTest() {
|
||||
fun ObservableRose<Int>.subscribeToAll() {
|
||||
remainingLatch.countDown()
|
||||
this.branches.subscribe { tree ->
|
||||
(tree.value + 1..treeDepth - 1).forEach {
|
||||
(tree.value + 1 until treeDepth).forEach {
|
||||
require(it in depthsSeen) { "Got ${tree.value} before $it" }
|
||||
}
|
||||
depthsSeen.add(tree.value)
|
||||
@ -165,11 +170,11 @@ class RPCConcurrencyTests : AbstractRPCTest() {
|
||||
val treeDepth = 2
|
||||
val treeBranchingFactor = 10
|
||||
val remainingLatch = CountDownLatch((intPower(treeBranchingFactor, treeDepth + 1) - 1) / (treeBranchingFactor - 1))
|
||||
val depthsSeen = Collections.synchronizedSet(HashSet<Int>())
|
||||
val depthsSeen = ConcurrentHashSet<Int>()
|
||||
fun ObservableRose<Int>.subscribeToAll() {
|
||||
remainingLatch.countDown()
|
||||
branches.subscribe { tree ->
|
||||
(tree.value + 1..treeDepth - 1).forEach {
|
||||
(tree.value + 1 until treeDepth).forEach {
|
||||
require(it in depthsSeen) { "Got ${tree.value} before $it" }
|
||||
}
|
||||
depthsSeen.add(tree.value)
|
||||
|
@ -5,12 +5,18 @@ import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import net.corda.testing.internal.rpcDriver
|
||||
import net.corda.testing.internal.startRpcClient
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class RPCFailureTests {
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule(true)
|
||||
|
||||
class Unserializable
|
||||
interface Ops : RPCOps {
|
||||
fun getUnserializable(): Unserializable
|
||||
|
@ -49,8 +49,7 @@ class IdentitySyncFlowTests {
|
||||
val alice: Party = aliceNode.info.singleIdentity()
|
||||
val bob: Party = bobNode.info.singleIdentity()
|
||||
val notary = mockNet.defaultNotaryIdentity
|
||||
bobNode.internals.registerInitiatedFlow(Receive::class.java)
|
||||
|
||||
bobNode.registerInitiatedFlow(Receive::class.java)
|
||||
// Alice issues then pays some cash to a new confidential identity that Bob doesn't know about
|
||||
val anonymous = true
|
||||
val ref = OpaqueBytes.of(0x01)
|
||||
@ -80,8 +79,7 @@ class IdentitySyncFlowTests {
|
||||
val bob: Party = bobNode.info.singleIdentity()
|
||||
val charlie: Party = charlieNode.info.singleIdentity()
|
||||
val notary = mockNet.defaultNotaryIdentity
|
||||
bobNode.internals.registerInitiatedFlow(Receive::class.java)
|
||||
|
||||
bobNode.registerInitiatedFlow(Receive::class.java)
|
||||
// Charlie issues then pays some cash to a new confidential identity
|
||||
val anonymous = true
|
||||
val ref = OpaqueBytes.of(0x01)
|
||||
|
@ -1,5 +1,7 @@
|
||||
gradlePluginsVersion=2.0.9
|
||||
gradlePluginsVersion=3.0.0
|
||||
kotlinVersion=1.1.60
|
||||
platformVersion=1
|
||||
guavaVersion=21.0
|
||||
bouncycastleVersion=1.57
|
||||
typesafeConfigVersion=1.3.1
|
||||
jsr305Version=3.0.2
|
||||
|
@ -78,7 +78,7 @@ dependencies {
|
||||
compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8"
|
||||
|
||||
// Thread safety annotations
|
||||
compile "com.google.code.findbugs:jsr305:3.0.1"
|
||||
compile "com.google.code.findbugs:jsr305:$jsr305_version"
|
||||
|
||||
// Log4J: logging framework (ONLY explicitly referenced by net.corda.core.utilities.Logging.kt)
|
||||
compile "org.apache.logging.log4j:log4j-core:${log4j_version}"
|
||||
|
@ -31,6 +31,8 @@ import java.time.Duration
|
||||
import java.time.temporal.Temporal
|
||||
import java.util.*
|
||||
import java.util.Spliterator.*
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.stream.IntStream
|
||||
import java.util.stream.Stream
|
||||
import java.util.stream.StreamSupport
|
||||
@ -307,3 +309,10 @@ fun TransactionBuilder.toLedgerTransaction(services: ServiceHub, serializationCo
|
||||
val KClass<*>.packageName: String get() = java.`package`.name
|
||||
|
||||
fun URL.openHttpConnection(): HttpURLConnection = openConnection() as HttpURLConnection
|
||||
/** Analogous to [Thread.join]. */
|
||||
fun ExecutorService.join() {
|
||||
shutdown() // Do not change to shutdownNow, tests use this method to assert the executor has no more tasks.
|
||||
while (!awaitTermination(1, TimeUnit.SECONDS)) {
|
||||
// Try forever. Do not give up, tests use this method to assert the executor has no more tasks.
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ class ThreadLocalToggleField<T>(name: String) : ToggleField<T>(name) {
|
||||
}
|
||||
|
||||
/** The named thread has leaked from a previous test. */
|
||||
class ThreadLeakException : RuntimeException("Leaked thread detected: ${Thread.currentThread().name}")
|
||||
class ThreadLeakException(valueToString: String) : RuntimeException("Leaked thread '${Thread.currentThread().name}' detected, value was: $valueToString")
|
||||
|
||||
/** @param isAGlobalThreadBeingCreated whether a global thread (that should not inherit any value) is being created. */
|
||||
class InheritableThreadLocalToggleField<T>(name: String,
|
||||
@ -54,16 +54,12 @@ class InheritableThreadLocalToggleField<T>(name: String,
|
||||
}
|
||||
|
||||
private inner class Holder(value: T) : AtomicReference<T?>(value) {
|
||||
fun valueOrDeclareLeak() = get() ?: throw ThreadLeakException()
|
||||
private val valueToString = value.toString() // We never set another non-null value.
|
||||
fun valueOrDeclareLeak() = get() ?: throw ThreadLeakException(valueToString)
|
||||
fun childValue(): Holder? {
|
||||
val e = ThreadLeakException() // Expensive, but so is starting the new thread.
|
||||
return if (isAGlobalThreadBeingCreated(e.stackTrace)) {
|
||||
get() ?: log.warn(e.message)
|
||||
null
|
||||
} else {
|
||||
get() ?: log.error(e.message)
|
||||
this
|
||||
}
|
||||
val e = ThreadLeakException(valueToString) // Expensive, but so is starting the new thread.
|
||||
get() ?: log.warn(e.message)
|
||||
return if (isAGlobalThreadBeingCreated(e.stackTrace)) null else this
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ interface SerializationEnvironment {
|
||||
val checkpointContext: SerializationContext
|
||||
}
|
||||
|
||||
class SerializationEnvironmentImpl(
|
||||
open class SerializationEnvironmentImpl(
|
||||
override val serializationFactory: SerializationFactory,
|
||||
override val p2pContext: SerializationContext,
|
||||
rpcServerContext: SerializationContext? = null,
|
||||
|
@ -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.smoketesting.NodeConfig
|
||||
import net.corda.smoketesting.NodeProcess
|
||||
import net.corda.smoketesting.NodeProcess.Companion.CORDAPPS_DIR_NAME
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import java.nio.file.Paths
|
||||
@ -22,7 +23,6 @@ import kotlin.streams.toList
|
||||
|
||||
class CordappSmokeTest {
|
||||
private companion object {
|
||||
private const val CORDAPPS_DIR_NAME = "cordapps"
|
||||
val user = User("user1", "test", permissions = setOf("ALL"))
|
||||
val port = AtomicInteger(15100)
|
||||
}
|
||||
@ -38,7 +38,6 @@ class CordappSmokeTest {
|
||||
users = listOf(user)
|
||||
)
|
||||
|
||||
|
||||
@Test
|
||||
fun `FlowContent appName returns the filename of the CorDapp jar`() {
|
||||
val cordappsDir = (factory.baseDirectory(aliceConfig) / CORDAPPS_DIR_NAME).createDirectories()
|
||||
|
@ -38,7 +38,7 @@ public class FlowsInJavaTest {
|
||||
|
||||
@Test
|
||||
public void suspendableActionInsideUnwrap() throws Exception {
|
||||
bobNode.getInternals().registerInitiatedFlow(SendHelloAndThenReceive.class);
|
||||
bobNode.registerInitiatedFlow(SendHelloAndThenReceive.class);
|
||||
Future<String> result = startFlow(aliceNode.getServices(), new SendInUnwrapFlow(bob)).getResultFuture();
|
||||
mockNet.runNetwork();
|
||||
assertThat(result.get()).isEqualTo("Hello");
|
||||
|
@ -8,7 +8,7 @@ import net.corda.core.internal.div
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import net.corda.node.utilities.*
|
||||
import net.corda.nodeapi.internal.crypto.*
|
||||
import net.corda.testing.kryoSpecific
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import org.junit.Rule
|
||||
|
@ -38,24 +38,20 @@ class PartialMerkleTreeTest {
|
||||
testLedger = ledger {
|
||||
unverifiedTransaction {
|
||||
attachments(Cash.PROGRAM_ID)
|
||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash") {
|
||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash",
|
||||
Cash.State(
|
||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||
owner = MEGA_CORP
|
||||
)
|
||||
}
|
||||
output(Cash.PROGRAM_ID, "dummy cash 1") {
|
||||
owner = MEGA_CORP))
|
||||
output(Cash.PROGRAM_ID, "dummy cash 1",
|
||||
Cash.State(
|
||||
amount = 900.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||
owner = MINI_CORP
|
||||
)
|
||||
}
|
||||
owner = MINI_CORP))
|
||||
}
|
||||
transaction {
|
||||
attachments(Cash.PROGRAM_ID)
|
||||
input("MEGA_CORP cash")
|
||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash".output<Cash.State>().copy(owner = MINI_CORP))
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package net.corda.core.crypto
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.toTypedArray
|
||||
import net.corda.core.internal.cert
|
||||
import net.corda.node.utilities.*
|
||||
import net.corda.nodeapi.internal.crypto.*
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.GeneralName
|
||||
import org.bouncycastle.asn1.x509.GeneralSubtree
|
||||
@ -50,7 +50,7 @@ class X509NameConstraintsTest {
|
||||
|
||||
val nameConstraints = NameConstraints(acceptableNames, arrayOf())
|
||||
val pathValidator = CertPathValidator.getInstance("PKIX")
|
||||
val certFactory = CertificateFactory.getInstance("X509")
|
||||
val certFactory = X509CertificateFactory().delegate
|
||||
|
||||
assertFailsWith(CertPathValidatorException::class) {
|
||||
val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank B"), nameConstraints)
|
||||
@ -85,7 +85,7 @@ class X509NameConstraintsTest {
|
||||
.map { GeneralSubtree(GeneralName(X500Name(it))) }.toTypedArray()
|
||||
|
||||
val nameConstraints = NameConstraints(acceptableNames, arrayOf())
|
||||
val certFactory = CertificateFactory.getInstance("X509")
|
||||
val certFactory = X509CertificateFactory().delegate
|
||||
Crypto.ECDSA_SECP256R1_SHA256
|
||||
val pathValidator = CertPathValidator.getInstance("PKIX", BouncyCastleProvider.PROVIDER_NAME)
|
||||
|
||||
|
@ -52,10 +52,8 @@ class AttachmentTests {
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
|
||||
val alice = aliceNode.info.singleIdentity()
|
||||
|
||||
aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
|
||||
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
// Insert an attachment into node zero's store directly.
|
||||
val id = aliceNode.database.transaction {
|
||||
aliceNode.attachments.importAttachment(ByteArrayInputStream(fakeAttachment()))
|
||||
@ -85,10 +83,8 @@ class AttachmentTests {
|
||||
fun `missing`() {
|
||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
|
||||
aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
|
||||
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
// Get node one to fetch a non-existent attachment.
|
||||
val hash = SecureHash.randomSHA256()
|
||||
val alice = aliceNode.info.singleIdentity()
|
||||
@ -108,10 +104,8 @@ class AttachmentTests {
|
||||
})
|
||||
val bobNode = mockNet.createNode(MockNodeParameters(legalName = BOB.name))
|
||||
val alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME)
|
||||
|
||||
aliceNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
bobNode.internals.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
|
||||
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
|
||||
val attachment = fakeAttachment()
|
||||
// Insert an attachment into node zero's store directly.
|
||||
val id = aliceNode.database.transaction {
|
||||
|
@ -50,7 +50,7 @@ class CollectSignaturesFlowTests {
|
||||
|
||||
private fun registerFlowOnAllNodes(flowClass: KClass<out FlowLogic<*>>) {
|
||||
listOf(aliceNode, bobNode, charlieNode).forEach {
|
||||
it.internals.registerInitiatedFlow(flowClass.java)
|
||||
it.registerInitiatedFlow(flowClass.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,7 +133,7 @@ class ContractUpgradeFlowTest {
|
||||
|
||||
@Test
|
||||
fun `2 parties contract upgrade using RPC`() {
|
||||
rpcDriver(initialiseSerialization = false) {
|
||||
rpcDriver {
|
||||
// Create dummy contract.
|
||||
val twoPartyDummyContract = DummyContract.generateInitial(0, notary, alice.ref(1), bob.ref(1))
|
||||
val signedByA = aliceNode.services.signInitialTransaction(twoPartyDummyContract)
|
||||
|
@ -38,14 +38,14 @@ class NoAnswer(private val closure: () -> Unit = {}) : FlowLogic<Unit>() {
|
||||
* Allows to register a flow of type [R] against an initiating flow of type [I].
|
||||
*/
|
||||
inline fun <I : FlowLogic<*>, reified R : FlowLogic<*>> StartedNode<*>.registerInitiatedFlow(initiatingFlowType: KClass<I>, crossinline construct: (session: FlowSession) -> R) {
|
||||
internals.internalRegisterFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> construct(session) }, R::class.javaObjectType, true)
|
||||
internalRegisterFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> construct(session) }, R::class.javaObjectType, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to register a flow of type [Answer] against an initiating flow of type [I], returning a valure of type [R].
|
||||
*/
|
||||
inline fun <I : FlowLogic<*>, reified R : Any> StartedNode<*>.registerAnswer(initiatingFlowType: KClass<I>, value: R) {
|
||||
internals.internalRegisterFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> Answer(session, value) }, Answer::class.javaObjectType, true)
|
||||
internalRegisterFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> Answer(session, value) }, Answer::class.javaObjectType, true)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,8 +4,8 @@ import net.corda.core.crypto.entropyToKeyPair
|
||||
import net.corda.core.internal.read
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.node.utilities.KEYSTORE_TYPE
|
||||
import net.corda.node.utilities.save
|
||||
import net.corda.nodeapi.internal.crypto.KEYSTORE_TYPE
|
||||
import net.corda.nodeapi.internal.crypto.save
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import net.corda.testing.getTestPartyAndCertificate
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
|
@ -42,8 +42,8 @@ class ResolveTransactionsFlowTest {
|
||||
notaryNode = mockNet.defaultNotaryNode
|
||||
megaCorpNode = mockNet.createPartyNode(MEGA_CORP.name)
|
||||
miniCorpNode = mockNet.createPartyNode(MINI_CORP.name)
|
||||
megaCorpNode.internals.registerInitiatedFlow(TestResponseFlow::class.java)
|
||||
miniCorpNode.internals.registerInitiatedFlow(TestResponseFlow::class.java)
|
||||
megaCorpNode.registerInitiatedFlow(TestResponseFlow::class.java)
|
||||
miniCorpNode.registerInitiatedFlow(TestResponseFlow::class.java)
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
megaCorp = megaCorpNode.info.singleIdentity()
|
||||
miniCorp = miniCorpNode.info.singleIdentity()
|
||||
|
@ -14,7 +14,6 @@ import org.junit.runners.model.Statement
|
||||
import org.slf4j.Logger
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
@ -23,10 +22,7 @@ private fun <T> withSingleThreadExecutor(callable: ExecutorService.() -> T) = Ex
|
||||
fork {}.getOrThrow() // Start the thread.
|
||||
callable()
|
||||
} finally {
|
||||
shutdown()
|
||||
while (!awaitTermination(1, TimeUnit.SECONDS)) {
|
||||
// Do nothing.
|
||||
}
|
||||
join()
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,6 +130,7 @@ class ToggleFieldTest {
|
||||
assertThatThrownBy { future.getOrThrow() }
|
||||
.isInstanceOf(ThreadLeakException::class.java)
|
||||
.hasMessageContaining(threadName)
|
||||
.hasMessageContaining("hello")
|
||||
}
|
||||
}
|
||||
withSingleThreadExecutor {
|
||||
@ -141,9 +138,9 @@ class ToggleFieldTest {
|
||||
}
|
||||
}
|
||||
|
||||
/** We log an error rather than failing-fast as the new thread may be an undetected global. */
|
||||
/** We log a warning rather than failing-fast as the new thread may be an undetected global. */
|
||||
@Test
|
||||
fun `leaked thread propagates holder to non-global thread, with error`() {
|
||||
fun `leaked thread propagates holder to non-global thread, with warning`() {
|
||||
val field = inheritableThreadLocalToggleField<String>()
|
||||
field.set("hello")
|
||||
withSingleThreadExecutor {
|
||||
@ -153,17 +150,18 @@ class ToggleFieldTest {
|
||||
val leakedThreadName = Thread.currentThread().name
|
||||
verifyNoMoreInteractions(log)
|
||||
withSingleThreadExecutor {
|
||||
// If ThreadLeakException is seen in practice, these errors form a trail of where the holder has been:
|
||||
verify(log).error(argThat { contains(leakedThreadName) })
|
||||
// If ThreadLeakException is seen in practice, these warnings form a trail of where the holder has been:
|
||||
verify(log).warn(argThat { contains(leakedThreadName) && contains("hello") })
|
||||
val newThreadName = fork { Thread.currentThread().name }.getOrThrow()
|
||||
val future = fork(field::get)
|
||||
assertThatThrownBy { future.getOrThrow() }
|
||||
.isInstanceOf(ThreadLeakException::class.java)
|
||||
.hasMessageContaining(newThreadName)
|
||||
.hasMessageContaining("hello")
|
||||
fork {
|
||||
verifyNoMoreInteractions(log)
|
||||
withSingleThreadExecutor {
|
||||
verify(log).error(argThat { contains(newThreadName) })
|
||||
verify(log).warn(argThat { contains(newThreadName) && contains("hello") })
|
||||
}
|
||||
}.getOrThrow()
|
||||
}
|
||||
@ -183,7 +181,7 @@ class ToggleFieldTest {
|
||||
globalThreadCreationMethod {
|
||||
verifyNoMoreInteractions(log)
|
||||
withSingleThreadExecutor {
|
||||
verify(log).warn(argThat { contains(leakedThreadName) })
|
||||
verify(log).warn(argThat { contains(leakedThreadName) && contains("hello") })
|
||||
// In practice the new thread is for example a static thread we can't get rid of:
|
||||
assertNull(fork(field::get).getOrThrow())
|
||||
}
|
||||
|
@ -2,13 +2,13 @@ package net.corda.core.internal.concurrent
|
||||
|
||||
import com.nhaarman.mockito_kotlin.*
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.internal.join
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.testing.rigorousMock
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.Test
|
||||
import org.slf4j.Logger
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
@ -108,10 +108,7 @@ class CordaFutureTest {
|
||||
val throwable = Exception("Boom")
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
executor.fork { throw throwable }.andForget(log)
|
||||
executor.shutdown()
|
||||
while (!executor.awaitTermination(1, TimeUnit.SECONDS)) {
|
||||
// Do nothing.
|
||||
}
|
||||
executor.join()
|
||||
verify(log).error(any(), same(throwable))
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.internal.InitiatedFlowFactory
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.node.utilities.currentDBSession
|
||||
import net.corda.nodeapi.internal.persistence.currentDBSession
|
||||
import net.corda.testing.ALICE_NAME
|
||||
import net.corda.testing.BOB_NAME
|
||||
import net.corda.testing.node.MockNetwork
|
||||
@ -148,7 +148,7 @@ class AttachmentSerializationTest {
|
||||
}
|
||||
|
||||
private fun launchFlow(clientLogic: ClientLogic, rounds: Int, sendData: Boolean = false) {
|
||||
server.internals.internalRegisterFlowFactory(
|
||||
server.internalRegisterFlowFactory(
|
||||
ClientLogic::class.java,
|
||||
InitiatedFlowFactory.Core { ServerLogic(it, sendData) },
|
||||
ServerLogic::class.java,
|
||||
|
@ -55,10 +55,10 @@ class TransactionEncumbranceTests {
|
||||
ledger {
|
||||
transaction {
|
||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||
input(Cash.PROGRAM_ID) { state }
|
||||
output(Cash.PROGRAM_ID, encumbrance = 1) { stateWithNewOwner }
|
||||
output(TEST_TIMELOCK_ID, "5pm time-lock") { timeLock }
|
||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
||||
input(Cash.PROGRAM_ID, state)
|
||||
output(Cash.PROGRAM_ID, encumbrance = 1, contractState = stateWithNewOwner)
|
||||
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
|
||||
command(MEGA_CORP.owningKey, Cash.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
@ -69,16 +69,16 @@ class TransactionEncumbranceTests {
|
||||
ledger {
|
||||
unverifiedTransaction {
|
||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock") { state }
|
||||
output(TEST_TIMELOCK_ID, "5pm time-lock") { timeLock }
|
||||
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", state)
|
||||
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
|
||||
}
|
||||
// Un-encumber the output if the time of the transaction is later than the timelock.
|
||||
transaction {
|
||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||
input("state encumbered by 5pm time-lock")
|
||||
input("5pm time-lock")
|
||||
output(Cash.PROGRAM_ID) { stateWithNewOwner }
|
||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
||||
output(Cash.PROGRAM_ID, stateWithNewOwner)
|
||||
command(MEGA_CORP.owningKey, Cash.Commands.Move())
|
||||
timeWindow(FIVE_PM)
|
||||
verifies()
|
||||
}
|
||||
@ -90,16 +90,16 @@ class TransactionEncumbranceTests {
|
||||
ledger {
|
||||
unverifiedTransaction {
|
||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock") { state }
|
||||
output(TEST_TIMELOCK_ID, "5pm time-lock") { timeLock }
|
||||
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", state)
|
||||
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
|
||||
}
|
||||
// The time of the transaction is earlier than the time specified in the encumbering timelock.
|
||||
transaction {
|
||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||
input("state encumbered by 5pm time-lock")
|
||||
input("5pm time-lock")
|
||||
output(Cash.PROGRAM_ID) { state }
|
||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
||||
output(Cash.PROGRAM_ID, state)
|
||||
command(MEGA_CORP.owningKey, Cash.Commands.Move())
|
||||
timeWindow(FOUR_PM)
|
||||
this `fails with` "the time specified in the time-lock has passed"
|
||||
}
|
||||
@ -111,14 +111,14 @@ class TransactionEncumbranceTests {
|
||||
ledger {
|
||||
unverifiedTransaction {
|
||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", encumbrance = 1) { state }
|
||||
output(TEST_TIMELOCK_ID, "5pm time-lock") { timeLock }
|
||||
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", encumbrance = 1, contractState = state)
|
||||
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
|
||||
}
|
||||
transaction {
|
||||
attachments(Cash.PROGRAM_ID)
|
||||
input("state encumbered by 5pm time-lock")
|
||||
output(Cash.PROGRAM_ID) { stateWithNewOwner }
|
||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
||||
output(Cash.PROGRAM_ID, stateWithNewOwner)
|
||||
command(MEGA_CORP.owningKey, Cash.Commands.Move())
|
||||
timeWindow(FIVE_PM)
|
||||
this `fails with` "Missing required encumbrance 1 in INPUT"
|
||||
}
|
||||
@ -130,9 +130,9 @@ class TransactionEncumbranceTests {
|
||||
ledger {
|
||||
transaction {
|
||||
attachments(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { state }
|
||||
output(Cash.PROGRAM_ID, encumbrance = 0) { stateWithNewOwner }
|
||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
||||
input(Cash.PROGRAM_ID, state)
|
||||
output(Cash.PROGRAM_ID, encumbrance = 0, contractState = stateWithNewOwner)
|
||||
command(MEGA_CORP.owningKey, Cash.Commands.Move())
|
||||
this `fails with` "Missing required encumbrance 0 in OUTPUT"
|
||||
}
|
||||
}
|
||||
@ -143,10 +143,10 @@ class TransactionEncumbranceTests {
|
||||
ledger {
|
||||
transaction {
|
||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||
input(Cash.PROGRAM_ID) { state }
|
||||
output(TEST_TIMELOCK_ID, encumbrance = 2) { stateWithNewOwner }
|
||||
output(TEST_TIMELOCK_ID) { timeLock }
|
||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
||||
input(Cash.PROGRAM_ID, state)
|
||||
output(TEST_TIMELOCK_ID, encumbrance = 2, contractState = stateWithNewOwner)
|
||||
output(TEST_TIMELOCK_ID, timeLock)
|
||||
command(MEGA_CORP.owningKey, Cash.Commands.Move())
|
||||
this `fails with` "Missing required encumbrance 2 in OUTPUT"
|
||||
}
|
||||
}
|
||||
@ -157,16 +157,16 @@ class TransactionEncumbranceTests {
|
||||
ledger {
|
||||
unverifiedTransaction {
|
||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||
output(Cash.PROGRAM_ID, "state encumbered by some other state", encumbrance = 1) { state }
|
||||
output(Cash.PROGRAM_ID, "some other state") { state }
|
||||
output(TEST_TIMELOCK_ID, "5pm time-lock") { timeLock }
|
||||
output(Cash.PROGRAM_ID, "state encumbered by some other state", encumbrance = 1, contractState = state)
|
||||
output(Cash.PROGRAM_ID, "some other state", state)
|
||||
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
|
||||
}
|
||||
transaction {
|
||||
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
|
||||
input("state encumbered by some other state")
|
||||
input("5pm time-lock")
|
||||
output(Cash.PROGRAM_ID) { stateWithNewOwner }
|
||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
||||
output(Cash.PROGRAM_ID, stateWithNewOwner)
|
||||
command(MEGA_CORP.owningKey, Cash.Commands.Move())
|
||||
timeWindow(FIVE_PM)
|
||||
this `fails with` "Missing required encumbrance 1 in INPUT"
|
||||
}
|
||||
|
@ -6,6 +6,9 @@ from the previous milestone release.
|
||||
|
||||
UNRELEASED
|
||||
----------
|
||||
* Removed confusing property database.initDatabase, enabling its guarded behaviour with the dev-mode.
|
||||
In devMode Hibernate will try to create or update database schemas, otherwise it will expect relevant schemas to be present
|
||||
in the database (pre configured via DDL scripts or equivalent), and validate these are correct.
|
||||
|
||||
* ``ConfigUtilities`` now read system properties for a node. This allow to specify data source properties at runtime.
|
||||
|
||||
|
@ -108,24 +108,26 @@ path to the node's base directory.
|
||||
:notary: Optional configuration object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt
|
||||
cluster then specify ``raft`` or ``bftSMaRt`` respectively as described below. If a single node notary then omit both.
|
||||
|
||||
:validating: Boolean to determine whether the notary is a validating or non-validating one.
|
||||
:validating: Boolean to determine whether the notary is a validating or non-validating one.
|
||||
|
||||
:raft: If part of a distributed Raft cluster specify this config object, with the following settings:
|
||||
:raft: If part of a distributed Raft cluster specify this config object, with the following settings:
|
||||
|
||||
:nodeAddress: The host and port to which to bind the embedded Raft server. Note that the Raft cluster uses a
|
||||
separate transport layer for communication that does not integrate with ArtemisMQ messaging services.
|
||||
:nodeAddress: The host and port to which to bind the embedded Raft server. Note that the Raft cluster uses a
|
||||
separate transport layer for communication that does not integrate with ArtemisMQ messaging services.
|
||||
|
||||
:clusterAddresses: List of Raft cluster member addresses used to join the cluster. At least one of the specified
|
||||
members must be active and be able to communicate with the cluster leader for joining. If empty, a new
|
||||
cluster will be bootstrapped.
|
||||
:clusterAddresses: Must list the addresses of all the members in the cluster. At least one of the members must
|
||||
be active and be able to communicate with the cluster leader for the node to join the cluster. If empty, a
|
||||
new cluster will be bootstrapped.
|
||||
|
||||
:bftSMaRt: If part of a distributed BFT-SMaRt cluster specify this config object, with the following settings:
|
||||
:bftSMaRt: If part of a distributed BFT-SMaRt cluster specify this config object, with the following settings:
|
||||
|
||||
:replicaId: The zero-based index of the current replica. All replicas must specify a unique replica id.
|
||||
:replicaId: The zero-based index of the current replica. All replicas must specify a unique replica id.
|
||||
|
||||
:clusterAddresses: List of all BFT-SMaRt cluster member addresses.
|
||||
:clusterAddresses: Must list the addresses of all the members in the cluster. At least one of the members must
|
||||
be active and be able to communicate with the cluster leader for the node to join the cluster. If empty, a
|
||||
new cluster will be bootstrapped.
|
||||
|
||||
:custom: If `true`, will load and install a notary service from a CorDapp. See :doc:`tutorial-custom-notary`.
|
||||
:custom: If `true`, will load and install a notary service from a CorDapp. See :doc:`tutorial-custom-notary`.
|
||||
|
||||
Only one of ``raft``, ``bftSMaRt`` or ``custom`` configuration values may be specified.
|
||||
|
||||
@ -136,17 +138,19 @@ path to the node's base directory.
|
||||
:rpcUsers: A list of users who are authorised to access the RPC system. Each user in the list is a config object with the
|
||||
following fields:
|
||||
|
||||
:username: Username consisting only of word characters (a-z, A-Z, 0-9 and _)
|
||||
:password: The password
|
||||
:permissions: A list of permission strings which RPC methods can use to control access
|
||||
|
||||
If this field is absent or an empty list then RPC is effectively locked down. Alternatively, if it contains the string
|
||||
``ALL`` then the user is permitted to use *any* RPC method. This value is intended for administrator users and for developers.
|
||||
:username: Username consisting only of word characters (a-z, A-Z, 0-9 and _)
|
||||
:password: The password
|
||||
:permissions: A list of permissions for starting flows via RPC. To give the user the permission to start the flow
|
||||
``foo.bar.FlowClass``, add the string ``StartFlow.foo.bar.FlowClass`` to the list. If the list
|
||||
contains the string ``ALL``, the user can start any flow via RPC. This value is intended for administrator
|
||||
users and for development.
|
||||
|
||||
:devMode: This flag sets the node to run in development mode. On startup, if the keystore ``<workspace>/certificates/sslkeystore.jks``
|
||||
does not exist, a developer keystore will be used if ``devMode`` is true. The node will exit if ``devMode`` is false
|
||||
and the keystore does not exist. ``devMode`` also turns on background checking of flow checkpoints to shake out any
|
||||
bugs in the checkpointing process.
|
||||
bugs in the checkpointing process. Also, if ``devMode`` is true, Hibernate will try to automatically create the schema required by Corda
|
||||
or update an existing schema in the SQL database; if ``devMode`` is false, Hibernate will simply validate an existing schema
|
||||
failing on node start if this schema is either not present or not compatible.
|
||||
|
||||
:detectPublicIp: This flag toggles the auto IP detection behaviour, it is enabled by default. On startup the node will
|
||||
attempt to discover its externally visible IP address first by looking for any public addresses on its network
|
||||
@ -168,7 +172,7 @@ path to the node's base directory.
|
||||
:sshd: If provided, node will start internal SSH server which will provide a management shell. It uses the same credentials
|
||||
and permissions as RPC subsystem. It has one required parameter.
|
||||
|
||||
:port: - the port to start SSH server on
|
||||
:port: The port to start SSH server on
|
||||
|
||||
:relay: If provided, the node will attempt to tunnel inbound connections via an external relay. The relay's address will be
|
||||
advertised to the network map service instead of the provided ``p2pAddress``.
|
||||
|
@ -4,8 +4,9 @@ Corda nodes
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
deploying-a-node
|
||||
generating-a-node
|
||||
running-a-node
|
||||
deploying-a-node
|
||||
corda-configuration-file
|
||||
clientrpc
|
||||
shell
|
||||
|
@ -118,9 +118,9 @@ Creating the CorDapp JAR
|
||||
The gradle ``jar`` task included in the CorDapp template build file will automatically build your CorDapp JAR correctly
|
||||
as long as your dependencies are set correctly.
|
||||
|
||||
Note that the hash of the resulting CorDapp JAR is not deterministic, as it depends on variables such as the timestamp
|
||||
at creation. Nodes running the same CorDapp must therefore ensure they are using the exact same CorDapp jar, and not
|
||||
different versions of the JAR created from identical sources.
|
||||
.. warning:: The hash of the generated CorDapp JAR is not deterministic, as it depends on variables such as the
|
||||
timestamp at creation. Nodes running the same CorDapp must therefore ensure they are using the exact same CorDapp
|
||||
jar, and not different versions of the JAR created from identical sources.
|
||||
|
||||
The filename of the JAR must include a unique identifier to deduplicate it from other releases of the same CorDapp.
|
||||
This is typically done by appending the version string to the CorDapp's name. This unique identifier should not change
|
||||
@ -131,7 +131,7 @@ Installing the CorDapp jar
|
||||
--------------------------
|
||||
|
||||
.. note:: Before installing a CorDapp, you must create one or more nodes to install it on. For instructions, please see
|
||||
:doc:`deploying-a-node`.
|
||||
:doc:`generating-a-node`.
|
||||
|
||||
At runtime, nodes will load any CorDapps present in their ``cordapps`` folder. Therefore in order to install a CorDapp on
|
||||
a node, the CorDapp JAR must be added to the ``<node_dir>/cordapps/`` folder, where ``node_dir`` is the folder in which
|
||||
|
@ -1,141 +1,242 @@
|
||||
Deploying a node
|
||||
================
|
||||
|
||||
Node structure
|
||||
--------------
|
||||
Each Corda node has the following structure:
|
||||
.. contents::
|
||||
|
||||
.. sourcecode:: none
|
||||
.. note:: These instructions are intended for people who want to deploy a Corda node to a server,
|
||||
whether they have developed and tested a CorDapp following the instructions in :doc:`generating-a-node`
|
||||
or are deploying a third-party CorDapp.
|
||||
|
||||
.
|
||||
├── certificates // The node's doorman certificates
|
||||
├── corda-webserver.jar // The built-in node webserver
|
||||
├── corda.jar // The core Corda libraries
|
||||
├── logs // The node logs
|
||||
├── node.conf // The node's configuration files
|
||||
├── persistence.mv.db // The node's database
|
||||
└── cordapps // The CorDapps jars installed on the node
|
||||
Linux (systemd): Installing and running Corda as a systemd service
|
||||
------------------------------------------------------------------
|
||||
We recommend creating systemd services to run a node and the optional webserver. This provides logging and service
|
||||
handling, and ensures the Corda service is run at boot.
|
||||
|
||||
The node is configured by editing its ``node.conf`` file. You install CorDapps on the node by dropping the CorDapp JARs
|
||||
into the ``cordapps`` folder.
|
||||
**Prerequisites**:
|
||||
|
||||
Node naming
|
||||
-----------
|
||||
A node's name must be a valid X500 name that obeys the following additional constraints:
|
||||
* Oracle Java 8. The supported versions are listed in :doc:`getting-set-up`
|
||||
|
||||
* The fields of the name have the following maximum character lengths:
|
||||
1. Add a system user which will be used to run Corda:
|
||||
|
||||
* Common name: 64
|
||||
* Organisation: 128
|
||||
* Organisation unit: 64
|
||||
* Locality: 64
|
||||
* State: 64
|
||||
``sudo adduser --system --no-create-home --group corda``
|
||||
|
||||
* The country code is a valid ISO 3166-1 two letter code in upper-case
|
||||
2. Create a directory called ``/opt/corda`` and change its ownership to the user you want to use to run Corda:
|
||||
|
||||
* The organisation, locality and country attributes are present
|
||||
``mkdir /opt/corda; chown corda:corda /opt/corda``
|
||||
|
||||
* The organisation field of the name obeys the following constraints:
|
||||
3. Download the `Corda jar <https://r3.bintray.com/corda/net/corda/corda/>`_
|
||||
(under ``/VERSION_NUMBER/corda-VERSION_NUMBER.jar``) and place it in ``/opt/corda``
|
||||
|
||||
* Has at least two letters
|
||||
* No leading or trailing whitespace
|
||||
* No double-spacing
|
||||
* Upper-case first letter
|
||||
* Does not contain the words "node" or "server"
|
||||
* Does not include the characters ',' or '=' or '$' or '"' or '\'' or '\\'
|
||||
* Is in NFKC normalization form
|
||||
* Only the latin, common and inherited unicode scripts are supported
|
||||
3. Create a directory called ``plugins`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively, download one of
|
||||
our `sample CorDapps <https://www.corda.net/samples/>`_ to the ``plugins`` directory
|
||||
|
||||
The deployNodes task
|
||||
--------------------
|
||||
The CorDapp template defines a ``deployNodes`` task that allows you to automatically generate and configure a set of
|
||||
nodes:
|
||||
4. Save the below as ``/opt/corda/node.conf``. See :doc:`corda-configuration-file` for a description of these options
|
||||
|
||||
.. sourcecode:: groovy
|
||||
.. code-block:: json
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
directory "./build/nodes"
|
||||
networkMap "O=Controller,L=London,C=GB"
|
||||
node {
|
||||
name "O=Controller,L=London,C=GB"
|
||||
// The notary will offer a validating notary service.
|
||||
notary = [validating : true]
|
||||
p2pPort 10002
|
||||
rpcPort 10003
|
||||
// No webport property, so no webserver will be created.
|
||||
h2Port 10004
|
||||
sshdPort 22
|
||||
// Includes the corda-finance CorDapp on our node.
|
||||
cordapps = ["net.corda:corda-finance:$corda_release_version"]
|
||||
basedir : "/opt/corda"
|
||||
p2pAddress : "example.com:10002"
|
||||
rpcAddress : "example.com:10003"
|
||||
webAddress : "0.0.0.0:10004"
|
||||
h2port : 11000
|
||||
emailAddress : "you@example.com"
|
||||
myLegalName : "O=Bank of Breakfast Tea, L=London, C=GB"
|
||||
keyStorePassword : "cordacadevpass"
|
||||
trustStorePassword : "trustpass"
|
||||
useHTTPS : false
|
||||
devMode : false
|
||||
networkMapService {
|
||||
address="networkmap.foo.bar.com:10002"
|
||||
legalName="O=FooBar NetworkMap, L=Dublin, C=IE"
|
||||
}
|
||||
rpcUsers=[
|
||||
{
|
||||
user=corda
|
||||
password=portal_password
|
||||
permissions=[
|
||||
ALL
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
5. Make the following changes to ``/opt/corda/node.conf``:
|
||||
|
||||
* Change the ``p2pAddress`` and ``rpcAddress`` values to start with your server's hostname or external IP address.
|
||||
This is the address other nodes or RPC interfaces will use to communicate with your node
|
||||
* Change the ports if necessary, for example if you are running multiple nodes on one server (see below)
|
||||
* Enter an email address which will be used as an administrative contact during the registration process. This is
|
||||
only visible to the permissioning service
|
||||
* Enter your node's desired legal name. This will be used during the issuance of your certificate and should rarely
|
||||
change as it should represent the legal identity of your node
|
||||
|
||||
* Organization (``O=``) should be a unique and meaningful identifier (e.g. Bank of Breakfast Tea)
|
||||
* Location (``L=``) is your nearest city
|
||||
* Country (``C=``) is the `ISO 3166-1 alpha-2 code <https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2>`_
|
||||
* Change the RPC username and password
|
||||
|
||||
6. Create a ``corda.service`` file based on the example below and save it in the ``/etc/systemd/system/`` directory
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
[Unit]
|
||||
Description=Corda Node - Bank of Breakfast Tea
|
||||
Requires=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=corda
|
||||
WorkingDirectory=/opt/corda
|
||||
ExecStart=/usr/bin/java -Xmx2048m -jar /opt/corda/corda.jar
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
7. Make the following changes to ``corda.service``:
|
||||
|
||||
* Make sure the service description is informative - particularly if you plan to run multiple nodes.
|
||||
* Change the username to the user account you want to use to run Corda. **We recommend that this is not root**
|
||||
* Set the maximum amount of memory available to the Corda process by changing the ``-Xmx2048m`` parameter
|
||||
* Make sure the ``corda.service`` file is owned by root with the correct permissions:
|
||||
* ``sudo chown root:root /etc/systemd/system/corda.service``
|
||||
* ``sudo chmod 644 /etc/systemd/system/corda.service``
|
||||
|
||||
.. note:: The Corda webserver provides a simple interface for interacting with your installed CorDapps in a browser.
|
||||
Running the webserver is optional.
|
||||
|
||||
8. Create a ``corda-webserver.service`` file based on the example below and save it in the ``/etc/systemd/system/``
|
||||
directory.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
[Unit]
|
||||
Description=Webserver for Corda Node - Bank of Breakfast Tea
|
||||
Requires=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=username
|
||||
WorkingDirectory=/opt/corda
|
||||
ExecStart=/usr/bin/java -jar /opt/corda/corda-webserver.jar
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
9. Provision the required certificates to your node. Contact the network permissioning service or see
|
||||
:doc:`permissioning`
|
||||
|
||||
10. You can now start a node and its webserver by running the following ``systemctl`` commands:
|
||||
|
||||
* ``sudo systemctl daemon-reload``
|
||||
* ``sudo systemctl corda start``
|
||||
* ``sudo systemctl corda-webserver start``
|
||||
|
||||
You can run multiple nodes by creating multiple directories and Corda services, modifying the ``node.conf`` and
|
||||
``service`` files so they are unique.
|
||||
|
||||
Windows: Installing and running Corda as a Windows service
|
||||
----------------------------------------------------------
|
||||
We recommend running Corda as a Windows service. This provides service handling, ensures the Corda service is run
|
||||
at boot, and means the Corda service stays running with no users connected to the server.
|
||||
|
||||
**Prerequisites**:
|
||||
|
||||
* Oracle Java 8. The supported versions are listed in :doc:`getting-set-up`
|
||||
|
||||
1. Create a Corda directory and download the Corda jar. Replace ``VERSION_NUMBER`` with the desired version. Here's an
|
||||
example using PowerShell:
|
||||
|
||||
.. code-block:: PowerShell
|
||||
|
||||
mkdir C:\Corda
|
||||
wget http://jcenter.bintray.com/net/corda/corda/VERSION_NUMBER/corda-VERSION_NUMBER.jar -OutFile C:\Corda\corda.jar
|
||||
|
||||
2. Create a directory called ``plugins`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively,
|
||||
download one of our `sample CorDapps <https://www.corda.net/samples/>`_ to the ``plugins`` directory
|
||||
|
||||
3. Save the below as ``C:\Corda\node.conf``. See :doc:`corda-configuration-file` for a description of these options
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
basedir : "C:\\Corda"
|
||||
p2pAddress : "example.com:10002"
|
||||
rpcAddress : "example.com:10003"
|
||||
webAddress : "0.0.0.0:10004"
|
||||
h2port : 11000
|
||||
emailAddress: "you@example.com"
|
||||
myLegalName : "O=Bank of Breakfast Tea, L=London, C=GB"
|
||||
keyStorePassword : "cordacadevpass"
|
||||
trustStorePassword : "trustpass"
|
||||
extraAdvertisedServiceIds: [ "" ]
|
||||
useHTTPS : false
|
||||
devMode : false
|
||||
networkMapService {
|
||||
address="networkmap.foo.bar.com:10002"
|
||||
legalName="O=FooBar NetworkMap, L=Dublin, C=IE"
|
||||
}
|
||||
node {
|
||||
name "O=PartyA,L=London,C=GB"
|
||||
advertisedServices = []
|
||||
p2pPort 10005
|
||||
rpcPort 10006
|
||||
webPort 10007
|
||||
h2Port 10008
|
||||
sshdPort 22
|
||||
cordapps = ["net.corda:corda-finance:$corda_release_version"]
|
||||
// Grants user1 all RPC permissions.
|
||||
rpcUsers = [[ user: "user1", "password": "test", "permissions": ["ALL"]]]
|
||||
}
|
||||
node {
|
||||
name "O=PartyB,L=New York,C=US"
|
||||
advertisedServices = []
|
||||
p2pPort 10009
|
||||
rpcPort 10010
|
||||
webPort 10011
|
||||
h2Port 10012
|
||||
sshdPort 22
|
||||
cordapps = ["net.corda:corda-finance:$corda_release_version"]
|
||||
// Grants user1 the ability to start the MyFlow flow.
|
||||
rpcUsers = [[ user: "user1", "password": "test", "permissions": ["StartFlow.net.corda.flows.MyFlow"]]]
|
||||
}
|
||||
}
|
||||
rpcUsers=[
|
||||
{
|
||||
user=corda
|
||||
password=portal_password
|
||||
permissions=[
|
||||
ALL
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Running this task will create three nodes in the ``build/nodes`` folder:
|
||||
4. Make the following changes to ``C:\Corda\node.conf``:
|
||||
|
||||
* A ``Controller`` node that:
|
||||
* Change the ``p2pAddress`` and ``rpcAddress`` values to start with your server's hostname or external IP address.
|
||||
This is the address other nodes or RPC interfaces will use to communicate with your node
|
||||
* Change the ports if necessary, for example if you are running multiple nodes on one server (see below)
|
||||
* Enter an email address which will be used as an administrative contact during the registration process. This is
|
||||
only visible to the permissioning service
|
||||
* Enter your node's desired legal name. This will be used during the issuance of your certificate and should rarely
|
||||
change as it should represent the legal identity of your node
|
||||
|
||||
* Serves as the network map
|
||||
* Offers a validating notary service
|
||||
* Will not have a webserver (since ``webPort`` is not defined)
|
||||
* Is running the ``corda-finance`` CorDapp
|
||||
* Organization (``O=``) should be a unique and meaningful identifier (e.g. Bank of Breakfast Tea)
|
||||
* Location (``L=``) is your nearest city
|
||||
* Country (``C=``) is the `ISO 3166-1 alpha-2 code <https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2>`_
|
||||
* Change the RPC username and password
|
||||
|
||||
* ``PartyA`` and ``PartyB`` nodes that:
|
||||
5. Copy the required Java keystores to the node. See :doc:`permissioning`
|
||||
|
||||
* Are pointing at the ``Controller`` as the network map service
|
||||
* Are not offering any services
|
||||
* Will have a webserver (since ``webPort`` is defined)
|
||||
* Are running the ``corda-finance`` CorDapp
|
||||
* Have an RPC user, ``user1``, that can be used to log into the node via RPC
|
||||
6. Download the `NSSM service manager <nssm.cc>`_
|
||||
|
||||
Additionally, all three nodes will include any CorDapps defined in the project's source folders, even though these
|
||||
CorDapps are not listed in each node's ``cordapps`` entry. This means that running the ``deployNodes`` task from the
|
||||
template CorDapp, for example, would automatically build and add the template CorDapp to each node.
|
||||
7. Unzip ``nssm-2.24\win64\nssm.exe`` to ``C:\Corda``
|
||||
|
||||
You can extend ``deployNodes`` to generate additional nodes. The only requirement is that you must specify
|
||||
a single node to run the network map service, by putting their name in the ``networkMap`` field.
|
||||
8. Save the following as ``C:\Corda\nssm.bat``:
|
||||
|
||||
.. warning:: When adding nodes, make sure that there are no port clashes!
|
||||
.. code-block:: batch
|
||||
|
||||
Running deployNodes
|
||||
-------------------
|
||||
To create the nodes defined in our ``deployNodes`` task, we'd run the following command in a terminal window from the
|
||||
root of the project:
|
||||
nssm install cordanode1 C:\ProgramData\Oracle\Java\javapath\java.exe
|
||||
nssm set cordanode1 AppDirectory C:\Corda
|
||||
nssm set cordanode1 AppParameters "-jar corda.jar -Xmx2048m --config-file=C:\corda\node.conf"
|
||||
nssm set cordanode1 AppStdout C:\Corda\service.log
|
||||
nssm set cordanode1 AppStderr C:\Corda\service.log
|
||||
nssm set cordanode1 Description Corda Node - Bank of Breakfast Tea
|
||||
sc start cordanode1
|
||||
|
||||
* Unix/Mac OSX: ``./gradlew deployNodes``
|
||||
* Windows: ``gradlew.bat deployNodes``
|
||||
9. Modify the batch file:
|
||||
|
||||
This will create the nodes in the ``build/nodes`` folder.
|
||||
* If you are installing multiple nodes, use a different service name (``cordanode1``) for each node
|
||||
* Set the amount of Java heap memory available to this node by modifying the -Xmx argument
|
||||
* Set an informative description
|
||||
|
||||
.. note:: During the build process each node generates a NodeInfo file which is written in its own root directory,
|
||||
the plug-in proceeds and copies each node NodeInfo to every other node ``additional-node-infos`` directory.
|
||||
The NodeInfo file contains a node hostname and port, legal name and security certificate.
|
||||
10. Run the batch file by clicking on it or from a command prompt
|
||||
|
||||
There will be a node folder generated for each node you defined, plus a ``runnodes`` shell script (or batch file on
|
||||
Windows) to run all the nodes at once. If you make any changes to your ``deployNodes`` task, you will need to re-run
|
||||
the task to see the changes take effect.
|
||||
11. Run ``services.msc`` and verify that a service called ``cordanode1`` is present and running
|
||||
|
||||
You can now run the nodes by following the instructions in :doc:`Running a node <running-a-node>`.
|
||||
12. Run ``netstat -ano`` and check for the ports you configured in ``node.conf``
|
||||
|
||||
13. You may need to open the ports on the Windows firewall
|
||||
|
||||
Testing your installation
|
||||
-------------------------
|
||||
You can verify Corda is running by connecting to your RPC port from another host, e.g.:
|
||||
|
||||
``telnet your-hostname.example.com 10002``
|
||||
|
||||
If you receive the message "Escape character is ^]", Corda is running and accessible. Press Ctrl-] and Ctrl-D to exit
|
||||
telnet.
|
@ -24,6 +24,7 @@ object CustomVaultQuery {
|
||||
private companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
fun rebalanceCurrencyReserves(): List<Amount<Currency>> {
|
||||
val nativeQuery = """
|
||||
select
|
||||
@ -44,16 +45,18 @@ object CustomVaultQuery {
|
||||
"""
|
||||
log.info("SQL to execute: $nativeQuery")
|
||||
val session = services.jdbcSession()
|
||||
val prepStatement = session.prepareStatement(nativeQuery)
|
||||
val rs = prepStatement.executeQuery()
|
||||
val topUpLimits: MutableList<Amount<Currency>> = mutableListOf()
|
||||
while (rs.next()) {
|
||||
val currencyStr = rs.getString(1)
|
||||
val amount = rs.getLong(2)
|
||||
log.info("$currencyStr : $amount")
|
||||
topUpLimits.add(Amount(amount, Currency.getInstance(currencyStr)))
|
||||
return session.prepareStatement(nativeQuery).use { prepStatement ->
|
||||
prepStatement.executeQuery().use { rs ->
|
||||
val topUpLimits: MutableList<Amount<Currency>> = mutableListOf()
|
||||
while (rs.next()) {
|
||||
val currencyStr = rs.getString(1)
|
||||
val amount = rs.getLong(2)
|
||||
log.info("$currencyStr : $amount")
|
||||
topUpLimits.add(Amount(amount, Currency.getInstance(currencyStr)))
|
||||
}
|
||||
topUpLimits
|
||||
}
|
||||
}
|
||||
return topUpLimits
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -69,6 +72,7 @@ object TopupIssuerFlow {
|
||||
data class TopupRequest(val issueToParty: Party,
|
||||
val issuerPartyRef: OpaqueBytes,
|
||||
val notaryParty: Party)
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class TopupIssuanceRequester(val issueToParty: Party,
|
||||
|
@ -33,7 +33,7 @@ class CommercialPaperTest {
|
||||
ledger {
|
||||
transaction {
|
||||
attachments(CP_PROGRAM_ID)
|
||||
input(CP_PROGRAM_ID) { inState }
|
||||
input(CP_PROGRAM_ID, inState)
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
@ -46,8 +46,8 @@ class CommercialPaperTest {
|
||||
val inState = getPaper()
|
||||
ledger {
|
||||
transaction {
|
||||
input(CP_PROGRAM_ID) { inState }
|
||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||
input(CP_PROGRAM_ID, inState)
|
||||
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
|
||||
attachments(CP_PROGRAM_ID)
|
||||
verifies()
|
||||
}
|
||||
@ -61,8 +61,8 @@ class CommercialPaperTest {
|
||||
val inState = getPaper()
|
||||
ledger {
|
||||
transaction {
|
||||
input(CP_PROGRAM_ID) { inState }
|
||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||
input(CP_PROGRAM_ID, inState)
|
||||
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
|
||||
attachments(CP_PROGRAM_ID)
|
||||
`fails with`("the state is propagated")
|
||||
}
|
||||
@ -76,11 +76,11 @@ class CommercialPaperTest {
|
||||
val inState = getPaper()
|
||||
ledger {
|
||||
transaction {
|
||||
input(CP_PROGRAM_ID) { inState }
|
||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||
input(CP_PROGRAM_ID, inState)
|
||||
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
|
||||
attachments(CP_PROGRAM_ID)
|
||||
`fails with`("the state is propagated")
|
||||
output(CP_PROGRAM_ID, "alice's paper") { inState.withOwner(ALICE) }
|
||||
output(CP_PROGRAM_ID, "alice's paper", inState.withOwner(ALICE))
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
@ -92,15 +92,15 @@ class CommercialPaperTest {
|
||||
fun `simple issuance with tweak`() {
|
||||
ledger {
|
||||
transaction {
|
||||
output(CP_PROGRAM_ID, "paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp.
|
||||
output(CP_PROGRAM_ID, "paper", getPaper()) // Some CP is issued onto the ledger by MegaCorp.
|
||||
attachments(CP_PROGRAM_ID)
|
||||
tweak {
|
||||
// The wrong pubkey.
|
||||
command(BIG_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
command(BIG_CORP_PUBKEY, CommercialPaper.Commands.Issue())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
`fails with`("output states are issued by a command signer")
|
||||
}
|
||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
verifies()
|
||||
}
|
||||
@ -112,15 +112,15 @@ class CommercialPaperTest {
|
||||
@Test
|
||||
fun `simple issuance with tweak and top level transaction`() {
|
||||
transaction {
|
||||
output(CP_PROGRAM_ID, "paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp.
|
||||
output(CP_PROGRAM_ID, "paper", getPaper()) // Some CP is issued onto the ledger by MegaCorp.
|
||||
attachments(CP_PROGRAM_ID)
|
||||
tweak {
|
||||
// The wrong pubkey.
|
||||
command(BIG_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
command(BIG_CORP_PUBKEY, CommercialPaper.Commands.Issue())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
`fails with`("output states are issued by a command signer")
|
||||
}
|
||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
verifies()
|
||||
}
|
||||
@ -140,8 +140,8 @@ class CommercialPaperTest {
|
||||
|
||||
// Some CP is issued onto the ledger by MegaCorp.
|
||||
transaction("Issuance") {
|
||||
output(CP_PROGRAM_ID, "paper") { getPaper() }
|
||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
output(CP_PROGRAM_ID, "paper", getPaper())
|
||||
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue())
|
||||
attachments(CP_PROGRAM_ID)
|
||||
timeWindow(TEST_TX_TIME)
|
||||
verifies()
|
||||
@ -151,10 +151,10 @@ class CommercialPaperTest {
|
||||
transaction("Trade") {
|
||||
input("paper")
|
||||
input("alice's $900")
|
||||
output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP }
|
||||
output(CP_PROGRAM_ID, "alice's paper") { "paper".output<ICommercialPaperState>().withOwner(ALICE) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||
output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP)
|
||||
output(CP_PROGRAM_ID, "alice's paper", "paper".output<ICommercialPaperState>().withOwner(ALICE))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
@ -173,8 +173,8 @@ class CommercialPaperTest {
|
||||
|
||||
// Some CP is issued onto the ledger by MegaCorp.
|
||||
transaction("Issuance") {
|
||||
output(CP_PROGRAM_ID, "paper") { getPaper() }
|
||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
output(CP_PROGRAM_ID, "paper", getPaper())
|
||||
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue())
|
||||
attachments(CP_PROGRAM_ID)
|
||||
timeWindow(TEST_TX_TIME)
|
||||
verifies()
|
||||
@ -183,18 +183,18 @@ class CommercialPaperTest {
|
||||
transaction("Trade") {
|
||||
input("paper")
|
||||
input("alice's $900")
|
||||
output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP }
|
||||
output(CP_PROGRAM_ID, "alice's paper") { "paper".output<ICommercialPaperState>().withOwner(ALICE) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||
output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP)
|
||||
output(CP_PROGRAM_ID, "alice's paper", "paper".output<ICommercialPaperState>().withOwner(ALICE))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("paper")
|
||||
// We moved a paper to another pubkey.
|
||||
output(CP_PROGRAM_ID, "bob's paper") { "paper".output<ICommercialPaperState>().withOwner(BOB) }
|
||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||
output(CP_PROGRAM_ID, "bob's paper", "paper".output<ICommercialPaperState>().withOwner(BOB))
|
||||
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
|
||||
@ -215,8 +215,8 @@ class CommercialPaperTest {
|
||||
|
||||
// Some CP is issued onto the ledger by MegaCorp.
|
||||
transaction("Issuance") {
|
||||
output(CP_PROGRAM_ID, "paper") { getPaper() }
|
||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
output(CP_PROGRAM_ID, "paper", getPaper())
|
||||
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Issue())
|
||||
attachments(CP_PROGRAM_ID)
|
||||
timeWindow(TEST_TX_TIME)
|
||||
verifies()
|
||||
@ -225,10 +225,10 @@ class CommercialPaperTest {
|
||||
transaction("Trade") {
|
||||
input("paper")
|
||||
input("alice's $900")
|
||||
output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP }
|
||||
output(CP_PROGRAM_ID, "alice's paper") { "paper".output<ICommercialPaperState>().withOwner(ALICE) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||
output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP)
|
||||
output(CP_PROGRAM_ID, "alice's paper", "paper".output<ICommercialPaperState>().withOwner(ALICE))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
|
||||
@ -236,8 +236,8 @@ class CommercialPaperTest {
|
||||
transaction {
|
||||
input("paper")
|
||||
// We moved a paper to another pubkey.
|
||||
output(CP_PROGRAM_ID, "bob's paper") { "paper".output<ICommercialPaperState>().withOwner(BOB) }
|
||||
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||
output(CP_PROGRAM_ID, "bob's paper", "paper".output<ICommercialPaperState>().withOwner(BOB))
|
||||
command(MEGA_CORP_PUBKEY, CommercialPaper.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
fails()
|
||||
|
@ -28,7 +28,7 @@ class CustomVaultQueryTest {
|
||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance", "net.corda.docs"))
|
||||
nodeA = mockNet.createPartyNode()
|
||||
nodeB = mockNet.createPartyNode()
|
||||
nodeA.internals.registerInitiatedFlow(TopupIssuerFlow.TopupIssuer::class.java)
|
||||
nodeA.registerInitiatedFlow(TopupIssuerFlow.TopupIssuer::class.java)
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ class FxTransactionBuildTutorialTest {
|
||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance"))
|
||||
nodeA = mockNet.createPartyNode()
|
||||
nodeB = mockNet.createPartyNode()
|
||||
nodeB.internals.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java)
|
||||
nodeB.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java)
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@ class WorkflowTransactionBuildTutorialTest {
|
||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.docs"))
|
||||
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
val bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||
aliceNode.internals.registerInitiatedFlow(RecordCompletionFlow::class.java)
|
||||
aliceNode.registerInitiatedFlow(RecordCompletionFlow::class.java)
|
||||
aliceServices = aliceNode.services
|
||||
bobServices = bobNode.services
|
||||
alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME)
|
||||
|
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
|
||||
------------------
|
||||
After following the steps in :doc:`deploying-a-node`, you should have deployed your node(s) with any chosen CorDapps
|
||||
already installed. You run each node by navigating to ``<node_dir>`` in a terminal window and running:
|
||||
.. contents::
|
||||
|
||||
.. note:: You should already have generated your node(s) with their CorDapps installed by following the instructions in
|
||||
:doc:`generating-a-node`.
|
||||
|
||||
There are several ways to run a Corda node locally for testing purposes.
|
||||
|
||||
Starting all nodes at once
|
||||
--------------------------
|
||||
|
||||
.. note:: ``runnodes`` is a shell script (or batch file on Windows) that is generated by ``deployNodes`` to allow you
|
||||
to quickly start up all nodes and their webservers. ``runnodes`` should only be used for testing purposes.
|
||||
|
||||
Start the nodes with ``runnodes`` by running the following command from the root of the project:
|
||||
|
||||
* Linux/macOS: ``build/nodes/runnodes``
|
||||
* Windows: ``call build\nodes\runnodes.bat``
|
||||
|
||||
.. warn:: On macOS, do not click/change focus until all the node terminal windows have opened, or some processes may
|
||||
fail to start.
|
||||
|
||||
Starting an individual Corda node
|
||||
---------------------------------
|
||||
Run the node by opening a terminal window in the node's folder and running:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
java -jar corda.jar
|
||||
|
||||
.. warning:: If your working directory is not ``<node_dir>`` your cordapps and configuration will not be used.
|
||||
.. warning:: By default, the node will look for a configuration file called ``node.conf`` and a CorDapps folder called
|
||||
``cordapps`` in the current working directory. You can override the configuration file and workspace paths on the
|
||||
command line (e.g. ``./corda.jar --config-file=test.conf --base-directory=/opt/r3corda/nodes/test``).
|
||||
|
||||
The configuration file and workspace paths can be overridden on the command line. For example:
|
||||
Optionally run the node's webserver as well by opening a terminal window in the node's folder and running:
|
||||
|
||||
``./corda.jar --config-file=test.conf --base-directory=/opt/r3corda/nodes/test``.
|
||||
.. code-block:: shell
|
||||
|
||||
Otherwise the workspace folder for the node is the current working path.
|
||||
java -jar corda-webserver.jar
|
||||
|
||||
Debugging your node
|
||||
-------------------
|
||||
.. warning:: The node webserver is for testing purposes only and will be removed soon.
|
||||
|
||||
Starting a node with remote debugging enabled
|
||||
---------------------------------------------
|
||||
To enable remote debugging of the node, run the following from the terminal window:
|
||||
|
||||
``java -Dcapsule.jvm.args="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005" -jar corda.jar``
|
||||
|
@ -137,7 +137,7 @@ which could be represented as ``{ first: foo, second: 123 }``.
|
||||
|
||||
.. note:: If your CorDapp is written in Java,
|
||||
named arguments won't work unless you compiled using the ``-parameters`` argument to javac.
|
||||
See :doc:`deploying-a-node` for how to specify it via Gradle.
|
||||
See :doc:`generating-a-node` for how to specify it via Gradle.
|
||||
|
||||
The same syntax is also used to specify the parameters for RPCs, accessed via the ``run`` command, like this:
|
||||
|
||||
|
@ -26,7 +26,8 @@ for gradle and IntelliJ, but it's possible this option is not present in your en
|
||||
"No matching constructor found: - [arg0: int, arg1: Party]: missing parameter arg0"
|
||||
***********************************************************************************
|
||||
|
||||
Your CorDapp is written in Java and you haven't specified the ``-parameters`` compiler argument. See :doc:`deploying-a-node` for how it can be done using Gradle.
|
||||
Your CorDapp is written in Java and you haven't specified the ``-parameters`` compiler argument. See
|
||||
:doc:`generating-a-node` for how it can be done using Gradle.
|
||||
|
||||
IDEA issues
|
||||
-----------
|
||||
|
@ -176,7 +176,7 @@ There are two ways to run the example CorDapp:
|
||||
* Via IntelliJ
|
||||
|
||||
In both cases, we will deploy a set of test nodes with our CorDapp installed, then run the nodes. You can read more
|
||||
about how we define the nodes to be deployed :doc:`here <deploying-a-node>`.
|
||||
about how we define the nodes to be deployed :doc:`here <generating-a-node>`.
|
||||
|
||||
Terminal
|
||||
~~~~~~~~
|
||||
|
@ -172,16 +172,14 @@ class Cap {
|
||||
@Test
|
||||
fun issue() {
|
||||
transaction {
|
||||
output(UNIVERSAL_PROGRAM_ID) { stateInitial }
|
||||
output(UNIVERSAL_PROGRAM_ID, stateInitial)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
|
||||
this `fails with` "the transaction is signed by all liable parties"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -189,44 +187,38 @@ class Cap {
|
||||
@Test
|
||||
fun `first fixing`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { stateInitial }
|
||||
output(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFirst }
|
||||
input(UNIVERSAL_PROGRAM_ID, stateInitial)
|
||||
output(UNIVERSAL_PROGRAM_ID, stateAfterFixingFirst)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
|
||||
this `fails with` "action must be defined"
|
||||
}
|
||||
|
||||
tweak {
|
||||
// wrong source
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("3M")), 1.0.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("3M")), 1.0.bd))))
|
||||
this `fails with` "relevant fixing must be included"
|
||||
}
|
||||
|
||||
tweak {
|
||||
// wrong date
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("3M")), 1.0.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("3M")), 1.0.bd))))
|
||||
this `fails with` "relevant fixing must be included"
|
||||
}
|
||||
|
||||
tweak {
|
||||
// wrong tenor
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("9M")), 1.0.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("9M")), 1.0.bd))))
|
||||
this `fails with` "relevant fixing must be included"
|
||||
}
|
||||
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.5.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.5.bd))))
|
||||
this `fails with` "output state does not reflect fix command"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -234,19 +226,16 @@ class Cap {
|
||||
@Test
|
||||
fun `first execute`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFirst }
|
||||
output(UNIVERSAL_PROGRAM_ID) { stateAfterExecutionFirst }
|
||||
output(UNIVERSAL_PROGRAM_ID) { statePaymentFirst }
|
||||
|
||||
input(UNIVERSAL_PROGRAM_ID, stateAfterFixingFirst)
|
||||
output(UNIVERSAL_PROGRAM_ID, stateAfterExecutionFirst)
|
||||
output(UNIVERSAL_PROGRAM_ID, statePaymentFirst)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
|
||||
this `fails with` "action must be defined"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("exercise") }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("exercise"))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -254,18 +243,15 @@ class Cap {
|
||||
@Test
|
||||
fun `final execute`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFinal }
|
||||
output(UNIVERSAL_PROGRAM_ID) { statePaymentFinal }
|
||||
|
||||
input(UNIVERSAL_PROGRAM_ID, stateAfterFixingFinal)
|
||||
output(UNIVERSAL_PROGRAM_ID, statePaymentFinal)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
|
||||
this `fails with` "action must be defined"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("exercise") }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("exercise"))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -273,44 +259,38 @@ class Cap {
|
||||
@Test
|
||||
fun `second fixing`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { stateAfterExecutionFirst }
|
||||
output(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFinal }
|
||||
input(UNIVERSAL_PROGRAM_ID, stateAfterExecutionFirst)
|
||||
output(UNIVERSAL_PROGRAM_ID, stateAfterFixingFinal)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
|
||||
this `fails with` "action must be defined"
|
||||
}
|
||||
|
||||
tweak {
|
||||
// wrong source
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.0.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.0.bd))))
|
||||
this `fails with` "relevant fixing must be included"
|
||||
}
|
||||
|
||||
tweak {
|
||||
// wrong date
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01").plusYears(1), Tenor("3M")), 1.0.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01").plusYears(1), Tenor("3M")), 1.0.bd))))
|
||||
this `fails with` "relevant fixing must be included"
|
||||
}
|
||||
|
||||
tweak {
|
||||
// wrong tenor
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("9M")), 1.0.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("9M")), 1.0.bd))))
|
||||
this `fails with` "relevant fixing must be included"
|
||||
}
|
||||
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.5.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.5.bd))))
|
||||
this `fails with` "output state does not reflect fix command"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.0.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.0.bd))))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
|
@ -55,16 +55,14 @@ class Caplet {
|
||||
@Test
|
||||
fun issue() {
|
||||
transaction {
|
||||
output(UNIVERSAL_PROGRAM_ID) { stateStart }
|
||||
output(UNIVERSAL_PROGRAM_ID, stateStart)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
|
||||
this `fails with` "the transaction is signed by all liable parties"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -72,17 +70,15 @@ class Caplet {
|
||||
@Test
|
||||
fun `execute`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { stateFixed }
|
||||
output(UNIVERSAL_PROGRAM_ID) { stateFinal }
|
||||
input(UNIVERSAL_PROGRAM_ID, stateFixed)
|
||||
output(UNIVERSAL_PROGRAM_ID, stateFinal)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
|
||||
this `fails with` "action must be defined"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("exercise") }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("exercise"))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -90,44 +86,38 @@ class Caplet {
|
||||
@Test
|
||||
fun `fixing`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { stateStart }
|
||||
output(UNIVERSAL_PROGRAM_ID) { stateFixed }
|
||||
input(UNIVERSAL_PROGRAM_ID, stateStart)
|
||||
output(UNIVERSAL_PROGRAM_ID, stateFixed)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
|
||||
this `fails with` "action must be defined"
|
||||
}
|
||||
|
||||
tweak {
|
||||
// wrong source
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("6M")), 1.0.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("6M")), 1.0.bd))))
|
||||
this `fails with` "relevant fixing must be included"
|
||||
}
|
||||
|
||||
tweak {
|
||||
// wrong date
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("6M")), 1.0.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("6M")), 1.0.bd))))
|
||||
this `fails with` "relevant fixing must be included"
|
||||
}
|
||||
|
||||
tweak {
|
||||
// wrong tenor
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))))
|
||||
this `fails with` "relevant fixing must be included"
|
||||
}
|
||||
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("6M")), 1.5.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("6M")), 1.5.bd))))
|
||||
this `fails with` "output state does not reflect fix command"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("6M")), 1.0.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("6M")), 1.0.bd))))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
|
@ -52,20 +52,18 @@ class FXFwdTimeOption {
|
||||
@Test
|
||||
fun `issue - signature`() {
|
||||
transaction {
|
||||
output(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
output(UNIVERSAL_PROGRAM_ID, inState)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
|
||||
this `fails with` "the transaction is signed by all liable parties"
|
||||
}
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
|
||||
this `fails with` "the transaction is signed by all liable parties"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey, acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
||||
|
||||
command(listOf(highStreetBank.owningKey, acmeCorp.owningKey), UniversalContract.Commands.Issue())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -73,31 +71,28 @@ class FXFwdTimeOption {
|
||||
@Test
|
||||
fun `maturity, bank exercise`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState2 }
|
||||
|
||||
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState2)
|
||||
timeWindow(TEST_TX_TIME_AFTER_MATURITY)
|
||||
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
|
||||
this `fails with` "action must be defined"
|
||||
}
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("exercise") }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("exercise"))
|
||||
this `fails with` "condition must be met"
|
||||
}
|
||||
tweak {
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("exercise") }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Action("exercise"))
|
||||
this `fails with` "condition must be met"
|
||||
}
|
||||
tweak {
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("expire") }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Action("expire"))
|
||||
this `fails with` "condition must be met"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("expire") }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("expire"))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -105,31 +100,28 @@ class FXFwdTimeOption {
|
||||
@Test
|
||||
fun `maturity, corp exercise`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState2 }
|
||||
|
||||
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState2)
|
||||
timeWindow(TEST_TX_TIME_BEFORE_MATURITY)
|
||||
|
||||
tweak {
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("some undefined name") }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Action("some undefined name"))
|
||||
this `fails with` "action must be defined"
|
||||
}
|
||||
tweak {
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("expire") }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Action("expire"))
|
||||
this `fails with` "condition must be met"
|
||||
}
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("expire") }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("expire"))
|
||||
this `fails with` "condition must be met"
|
||||
}
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("exercise") }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("exercise"))
|
||||
this `fails with` "condition must be met"
|
||||
}
|
||||
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("exercise") }
|
||||
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Action("exercise"))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
|
@ -44,20 +44,18 @@ class FXSwap {
|
||||
fun `issue - signature`() {
|
||||
|
||||
transaction {
|
||||
output(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
output(UNIVERSAL_PROGRAM_ID, inState)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
|
||||
this `fails with` "the transaction is signed by all liable parties"
|
||||
}
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
|
||||
this `fails with` "the transaction is signed by all liable parties"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey, acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
||||
|
||||
command(listOf(highStreetBank.owningKey, acmeCorp.owningKey), UniversalContract.Commands.Issue())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -65,18 +63,16 @@ class FXSwap {
|
||||
@Test
|
||||
fun `execute`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState2 }
|
||||
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState2)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
|
||||
this `fails with` "action must be defined"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("execute") }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("execute"))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -84,18 +80,16 @@ class FXSwap {
|
||||
@Test
|
||||
fun `execute - reversed order`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState2 }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
||||
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState2)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
|
||||
this `fails with` "action must be defined"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("execute") }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("execute"))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -103,12 +97,11 @@ class FXSwap {
|
||||
@Test
|
||||
fun `execute - not authorized`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState2 }
|
||||
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState2)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
command(momAndPop.owningKey) { UniversalContract.Commands.Action("execute") }
|
||||
command(momAndPop.owningKey, UniversalContract.Commands.Action("execute"))
|
||||
this `fails with` "condition must be met"
|
||||
}
|
||||
}
|
||||
@ -116,12 +109,11 @@ class FXSwap {
|
||||
@Test
|
||||
fun `execute - before maturity`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState2 }
|
||||
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState2)
|
||||
timeWindow(TEST_TX_TIME_TOO_EARLY)
|
||||
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Action("execute"))
|
||||
this `fails with` "condition must be met"
|
||||
}
|
||||
}
|
||||
@ -129,11 +121,10 @@ class FXSwap {
|
||||
@Test
|
||||
fun `execute - outState mismatch 1`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
||||
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Action("execute"))
|
||||
this `fails with` "output state must match action result state"
|
||||
}
|
||||
}
|
||||
@ -141,12 +132,11 @@ class FXSwap {
|
||||
@Test
|
||||
fun `execute - outState mismatch 2`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outStateBad2 }
|
||||
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||
output(UNIVERSAL_PROGRAM_ID, outStateBad2)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Action("execute"))
|
||||
this `fails with` "output states must match action result state"
|
||||
}
|
||||
}
|
||||
@ -154,12 +144,11 @@ class FXSwap {
|
||||
@Test
|
||||
fun `execute - outState mismatch 3`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outStateBad1 }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState2 }
|
||||
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||
output(UNIVERSAL_PROGRAM_ID, outStateBad1)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState2)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Action("execute"))
|
||||
this `fails with` "output states must match action result state"
|
||||
}
|
||||
}
|
||||
@ -167,12 +156,11 @@ class FXSwap {
|
||||
@Test
|
||||
fun `execute - outState mismatch 4`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState1 }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outStateBad3 }
|
||||
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState1)
|
||||
output(UNIVERSAL_PROGRAM_ID, outStateBad3)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Action("execute"))
|
||||
this `fails with` "output states must match action result state"
|
||||
}
|
||||
}
|
||||
|
@ -134,16 +134,14 @@ class IRS {
|
||||
@Test
|
||||
fun issue() {
|
||||
transaction {
|
||||
output(UNIVERSAL_PROGRAM_ID) { stateInitial }
|
||||
output(UNIVERSAL_PROGRAM_ID, stateInitial)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
|
||||
this `fails with` "the transaction is signed by all liable parties"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -151,44 +149,38 @@ class IRS {
|
||||
@Test
|
||||
fun `first fixing`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { stateInitial }
|
||||
output(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFirst }
|
||||
input(UNIVERSAL_PROGRAM_ID, stateInitial)
|
||||
output(UNIVERSAL_PROGRAM_ID, stateAfterFixingFirst)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
|
||||
this `fails with` "action must be defined"
|
||||
}
|
||||
|
||||
tweak {
|
||||
// wrong source
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("3M")), 1.0.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("3M")), 1.0.bd))))
|
||||
this `fails with` "relevant fixing must be included"
|
||||
}
|
||||
|
||||
tweak {
|
||||
// wrong date
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("3M")), 1.0.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("3M")), 1.0.bd))))
|
||||
this `fails with` "relevant fixing must be included"
|
||||
}
|
||||
|
||||
tweak {
|
||||
// wrong tenor
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("9M")), 1.0.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("9M")), 1.0.bd))))
|
||||
this `fails with` "relevant fixing must be included"
|
||||
}
|
||||
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.5.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.5.bd))))
|
||||
this `fails with` "output state does not reflect fix command"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -196,19 +188,16 @@ class IRS {
|
||||
@Test
|
||||
fun `first execute`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { stateAfterFixingFirst }
|
||||
output(UNIVERSAL_PROGRAM_ID) { stateAfterExecutionFirst }
|
||||
output(UNIVERSAL_PROGRAM_ID) { statePaymentFirst }
|
||||
|
||||
input(UNIVERSAL_PROGRAM_ID, stateAfterFixingFirst)
|
||||
output(UNIVERSAL_PROGRAM_ID, stateAfterExecutionFirst)
|
||||
output(UNIVERSAL_PROGRAM_ID, statePaymentFirst)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
|
||||
this `fails with` "action must be defined"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("pay floating") }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("pay floating"))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
|
@ -145,16 +145,14 @@ class RollOutTests {
|
||||
@Test
|
||||
fun issue() {
|
||||
transaction {
|
||||
output(UNIVERSAL_PROGRAM_ID) { stateStart }
|
||||
output(UNIVERSAL_PROGRAM_ID, stateStart)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
|
||||
this `fails with` "the transaction is signed by all liable parties"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -162,18 +160,16 @@ class RollOutTests {
|
||||
@Test
|
||||
fun `execute`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { stateStart }
|
||||
output(UNIVERSAL_PROGRAM_ID) { stateStep1a }
|
||||
output(UNIVERSAL_PROGRAM_ID) { stateStep1b }
|
||||
input(UNIVERSAL_PROGRAM_ID, stateStart)
|
||||
output(UNIVERSAL_PROGRAM_ID, stateStep1a)
|
||||
output(UNIVERSAL_PROGRAM_ID, stateStep1b)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
/* tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
|
||||
this `fails with` "action must be defined"
|
||||
}*/
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("transfer") }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("transfer"))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
|
@ -61,16 +61,14 @@ class Swaption {
|
||||
@Test
|
||||
fun issue() {
|
||||
transaction {
|
||||
output(UNIVERSAL_PROGRAM_ID) { stateInitial }
|
||||
output(UNIVERSAL_PROGRAM_ID, stateInitial)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
|
||||
this `fails with` "the transaction is signed by all liable parties"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
|
@ -51,15 +51,12 @@ class ZeroCouponBond {
|
||||
@Test
|
||||
fun `issue - signature`() {
|
||||
transaction {
|
||||
output(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
|
||||
output(UNIVERSAL_PROGRAM_ID, inState)
|
||||
tweak {
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Issue())
|
||||
this `fails with` "the transaction is signed by all liable parties"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Issue() }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Issue())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -67,17 +64,15 @@ class ZeroCouponBond {
|
||||
@Test
|
||||
fun `execute`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState }
|
||||
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
tweak {
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") }
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("some undefined name"))
|
||||
this `fails with` "action must be defined"
|
||||
}
|
||||
|
||||
command(highStreetBank.owningKey) { UniversalContract.Commands.Action("execute") }
|
||||
|
||||
command(highStreetBank.owningKey, UniversalContract.Commands.Action("execute"))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -85,11 +80,10 @@ class ZeroCouponBond {
|
||||
@Test
|
||||
fun `execute - not authorized`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outState }
|
||||
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||
output(UNIVERSAL_PROGRAM_ID, outState)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
command(momAndPop.owningKey) { UniversalContract.Commands.Action("execute") }
|
||||
command(momAndPop.owningKey, UniversalContract.Commands.Action("execute"))
|
||||
this `fails with` "condition must be met"
|
||||
}
|
||||
}
|
||||
@ -97,11 +91,10 @@ class ZeroCouponBond {
|
||||
@Test
|
||||
fun `execute - outState mismatch`() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
output(UNIVERSAL_PROGRAM_ID) { outStateWrong }
|
||||
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||
output(UNIVERSAL_PROGRAM_ID, outStateWrong)
|
||||
timeWindow(TEST_TX_TIME_1)
|
||||
|
||||
command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") }
|
||||
command(acmeCorp.owningKey, UniversalContract.Commands.Action("execute"))
|
||||
this `fails with` "output state must match action result state"
|
||||
}
|
||||
}
|
||||
@ -109,29 +102,23 @@ class ZeroCouponBond {
|
||||
@Test
|
||||
fun move() {
|
||||
transaction {
|
||||
input(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
|
||||
input(UNIVERSAL_PROGRAM_ID, inState)
|
||||
tweak {
|
||||
output(UNIVERSAL_PROGRAM_ID) { outStateMove }
|
||||
command(acmeCorp.owningKey) {
|
||||
UniversalContract.Commands.Move(acmeCorp, momAndPop)
|
||||
}
|
||||
output(UNIVERSAL_PROGRAM_ID, outStateMove)
|
||||
command(acmeCorp.owningKey,
|
||||
UniversalContract.Commands.Move(acmeCorp, momAndPop))
|
||||
this `fails with` "the transaction is signed by all liable parties"
|
||||
}
|
||||
|
||||
tweak {
|
||||
output(UNIVERSAL_PROGRAM_ID) { inState }
|
||||
command(acmeCorp.owningKey, momAndPop.owningKey, highStreetBank.owningKey) {
|
||||
UniversalContract.Commands.Move(acmeCorp, momAndPop)
|
||||
}
|
||||
output(UNIVERSAL_PROGRAM_ID, inState)
|
||||
command(listOf(acmeCorp.owningKey, momAndPop.owningKey, highStreetBank.owningKey),
|
||||
UniversalContract.Commands.Move(acmeCorp, momAndPop))
|
||||
this `fails with` "output state does not reflect move command"
|
||||
}
|
||||
|
||||
output(UNIVERSAL_PROGRAM_ID) { outStateMove }
|
||||
|
||||
command(acmeCorp.owningKey, momAndPop.owningKey, highStreetBank.owningKey) {
|
||||
UniversalContract.Commands.Move(acmeCorp, momAndPop)
|
||||
}
|
||||
output(UNIVERSAL_PROGRAM_ID, outStateMove)
|
||||
command(listOf(acmeCorp.owningKey, momAndPop.owningKey, highStreetBank.owningKey),
|
||||
UniversalContract.Commands.Move(acmeCorp, momAndPop))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
|
@ -67,13 +67,14 @@ abstract class AbstractCashSelection(private val maxRetries : Int = 8, private v
|
||||
* with this notary are included.
|
||||
* @param onlyFromIssuerParties Optional issuer parties to match against.
|
||||
* @param withIssuerRefs Optional issuer references to match against.
|
||||
* @return JDBC ResultSet with the matching states that were found. If sufficient funds were found these will be locked,
|
||||
* @param withResultSet Function that contains the business logic. The JDBC ResultSet with the matching states that were found. If sufficient funds were found these will be locked,
|
||||
* otherwise what is available is returned unlocked for informational purposes.
|
||||
* @return The result of the withResultSet function
|
||||
*/
|
||||
abstract fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?,
|
||||
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet
|
||||
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean
|
||||
|
||||
override abstract fun toString() : String
|
||||
override abstract fun toString(): String
|
||||
|
||||
/**
|
||||
* Query to gather Cash states that are available and retry if they are temporarily unavailable.
|
||||
@ -122,34 +123,40 @@ abstract class AbstractCashSelection(private val maxRetries : Int = 8, private v
|
||||
try {
|
||||
// we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null)
|
||||
// the softLockReserve update will detect whether we try to lock states locked by others
|
||||
val rs = executeQuery(connection, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs)
|
||||
stateAndRefs.clear()
|
||||
return executeQuery(connection, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs) { rs ->
|
||||
stateAndRefs.clear()
|
||||
|
||||
var totalPennies = 0L
|
||||
val stateRefs = mutableSetOf<StateRef>()
|
||||
while (rs.next()) {
|
||||
val txHash = SecureHash.parse(rs.getString(1))
|
||||
val index = rs.getInt(2)
|
||||
val pennies = rs.getLong(3)
|
||||
totalPennies = rs.getLong(4)
|
||||
val rowLockId = rs.getString(5)
|
||||
stateRefs.add(StateRef(txHash, index))
|
||||
log.trace { "ROW: $rowLockId ($lockId): ${StateRef(txHash, index)} : $pennies ($totalPennies)" }
|
||||
var totalPennies = 0L
|
||||
val stateRefs = mutableSetOf<StateRef>()
|
||||
while (rs.next()) {
|
||||
val txHash = SecureHash.parse(rs.getString(1))
|
||||
val index = rs.getInt(2)
|
||||
val pennies = rs.getLong(3)
|
||||
totalPennies = rs.getLong(4)
|
||||
val rowLockId = rs.getString(5)
|
||||
stateRefs.add(StateRef(txHash, index))
|
||||
log.trace { "ROW: $rowLockId ($lockId): ${StateRef(txHash, index)} : $pennies ($totalPennies)" }
|
||||
}
|
||||
|
||||
if (stateRefs.isNotEmpty()) {
|
||||
// TODO: future implementation to retrieve contract states from a Vault BLOB store
|
||||
stateAndRefs.addAll(services.loadStates(stateRefs) as Collection<StateAndRef<Cash.State>>)
|
||||
}
|
||||
|
||||
val success = stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity
|
||||
if (success) {
|
||||
// we should have a minimum number of states to satisfy our selection `amount` criteria
|
||||
log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs")
|
||||
|
||||
// With the current single threaded state machine available states are guaranteed to lock.
|
||||
// TODO However, we will have to revisit these methods in the future multi-threaded.
|
||||
services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet())
|
||||
} else {
|
||||
log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}")
|
||||
}
|
||||
success
|
||||
}
|
||||
if (stateRefs.isNotEmpty())
|
||||
// TODO: future implementation to retrieve contract states from a Vault BLOB store
|
||||
stateAndRefs.addAll(services.loadStates(stateRefs) as Collection<StateAndRef<Cash.State>>)
|
||||
|
||||
if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) {
|
||||
// we should have a minimum number of states to satisfy our selection `amount` criteria
|
||||
log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs")
|
||||
|
||||
// With the current single threaded state machine available states are guaranteed to lock.
|
||||
// TODO However, we will have to revisit these methods in the future multi-threaded.
|
||||
services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet())
|
||||
return true
|
||||
}
|
||||
log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}")
|
||||
// retry as more states may become available
|
||||
} catch (e: SQLException) {
|
||||
log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId]
|
||||
|
@ -30,9 +30,8 @@ class CashSelectionH2Impl : AbstractCashSelection() {
|
||||
// 2) H2 uses session variables to perform this accumulator function:
|
||||
// http://www.h2database.com/html/functions.html#set
|
||||
// 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries)
|
||||
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?,
|
||||
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet {
|
||||
connection.createStatement().execute("CALL SET(@t, CAST(0 AS BIGINT));")
|
||||
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
|
||||
connection.createStatement().use { it.execute("CALL SET(@t, CAST(0 AS BIGINT));") }
|
||||
|
||||
val selectJoin = """
|
||||
SELECT vs.transaction_id, vs.output_index, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
|
||||
@ -50,19 +49,22 @@ class CashSelectionH2Impl : AbstractCashSelection() {
|
||||
" AND ccs.issuer_ref IN (?)" else "")
|
||||
|
||||
// Use prepared statement for protection against SQL Injection (http://www.h2database.com/html/advanced.html#sql_injection)
|
||||
val psSelectJoin = connection.prepareStatement(selectJoin)
|
||||
var pIndex = 0
|
||||
psSelectJoin.setString(++pIndex, amount.token.currencyCode)
|
||||
psSelectJoin.setLong(++pIndex, amount.quantity)
|
||||
psSelectJoin.setString(++pIndex, lockId.toString())
|
||||
if (notary != null)
|
||||
psSelectJoin.setString(++pIndex, notary.name.toString())
|
||||
if (onlyFromIssuerParties.isNotEmpty())
|
||||
psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toStringShort() as Any}.toTypedArray() )
|
||||
if (withIssuerRefs.isNotEmpty())
|
||||
psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray())
|
||||
log.debug { psSelectJoin.toString() }
|
||||
connection.prepareStatement(selectJoin).use { psSelectJoin ->
|
||||
var pIndex = 0
|
||||
psSelectJoin.setString(++pIndex, amount.token.currencyCode)
|
||||
psSelectJoin.setLong(++pIndex, amount.quantity)
|
||||
psSelectJoin.setString(++pIndex, lockId.toString())
|
||||
if (notary != null)
|
||||
psSelectJoin.setString(++pIndex, notary.name.toString())
|
||||
if (onlyFromIssuerParties.isNotEmpty())
|
||||
psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toStringShort() as Any }.toTypedArray())
|
||||
if (withIssuerRefs.isNotEmpty())
|
||||
psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray())
|
||||
log.debug { psSelectJoin.toString() }
|
||||
|
||||
return psSelectJoin.executeQuery()
|
||||
psSelectJoin.executeQuery().use { rs ->
|
||||
return withResultSet(rs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@ class CashSelectionMySQLImpl : AbstractCashSelection() {
|
||||
return metadata.driverName == JDBC_DRIVER_NAME
|
||||
}
|
||||
|
||||
override fun executeQuery(statement: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, issuerKeysStr: Set<AbstractParty>, issuerRefsStr: Set<OpaqueBytes>): ResultSet {
|
||||
override fun executeQuery(statement: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, issuerKeysStr: Set<AbstractParty>, issuerRefsStr: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
|
||||
TODO("MySQL cash selection not implemented")
|
||||
}
|
||||
|
||||
|
@ -27,8 +27,7 @@ class CashSelectionPostgreSQLImpl : AbstractCashSelection() {
|
||||
// 2) The window function accumulated column (`total`) does not include the current row (starts from 0) and cannot
|
||||
// appear in the WHERE clause, hence restricting row selection and adjusting the returned total in the outer query.
|
||||
// 3) Currently (version 9.6), FOR UPDATE cannot be specified with window functions
|
||||
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?,
|
||||
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet {
|
||||
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
|
||||
val selectJoin = """SELECT nested.transaction_id, nested.output_index, nested.pennies,
|
||||
nested.total+nested.pennies as total_pennies, nested.lock_id
|
||||
FROM
|
||||
@ -51,29 +50,32 @@ class CashSelectionPostgreSQLImpl : AbstractCashSelection() {
|
||||
nested WHERE nested.total < ?
|
||||
"""
|
||||
|
||||
val statement = connection.prepareStatement(selectJoin)
|
||||
statement.setString(1, amount.token.toString())
|
||||
statement.setString(2, lockId.toString())
|
||||
var paramOffset = 0
|
||||
if (notary != null) {
|
||||
statement.setString(3, notary.name.toString())
|
||||
paramOffset += 1
|
||||
}
|
||||
if (onlyFromIssuerParties.isNotEmpty()) {
|
||||
val issuerKeys = connection.createArrayOf("VARCHAR", onlyFromIssuerParties.map
|
||||
{ it.owningKey.toBase58String() }.toTypedArray())
|
||||
statement.setArray(3 + paramOffset, issuerKeys)
|
||||
paramOffset += 1
|
||||
}
|
||||
if (withIssuerRefs.isNotEmpty()) {
|
||||
val issuerRefs = connection.createArrayOf("BYTEA", withIssuerRefs.map
|
||||
{ it.bytes }.toTypedArray())
|
||||
statement.setArray(3 + paramOffset, issuerRefs)
|
||||
paramOffset += 1
|
||||
}
|
||||
statement.setLong(3 + paramOffset, amount.quantity)
|
||||
log.debug { statement.toString() }
|
||||
connection.prepareStatement(selectJoin).use { statement ->
|
||||
statement.setString(1, amount.token.toString())
|
||||
statement.setString(2, lockId.toString())
|
||||
var paramOffset = 0
|
||||
if (notary != null) {
|
||||
statement.setString(3, notary.name.toString())
|
||||
paramOffset += 1
|
||||
}
|
||||
if (onlyFromIssuerParties.isNotEmpty()) {
|
||||
val issuerKeys = connection.createArrayOf("VARCHAR", onlyFromIssuerParties.map
|
||||
{ it.owningKey.toBase58String() }.toTypedArray())
|
||||
statement.setArray(3 + paramOffset, issuerKeys)
|
||||
paramOffset += 1
|
||||
}
|
||||
if (withIssuerRefs.isNotEmpty()) {
|
||||
val issuerRefs = connection.createArrayOf("BYTEA", withIssuerRefs.map
|
||||
{ it.bytes }.toTypedArray())
|
||||
statement.setArray(3 + paramOffset, issuerRefs)
|
||||
paramOffset += 1
|
||||
}
|
||||
statement.setLong(3 + paramOffset, amount.quantity)
|
||||
log.debug { statement.toString() }
|
||||
|
||||
return statement.executeQuery()
|
||||
statement.executeQuery().use { rs ->
|
||||
return withResultSet(rs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ class CashSelectionSQLServerImpl : AbstractCashSelection() {
|
||||
override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME"
|
||||
|
||||
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?,
|
||||
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet {
|
||||
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
|
||||
|
||||
val selectJoin = """
|
||||
WITH row(transaction_id, output_index, pennies, total, lock_id) AS
|
||||
@ -51,19 +51,22 @@ class CashSelectionSQLServerImpl : AbstractCashSelection() {
|
||||
FROM row where row.total <= ? + row.pennies"""
|
||||
|
||||
// Use prepared statement for protection against SQL Injection
|
||||
val psSelectJoin = connection.prepareStatement(selectJoin)
|
||||
var pIndex = 0
|
||||
psSelectJoin.setString(++pIndex, amount.token.currencyCode)
|
||||
psSelectJoin.setString(++pIndex, lockId.toString())
|
||||
if (notary != null)
|
||||
psSelectJoin.setString(++pIndex, notary.name.toString())
|
||||
if (onlyFromIssuerParties.isNotEmpty())
|
||||
psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any}.toTypedArray() )
|
||||
if (withIssuerRefs.isNotEmpty())
|
||||
psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray())
|
||||
psSelectJoin.setLong(++pIndex, amount.quantity)
|
||||
log.debug(selectJoin)
|
||||
connection.prepareStatement(selectJoin).use { statement ->
|
||||
var pIndex = 0
|
||||
statement.setString(++pIndex, amount.token.currencyCode)
|
||||
statement.setString(++pIndex, lockId.toString())
|
||||
if (notary != null)
|
||||
statement.setString(++pIndex, notary.name.toString())
|
||||
if (onlyFromIssuerParties.isNotEmpty())
|
||||
statement.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any }.toTypedArray())
|
||||
if (withIssuerRefs.isNotEmpty())
|
||||
statement.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray())
|
||||
statement.setLong(++pIndex, amount.quantity)
|
||||
log.debug(selectJoin)
|
||||
|
||||
return psSelectJoin.executeQuery()
|
||||
statement.executeQuery().use { rs ->
|
||||
return withResultSet(rs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -32,34 +32,34 @@ public class CashTestsJava {
|
||||
tx.input(Cash.PROGRAM_ID, inState);
|
||||
|
||||
tx.tweak(tw -> {
|
||||
tw.output(Cash.PROGRAM_ID, () -> new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), new AnonymousParty(getMINI_CORP_PUBKEY())));
|
||||
tw.output(Cash.PROGRAM_ID, new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), new AnonymousParty(getMINI_CORP_PUBKEY())));
|
||||
tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
|
||||
return tw.failsWith("the amounts balance");
|
||||
});
|
||||
|
||||
tx.tweak(tw -> {
|
||||
tw.output(Cash.PROGRAM_ID, () -> outState);
|
||||
tw.output(Cash.PROGRAM_ID, outState);
|
||||
tw.command(getMEGA_CORP_PUBKEY(), DummyCommandData.INSTANCE);
|
||||
// Invalid command
|
||||
return tw.failsWith("required net.corda.finance.contracts.asset.Cash.Commands.Move command");
|
||||
});
|
||||
tx.tweak(tw -> {
|
||||
tw.output(Cash.PROGRAM_ID, () -> outState);
|
||||
tw.output(Cash.PROGRAM_ID, outState);
|
||||
tw.command(getMINI_CORP_PUBKEY(), new Cash.Commands.Move());
|
||||
return tw.failsWith("the owning keys are a subset of the signing keys");
|
||||
});
|
||||
tx.tweak(tw -> {
|
||||
tw.output(Cash.PROGRAM_ID, () -> outState);
|
||||
tw.output(Cash.PROGRAM_ID, outState);
|
||||
// issuedBy() can't be directly imported because it conflicts with other identically named functions
|
||||
// with different overloads (for some reason).
|
||||
tw.output(Cash.PROGRAM_ID, () -> outState.issuedBy(getMINI_CORP()));
|
||||
tw.output(Cash.PROGRAM_ID, outState.issuedBy(getMINI_CORP()));
|
||||
tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
|
||||
return tw.failsWith("at least one cash input");
|
||||
});
|
||||
|
||||
// Simple reallocation works.
|
||||
return tx.tweak(tw -> {
|
||||
tw.output(Cash.PROGRAM_ID, () -> outState);
|
||||
tw.output(Cash.PROGRAM_ID, outState);
|
||||
tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
|
||||
return tw.verifies();
|
||||
});
|
||||
|
@ -13,7 +13,7 @@ import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.`issued by`
|
||||
import net.corda.finance.contracts.asset.*
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.fillWithSomeTestCash
|
||||
import net.corda.testing.contracts.VaultFiller
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
|
||||
import org.junit.Ignore
|
||||
@ -106,8 +106,8 @@ class CommercialPaperTestsGeneric {
|
||||
// Some CP is issued onto the ledger by MegaCorp.
|
||||
transaction("Issuance") {
|
||||
attachments(CP_PROGRAM_ID, JavaCommercialPaper.JCP_PROGRAM_ID)
|
||||
output(thisTest.getContract(), "paper") { thisTest.getPaper() }
|
||||
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
|
||||
output(thisTest.getContract(), "paper", thisTest.getPaper())
|
||||
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
@ -118,10 +118,10 @@ class CommercialPaperTestsGeneric {
|
||||
attachments(Cash.PROGRAM_ID, JavaCommercialPaper.JCP_PROGRAM_ID)
|
||||
input("paper")
|
||||
input("alice's $900")
|
||||
output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP }
|
||||
output(thisTest.getContract(), "alice's paper") { "paper".output<ICommercialPaperState>().withOwner(ALICE) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() }
|
||||
output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP)
|
||||
output(thisTest.getContract(), "alice's paper", "paper".output<ICommercialPaperState>().withOwner(ALICE))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
command(MEGA_CORP_PUBKEY, thisTest.getMoveCommand())
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
@ -133,13 +133,11 @@ class CommercialPaperTestsGeneric {
|
||||
input("some profits")
|
||||
|
||||
fun TransactionDSL<TransactionDSLInterpreter>.outputs(aliceGetsBack: Amount<Issued<Currency>>) {
|
||||
output(Cash.PROGRAM_ID, "Alice's profit") { aliceGetsBack.STATE ownedBy ALICE }
|
||||
output(Cash.PROGRAM_ID, "Change") { (someProfits - aliceGetsBack).STATE ownedBy MEGA_CORP }
|
||||
output(Cash.PROGRAM_ID, "Alice's profit", aliceGetsBack.STATE ownedBy ALICE)
|
||||
output(Cash.PROGRAM_ID, "Change", (someProfits - aliceGetsBack).STATE ownedBy MEGA_CORP)
|
||||
}
|
||||
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
command(ALICE_PUBKEY) { thisTest.getRedeemCommand(DUMMY_NOTARY) }
|
||||
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
|
||||
command(ALICE_PUBKEY, thisTest.getRedeemCommand(DUMMY_NOTARY))
|
||||
tweak {
|
||||
outputs(700.DOLLARS `issued by` issuer)
|
||||
timeWindow(TEST_TX_TIME + 8.days)
|
||||
@ -155,7 +153,7 @@ class CommercialPaperTestsGeneric {
|
||||
timeWindow(TEST_TX_TIME + 8.days)
|
||||
|
||||
tweak {
|
||||
output(thisTest.getContract()) { "paper".output<ICommercialPaperState>() }
|
||||
output(thisTest.getContract(), "paper".output<ICommercialPaperState>())
|
||||
this `fails with` "must be destroyed"
|
||||
}
|
||||
|
||||
@ -169,8 +167,8 @@ class CommercialPaperTestsGeneric {
|
||||
transaction {
|
||||
attachment(CP_PROGRAM_ID)
|
||||
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
|
||||
output(thisTest.getContract()) { thisTest.getPaper() }
|
||||
command(MINI_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
|
||||
output(thisTest.getContract(), thisTest.getPaper())
|
||||
command(MINI_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "output states are issued by a command signer"
|
||||
}
|
||||
@ -181,8 +179,8 @@ class CommercialPaperTestsGeneric {
|
||||
transaction {
|
||||
attachment(CP_PROGRAM_ID)
|
||||
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
|
||||
output(thisTest.getContract()) { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) }
|
||||
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
|
||||
output(thisTest.getContract(), thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer))
|
||||
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "output values sum to more than the inputs"
|
||||
}
|
||||
@ -193,8 +191,8 @@ class CommercialPaperTestsGeneric {
|
||||
transaction {
|
||||
attachment(CP_PROGRAM_ID)
|
||||
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
|
||||
output(thisTest.getContract()) { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) }
|
||||
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
|
||||
output(thisTest.getContract(), thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days))
|
||||
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "maturity date is not in the past"
|
||||
}
|
||||
@ -206,8 +204,8 @@ class CommercialPaperTestsGeneric {
|
||||
attachment(CP_PROGRAM_ID)
|
||||
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
|
||||
input(thisTest.getContract(), thisTest.getPaper())
|
||||
output(thisTest.getContract()) { thisTest.getPaper() }
|
||||
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
|
||||
output(thisTest.getContract(), thisTest.getPaper())
|
||||
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "output values sum to more than the inputs"
|
||||
}
|
||||
@ -240,7 +238,7 @@ class CommercialPaperTestsGeneric {
|
||||
aliceVaultService = aliceServices.vaultService
|
||||
|
||||
databaseAlice.transaction {
|
||||
alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, issuerServices, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = DUMMY_CASH_ISSUER)
|
||||
alicesVault = VaultFiller(aliceServices, DUMMY_NOTARY, DUMMY_NOTARY_KEY, rngFactory = ::Random).fillWithSomeTestCash(9000.DOLLARS, issuerServices, 1, DUMMY_CASH_ISSUER)
|
||||
aliceVaultService = aliceServices.vaultService
|
||||
}
|
||||
|
||||
@ -250,7 +248,7 @@ class CommercialPaperTestsGeneric {
|
||||
bigCorpVaultService = bigCorpServices.vaultService
|
||||
|
||||
databaseBigCorp.transaction {
|
||||
bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, issuerServices, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = DUMMY_CASH_ISSUER)
|
||||
bigCorpVault = VaultFiller(bigCorpServices, DUMMY_NOTARY, DUMMY_NOTARY_KEY, rngFactory = ::Random).fillWithSomeTestCash(13000.DOLLARS, issuerServices, 1, DUMMY_CASH_ISSUER)
|
||||
bigCorpVaultService = bigCorpServices.vaultService
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,10 @@ package net.corda.finance.contracts.asset
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.identity.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.node.services.queryBy
|
||||
@ -16,10 +19,10 @@ import net.corda.finance.utils.sumCashBy
|
||||
import net.corda.finance.utils.sumCashOrNull
|
||||
import net.corda.finance.utils.sumCashOrZero
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.contracts.fillWithSomeTestCash
|
||||
import net.corda.testing.contracts.VaultFiller
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
|
||||
import org.junit.After
|
||||
@ -81,15 +84,12 @@ class CashTests {
|
||||
}
|
||||
|
||||
// Create some cash. Any attempt to spend >$500 will require multiple issuers to be involved.
|
||||
database.transaction {
|
||||
ourServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
|
||||
owner = ourIdentity, issuedBy = MEGA_CORP.ref(1), issuerServices = megaCorpServices)
|
||||
ourServices.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
|
||||
owner = ourIdentity, issuedBy = MEGA_CORP.ref(1), issuerServices = megaCorpServices)
|
||||
ourServices.fillWithSomeTestCash(howMuch = 80.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
|
||||
owner = ourIdentity, issuedBy = MINI_CORP.ref(1), issuerServices = miniCorpServices)
|
||||
ourServices.fillWithSomeTestCash(howMuch = 80.SWISS_FRANCS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
|
||||
owner = ourIdentity, issuedBy = MINI_CORP.ref(1), issuerServices = miniCorpServices)
|
||||
database.transaction {
|
||||
val vaultFiller = VaultFiller(ourServices, DUMMY_NOTARY, DUMMY_NOTARY_KEY, rngFactory = ::Random)
|
||||
vaultFiller.fillWithSomeTestCash(100.DOLLARS, megaCorpServices, 1, MEGA_CORP.ref(1), ourIdentity)
|
||||
vaultFiller.fillWithSomeTestCash(400.DOLLARS, megaCorpServices, 1, MEGA_CORP.ref(1), ourIdentity)
|
||||
vaultFiller.fillWithSomeTestCash(80.DOLLARS, miniCorpServices, 1, MINI_CORP.ref(1), ourIdentity)
|
||||
vaultFiller.fillWithSomeTestCash(80.SWISS_FRANCS, miniCorpServices, 1, MINI_CORP.ref(1), ourIdentity)
|
||||
}
|
||||
database.transaction {
|
||||
vaultStatesUnconsumed = ourServices.vaultService.queryBy<Cash.State>().states
|
||||
@ -111,34 +111,33 @@ class CashTests {
|
||||
fun trivial() {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { inState }
|
||||
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
tweak {
|
||||
output(Cash.PROGRAM_ID) { outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
output(Cash.PROGRAM_ID, outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
tweak {
|
||||
output(Cash.PROGRAM_ID) { outState }
|
||||
command(ALICE_PUBKEY) { DummyCommandData }
|
||||
output(Cash.PROGRAM_ID, outState)
|
||||
command(ALICE_PUBKEY, DummyCommandData)
|
||||
// Invalid command
|
||||
this `fails with` "required net.corda.finance.contracts.asset.Cash.Commands.Move command"
|
||||
}
|
||||
tweak {
|
||||
output(Cash.PROGRAM_ID) { outState }
|
||||
command(BOB_PUBKEY) { Cash.Commands.Move() }
|
||||
output(Cash.PROGRAM_ID, outState)
|
||||
command(BOB_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "the owning keys are a subset of the signing keys"
|
||||
}
|
||||
tweak {
|
||||
output(Cash.PROGRAM_ID) { outState }
|
||||
output(Cash.PROGRAM_ID) { outState issuedBy MINI_CORP }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
output(Cash.PROGRAM_ID, outState)
|
||||
output(Cash.PROGRAM_ID, outState issuedBy MINI_CORP)
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "at least one cash input"
|
||||
}
|
||||
// Simple reallocation works.
|
||||
tweak {
|
||||
output(Cash.PROGRAM_ID) { outState }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
output(Cash.PROGRAM_ID, outState)
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -149,10 +148,9 @@ class CashTests {
|
||||
// Check we can't "move" money into existence.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { DummyState() }
|
||||
output(Cash.PROGRAM_ID) { outState }
|
||||
command(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
|
||||
input(Cash.PROGRAM_ID, DummyState())
|
||||
output(Cash.PROGRAM_ID, outState)
|
||||
command(MINI_CORP_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "there is at least one cash input for this group"
|
||||
}
|
||||
}
|
||||
@ -163,19 +161,17 @@ class CashTests {
|
||||
// institution is allowed to issue as much cash as they want.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
output(Cash.PROGRAM_ID) { outState }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Issue() }
|
||||
output(Cash.PROGRAM_ID, outState)
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
this `fails with` "output states are issued by a command signer"
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
output(Cash.PROGRAM_ID) {
|
||||
output(Cash.PROGRAM_ID,
|
||||
Cash.State(
|
||||
amount = 1000.DOLLARS `issued by` MINI_CORP.ref(12, 34),
|
||||
owner = AnonymousParty(ALICE_PUBKEY)
|
||||
)
|
||||
}
|
||||
command(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
owner = AnonymousParty(ALICE_PUBKEY)))
|
||||
command(MINI_CORP_PUBKEY, Cash.Commands.Issue())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -211,18 +207,17 @@ class CashTests {
|
||||
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { issuerInState }
|
||||
output(Cash.PROGRAM_ID) { inState.copy(amount = inState.amount * 2) }
|
||||
|
||||
input(Cash.PROGRAM_ID, issuerInState)
|
||||
output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2))
|
||||
// Move fails: not allowed to summon money.
|
||||
tweak {
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
|
||||
// Issue works.
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -230,29 +225,29 @@ class CashTests {
|
||||
// Can't use an issue command to lower the amount.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { inState }
|
||||
output(Cash.PROGRAM_ID) { inState.copy(amount = inState.amount.splitEvenly(2).first()) }
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount.splitEvenly(2).first()))
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
|
||||
this `fails with` "output values sum to more than the inputs"
|
||||
}
|
||||
|
||||
// Can't have an issue command that doesn't actually issue money.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { inState }
|
||||
output(Cash.PROGRAM_ID) { inState }
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
output(Cash.PROGRAM_ID, inState)
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
|
||||
this `fails with` "output values sum to more than the inputs"
|
||||
}
|
||||
|
||||
// Can't have any other commands if we have an issue command (because the issue command overrules them)
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { inState }
|
||||
output(Cash.PROGRAM_ID) { inState.copy(amount = inState.amount * 2) }
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2))
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
|
||||
this `fails with` "there is only a single issue command"
|
||||
}
|
||||
this.verifies()
|
||||
@ -282,26 +277,26 @@ class CashTests {
|
||||
// Splitting value works.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
tweak {
|
||||
input(Cash.PROGRAM_ID) { inState }
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
val splits4 = inState.amount.splitEvenly(4)
|
||||
for (i in 0..3) output(Cash.PROGRAM_ID) { inState.copy(amount = splits4[i]) }
|
||||
for (i in 0..3) output(Cash.PROGRAM_ID, inState.copy(amount = splits4[i]))
|
||||
this.verifies()
|
||||
}
|
||||
// Merging 4 inputs into 2 outputs works.
|
||||
tweak {
|
||||
val splits2 = inState.amount.splitEvenly(2)
|
||||
val splits4 = inState.amount.splitEvenly(4)
|
||||
for (i in 0..3) input(Cash.PROGRAM_ID) { inState.copy(amount = splits4[i]) }
|
||||
for (i in 0..1) output(Cash.PROGRAM_ID) { inState.copy(amount = splits2[i]) }
|
||||
for (i in 0..3) input(Cash.PROGRAM_ID, inState.copy(amount = splits4[i]))
|
||||
for (i in 0..1) output(Cash.PROGRAM_ID, inState.copy(amount = splits2[i]))
|
||||
this.verifies()
|
||||
}
|
||||
// Merging 2 inputs into 1 works.
|
||||
tweak {
|
||||
val splits2 = inState.amount.splitEvenly(2)
|
||||
for (i in 0..1) input(Cash.PROGRAM_ID) { inState.copy(amount = splits2[i]) }
|
||||
output(Cash.PROGRAM_ID) { inState }
|
||||
for (i in 0..1) input(Cash.PROGRAM_ID, inState.copy(amount = splits2[i]))
|
||||
output(Cash.PROGRAM_ID, inState)
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -311,17 +306,17 @@ class CashTests {
|
||||
fun zeroSizedValues() {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { inState }
|
||||
input(Cash.PROGRAM_ID) { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
input(Cash.PROGRAM_ID, inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "zero sized inputs"
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { inState }
|
||||
output(Cash.PROGRAM_ID) { inState }
|
||||
output(Cash.PROGRAM_ID) { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
output(Cash.PROGRAM_ID, inState)
|
||||
output(Cash.PROGRAM_ID, inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "zero sized outputs"
|
||||
}
|
||||
}
|
||||
@ -331,58 +326,56 @@ class CashTests {
|
||||
// Can't change issuer.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { inState }
|
||||
output(Cash.PROGRAM_ID) { outState issuedBy MINI_CORP }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
output(Cash.PROGRAM_ID, outState issuedBy MINI_CORP)
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
// Can't change deposit reference when splitting.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
val splits2 = inState.amount.splitEvenly(2)
|
||||
input(Cash.PROGRAM_ID) { inState }
|
||||
for (i in 0..1) output(Cash.PROGRAM_ID) { outState.copy(amount = splits2[i]).editDepositRef(i.toByte()) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
for (i in 0..1) output(Cash.PROGRAM_ID, outState.copy(amount = splits2[i]).editDepositRef(i.toByte()))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
// Can't mix currencies.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { inState }
|
||||
output(Cash.PROGRAM_ID) { outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer) }
|
||||
output(Cash.PROGRAM_ID) { outState.copy(amount = 200.POUNDS `issued by` defaultIssuer) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
output(Cash.PROGRAM_ID, outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer))
|
||||
output(Cash.PROGRAM_ID, outState.copy(amount = 200.POUNDS `issued by` defaultIssuer))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { inState }
|
||||
input(Cash.PROGRAM_ID) {
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
input(Cash.PROGRAM_ID,
|
||||
inState.copy(
|
||||
amount = 150.POUNDS `issued by` defaultIssuer,
|
||||
owner = AnonymousParty(BOB_PUBKEY)
|
||||
)
|
||||
}
|
||||
output(Cash.PROGRAM_ID) { outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
owner = AnonymousParty(BOB_PUBKEY)))
|
||||
output(Cash.PROGRAM_ID, outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
// Can't have superfluous input states from different issuers.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { inState }
|
||||
input(Cash.PROGRAM_ID) { inState issuedBy MINI_CORP }
|
||||
output(Cash.PROGRAM_ID) { outState }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
input(Cash.PROGRAM_ID, inState issuedBy MINI_CORP)
|
||||
output(Cash.PROGRAM_ID, outState)
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
// Can't combine two different deposits at the same issuer.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { inState }
|
||||
input(Cash.PROGRAM_ID) { inState.editDepositRef(3) }
|
||||
output(Cash.PROGRAM_ID) { outState.copy(amount = inState.amount * 2).editDepositRef(3) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
input(Cash.PROGRAM_ID, inState.editDepositRef(3))
|
||||
output(Cash.PROGRAM_ID, outState.copy(amount = inState.amount * 2).editDepositRef(3))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "for reference [01]"
|
||||
}
|
||||
}
|
||||
@ -392,21 +385,20 @@ class CashTests {
|
||||
// Single input/output straightforward case.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { issuerInState }
|
||||
output(Cash.PROGRAM_ID) { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
|
||||
|
||||
input(Cash.PROGRAM_ID, issuerInState)
|
||||
output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)))
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer) }
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer))
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
|
||||
this `fails with` "required net.corda.finance.contracts.asset.Cash.Commands.Move command"
|
||||
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -418,20 +410,15 @@ class CashTests {
|
||||
// Multi-issuer case.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { issuerInState }
|
||||
input(Cash.PROGRAM_ID) { issuerInState.copy(owner = MINI_CORP) issuedBy MINI_CORP }
|
||||
|
||||
output(Cash.PROGRAM_ID) { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) issuedBy MINI_CORP }
|
||||
output(Cash.PROGRAM_ID) { issuerInState.copy(owner = MINI_CORP, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
|
||||
|
||||
command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
|
||||
input(Cash.PROGRAM_ID, issuerInState)
|
||||
input(Cash.PROGRAM_ID, issuerInState.copy(owner = MINI_CORP) issuedBy MINI_CORP)
|
||||
output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) issuedBy MINI_CORP)
|
||||
output(Cash.PROGRAM_ID, issuerInState.copy(owner = MINI_CORP, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)))
|
||||
command(listOf(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), Cash.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
|
||||
this `fails with` "the amounts balance"
|
||||
|
||||
command(MINI_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef)) }
|
||||
command(MINI_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef)))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -441,10 +428,10 @@ class CashTests {
|
||||
// Single input/output straightforward case.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID) { inState }
|
||||
output(Cash.PROGRAM_ID) { outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
output(Cash.PROGRAM_ID, outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)))
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
}
|
||||
@ -454,25 +441,24 @@ class CashTests {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
// Gather 2000 dollars from two different issuers.
|
||||
input(Cash.PROGRAM_ID) { inState }
|
||||
input(Cash.PROGRAM_ID) { inState issuedBy MINI_CORP }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
input(Cash.PROGRAM_ID, inState issuedBy MINI_CORP)
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
// Can't merge them together.
|
||||
tweak {
|
||||
output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY), amount = 2000.DOLLARS `issued by` defaultIssuer) }
|
||||
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY), amount = 2000.DOLLARS `issued by` defaultIssuer))
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
// Missing MiniCorp deposit
|
||||
tweak {
|
||||
output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) }
|
||||
output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) }
|
||||
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)))
|
||||
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)))
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
|
||||
// This works.
|
||||
output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) }
|
||||
output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) issuedBy MINI_CORP }
|
||||
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)))
|
||||
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)) issuedBy MINI_CORP)
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -483,12 +469,11 @@ class CashTests {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
val pounds = Cash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), AnonymousParty(BOB_PUBKEY))
|
||||
input(Cash.PROGRAM_ID) { inState ownedBy AnonymousParty(ALICE_PUBKEY) }
|
||||
input(Cash.PROGRAM_ID) { pounds }
|
||||
output(Cash.PROGRAM_ID) { inState ownedBy AnonymousParty(BOB_PUBKEY) }
|
||||
output(Cash.PROGRAM_ID) { pounds ownedBy AnonymousParty(ALICE_PUBKEY) }
|
||||
command(ALICE_PUBKEY, BOB_PUBKEY) { Cash.Commands.Move() }
|
||||
|
||||
input(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(ALICE_PUBKEY))
|
||||
input(Cash.PROGRAM_ID, pounds)
|
||||
output(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(BOB_PUBKEY))
|
||||
output(Cash.PROGRAM_ID, pounds ownedBy AnonymousParty(ALICE_PUBKEY))
|
||||
command(listOf(ALICE_PUBKEY, BOB_PUBKEY), Cash.Commands.Move())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -792,19 +777,17 @@ class CashTests {
|
||||
ledger(mockService) {
|
||||
unverifiedTransaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash") {
|
||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash",
|
||||
Cash.State(
|
||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||
owner = MEGA_CORP
|
||||
)
|
||||
}
|
||||
owner = MEGA_CORP))
|
||||
}
|
||||
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input("MEGA_CORP cash")
|
||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash 2", "MEGA_CORP cash".output<Cash.State>().copy(owner = AnonymousParty(ALICE_PUBKEY)))
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
@ -814,7 +797,7 @@ class CashTests {
|
||||
input("MEGA_CORP cash")
|
||||
// We send it to another pubkey so that the transaction is not identical to the previous one
|
||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash 3", "MEGA_CORP cash".output<Cash.State>().copy(owner = ALICE))
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
|
||||
this.verifies()
|
||||
}
|
||||
this.fails()
|
||||
|
@ -71,34 +71,33 @@ class ObligationTests {
|
||||
fun trivial() {
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input(Obligation.PROGRAM_ID) { inState }
|
||||
|
||||
input(Obligation.PROGRAM_ID, inState)
|
||||
tweak {
|
||||
output(Obligation.PROGRAM_ID) { outState.copy(quantity = 2000.DOLLARS.quantity) }
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
||||
output(Obligation.PROGRAM_ID, outState.copy(quantity = 2000.DOLLARS.quantity))
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
tweak {
|
||||
output(Obligation.PROGRAM_ID) { outState }
|
||||
command(CHARLIE.owningKey) { DummyCommandData }
|
||||
output(Obligation.PROGRAM_ID, outState)
|
||||
command(CHARLIE.owningKey, DummyCommandData)
|
||||
// Invalid command
|
||||
this `fails with` "required net.corda.finance.contracts.asset.Obligation.Commands.Move command"
|
||||
}
|
||||
tweak {
|
||||
output(Obligation.PROGRAM_ID) { outState }
|
||||
command(BOB_PUBKEY) { Obligation.Commands.Move() }
|
||||
output(Obligation.PROGRAM_ID, outState)
|
||||
command(BOB_PUBKEY, Obligation.Commands.Move())
|
||||
this `fails with` "the owning keys are a subset of the signing keys"
|
||||
}
|
||||
tweak {
|
||||
output(Obligation.PROGRAM_ID) { outState }
|
||||
output(Obligation.PROGRAM_ID) { outState `issued by` MINI_CORP }
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
||||
output(Obligation.PROGRAM_ID, outState)
|
||||
output(Obligation.PROGRAM_ID, outState `issued by` MINI_CORP)
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||
this `fails with` "at least one obligation input"
|
||||
}
|
||||
// Simple reallocation works.
|
||||
tweak {
|
||||
output(Obligation.PROGRAM_ID) { outState }
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
||||
output(Obligation.PROGRAM_ID, outState)
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -109,10 +108,9 @@ class ObligationTests {
|
||||
// Check we can't "move" debt into existence.
|
||||
transaction {
|
||||
attachments(DummyContract.PROGRAM_ID, Obligation.PROGRAM_ID)
|
||||
input(DummyContract.PROGRAM_ID) { DummyState() }
|
||||
output(Obligation.PROGRAM_ID) { outState }
|
||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() }
|
||||
|
||||
input(DummyContract.PROGRAM_ID, DummyState())
|
||||
output(Obligation.PROGRAM_ID, outState)
|
||||
command(MINI_CORP_PUBKEY, Obligation.Commands.Move())
|
||||
this `fails with` "at least one obligation input"
|
||||
}
|
||||
|
||||
@ -120,21 +118,19 @@ class ObligationTests {
|
||||
// institution is allowed to issue as much cash as they want.
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
output(Obligation.PROGRAM_ID) { outState }
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Issue() }
|
||||
output(Obligation.PROGRAM_ID, outState)
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Issue())
|
||||
this `fails with` "output states are issued by a command signer"
|
||||
}
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
output(Obligation.PROGRAM_ID) {
|
||||
output(Obligation.PROGRAM_ID,
|
||||
Obligation.State(
|
||||
obligor = MINI_CORP,
|
||||
quantity = 1000.DOLLARS.quantity,
|
||||
beneficiary = CHARLIE,
|
||||
template = megaCorpDollarSettlement
|
||||
)
|
||||
}
|
||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
||||
template = megaCorpDollarSettlement))
|
||||
command(MINI_CORP_PUBKEY, Obligation.Commands.Issue())
|
||||
this.verifies()
|
||||
}
|
||||
run {
|
||||
@ -157,18 +153,17 @@ class ObligationTests {
|
||||
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input(Obligation.PROGRAM_ID) { inState }
|
||||
output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.amount.quantity * 2) }
|
||||
|
||||
input(Obligation.PROGRAM_ID, inState)
|
||||
output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.amount.quantity * 2))
|
||||
// Move fails: not allowed to summon money.
|
||||
tweak {
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
|
||||
// Issue works.
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
||||
command(MEGA_CORP_PUBKEY, Obligation.Commands.Issue())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -176,29 +171,29 @@ class ObligationTests {
|
||||
// Can't use an issue command to lower the amount.
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input(Obligation.PROGRAM_ID) { inState }
|
||||
output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.amount.quantity / 2) }
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
||||
input(Obligation.PROGRAM_ID, inState)
|
||||
output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.amount.quantity / 2))
|
||||
command(MEGA_CORP_PUBKEY, Obligation.Commands.Issue())
|
||||
this `fails with` "output values sum to more than the inputs"
|
||||
}
|
||||
|
||||
// Can't have an issue command that doesn't actually issue money.
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input(Obligation.PROGRAM_ID) { inState }
|
||||
output(Obligation.PROGRAM_ID) { inState }
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
||||
input(Obligation.PROGRAM_ID, inState)
|
||||
output(Obligation.PROGRAM_ID, inState)
|
||||
command(MEGA_CORP_PUBKEY, Obligation.Commands.Issue())
|
||||
this `fails with` ""
|
||||
}
|
||||
|
||||
// Can't have any other commands if we have an issue command (because the issue command overrules them).
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input(Obligation.PROGRAM_ID) { inState }
|
||||
output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.amount.quantity * 2) }
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
||||
input(Obligation.PROGRAM_ID, inState)
|
||||
output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.amount.quantity * 2))
|
||||
command(MEGA_CORP_PUBKEY, Obligation.Commands.Issue())
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
||||
command(MEGA_CORP_PUBKEY, Obligation.Commands.Issue())
|
||||
this `fails with` "there is only a single issue command"
|
||||
}
|
||||
this.verifies()
|
||||
@ -352,7 +347,7 @@ class ObligationTests {
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
// Note we can sign with either key here
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
|
||||
command(ALICE_PUBKEY, Obligation.Commands.Net(NetType.CLOSE_OUT))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
@ -368,8 +363,8 @@ class ObligationTests {
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
input("MegaCorp's $1,000,000 obligation to Bob")
|
||||
output(Obligation.PROGRAM_ID, "change") { oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, BOB) }
|
||||
command(BOB_PUBKEY, MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
|
||||
output(Obligation.PROGRAM_ID, "change", oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, BOB))
|
||||
command(listOf(BOB_PUBKEY, MEGA_CORP_PUBKEY), Obligation.Commands.Net(NetType.CLOSE_OUT))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
@ -383,8 +378,8 @@ class ObligationTests {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
output(Obligation.PROGRAM_ID, "change") { (oneMillionDollars.splitEvenly(2).first()).OBLIGATION between Pair(ALICE, BOB) }
|
||||
command(BOB_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
|
||||
output(Obligation.PROGRAM_ID, "change", oneMillionDollars.splitEvenly(2).first().OBLIGATION between Pair(ALICE, BOB))
|
||||
command(BOB_PUBKEY, Obligation.Commands.Net(NetType.CLOSE_OUT))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "amounts owed on input and output must match"
|
||||
}
|
||||
@ -397,7 +392,7 @@ class ObligationTests {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
|
||||
command(MEGA_CORP_PUBKEY, Obligation.Commands.Net(NetType.CLOSE_OUT))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "any involved party has signed"
|
||||
}
|
||||
@ -413,7 +408,7 @@ class ObligationTests {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
command(ALICE_PUBKEY, BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
|
||||
command(listOf(ALICE_PUBKEY, BOB_PUBKEY), Obligation.Commands.Net(NetType.PAYMENT))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
@ -428,7 +423,7 @@ class ObligationTests {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
command(BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
|
||||
command(BOB_PUBKEY, Obligation.Commands.Net(NetType.PAYMENT))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "all involved parties have signed"
|
||||
}
|
||||
@ -441,8 +436,8 @@ class ObligationTests {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
input("MegaCorp's $1,000,000 obligation to Bob")
|
||||
output(Obligation.PROGRAM_ID, "MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, ALICE) }
|
||||
command(ALICE_PUBKEY, BOB_PUBKEY, MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
|
||||
output(Obligation.PROGRAM_ID, "MegaCorp's $1,000,000 obligation to Alice", oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, ALICE))
|
||||
command(listOf(ALICE_PUBKEY, BOB_PUBKEY, MEGA_CORP_PUBKEY), Obligation.Commands.Net(NetType.PAYMENT))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
@ -456,8 +451,8 @@ class ObligationTests {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
input("MegaCorp's $1,000,000 obligation to Bob")
|
||||
output(Obligation.PROGRAM_ID, "MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, ALICE) }
|
||||
command(ALICE_PUBKEY, BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
|
||||
output(Obligation.PROGRAM_ID, "MegaCorp's $1,000,000 obligation to Alice", oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, ALICE))
|
||||
command(listOf(ALICE_PUBKEY, BOB_PUBKEY), Obligation.Commands.Net(NetType.PAYMENT))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "all involved parties have signed"
|
||||
}
|
||||
@ -473,9 +468,9 @@ class ObligationTests {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Alice's $1,000,000")
|
||||
output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) }
|
||||
output(Obligation.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB)
|
||||
command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java))
|
||||
attachment(attachment(cashContractBytes.inputStream()))
|
||||
this.verifies()
|
||||
}
|
||||
@ -488,10 +483,10 @@ class ObligationTests {
|
||||
attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID)
|
||||
input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB))
|
||||
input(Cash.PROGRAM_ID, 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE)
|
||||
output(Obligation.PROGRAM_ID, "Alice's $500,000 obligation to Bob") { halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB) }
|
||||
output(Obligation.PROGRAM_ID, "Bob's $500,000") { 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) }
|
||||
output(Obligation.PROGRAM_ID, "Alice's $500,000 obligation to Bob", halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB))
|
||||
output(Obligation.PROGRAM_ID, "Bob's $500,000", 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB)
|
||||
command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java))
|
||||
attachment(attachment(cashContractBytes.inputStream()))
|
||||
this.verifies()
|
||||
}
|
||||
@ -504,9 +499,9 @@ class ObligationTests {
|
||||
attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID)
|
||||
input(Obligation.PROGRAM_ID, defaultedObligation) // Alice's defaulted $1,000,000 obligation to Bob
|
||||
input(Cash.PROGRAM_ID, 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE)
|
||||
output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) }
|
||||
output(Obligation.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB)
|
||||
command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java))
|
||||
this `fails with` "all inputs are in the normal state"
|
||||
}
|
||||
}
|
||||
@ -518,9 +513,9 @@ class ObligationTests {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Alice's $1,000,000")
|
||||
output(Obligation.PROGRAM_ID, "Bob's $1,000,000") { 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) }
|
||||
output(Obligation.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB)
|
||||
command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java))
|
||||
attachment(attachment(cashContractBytes.inputStream()))
|
||||
this `fails with` "amount in settle command"
|
||||
}
|
||||
@ -546,9 +541,9 @@ class ObligationTests {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input("Alice's 1 FCOJ obligation to Bob")
|
||||
input("Alice's 1 FCOJ")
|
||||
output(Obligation.PROGRAM_ID, "Bob's 1 FCOJ") { CommodityContract.State(oneUnitFcoj, BOB) }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneUnitFcoj.quantity, oneUnitFcojObligation.amount.token)) }
|
||||
command(ALICE_PUBKEY) { CommodityContract.Commands.Move(Obligation::class.java) }
|
||||
output(Obligation.PROGRAM_ID, "Bob's 1 FCOJ", CommodityContract.State(oneUnitFcoj, BOB))
|
||||
command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneUnitFcoj.quantity, oneUnitFcojObligation.amount.token)))
|
||||
command(ALICE_PUBKEY, CommodityContract.Commands.Move(Obligation::class.java))
|
||||
attachment(attachment(commodityContractBytes.inputStream()))
|
||||
verifies()
|
||||
}
|
||||
@ -563,8 +558,8 @@ class ObligationTests {
|
||||
transaction("Settlement") {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
output(Obligation.PROGRAM_ID, "Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)).copy(lifecycle = Lifecycle.DEFAULTED) }
|
||||
command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED) }
|
||||
output(Obligation.PROGRAM_ID, "Alice's defaulted $1,000,000 obligation to Bob", (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)).copy(lifecycle = Lifecycle.DEFAULTED))
|
||||
command(BOB_PUBKEY, Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED))
|
||||
this `fails with` "there is a time-window from the authority"
|
||||
}
|
||||
}
|
||||
@ -575,8 +570,8 @@ class ObligationTests {
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` futureTestTime)
|
||||
output(Obligation.PROGRAM_ID, "Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` futureTestTime).copy(lifecycle = Lifecycle.DEFAULTED) }
|
||||
command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED) }
|
||||
output(Obligation.PROGRAM_ID, "Alice's defaulted $1,000,000 obligation to Bob", (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` futureTestTime).copy(lifecycle = Lifecycle.DEFAULTED))
|
||||
command(BOB_PUBKEY, Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "the due date has passed"
|
||||
}
|
||||
@ -586,8 +581,8 @@ class ObligationTests {
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` pastTestTime)
|
||||
output(Obligation.PROGRAM_ID, "Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` pastTestTime).copy(lifecycle = Lifecycle.DEFAULTED) }
|
||||
command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED) }
|
||||
output(Obligation.PROGRAM_ID, "Alice's defaulted $1,000,000 obligation to Bob", (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` pastTestTime).copy(lifecycle = Lifecycle.DEFAULTED))
|
||||
command(BOB_PUBKEY, Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
@ -600,24 +595,24 @@ class ObligationTests {
|
||||
// Splitting value works.
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||
tweak {
|
||||
input(Obligation.PROGRAM_ID) { inState }
|
||||
repeat(4) { output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.quantity / 4) } }
|
||||
input(Obligation.PROGRAM_ID, inState)
|
||||
repeat(4) { output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.quantity / 4)) }
|
||||
this.verifies()
|
||||
}
|
||||
// Merging 4 inputs into 2 outputs works.
|
||||
tweak {
|
||||
repeat(4) { input(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.quantity / 4) } }
|
||||
output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.quantity / 2) }
|
||||
output(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.quantity / 2) }
|
||||
repeat(4) { input(Obligation.PROGRAM_ID, inState.copy(quantity = inState.quantity / 4)) }
|
||||
output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.quantity / 2))
|
||||
output(Obligation.PROGRAM_ID, inState.copy(quantity = inState.quantity / 2))
|
||||
this.verifies()
|
||||
}
|
||||
// Merging 2 inputs into 1 works.
|
||||
tweak {
|
||||
input(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.quantity / 2) }
|
||||
input(Obligation.PROGRAM_ID) { inState.copy(quantity = inState.quantity / 2) }
|
||||
output(Obligation.PROGRAM_ID) { inState }
|
||||
input(Obligation.PROGRAM_ID, inState.copy(quantity = inState.quantity / 2))
|
||||
input(Obligation.PROGRAM_ID, inState.copy(quantity = inState.quantity / 2))
|
||||
output(Obligation.PROGRAM_ID, inState)
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -627,18 +622,16 @@ class ObligationTests {
|
||||
fun zeroSizedValues() {
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||
tweak {
|
||||
input(Obligation.PROGRAM_ID) { inState }
|
||||
input(Obligation.PROGRAM_ID) { inState.copy(quantity = 0L) }
|
||||
|
||||
input(Obligation.PROGRAM_ID, inState)
|
||||
input(Obligation.PROGRAM_ID, inState.copy(quantity = 0L))
|
||||
this `fails with` "zero sized inputs"
|
||||
}
|
||||
tweak {
|
||||
input(Obligation.PROGRAM_ID) { inState }
|
||||
output(Obligation.PROGRAM_ID) { inState }
|
||||
output(Obligation.PROGRAM_ID) { inState.copy(quantity = 0L) }
|
||||
|
||||
input(Obligation.PROGRAM_ID, inState)
|
||||
output(Obligation.PROGRAM_ID, inState)
|
||||
output(Obligation.PROGRAM_ID, inState.copy(quantity = 0L))
|
||||
this `fails with` "zero sized outputs"
|
||||
}
|
||||
}
|
||||
@ -649,41 +642,39 @@ class ObligationTests {
|
||||
// Can't change issuer.
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input(Obligation.PROGRAM_ID) { inState }
|
||||
output(Obligation.PROGRAM_ID) { outState `issued by` MINI_CORP }
|
||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() }
|
||||
input(Obligation.PROGRAM_ID, inState)
|
||||
output(Obligation.PROGRAM_ID, outState `issued by` MINI_CORP)
|
||||
command(MINI_CORP_PUBKEY, Obligation.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
// Can't mix currencies.
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input(Obligation.PROGRAM_ID) { inState }
|
||||
output(Obligation.PROGRAM_ID) { outState.copy(quantity = 80000, template = megaCorpDollarSettlement) }
|
||||
output(Obligation.PROGRAM_ID) { outState.copy(quantity = 20000, template = megaCorpPoundSettlement) }
|
||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() }
|
||||
input(Obligation.PROGRAM_ID, inState)
|
||||
output(Obligation.PROGRAM_ID, outState.copy(quantity = 80000, template = megaCorpDollarSettlement))
|
||||
output(Obligation.PROGRAM_ID, outState.copy(quantity = 20000, template = megaCorpPoundSettlement))
|
||||
command(MINI_CORP_PUBKEY, Obligation.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input(Obligation.PROGRAM_ID) { inState }
|
||||
input(Obligation.PROGRAM_ID) {
|
||||
input(Obligation.PROGRAM_ID, inState)
|
||||
input(Obligation.PROGRAM_ID,
|
||||
inState.copy(
|
||||
quantity = 15000,
|
||||
template = megaCorpPoundSettlement,
|
||||
beneficiary = AnonymousParty(BOB_PUBKEY)
|
||||
)
|
||||
}
|
||||
output(Obligation.PROGRAM_ID) { outState.copy(quantity = 115000) }
|
||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() }
|
||||
beneficiary = AnonymousParty(BOB_PUBKEY)))
|
||||
output(Obligation.PROGRAM_ID, outState.copy(quantity = 115000))
|
||||
command(MINI_CORP_PUBKEY, Obligation.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
// Can't have superfluous input states from different issuers.
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input(Obligation.PROGRAM_ID) { inState }
|
||||
input(Obligation.PROGRAM_ID) { inState `issued by` MINI_CORP }
|
||||
output(Obligation.PROGRAM_ID) { outState }
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
||||
input(Obligation.PROGRAM_ID, inState)
|
||||
input(Obligation.PROGRAM_ID, inState `issued by` MINI_CORP)
|
||||
output(Obligation.PROGRAM_ID, outState)
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
}
|
||||
@ -693,21 +684,20 @@ class ObligationTests {
|
||||
// Single input/output straightforward case.
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
input(Obligation.PROGRAM_ID) { inState }
|
||||
output(Obligation.PROGRAM_ID) { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) }
|
||||
|
||||
input(Obligation.PROGRAM_ID, inState)
|
||||
output(Obligation.PROGRAM_ID, outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity))
|
||||
tweak {
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(100.DOLLARS.quantity, inState.amount.token)) }
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Exit(Amount(100.DOLLARS.quantity, inState.amount.token)))
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
|
||||
tweak {
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token)) }
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token)))
|
||||
this `fails with` "required net.corda.finance.contracts.asset.Obligation.Commands.Move command"
|
||||
|
||||
tweak {
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -720,21 +710,15 @@ class ObligationTests {
|
||||
// Multi-product case.
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
|
||||
input(Obligation.PROGRAM_ID) { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedPounds)) }
|
||||
input(Obligation.PROGRAM_ID) { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedDollars)) }
|
||||
|
||||
output(Obligation.PROGRAM_ID) { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedPounds), quantity = inState.quantity - 200.POUNDS.quantity) }
|
||||
output(Obligation.PROGRAM_ID) { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedDollars), quantity = inState.quantity - 200.DOLLARS.quantity) }
|
||||
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
||||
|
||||
input(Obligation.PROGRAM_ID, inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedPounds)))
|
||||
input(Obligation.PROGRAM_ID, inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedDollars)))
|
||||
output(Obligation.PROGRAM_ID, inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedPounds), quantity = inState.quantity - 200.POUNDS.quantity))
|
||||
output(Obligation.PROGRAM_ID, inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedDollars), quantity = inState.quantity - 200.DOLLARS.quantity))
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token.copy(product = megaCorpDollarSettlement))) }
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token.copy(product = megaCorpDollarSettlement))))
|
||||
this `fails with` "the amounts balance"
|
||||
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(200.POUNDS.quantity, inState.amount.token.copy(product = megaCorpPoundSettlement))) }
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Exit(Amount(200.POUNDS.quantity, inState.amount.token.copy(product = megaCorpPoundSettlement))))
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -745,27 +729,26 @@ class ObligationTests {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
|
||||
// Gather 2000 dollars from two different issuers.
|
||||
input(Obligation.PROGRAM_ID) { inState }
|
||||
input(Obligation.PROGRAM_ID) { inState `issued by` MINI_CORP }
|
||||
|
||||
input(Obligation.PROGRAM_ID, inState)
|
||||
input(Obligation.PROGRAM_ID, inState `issued by` MINI_CORP)
|
||||
// Can't merge them together.
|
||||
tweak {
|
||||
output(Obligation.PROGRAM_ID) { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY), quantity = 200000L) }
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
||||
output(Obligation.PROGRAM_ID, inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY), quantity = 200000L))
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
// Missing MiniCorp deposit
|
||||
tweak {
|
||||
output(Obligation.PROGRAM_ID) { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) }
|
||||
output(Obligation.PROGRAM_ID) { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) }
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
||||
output(Obligation.PROGRAM_ID, inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)))
|
||||
output(Obligation.PROGRAM_ID, inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)))
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
|
||||
// This works.
|
||||
output(Obligation.PROGRAM_ID) { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) }
|
||||
output(Obligation.PROGRAM_ID) { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) `issued by` MINI_CORP }
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
||||
output(Obligation.PROGRAM_ID, inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)))
|
||||
output(Obligation.PROGRAM_ID, inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) `issued by` MINI_CORP)
|
||||
command(CHARLIE.owningKey, Obligation.Commands.Move())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -776,12 +759,11 @@ class ObligationTests {
|
||||
transaction {
|
||||
attachments(Obligation.PROGRAM_ID)
|
||||
val pounds = Obligation.State(Lifecycle.NORMAL, MINI_CORP, megaCorpPoundSettlement, 658.POUNDS.quantity, AnonymousParty(BOB_PUBKEY))
|
||||
input(Obligation.PROGRAM_ID) { inState `owned by` CHARLIE }
|
||||
input(Obligation.PROGRAM_ID) { pounds }
|
||||
output(Obligation.PROGRAM_ID) { inState `owned by` AnonymousParty(BOB_PUBKEY) }
|
||||
output(Obligation.PROGRAM_ID) { pounds `owned by` CHARLIE }
|
||||
command(CHARLIE.owningKey, BOB_PUBKEY) { Obligation.Commands.Move() }
|
||||
|
||||
input(Obligation.PROGRAM_ID, inState `owned by` CHARLIE)
|
||||
input(Obligation.PROGRAM_ID, pounds)
|
||||
output(Obligation.PROGRAM_ID, inState `owned by` AnonymousParty(BOB_PUBKEY))
|
||||
output(Obligation.PROGRAM_ID, pounds `owned by` CHARLIE)
|
||||
command(listOf(CHARLIE.owningKey, BOB_PUBKEY), Obligation.Commands.Move())
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ buildscript {
|
||||
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
|
||||
ext.bouncycastle_version = constants.getProperty("bouncycastleVersion")
|
||||
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
|
||||
ext.jsr305_version = constants.getProperty("jsr305Version")
|
||||
ext.kotlin_version = constants.getProperty("kotlinVersion")
|
||||
|
||||
repositories {
|
||||
|
@ -11,6 +11,9 @@ version gradle_plugins_version
|
||||
group 'net.corda.plugins'
|
||||
|
||||
dependencies {
|
||||
// JSR 305: Nullability annotations
|
||||
compile "com.google.code.findbugs:jsr305:$jsr305_version"
|
||||
|
||||
// TypeSafe Config: for simple and human friendly config files.
|
||||
compile "com.typesafe:config:$typesafe_config_version"
|
||||
|
||||
|
@ -1,24 +1,40 @@
|
||||
package net.corda.cordform;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public abstract class CordformDefinition {
|
||||
public final Path driverDirectory;
|
||||
public final ArrayList<Consumer<? super CordformNode>> nodeConfigurers = new ArrayList<>();
|
||||
private Path nodesDirectory = Paths.get("build", "nodes");
|
||||
private final List<Consumer<CordformNode>> nodeConfigurers = new ArrayList<>();
|
||||
private final List<String> cordappPackages = new ArrayList<>();
|
||||
|
||||
public CordformDefinition(Path driverDirectory) {
|
||||
this.driverDirectory = driverDirectory;
|
||||
public Path getNodesDirectory() {
|
||||
return nodesDirectory;
|
||||
}
|
||||
|
||||
public void addNode(Consumer<? super CordformNode> configurer) {
|
||||
public void setNodesDirectory(Path nodesDirectory) {
|
||||
this.nodesDirectory = nodesDirectory;
|
||||
}
|
||||
|
||||
public List<Consumer<CordformNode>> getNodeConfigurers() {
|
||||
return nodeConfigurers;
|
||||
}
|
||||
|
||||
public void addNode(Consumer<CordformNode> configurer) {
|
||||
nodeConfigurers.add(configurer);
|
||||
}
|
||||
|
||||
public List<String> getCordappPackages() {
|
||||
return cordappPackages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make arbitrary changes to the node directories before they are started.
|
||||
* @param context Lookup of node directory by node name.
|
||||
*/
|
||||
public abstract void setup(CordformContext context);
|
||||
public abstract void setup(@Nonnull CordformContext context);
|
||||
}
|
||||
|
@ -2,6 +2,9 @@ package net.corda.cordform;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import com.typesafe.config.*;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -54,7 +57,7 @@ public class CordformNode implements NodeDefinition {
|
||||
*/
|
||||
public void name(String name) {
|
||||
this.name = name;
|
||||
config = config.withValue("myLegalName", ConfigValueFactory.fromAnyRef(name));
|
||||
setValue("myLegalName", name);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -62,6 +65,7 @@ public class CordformNode implements NodeDefinition {
|
||||
*
|
||||
* @return This node's P2P address.
|
||||
*/
|
||||
@Nonnull
|
||||
public String getP2pAddress() {
|
||||
return config.getString("p2pAddress");
|
||||
}
|
||||
@ -71,8 +75,8 @@ public class CordformNode implements NodeDefinition {
|
||||
*
|
||||
* @param p2pPort The Artemis messaging queue port.
|
||||
*/
|
||||
public void p2pPort(Integer p2pPort) {
|
||||
config = config.withValue("p2pAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + p2pPort));
|
||||
public void p2pPort(int p2pPort) {
|
||||
p2pAddress(DEFAULT_HOST + ':' + p2pPort);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,7 +85,15 @@ public class CordformNode implements NodeDefinition {
|
||||
* @param p2pAddress The Artemis messaging queue host and port.
|
||||
*/
|
||||
public void p2pAddress(String p2pAddress) {
|
||||
config = config.withValue("p2pAddress", ConfigValueFactory.fromAnyRef(p2pAddress));
|
||||
setValue("p2pAddress", p2pAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RPC address for this node, or null if one hasn't been specified.
|
||||
*/
|
||||
@Nullable
|
||||
public String getRpcAddress() {
|
||||
return getOptionalString("rpcAddress");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -89,8 +101,8 @@ public class CordformNode implements NodeDefinition {
|
||||
*
|
||||
* @param rpcPort The Artemis RPC queue port.
|
||||
*/
|
||||
public void rpcPort(Integer rpcPort) {
|
||||
config = config.withValue("rpcAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + rpcPort));
|
||||
public void rpcPort(int rpcPort) {
|
||||
rpcAddress(DEFAULT_HOST + ':' + rpcPort);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -99,7 +111,31 @@ public class CordformNode implements NodeDefinition {
|
||||
* @param rpcAddress The Artemis RPC queue host and port.
|
||||
*/
|
||||
public void rpcAddress(String rpcAddress) {
|
||||
config = config.withValue("rpcAddress", ConfigValueFactory.fromAnyRef(rpcAddress));
|
||||
setValue("rpcAddress", rpcAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the address of the web server that will connect to the node, or null if one hasn't been specified.
|
||||
*/
|
||||
@Nullable
|
||||
public String getWebAddress() {
|
||||
return getOptionalString("webAddress");
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a webserver to connect to the node via RPC. This port will specify the port it will listen on. The node
|
||||
* must have an RPC address configured.
|
||||
*/
|
||||
public void webPort(int webPort) {
|
||||
webAddress(DEFAULT_HOST + ':' + webPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a webserver to connect to the node via RPC. This address will specify the port it will listen on. The node
|
||||
* must have an RPC address configured.
|
||||
*/
|
||||
public void webAddress(String webAddress) {
|
||||
setValue("webAddress", webAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -108,6 +144,14 @@ public class CordformNode implements NodeDefinition {
|
||||
* @param configFile The file path.
|
||||
*/
|
||||
public void configFile(String configFile) {
|
||||
config = config.withValue("configFile", ConfigValueFactory.fromAnyRef(configFile));
|
||||
setValue("configFile", configFile);
|
||||
}
|
||||
|
||||
private String getOptionalString(String path) {
|
||||
return config.hasPath(path) ? config.getString(path) : null;
|
||||
}
|
||||
|
||||
private void setValue(String path, Object value) {
|
||||
config = config.withValue(path, ConfigValueFactory.fromAnyRef(value));
|
||||
}
|
||||
}
|
||||
|
@ -11,10 +11,10 @@ import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import java.io.File
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.jar.JarInputStream
|
||||
|
||||
/**
|
||||
* Creates nodes based on the configuration of this task in the gradle configuration DSL.
|
||||
@ -23,12 +23,16 @@ import java.util.concurrent.TimeUnit
|
||||
*/
|
||||
@Suppress("unused")
|
||||
open class Cordform : DefaultTask() {
|
||||
private companion object {
|
||||
private val defaultDirectory: Path = Paths.get("build", "nodes")
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally the name of a CordformDefinition subclass to which all configuration will be delegated.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanPrivate")
|
||||
var definitionClass: String? = null
|
||||
private var directory = Paths.get("build", "nodes")
|
||||
private var directory = defaultDirectory
|
||||
private val nodes = mutableListOf<Node>()
|
||||
|
||||
/**
|
||||
@ -116,7 +120,6 @@ open class Cordform : DefaultTask() {
|
||||
/**
|
||||
* This task action will create and install the nodes based on the node configurations added.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
@TaskAction
|
||||
fun build() {
|
||||
project.logger.info("Running Cordform task")
|
||||
@ -129,10 +132,18 @@ open class Cordform : DefaultTask() {
|
||||
private fun initializeConfiguration() {
|
||||
if (definitionClass != null) {
|
||||
val cd = loadCordformDefinition()
|
||||
// If the user has specified their own directory (even if it's the same default path) then let them know
|
||||
// it's not used and should just rely on the one in CordformDefinition
|
||||
require(directory === defaultDirectory) {
|
||||
"'directory' cannot be used when 'definitionClass' is specified. Use CordformDefinition.nodesDirectory instead."
|
||||
}
|
||||
directory = cd.nodesDirectory
|
||||
val cordapps = cd.getMatchingCordapps()
|
||||
cd.nodeConfigurers.forEach {
|
||||
val node = node { }
|
||||
it.accept(node)
|
||||
node.rootDir(directory)
|
||||
node.installCordapps(cordapps)
|
||||
}
|
||||
cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) }
|
||||
} else {
|
||||
@ -142,6 +153,30 @@ open class Cordform : DefaultTask() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun CordformDefinition.getMatchingCordapps(): List<File> {
|
||||
val cordappJars = project.configuration("cordapp").files
|
||||
return cordappPackages.map { `package` ->
|
||||
val cordappsWithPackage = cordappJars.filter { it.containsPackage(`package`) }
|
||||
when (cordappsWithPackage.size) {
|
||||
0 -> throw IllegalArgumentException("There are no cordapp dependencies containing the package $`package`")
|
||||
1 -> cordappsWithPackage[0]
|
||||
else -> throw IllegalArgumentException("More than one cordapp dependency contains the package $`package`: $cordappsWithPackage")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun File.containsPackage(`package`: String): Boolean {
|
||||
JarInputStream(inputStream()).use {
|
||||
while (true) {
|
||||
val name = it.nextJarEntry?.name ?: break
|
||||
if (name.endsWith(".class") && name.replace('/', '.').startsWith(`package`)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateAndInstallNodeInfos() {
|
||||
generateNodeInfos()
|
||||
installNodeInfos()
|
||||
@ -149,7 +184,7 @@ open class Cordform : DefaultTask() {
|
||||
|
||||
private fun generateNodeInfos() {
|
||||
project.logger.info("Generating node infos")
|
||||
var nodeProcesses = buildNodeProcesses()
|
||||
val nodeProcesses = buildNodeProcesses()
|
||||
try {
|
||||
validateNodeProcessess(nodeProcesses)
|
||||
} finally {
|
||||
@ -177,7 +212,7 @@ open class Cordform : DefaultTask() {
|
||||
|
||||
private fun buildNodeProcess(node: Node): Pair<Node, Process> {
|
||||
node.makeLogDirectory()
|
||||
var process = ProcessBuilder(generateNodeInfoCommand())
|
||||
val process = ProcessBuilder(generateNodeInfoCommand())
|
||||
.directory(node.fullPath().toFile())
|
||||
.redirectErrorStream(true)
|
||||
// InheritIO causes hangs on windows due the gradle buffer also not being flushed.
|
||||
@ -224,6 +259,8 @@ open class Cordform : DefaultTask() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Node.logFile(): Path = this.logDirectory().resolve("generate-info.log")
|
||||
|
||||
private fun ProcessBuilder.addEnvironment(key: String, value: String) = this.apply { environment().put(key, value) }
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package net.corda.plugins
|
||||
import com.typesafe.config.*
|
||||
import net.corda.cordform.CordformNode
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x500.RDN
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle
|
||||
import org.gradle.api.Project
|
||||
import java.io.File
|
||||
@ -39,6 +38,7 @@ class Node(private val project: Project) : CordformNode() {
|
||||
|
||||
private val releaseVersion = project.rootProject.ext<String>("corda_release_version")
|
||||
internal lateinit var nodeDir: File
|
||||
private set
|
||||
|
||||
/**
|
||||
* Sets whether this node will use HTTPS communication.
|
||||
@ -60,26 +60,6 @@ class Node(private val project: Project) : CordformNode() {
|
||||
config = config.withValue("useTestClock", ConfigValueFactory.fromAnyRef(useTestClock))
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HTTP web server port for this node. Will use localhost as the address.
|
||||
*
|
||||
* @param webPort The web port number for this node.
|
||||
*/
|
||||
fun webPort(webPort: Int) {
|
||||
config = config.withValue("webAddress",
|
||||
ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$webPort"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HTTP web server address and port for this node.
|
||||
*
|
||||
* @param webAddress The web address for this node.
|
||||
*/
|
||||
fun webAddress(webAddress: String) {
|
||||
config = config.withValue("webAddress",
|
||||
ConfigValueFactory.fromAnyRef(webAddress))
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the network map address for this node.
|
||||
*
|
||||
@ -104,7 +84,6 @@ class Node(private val project: Project) : CordformNode() {
|
||||
config = config.withValue("sshd.port", ConfigValueFactory.fromAnyRef(sshdPort))
|
||||
}
|
||||
|
||||
|
||||
internal fun build() {
|
||||
configureProperties()
|
||||
installCordaJar()
|
||||
@ -118,19 +97,15 @@ class Node(private val project: Project) : CordformNode() {
|
||||
}
|
||||
|
||||
internal fun rootDir(rootDir: Path) {
|
||||
if(name == null) {
|
||||
if (name == null) {
|
||||
project.logger.error("Node has a null name - cannot create node")
|
||||
throw IllegalStateException("Node has a null name - cannot create node")
|
||||
}
|
||||
|
||||
val dirName = try {
|
||||
val o = X500Name(name).getRDNs(BCStyle.O)
|
||||
if (o.size > 0) {
|
||||
o.first().first.value.toString()
|
||||
} else {
|
||||
name
|
||||
}
|
||||
} catch(_ : IllegalArgumentException) {
|
||||
if (o.isNotEmpty()) o.first().first.value.toString() else name
|
||||
} catch (_ : IllegalArgumentException) {
|
||||
// Can't parse as an X500 name, use the full string
|
||||
name
|
||||
}
|
||||
@ -192,9 +167,8 @@ class Node(private val project: Project) : CordformNode() {
|
||||
/**
|
||||
* Installs other cordapps to this node's cordapps directory.
|
||||
*/
|
||||
private fun installCordapps() {
|
||||
internal fun installCordapps(cordapps: Collection<File> = getCordappList()) {
|
||||
val cordappsDir = File(nodeDir, "cordapps")
|
||||
val cordapps = getCordappList()
|
||||
project.copy {
|
||||
it.apply {
|
||||
from(cordapps)
|
||||
@ -280,7 +254,7 @@ class Node(private val project: Project) : CordformNode() {
|
||||
throw RuntimeException("No Corda Webserver JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-webserver-$releaseVersion.jar\"")
|
||||
} else {
|
||||
val jar = maybeJar.singleFile
|
||||
assert(jar.isFile)
|
||||
require(jar.isFile)
|
||||
return jar
|
||||
}
|
||||
}
|
||||
|
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
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
# Gradle >= 4.3.1 required for Enterprise only experimental:intellij-plugin
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.3.1-all.zip
|
||||
|
@ -3,7 +3,7 @@ apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
description 'Corda node Artemis API'
|
||||
description 'Corda node API'
|
||||
|
||||
dependencies {
|
||||
compile project(":core")
|
||||
|
@ -1,4 +1,4 @@
|
||||
package net.corda.node.utilities
|
||||
package net.corda.nodeapi.internal.crypto
|
||||
|
||||
import net.corda.core.crypto.SignatureScheme
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
|
@ -1,9 +1,8 @@
|
||||
@file:JvmName("KeyStoreUtilities")
|
||||
|
||||
package net.corda.node.utilities
|
||||
package net.corda.nodeapi.internal.crypto
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.*
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import java.io.IOException
|
||||
@ -11,9 +10,7 @@ import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.Path
|
||||
import java.security.*
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
const val KEYSTORE_TYPE = "JKS"
|
||||
@ -169,44 +166,3 @@ fun KeyStore.getSupportedKey(alias: String, keyPassword: String): PrivateKey {
|
||||
val key = getKey(alias, keyPass) as PrivateKey
|
||||
return Crypto.toSupportedPrivateKey(key)
|
||||
}
|
||||
|
||||
class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) {
|
||||
private val keyStore = storePath.read { loadKeyStore(it, storePassword) }
|
||||
|
||||
private fun createCertificate(serviceName: CordaX500Name, pubKey: PublicKey): CertPath {
|
||||
val clientCertPath = keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
|
||||
// Assume key password = store password.
|
||||
val clientCA = certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
|
||||
// Create new keys and store in keystore.
|
||||
val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, pubKey)
|
||||
val certPath = CertificateFactory.getInstance("X509").generateCertPath(listOf(cert.cert) + clientCertPath)
|
||||
require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" }
|
||||
// TODO: X509Utilities.validateCertificateChain()
|
||||
return certPath
|
||||
}
|
||||
|
||||
fun signAndSaveNewKeyPair(serviceName: CordaX500Name, privateKeyAlias: String, keyPair: KeyPair) {
|
||||
val certPath = createCertificate(serviceName, keyPair.public)
|
||||
// Assume key password = store password.
|
||||
keyStore.addOrReplaceKey(privateKeyAlias, keyPair.private, storePassword.toCharArray(), certPath.certificates.toTypedArray())
|
||||
keyStore.save(storePath, storePassword)
|
||||
}
|
||||
|
||||
fun savePublicKey(serviceName: CordaX500Name, pubKeyAlias: String, pubKey: PublicKey) {
|
||||
val certPath = createCertificate(serviceName, pubKey)
|
||||
// Assume key password = store password.
|
||||
keyStore.addOrReplaceCertificate(pubKeyAlias, certPath.certificates.first())
|
||||
keyStore.save(storePath, storePassword)
|
||||
}
|
||||
|
||||
// Delegate methods to keystore. Sadly keystore doesn't have an interface.
|
||||
fun containsAlias(alias: String) = keyStore.containsAlias(alias)
|
||||
|
||||
fun getX509Certificate(alias: String) = keyStore.getX509Certificate(alias)
|
||||
|
||||
fun getCertificateChain(alias: String): Array<out Certificate> = keyStore.getCertificateChain(alias)
|
||||
|
||||
fun getCertificate(alias: String): Certificate = keyStore.getCertificate(alias)
|
||||
|
||||
fun certificateAndKeyPair(alias: String): CertificateAndKeyPair = keyStore.getCertificateAndKeyPair(alias, storePassword)
|
||||
}
|
@ -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.SignatureScheme
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.read
|
||||
import net.corda.core.internal.write
|
||||
import net.corda.core.internal.x500Name
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.utilities.millis
|
||||
@ -25,10 +27,10 @@ import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder
|
||||
import org.bouncycastle.util.io.pem.PemReader
|
||||
import java.io.FileReader
|
||||
import java.io.FileWriter
|
||||
import java.io.InputStream
|
||||
import java.math.BigInteger
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
@ -52,6 +54,7 @@ object X509Utilities {
|
||||
const val CORDA_CLIENT_CA_CN = "Corda Client CA Certificate"
|
||||
|
||||
private val DEFAULT_VALIDITY_WINDOW = Pair(0.millis, 3650.days)
|
||||
|
||||
/**
|
||||
* Helper function to return the latest out of an instant and an optional date.
|
||||
*/
|
||||
@ -89,7 +92,9 @@ object X509Utilities {
|
||||
* Create a de novo root self-signed X509 v3 CA cert.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun createSelfSignedCACertificate(subject: CordaX500Name, keyPair: KeyPair, validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW): X509CertificateHolder {
|
||||
fun createSelfSignedCACertificate(subject: CordaX500Name,
|
||||
keyPair: KeyPair,
|
||||
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW): X509CertificateHolder {
|
||||
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second)
|
||||
return createCertificate(CertificateType.ROOT_CA, subject.x500Name, keyPair, subject.x500Name, keyPair.public, window)
|
||||
}
|
||||
@ -114,8 +119,9 @@ object X509Utilities {
|
||||
subject: CordaX500Name,
|
||||
subjectPublicKey: PublicKey,
|
||||
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW,
|
||||
nameConstraints: NameConstraints? = null): X509CertificateHolder
|
||||
= createCertificate(certificateType, issuerCertificate, issuerKeyPair, subject.x500Name, subjectPublicKey, validityWindow, nameConstraints)
|
||||
nameConstraints: NameConstraints? = null): X509CertificateHolder {
|
||||
return createCertificate(certificateType, issuerCertificate, issuerKeyPair, subject.x500Name, subjectPublicKey, validityWindow, nameConstraints)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a X509 v3 certificate for use as a CA or for TLS. This does not require a [CordaX500Name] because the
|
||||
@ -145,10 +151,9 @@ object X509Utilities {
|
||||
@Throws(CertPathValidatorException::class)
|
||||
fun validateCertificateChain(trustedRoot: X509Certificate, vararg certificates: Certificate) {
|
||||
require(certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" }
|
||||
val certFactory = CertificateFactory.getInstance("X509")
|
||||
val params = PKIXParameters(setOf(TrustAnchor(trustedRoot, null)))
|
||||
params.isRevocationEnabled = false
|
||||
val certPath = certFactory.generateCertPath(certificates.toList())
|
||||
val certPath = X509CertificateFactory().delegate.generateCertPath(certificates.toList())
|
||||
val pathValidator = CertPathValidator.getInstance("PKIX")
|
||||
pathValidator.validate(certPath, params)
|
||||
}
|
||||
@ -156,30 +161,29 @@ object X509Utilities {
|
||||
/**
|
||||
* Helper method to store a .pem/.cer format file copy of a certificate if required for import into a PC/Mac, or for inspection.
|
||||
* @param x509Certificate certificate to save.
|
||||
* @param filename Target filename.
|
||||
* @param file Target file.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun saveCertificateAsPEMFile(x509Certificate: X509CertificateHolder, filename: Path) {
|
||||
FileWriter(filename.toFile()).use {
|
||||
JcaPEMWriter(it).use {
|
||||
it.writeObject(x509Certificate)
|
||||
}
|
||||
fun saveCertificateAsPEMFile(x509Certificate: X509CertificateHolder, file: Path) {
|
||||
JcaPEMWriter(file.toFile().writer()).use {
|
||||
it.writeObject(x509Certificate)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to load back a .pem/.cer format file copy of a certificate.
|
||||
* @param filename Source filename.
|
||||
* @param file Source file.
|
||||
* @return The X509Certificate that was encoded in the file.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun loadCertificateFromPEMFile(filename: Path): X509CertificateHolder {
|
||||
val reader = PemReader(FileReader(filename.toFile()))
|
||||
val pemObject = reader.readPemObject()
|
||||
val cert = X509CertificateHolder(pemObject.content)
|
||||
return cert.apply {
|
||||
isValidOn(Date())
|
||||
fun loadCertificateFromPEMFile(file: Path): X509CertificateHolder {
|
||||
val cert = file.read {
|
||||
val reader = PemReader(it.reader())
|
||||
val pemObject = reader.readPemObject()
|
||||
X509CertificateHolder(pemObject.content)
|
||||
}
|
||||
cert.isValidOn(Date())
|
||||
return cert
|
||||
}
|
||||
|
||||
/**
|
||||
@ -243,13 +247,13 @@ object X509Utilities {
|
||||
* @param validityWindow the time period the certificate is valid for.
|
||||
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
|
||||
*/
|
||||
internal fun createCertificate(certificateType: CertificateType,
|
||||
issuer: X500Name,
|
||||
issuerSigner: ContentSigner,
|
||||
subject: CordaX500Name,
|
||||
subjectPublicKey: PublicKey,
|
||||
validityWindow: Pair<Date, Date>,
|
||||
nameConstraints: NameConstraints? = null): X509CertificateHolder {
|
||||
fun createCertificate(certificateType: CertificateType,
|
||||
issuer: X500Name,
|
||||
issuerSigner: ContentSigner,
|
||||
subject: CordaX500Name,
|
||||
subjectPublicKey: PublicKey,
|
||||
validityWindow: Pair<Date, Date>,
|
||||
nameConstraints: NameConstraints? = null): X509CertificateHolder {
|
||||
val builder = createCertificate(certificateType, issuer, subject.x500Name, subjectPublicKey, validityWindow, nameConstraints)
|
||||
return builder.build(issuerSigner).apply {
|
||||
require(isValidOn(Date()))
|
||||
@ -266,11 +270,13 @@ object X509Utilities {
|
||||
* @param validityWindow the time period the certificate is valid for.
|
||||
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
|
||||
*/
|
||||
internal fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerKeyPair: KeyPair,
|
||||
subject: X500Name, subjectPublicKey: PublicKey,
|
||||
validityWindow: Pair<Date, Date>,
|
||||
nameConstraints: NameConstraints? = null): X509CertificateHolder {
|
||||
|
||||
fun createCertificate(certificateType: CertificateType,
|
||||
issuer: X500Name,
|
||||
issuerKeyPair: KeyPair,
|
||||
subject: X500Name,
|
||||
subjectPublicKey: PublicKey,
|
||||
validityWindow: Pair<Date, Date>,
|
||||
nameConstraints: NameConstraints? = null): X509CertificateHolder {
|
||||
val signatureScheme = Crypto.findSignatureScheme(issuerKeyPair.private)
|
||||
val provider = Crypto.findProvider(signatureScheme.providerName)
|
||||
val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints)
|
||||
@ -285,28 +291,71 @@ object X509Utilities {
|
||||
/**
|
||||
* Create certificate signing request using provided information.
|
||||
*/
|
||||
internal fun createCertificateSigningRequest(subject: CordaX500Name, email: String, keyPair: KeyPair, signatureScheme: SignatureScheme): PKCS10CertificationRequest {
|
||||
private fun createCertificateSigningRequest(subject: CordaX500Name,
|
||||
email: String,
|
||||
keyPair: KeyPair,
|
||||
signatureScheme: SignatureScheme): PKCS10CertificationRequest {
|
||||
val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, Crypto.findProvider(signatureScheme.providerName))
|
||||
return JcaPKCS10CertificationRequestBuilder(subject.x500Name, keyPair.public).addAttribute(BCStyle.E, DERUTF8String(email)).build(signer)
|
||||
}
|
||||
|
||||
fun createCertificateSigningRequest(subject: CordaX500Name, email: String, keyPair: KeyPair) = createCertificateSigningRequest(subject, email, keyPair, DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
fun createCertificateSigningRequest(subject: CordaX500Name, email: String, keyPair: KeyPair): PKCS10CertificationRequest {
|
||||
return createCertificateSigningRequest(subject, email, keyPair, DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CertificateStream(val input: InputStream) {
|
||||
private val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||
|
||||
fun nextCertificate(): X509Certificate = certificateFactory.generateCertificate(input) as X509Certificate
|
||||
/**
|
||||
* Wraps a [CertificateFactory] to remove boilerplate. It's unclear whether [CertificateFactory] is threadsafe so best
|
||||
* so assume this class is not.
|
||||
*/
|
||||
class X509CertificateFactory {
|
||||
val delegate: CertificateFactory = CertificateFactory.getInstance("X.509")
|
||||
fun generateCertificate(input: InputStream): X509Certificate {
|
||||
return delegate.generateCertificate(input) as X509Certificate
|
||||
}
|
||||
}
|
||||
|
||||
enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurposeId, val isCA: Boolean) {
|
||||
ROOT_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true),
|
||||
INTERMEDIATE_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true),
|
||||
CLIENT_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true),
|
||||
TLS(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.keyAgreement), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = false),
|
||||
ROOT_CA(
|
||||
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign),
|
||||
KeyPurposeId.id_kp_serverAuth,
|
||||
KeyPurposeId.id_kp_clientAuth,
|
||||
KeyPurposeId.anyExtendedKeyUsage,
|
||||
isCA = true
|
||||
),
|
||||
|
||||
INTERMEDIATE_CA(
|
||||
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign),
|
||||
KeyPurposeId.id_kp_serverAuth,
|
||||
KeyPurposeId.id_kp_clientAuth,
|
||||
KeyPurposeId.anyExtendedKeyUsage,
|
||||
isCA = true
|
||||
),
|
||||
|
||||
CLIENT_CA(
|
||||
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign),
|
||||
KeyPurposeId.id_kp_serverAuth,
|
||||
KeyPurposeId.id_kp_clientAuth,
|
||||
KeyPurposeId.anyExtendedKeyUsage,
|
||||
isCA = true
|
||||
),
|
||||
|
||||
TLS(
|
||||
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.keyAgreement),
|
||||
KeyPurposeId.id_kp_serverAuth,
|
||||
KeyPurposeId.id_kp_clientAuth,
|
||||
KeyPurposeId.anyExtendedKeyUsage,
|
||||
isCA = false
|
||||
),
|
||||
|
||||
// TODO: Identity certs should have only limited depth (i.e. 1) CA signing capability, with tight name constraints
|
||||
IDENTITY(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true)
|
||||
IDENTITY(
|
||||
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign),
|
||||
KeyPurposeId.id_kp_serverAuth,
|
||||
KeyPurposeId.id_kp_clientAuth,
|
||||
KeyPurposeId.anyExtendedKeyUsage,
|
||||
isCA = true
|
||||
)
|
||||
}
|
||||
|
||||
data class CertificateAndKeyPair(val certificate: X509CertificateHolder, val keyPair: KeyPair)
|
@ -1,63 +1,80 @@
|
||||
package net.corda.node.utilities
|
||||
package net.corda.nodeapi.internal.persistence
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.node.services.api.SchemaService
|
||||
import net.corda.node.services.config.DatabaseConfig
|
||||
import net.corda.node.services.persistence.HibernateConfiguration
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import rx.Observable
|
||||
import rx.Subscriber
|
||||
import rx.subjects.UnicastSubject
|
||||
import java.io.Closeable
|
||||
import java.sql.Connection
|
||||
import java.sql.SQLException
|
||||
import java.util.*
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import javax.persistence.AttributeConverter
|
||||
import javax.sql.DataSource
|
||||
|
||||
/**
|
||||
* Table prefix for all tables owned by the node module.
|
||||
*/
|
||||
const val NODE_DATABASE_PREFIX = "node_"
|
||||
|
||||
//HikariDataSource implements Closeable which allows CordaPersistence to be Closeable
|
||||
// This class forms part of the node config and so any changes to it must be handled with care
|
||||
data class DatabaseConfig(
|
||||
val initialiseSchema: Boolean = true,
|
||||
val serverNameTablePrefix: String = "",
|
||||
val transactionIsolationLevel: TransactionIsolationLevel = TransactionIsolationLevel.REPEATABLE_READ,
|
||||
val schema: String? = null
|
||||
)
|
||||
|
||||
// This class forms part of the node config and so any changes to it must be handled with care
|
||||
enum class TransactionIsolationLevel {
|
||||
NONE,
|
||||
READ_UNCOMMITTED,
|
||||
READ_COMMITTED,
|
||||
REPEATABLE_READ,
|
||||
SERIALIZABLE;
|
||||
|
||||
/**
|
||||
* The JDBC constant value of the same name but prefixed with TRANSACTION_ defined in [java.sql.Connection].
|
||||
*/
|
||||
val jdbcValue: Int = java.sql.Connection::class.java.getField("TRANSACTION_$name").get(null) as Int
|
||||
}
|
||||
|
||||
class CordaPersistence(
|
||||
val dataSource: HikariDataSource,
|
||||
private val schemaService: SchemaService,
|
||||
private val identityService: IdentityService,
|
||||
databaseConfig: DatabaseConfig
|
||||
val dataSource: DataSource,
|
||||
databaseConfig: DatabaseConfig,
|
||||
schemas: Set<MappedSchema>,
|
||||
attributeConverters: Collection<AttributeConverter<*, *>> = emptySet()
|
||||
) : Closeable {
|
||||
val transactionIsolationLevel = databaseConfig.transactionIsolationLevel.jdbcValue
|
||||
val defaultIsolationLevel = databaseConfig.transactionIsolationLevel
|
||||
val hibernateConfig: HibernateConfiguration by lazy {
|
||||
transaction {
|
||||
HibernateConfiguration(schemaService, databaseConfig, identityService)
|
||||
HibernateConfiguration(schemas, databaseConfig, attributeConverters)
|
||||
}
|
||||
}
|
||||
val entityManagerFactory get() = hibernateConfig.sessionFactoryForRegisteredSchemas
|
||||
|
||||
companion object {
|
||||
fun connect(dataSource: HikariDataSource, schemaService: SchemaService, identityService: IdentityService, databaseConfig: DatabaseConfig): CordaPersistence {
|
||||
return CordaPersistence(dataSource, schemaService, identityService, databaseConfig).apply {
|
||||
DatabaseTransactionManager(this)
|
||||
init {
|
||||
DatabaseTransactionManager(this)
|
||||
// Check not in read-only mode.
|
||||
transaction {
|
||||
dataSource.connection.use {
|
||||
check(!it.metaData.isReadOnly) { "Database should not be readonly." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of [DatabaseTransaction], with the given isolation level.
|
||||
* @param isolationLevel isolation level for the transaction. If not specified the default (i.e. provided at the creation time) is used.
|
||||
* Creates an instance of [DatabaseTransaction], with the given transaction isolation level.
|
||||
*/
|
||||
fun createTransaction(isolationLevel: Int): DatabaseTransaction {
|
||||
fun createTransaction(isolationLevel: TransactionIsolationLevel): DatabaseTransaction {
|
||||
// We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases.
|
||||
DatabaseTransactionManager.dataSource = this
|
||||
return DatabaseTransactionManager.currentOrNew(isolationLevel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of [DatabaseTransaction], with the transaction isolation level specified at the creation time.
|
||||
* Creates an instance of [DatabaseTransaction], with the default transaction isolation level.
|
||||
*/
|
||||
fun createTransaction(): DatabaseTransaction = createTransaction(transactionIsolationLevel)
|
||||
fun createTransaction(): DatabaseTransaction = createTransaction(defaultIsolationLevel)
|
||||
|
||||
fun createSession(): Connection {
|
||||
// We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases.
|
||||
@ -71,7 +88,7 @@ class CordaPersistence(
|
||||
* @param isolationLevel isolation level for the transaction.
|
||||
* @param statement to be executed in the scope of this transaction.
|
||||
*/
|
||||
fun <T> transaction(isolationLevel: Int, statement: DatabaseTransaction.() -> T): T {
|
||||
fun <T> transaction(isolationLevel: TransactionIsolationLevel, statement: DatabaseTransaction.() -> T): T {
|
||||
DatabaseTransactionManager.dataSource = this
|
||||
return transaction(isolationLevel, 3, statement)
|
||||
}
|
||||
@ -80,22 +97,21 @@ class CordaPersistence(
|
||||
* Executes given statement in the scope of transaction with the transaction level specified at the creation time.
|
||||
* @param statement to be executed in the scope of this transaction.
|
||||
*/
|
||||
fun <T> transaction(statement: DatabaseTransaction.() -> T): T = transaction(transactionIsolationLevel, statement)
|
||||
fun <T> transaction(statement: DatabaseTransaction.() -> T): T = transaction(defaultIsolationLevel, statement)
|
||||
|
||||
private fun <T> transaction(transactionIsolation: Int, repetitionAttempts: Int, statement: DatabaseTransaction.() -> T): T {
|
||||
private fun <T> transaction(isolationLevel: TransactionIsolationLevel, repetitionAttempts: Int, statement: DatabaseTransaction.() -> T): T {
|
||||
val outer = DatabaseTransactionManager.currentOrNull()
|
||||
|
||||
return if (outer != null) {
|
||||
outer.statement()
|
||||
} else {
|
||||
inTopLevelTransaction(transactionIsolation, repetitionAttempts, statement)
|
||||
inTopLevelTransaction(isolationLevel, repetitionAttempts, statement)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> inTopLevelTransaction(transactionIsolation: Int, repetitionAttempts: Int, statement: DatabaseTransaction.() -> T): T {
|
||||
private fun <T> inTopLevelTransaction(isolationLevel: TransactionIsolationLevel, repetitionAttempts: Int, statement: DatabaseTransaction.() -> T): T {
|
||||
var repetitions = 0
|
||||
while (true) {
|
||||
val transaction = DatabaseTransactionManager.currentOrNew(transactionIsolation)
|
||||
val transaction = DatabaseTransactionManager.currentOrNew(isolationLevel)
|
||||
try {
|
||||
val answer = transaction.statement()
|
||||
transaction.commit()
|
||||
@ -116,23 +132,11 @@ class CordaPersistence(
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
dataSource.close()
|
||||
// DataSource doesn't implement AutoCloseable so we just have to hope that the implementation does so that we can close it
|
||||
(dataSource as? AutoCloseable)?.close()
|
||||
}
|
||||
}
|
||||
|
||||
fun configureDatabase(dataSourceProperties: Properties, databaseConfig: DatabaseConfig, identityService: IdentityService, schemaService: SchemaService = NodeSchemaService(null)): CordaPersistence {
|
||||
val config = HikariConfig(dataSourceProperties)
|
||||
val dataSource = HikariDataSource(config)
|
||||
val persistence = CordaPersistence.connect(dataSource, schemaService, identityService, databaseConfig)
|
||||
// Check not in read-only mode.
|
||||
persistence.transaction {
|
||||
persistence.dataSource.connection.use {
|
||||
check(!it.metaData.isReadOnly) { "Database should not be readonly." }
|
||||
}
|
||||
}
|
||||
return persistence
|
||||
}
|
||||
|
||||
/**
|
||||
* Buffer observations until after the current database transaction has been closed. Observations are never
|
||||
* dropped, simply delayed.
|
||||
@ -144,7 +148,7 @@ fun configureDatabase(dataSourceProperties: Properties, databaseConfig: Database
|
||||
*/
|
||||
fun <T : Any> rx.Observer<T>.bufferUntilDatabaseCommit(): rx.Observer<T> {
|
||||
val currentTxId = DatabaseTransactionManager.transactionId
|
||||
val databaseTxBoundary: Observable<DatabaseTransactionManager.Boundary> = DatabaseTransactionManager.transactionBoundaries.filter { it.txId == currentTxId }.first()
|
||||
val databaseTxBoundary: Observable<DatabaseTransactionManager.Boundary> = DatabaseTransactionManager.transactionBoundaries.first { it.txId == currentTxId }
|
||||
val subject = UnicastSubject.create<T>()
|
||||
subject.delaySubscription(databaseTxBoundary).subscribe(this)
|
||||
databaseTxBoundary.doOnCompleted { subject.onCompleted() }
|
||||
@ -183,14 +187,9 @@ private class DatabaseTransactionWrappingSubscriber<U>(val db: CordaPersistence?
|
||||
|
||||
// A subscriber that wraps another but does not pass on observations to it.
|
||||
private class NoOpSubscriber<U>(t: Subscriber<in U>) : Subscriber<U>(t) {
|
||||
override fun onCompleted() {
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable?) {
|
||||
}
|
||||
|
||||
override fun onNext(s: U) {
|
||||
}
|
||||
override fun onCompleted() {}
|
||||
override fun onError(e: Throwable?) {}
|
||||
override fun onNext(s: U) {}
|
||||
}
|
||||
|
||||
/**
|
@ -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 org.hibernate.Session
|
||||
import org.hibernate.Transaction
|
||||
import rx.subjects.PublishSubject
|
||||
import rx.subjects.Subject
|
||||
import java.sql.Connection
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class DatabaseTransaction(isolation: Int, val threadLocal: ThreadLocal<DatabaseTransaction>,
|
||||
val transactionBoundaries: Subject<DatabaseTransactionManager.Boundary, DatabaseTransactionManager.Boundary>,
|
||||
val cordaPersistence: CordaPersistence) {
|
||||
fun currentDBSession(): Session = DatabaseTransactionManager.current().session
|
||||
|
||||
val id: UUID = UUID.randomUUID()
|
||||
|
||||
val connection: Connection by lazy(LazyThreadSafetyMode.NONE) {
|
||||
cordaPersistence.dataSource.connection
|
||||
.apply {
|
||||
autoCommit = false
|
||||
transactionIsolation = isolation
|
||||
}
|
||||
}
|
||||
|
||||
private val sessionDelegate = lazy {
|
||||
val session = cordaPersistence.entityManagerFactory.withOptions().connection(connection).openSession()
|
||||
hibernateTransaction = session.beginTransaction()
|
||||
session
|
||||
}
|
||||
|
||||
val session: Session by sessionDelegate
|
||||
private lateinit var hibernateTransaction: Transaction
|
||||
|
||||
private val outerTransaction: DatabaseTransaction? = threadLocal.get()
|
||||
|
||||
fun commit() {
|
||||
if (sessionDelegate.isInitialized()) {
|
||||
hibernateTransaction.commit()
|
||||
}
|
||||
connection.commit()
|
||||
}
|
||||
|
||||
fun rollback() {
|
||||
if (sessionDelegate.isInitialized() && session.isOpen) {
|
||||
session.clear()
|
||||
}
|
||||
if (!connection.isClosed) {
|
||||
connection.rollback()
|
||||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
if (sessionDelegate.isInitialized() && session.isOpen) {
|
||||
session.close()
|
||||
}
|
||||
connection.close()
|
||||
threadLocal.set(outerTransaction)
|
||||
if (outerTransaction == null) {
|
||||
transactionBoundaries.onNext(DatabaseTransactionManager.Boundary(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun currentDBSession() = DatabaseTransactionManager.current().session
|
||||
class DatabaseTransactionManager(initDataSource: CordaPersistence) {
|
||||
companion object {
|
||||
private val threadLocalDb = ThreadLocal<CordaPersistence>()
|
||||
@ -95,11 +41,15 @@ class DatabaseTransactionManager(initDataSource: CordaPersistence) {
|
||||
|
||||
fun currentOrNull(): DatabaseTransaction? = manager.currentOrNull()
|
||||
|
||||
fun currentOrNew(isolation: Int = dataSource.transactionIsolationLevel) = currentOrNull() ?: manager.newTransaction(isolation)
|
||||
fun currentOrNew(isolation: TransactionIsolationLevel = dataSource.defaultIsolationLevel): DatabaseTransaction {
|
||||
return currentOrNull() ?: manager.newTransaction(isolation.jdbcValue)
|
||||
}
|
||||
|
||||
fun current(): DatabaseTransaction = currentOrNull() ?: error("No transaction in context.")
|
||||
|
||||
fun newTransaction(isolation: Int = dataSource.transactionIsolationLevel) = manager.newTransaction(isolation)
|
||||
fun newTransaction(isolation: TransactionIsolationLevel = dataSource.defaultIsolationLevel): DatabaseTransaction {
|
||||
return manager.newTransaction(isolation.jdbcValue)
|
||||
}
|
||||
}
|
||||
|
||||
data class Boundary(val txId: UUID)
|
@ -1,13 +1,9 @@
|
||||
package net.corda.node.services.persistence
|
||||
package net.corda.nodeapi.internal.persistence
|
||||
|
||||
import net.corda.core.internal.castIfPossible
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.toHexString
|
||||
import net.corda.node.services.api.SchemaService
|
||||
import net.corda.node.services.config.DatabaseConfig
|
||||
import net.corda.node.utilities.DatabaseTransactionManager
|
||||
import org.hibernate.SessionFactory
|
||||
import org.hibernate.boot.MetadataSources
|
||||
import org.hibernate.boot.model.naming.Identifier
|
||||
@ -18,14 +14,18 @@ import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider
|
||||
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment
|
||||
import org.hibernate.service.UnknownUnwrapTypeException
|
||||
import org.hibernate.type.AbstractSingleColumnStandardBasicType
|
||||
import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry
|
||||
import org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor
|
||||
import org.hibernate.type.descriptor.sql.BlobTypeDescriptor
|
||||
import org.hibernate.type.descriptor.sql.VarbinaryTypeDescriptor
|
||||
import java.sql.Connection
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.persistence.AttributeConverter
|
||||
|
||||
class HibernateConfiguration(val schemaService: SchemaService, private val databaseConfig: DatabaseConfig, private val identityService: IdentityService) {
|
||||
class HibernateConfiguration(
|
||||
schemas: Set<MappedSchema>,
|
||||
private val databaseConfig: DatabaseConfig,
|
||||
private val attributeConverters: Collection<AttributeConverter<*, *>>
|
||||
) {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
@ -33,13 +33,8 @@ class HibernateConfiguration(val schemaService: SchemaService, private val datab
|
||||
// TODO: make this a guava cache or similar to limit ability for this to grow forever.
|
||||
private val sessionFactories = ConcurrentHashMap<Set<MappedSchema>, SessionFactory>()
|
||||
|
||||
val sessionFactoryForRegisteredSchemas = schemaService.schemaOptions.keys.let {
|
||||
val sessionFactoryForRegisteredSchemas = schemas.let {
|
||||
logger.info("Init HibernateConfiguration for schemas: $it")
|
||||
// Register the AbstractPartyDescriptor so Hibernate doesn't warn when encountering AbstractParty. Unfortunately
|
||||
// Hibernate warns about not being able to find a descriptor if we don't provide one, but won't use it by default
|
||||
// so we end up providing both descriptor and converter. We should re-examine this in later versions to see if
|
||||
// either Hibernate can be convinced to stop warning, use the descriptor by default, or something else.
|
||||
JavaTypeDescriptorRegistry.INSTANCE.addDescriptor(AbstractPartyDescriptor(identityService))
|
||||
sessionFactoryForSchemas(it)
|
||||
}
|
||||
|
||||
@ -54,7 +49,7 @@ class HibernateConfiguration(val schemaService: SchemaService, private val datab
|
||||
// necessarily remain and would likely be replaced by something like Liquibase. For now it is very convenient though.
|
||||
// TODO: replace auto schema generation as it isn't intended for production use, according to Hibernate docs.
|
||||
val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", NodeDatabaseConnectionProvider::class.java.name)
|
||||
.setProperty("hibernate.hbm2ddl.auto", if (databaseConfig.initDatabase) "update" else "validate")
|
||||
.setProperty("hibernate.hbm2ddl.auto", if (databaseConfig.initialiseSchema) "update" else "validate")
|
||||
.setProperty("hibernate.format_sql", "true")
|
||||
.setProperty("hibernate.connection.isolation", databaseConfig.transactionIsolationLevel.jdbcValue.toString())
|
||||
|
||||
@ -83,7 +78,7 @@ class HibernateConfiguration(val schemaService: SchemaService, private val datab
|
||||
}
|
||||
})
|
||||
// register custom converters
|
||||
applyAttributeConverter(AbstractPartyToX500NameAsStringConverter(identityService))
|
||||
attributeConverters.forEach { applyAttributeConverter(it) }
|
||||
// Register a tweaked version of `org.hibernate.type.MaterializedBlobType` that truncates logged messages.
|
||||
// to avoid OOM when large blobs might get logged.
|
||||
applyBasicType(CordaMaterializedBlobType, CordaMaterializedBlobType.name)
|
@ -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.
|
||||
*/
|
||||
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<*>) {
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -77,8 +77,8 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) {
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) {
|
||||
// TODO: Can we verify the entries in the list?
|
||||
concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, schema, declaredType.actualTypeArguments[0]) })
|
||||
concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, schemas, declaredType.actualTypeArguments[0]) })
|
||||
}
|
||||
}
|
@ -67,8 +67,8 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T> {
|
||||
superClassSerializer.writeDescribedObject(obj, data, type, output)
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T {
|
||||
return superClassSerializer.readObject(obj, schema, input)
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): T {
|
||||
return superClassSerializer.readObject(obj, schemas, input)
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,8 +133,8 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T> {
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T {
|
||||
val proxy: P = uncheckedCast(proxySerializer.readObject(obj, schema, input))
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): T {
|
||||
val proxy: P = uncheckedCast(proxySerializer.readObject(obj, schemas, input))
|
||||
return fromProxy(proxy)
|
||||
}
|
||||
}
|
||||
@ -166,7 +166,7 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T> {
|
||||
data.putString(unmaker(obj))
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T {
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): T {
|
||||
val proxy = obj as String
|
||||
return maker(proxy)
|
||||
}
|
||||
|
@ -97,21 +97,21 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
|
||||
@Throws(NotSerializableException::class)
|
||||
fun <T : Any> deserialize(bytes: ByteSequence, clazz: Class<T>): T = des {
|
||||
val envelope = getEnvelope(bytes)
|
||||
clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz))
|
||||
clazz.cast(readObjectOrNull(envelope.obj, SerializationSchemas(envelope.schema, envelope.transformsSchema), clazz))
|
||||
}
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
fun <T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>, clazz: Class<T>): ObjectAndEnvelope<T> = des {
|
||||
val envelope = getEnvelope(bytes)
|
||||
// Now pick out the obj and schema from the envelope.
|
||||
ObjectAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)), envelope)
|
||||
ObjectAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, SerializationSchemas(envelope.schema, envelope.transformsSchema), clazz)), envelope)
|
||||
}
|
||||
|
||||
internal fun readObjectOrNull(obj: Any?, schema: Schema, type: Type): Any? {
|
||||
internal fun readObjectOrNull(obj: Any?, schema: SerializationSchemas, type: Type): Any? {
|
||||
return if (obj == null) null else readObject(obj, schema, type)
|
||||
}
|
||||
|
||||
internal fun readObject(obj: Any, schema: Schema, type: Type): Any =
|
||||
internal fun readObject(obj: Any, schemas: SerializationSchemas, type: Type): Any =
|
||||
if (obj is DescribedType && ReferencedObject.DESCRIPTOR == obj.descriptor) {
|
||||
// It must be a reference to an instance that has already been read, cheaply and quickly returning it by reference.
|
||||
val objectIndex = (obj.described as UnsignedInteger).toInt()
|
||||
@ -127,11 +127,11 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
|
||||
val objectRead = when (obj) {
|
||||
is DescribedType -> {
|
||||
// Look up serializer in factory by descriptor
|
||||
val serializer = serializerFactory.get(obj.descriptor, schema)
|
||||
val serializer = serializerFactory.get(obj.descriptor, schemas)
|
||||
if (SerializerFactory.AnyType != type && serializer.type != type && with(serializer.type) { !isSubClassOf(type) && !materiallyEquivalentTo(type) })
|
||||
throw NotSerializableException("Described type with descriptor ${obj.descriptor} was " +
|
||||
"expected to be of type $type but was ${serializer.type}")
|
||||
serializer.readObject(obj.described, schema, this)
|
||||
serializer.readObject(obj.described, schemas, this)
|
||||
}
|
||||
is Binary -> obj.array
|
||||
else -> obj // this will be the case for primitive types like [boolean] et al.
|
||||
|
@ -27,7 +27,7 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Seria
|
||||
output.writeTypeNotations(typeNotation)
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any {
|
||||
val enumName = (obj as List<*>)[0] as String
|
||||
val enumOrd = obj[1] as Int
|
||||
val fromOrd = type.asClass()!!.enumConstants[enumOrd] as Enum<*>?
|
||||
|
@ -32,8 +32,8 @@ class EvolutionSerializer(
|
||||
* @param property object to read the actual property value
|
||||
*/
|
||||
data class OldParam(val type: Type, val idx: Int, val property: PropertySerializer) {
|
||||
fun readProperty(paramValues: List<*>, schema: Schema, input: DeserializationInput) =
|
||||
property.readProperty(paramValues[idx], schema, input)
|
||||
fun readProperty(paramValues: List<*>, schemas: SerializationSchemas, input: DeserializationInput) =
|
||||
property.readProperty(paramValues[idx], schemas, input)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -121,10 +121,10 @@ class EvolutionSerializer(
|
||||
*
|
||||
* TODO: Object references
|
||||
*/
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any {
|
||||
if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj")
|
||||
|
||||
return construct(readers.map { it?.readProperty(obj, schema, input) })
|
||||
return construct(readers.map { it?.readProperty(obj, schemas, input) })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,15 +88,15 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) {
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) {
|
||||
// TODO: General generics question. Do we need to validate that entries in Maps and Collections match the generic type? Is it a security hole?
|
||||
val entries: Iterable<Pair<Any?, Any?>> = (obj as Map<*, *>).map { readEntry(schema, input, it) }
|
||||
val entries: Iterable<Pair<Any?, Any?>> = (obj as Map<*, *>).map { readEntry(schemas, input, it) }
|
||||
concreteBuilder(entries.toMap())
|
||||
}
|
||||
|
||||
private fun readEntry(schema: Schema, input: DeserializationInput, entry: Map.Entry<Any?, Any?>) =
|
||||
input.readObjectOrNull(entry.key, schema, declaredType.actualTypeArguments[0]) to
|
||||
input.readObjectOrNull(entry.value, schema, declaredType.actualTypeArguments[1])
|
||||
private fun readEntry(schemas: SerializationSchemas, input: DeserializationInput, entry: Map.Entry<Any?, Any?>) =
|
||||
input.readObjectOrNull(entry.key, schemas, declaredType.actualTypeArguments[0]) to
|
||||
input.readObjectOrNull(entry.value, schemas, declaredType.actualTypeArguments[1])
|
||||
|
||||
// Cannot use * as a bound for EnumMap and EnumSet since * is not an enum. So, we use a sample enum instead.
|
||||
// We don't actually care about the type, we just need to make the compiler happier.
|
||||
|
@ -55,10 +55,15 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ clazz.typeName }) {
|
||||
override fun readObject(
|
||||
obj: Any,
|
||||
schemas: SerializationSchemas,
|
||||
input: DeserializationInput): Any = ifThrowsAppend({ clazz.typeName }) {
|
||||
if (obj is List<*>) {
|
||||
if (obj.size > propertySerializers.size) throw NotSerializableException("Too many properties in described type $typeName")
|
||||
val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schema, input) }
|
||||
if (obj.size > propertySerializers.size) {
|
||||
throw NotSerializableException("Too many properties in described type $typeName")
|
||||
}
|
||||
val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schemas, input) }
|
||||
construct(params)
|
||||
} else throw NotSerializableException("Body of described type is unexpected $obj")
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import kotlin.reflect.jvm.javaGetter
|
||||
sealed class PropertySerializer(val name: String, val readMethod: Method?, val resolvedType: Type) {
|
||||
abstract fun writeClassInfo(output: SerializationOutput)
|
||||
abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput)
|
||||
abstract fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any?
|
||||
abstract fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any?
|
||||
|
||||
val type: String = generateType()
|
||||
val requires: List<String> = generateRequires()
|
||||
@ -91,8 +91,8 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
|
||||
}
|
||||
}
|
||||
|
||||
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? = ifThrowsAppend({ nameForDebug }) {
|
||||
input.readObjectOrNull(obj, schema, resolvedType)
|
||||
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any? = ifThrowsAppend({ nameForDebug }) {
|
||||
input.readObjectOrNull(obj, schemas, resolvedType)
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) = ifThrowsAppend({ nameForDebug }) {
|
||||
@ -108,7 +108,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
|
||||
class AMQPPrimitivePropertySerializer(name: String, readMethod: Method?, resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) {
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
|
||||
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? {
|
||||
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any? {
|
||||
return if (obj is Binary) obj.array else obj
|
||||
}
|
||||
|
||||
@ -131,7 +131,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
|
||||
PropertySerializer(name, readMethod, Character::class.java) {
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
|
||||
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? {
|
||||
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any? {
|
||||
return if (obj == null) null else (obj as Short).toChar()
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,9 @@ import com.google.common.primitives.Primitives
|
||||
import com.google.common.reflect.TypeResolver
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.nodeapi.internal.serialization.carpenter.*
|
||||
import net.corda.nodeapi.internal.serialization.carpenter.CarpenterMetaSchema
|
||||
import net.corda.nodeapi.internal.serialization.carpenter.ClassCarpenter
|
||||
import net.corda.nodeapi.internal.serialization.carpenter.MetaCarpenter
|
||||
import org.apache.qpid.proton.amqp.*
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.*
|
||||
@ -13,7 +15,8 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
data class FactorySchemaAndDescriptor(val schema: Schema, val typeDescriptor: Any)
|
||||
data class SerializationSchemas(val schema: Schema, val transforms: TransformsSchema)
|
||||
data class FactorySchemaAndDescriptor(val schemas: SerializationSchemas, val typeDescriptor: Any)
|
||||
|
||||
/**
|
||||
* Factory of serializers designed to be shared across threads and invocations.
|
||||
@ -40,7 +43,10 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
||||
val classloader: ClassLoader
|
||||
get() = classCarpenter.classloader
|
||||
|
||||
private fun getEvolutionSerializer(typeNotation: TypeNotation, newSerializer: AMQPSerializer<Any>): AMQPSerializer<Any> {
|
||||
private fun getEvolutionSerializer(
|
||||
typeNotation: TypeNotation,
|
||||
newSerializer: AMQPSerializer<Any>,
|
||||
transforms: TransformsSchema): AMQPSerializer<Any> {
|
||||
return serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
|
||||
when (typeNotation) {
|
||||
is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, this)
|
||||
@ -168,7 +174,7 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
||||
* contained in the [Schema].
|
||||
*/
|
||||
@Throws(NotSerializableException::class)
|
||||
fun get(typeDescriptor: Any, schema: Schema): AMQPSerializer<Any> {
|
||||
fun get(typeDescriptor: Any, schema: SerializationSchemas): AMQPSerializer<Any> {
|
||||
return serializersByDescriptor[typeDescriptor] ?: {
|
||||
processSchema(FactorySchemaAndDescriptor(schema, typeDescriptor))
|
||||
serializersByDescriptor[typeDescriptor] ?: throw NotSerializableException(
|
||||
@ -194,9 +200,9 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
||||
* Iterate over an AMQP schema, for each type ascertain weather it's on ClassPath of [classloader] amd
|
||||
* if not use the [ClassCarpenter] to generate a class to use in it's place
|
||||
*/
|
||||
private fun processSchema(schema: FactorySchemaAndDescriptor, sentinel: Boolean = false) {
|
||||
private fun processSchema(schemaAndDescriptor: FactorySchemaAndDescriptor, sentinel: Boolean = false) {
|
||||
val metaSchema = CarpenterMetaSchema.newInstance()
|
||||
for (typeNotation in schema.schema.types) {
|
||||
for (typeNotation in schemaAndDescriptor.schemas.schema.types) {
|
||||
try {
|
||||
val serialiser = processSchemaEntry(typeNotation)
|
||||
|
||||
@ -204,7 +210,7 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
||||
// doesn't match that of the serialised object then we are dealing with different
|
||||
// instance of the class, as such we need to build an EvolutionSerialiser
|
||||
if (serialiser.typeDescriptor != typeNotation.descriptor.name) {
|
||||
getEvolutionSerializer(typeNotation, serialiser)
|
||||
getEvolutionSerializer(typeNotation, serialiser, schemaAndDescriptor.schemas.transforms)
|
||||
}
|
||||
} catch (e: ClassNotFoundException) {
|
||||
if (sentinel) throw e
|
||||
@ -215,7 +221,7 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
||||
if (metaSchema.isNotEmpty()) {
|
||||
val mc = MetaCarpenter(metaSchema, classCarpenter)
|
||||
mc.build()
|
||||
processSchema(schema, true)
|
||||
processSchema(schemaAndDescriptor, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -85,6 +85,9 @@ class UnknownTransform : Transform() {
|
||||
override val name: String get() = typeName
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the unit testing framework
|
||||
*/
|
||||
class UnknownTestTransform(val a: Int, val b: Int, val c: Int) : Transform() {
|
||||
companion object : DescribedTypeConstructor<UnknownTestTransform> {
|
||||
val typeName = "UnknownTest"
|
||||
|
@ -34,8 +34,8 @@ object InputStreamSerializer : CustomSerializer.Implements<InputStream>(InputStr
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): InputStream {
|
||||
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): InputStream {
|
||||
val bits = input.readObject(obj, schemas, ByteArray::class.java) as ByteArray
|
||||
return ByteArrayInputStream(bits)
|
||||
}
|
||||
}
|
@ -20,8 +20,8 @@ object PrivateKeySerializer : CustomSerializer.Implements<PrivateKey>(PrivateKey
|
||||
output.writeObject(obj.encoded, data, clazz)
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): PrivateKey {
|
||||
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): PrivateKey {
|
||||
val bits = input.readObject(obj, schemas, ByteArray::class.java) as ByteArray
|
||||
return Crypto.decodePrivateKey(bits)
|
||||
}
|
||||
}
|
@ -17,8 +17,8 @@ object PublicKeySerializer : CustomSerializer.Implements<PublicKey>(PublicKey::c
|
||||
output.writeObject(obj.encoded, data, clazz)
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): PublicKey {
|
||||
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): PublicKey {
|
||||
val bits = input.readObject(obj, schemas, ByteArray::class.java) as ByteArray
|
||||
return Crypto.decodePublicKey(bits)
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.serialization.amqp.*
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Type
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
object X509CertificateSerializer : CustomSerializer.Implements<X509Certificate>(X509Certificate::class.java) {
|
||||
@ -20,8 +20,8 @@ object X509CertificateSerializer : CustomSerializer.Implements<X509Certificate>(
|
||||
output.writeObject(obj.encoded, data, clazz)
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): X509Certificate {
|
||||
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
|
||||
return CertificateFactory.getInstance("X.509").generateCertificate(bits.inputStream()) as X509Certificate
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): X509Certificate {
|
||||
val bits = input.readObject(obj, schemas, ByteArray::class.java) as ByteArray
|
||||
return X509CertificateFactory().generateCertificate(bits.inputStream())
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.toObservable
|
||||
import net.corda.core.transactions.*
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.core.utilities.SgxSupport
|
||||
import net.corda.nodeapi.internal.serialization.CordaClassResolver
|
||||
import net.corda.nodeapi.internal.serialization.serializationContextKey
|
||||
@ -490,8 +491,7 @@ object CertPathSerializer : Serializer<CertPath>() {
|
||||
@ThreadSafe
|
||||
object X509CertificateSerializer : Serializer<X509Certificate>() {
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<X509Certificate>): X509Certificate {
|
||||
val factory = CertificateFactory.getInstance("X.509")
|
||||
return factory.generateCertificate(input.readBytesWithLength().inputStream()) as X509Certificate
|
||||
return X509CertificateFactory().generateCertificate(input.readBytesWithLength().inputStream())
|
||||
}
|
||||
|
||||
override fun write(kryo: Kryo, output: Output, obj: X509Certificate) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
package net.corda.node.utilities
|
||||
package net.corda.nodeapi.internal.crypto
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512
|
||||
@ -14,9 +14,9 @@ import net.corda.core.serialization.serialize
|
||||
import net.corda.node.serialization.KryoServerSerializationScheme
|
||||
import net.corda.node.services.config.createKeystoreForCordaNode
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.nodeapi.internal.serialization.SerializationContextImpl
|
||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.BOB
|
||||
import net.corda.testing.BOB_PUBKEY
|
||||
@ -41,7 +41,6 @@ import java.security.PrivateKey
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.stream.Stream
|
||||
@ -49,7 +48,6 @@ import javax.net.ssl.*
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.test.*
|
||||
|
||||
|
||||
class X509UtilitiesTest {
|
||||
@Rule
|
||||
@JvmField
|
||||
@ -360,10 +358,16 @@ class X509UtilitiesTest {
|
||||
trustStorePassword: String
|
||||
): KeyStore {
|
||||
val rootCAKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Node Root CA", organisation = "R3CEV", locality = "London", country = "GB"), rootCAKey)
|
||||
val baseName = CordaX500Name(organisation = "R3CEV", locality = "London", country = "GB")
|
||||
val rootCACert = X509Utilities.createSelfSignedCACertificate(baseName.copy(commonName = "Corda Node Root CA"), rootCAKey)
|
||||
|
||||
val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, CordaX500Name(commonName = "Corda Node Intermediate CA", organisation = "R3CEV", locality = "London", country = "GB"), intermediateCAKeyPair.public)
|
||||
val intermediateCACert = X509Utilities.createCertificate(
|
||||
CertificateType.INTERMEDIATE_CA,
|
||||
rootCACert,
|
||||
rootCAKey,
|
||||
baseName.copy(commonName = "Corda Node Intermediate CA"),
|
||||
intermediateCAKeyPair.public)
|
||||
|
||||
val keyPass = keyPassword.toCharArray()
|
||||
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
|
||||
@ -426,11 +430,10 @@ class X509UtilitiesTest {
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.P2P)
|
||||
val certFactory = CertificateFactory.getInstance("X509")
|
||||
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val rootCACert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootCAKey)
|
||||
val certificate = X509Utilities.createCertificate(CertificateType.TLS, rootCACert, rootCAKey, BOB.name.x500Name, BOB_PUBKEY)
|
||||
val expected = certFactory.generateCertPath(listOf(certificate.cert, rootCACert.cert))
|
||||
val expected = X509CertificateFactory().delegate.generateCertPath(listOf(certificate.cert, rootCACert.cert))
|
||||
val serialized = expected.serialize(factory, context).bytes
|
||||
val actual: CertPath = serialized.deserialize(factory, context)
|
||||
assertEquals(expected, actual)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user