mirror of
https://github.com/corda/corda.git
synced 2024-12-27 08:22:35 +00:00
Merge remote-tracking branch 'open/master' into colljos-os-merge-rc01
This commit is contained in:
commit
e9fc6f7c8d
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -10,6 +10,8 @@
|
|||||||
<module name="bank-of-corda-demo_integrationTest" target="1.8" />
|
<module name="bank-of-corda-demo_integrationTest" target="1.8" />
|
||||||
<module name="bank-of-corda-demo_main" target="1.8" />
|
<module name="bank-of-corda-demo_main" target="1.8" />
|
||||||
<module name="bank-of-corda-demo_test" target="1.8" />
|
<module name="bank-of-corda-demo_test" target="1.8" />
|
||||||
|
<module name="bootstrapper_main" target="1.8" />
|
||||||
|
<module name="bootstrapper_test" target="1.8" />
|
||||||
<module name="buildSrc_main" target="1.8" />
|
<module name="buildSrc_main" target="1.8" />
|
||||||
<module name="buildSrc_test" target="1.8" />
|
<module name="buildSrc_test" target="1.8" />
|
||||||
<module name="business-network-demo_integrationTest" target="1.8" />
|
<module name="business-network-demo_integrationTest" target="1.8" />
|
||||||
|
18
build.gradle
18
build.gradle
@ -4,7 +4,7 @@ buildscript {
|
|||||||
file("$projectDir/constants.properties").withInputStream { constants.load(it) }
|
file("$projectDir/constants.properties").withInputStream { constants.load(it) }
|
||||||
|
|
||||||
// Our version: bump this on release.
|
// Our version: bump this on release.
|
||||||
ext.corda_release_version = "0.16-SNAPSHOT"
|
ext.corda_release_version = "3.0-ENT-RC01"
|
||||||
// Increment this on any release that changes public APIs anywhere in the Corda platform
|
// Increment this on any release that changes public APIs anywhere in the Corda platform
|
||||||
ext.corda_platform_version = constants.getProperty("platformVersion")
|
ext.corda_platform_version = constants.getProperty("platformVersion")
|
||||||
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
|
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
|
||||||
@ -20,7 +20,21 @@ buildscript {
|
|||||||
ext.capsule_version = '1.0.1'
|
ext.capsule_version = '1.0.1'
|
||||||
|
|
||||||
ext.asm_version = '0.5.3'
|
ext.asm_version = '0.5.3'
|
||||||
ext.artemis_version = '2.4.0'
|
|
||||||
|
/*
|
||||||
|
* TODO Upgrade to version 2.4 for large message streaming support
|
||||||
|
*
|
||||||
|
* Due to a memory leak in the connection handling code in Artemis, we are
|
||||||
|
* temporarily downgrading to version 2.2.0.
|
||||||
|
*
|
||||||
|
* The memory leak essentially triggers an out-of-memory exception within
|
||||||
|
* less than 10 seconds and can take down a node if a non-TLS connection is
|
||||||
|
* attempted against the P2P port.
|
||||||
|
*
|
||||||
|
* The issue has been reported to upstream:
|
||||||
|
* https://issues.apache.org/jira/browse/ARTEMIS-1559
|
||||||
|
*/
|
||||||
|
ext.artemis_version = '2.2.0'
|
||||||
ext.jackson_version = '2.9.2'
|
ext.jackson_version = '2.9.2'
|
||||||
ext.jetty_version = '9.4.7.v20170914'
|
ext.jetty_version = '9.4.7.v20170914'
|
||||||
ext.jersey_version = '2.25'
|
ext.jersey_version = '2.25'
|
||||||
|
@ -12,6 +12,7 @@ import net.corda.core.transactions.SignedTransaction
|
|||||||
import net.corda.finance.USD
|
import net.corda.finance.USD
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -24,7 +25,7 @@ class JacksonSupportTest {
|
|||||||
private companion object {
|
private companion object {
|
||||||
val SEED = BigInteger.valueOf(20170922L)!!
|
val SEED = BigInteger.valueOf(20170922L)!!
|
||||||
val mapper = JacksonSupport.createNonRpcMapper()
|
val mapper = JacksonSupport.createNonRpcMapper()
|
||||||
val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).pubkey
|
val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).publicKey
|
||||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||||
val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party
|
val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party
|
||||||
}
|
}
|
||||||
|
@ -31,8 +31,8 @@ import static net.corda.finance.Currencies.DOLLARS;
|
|||||||
import static net.corda.finance.contracts.GetBalances.getCashBalance;
|
import static net.corda.finance.contracts.GetBalances.getCashBalance;
|
||||||
import static net.corda.node.services.Permissions.invokeRpc;
|
import static net.corda.node.services.Permissions.invokeRpc;
|
||||||
import static net.corda.node.services.Permissions.startFlow;
|
import static net.corda.node.services.Permissions.startFlow;
|
||||||
import static net.corda.testing.TestConstants.getALICE_NAME;
|
import static net.corda.testing.TestConstants.ALICE_NAME;
|
||||||
import static net.corda.testing.TestConstants.getDUMMY_NOTARY_NAME;
|
import static net.corda.testing.TestConstants.DUMMY_NOTARY_NAME;
|
||||||
|
|
||||||
public class CordaRPCJavaClientTest extends NodeBasedTest {
|
public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||||
public CordaRPCJavaClientTest() {
|
public CordaRPCJavaClientTest() {
|
||||||
@ -41,7 +41,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
|
|||||||
|
|
||||||
@ClassRule
|
@ClassRule
|
||||||
public static IntegrationTestSchemas databaseSchemas = new IntegrationTestSchemas(IntegrationTestKt.toDatabaseSchemaName(getALICE_NAME()),
|
public static IntegrationTestSchemas databaseSchemas = new IntegrationTestSchemas(IntegrationTestKt.toDatabaseSchemaName(getALICE_NAME()),
|
||||||
IntegrationTestKt.toDatabaseSchemaName(getDUMMY_NOTARY_NAME()));
|
IntegrationTestKt.toDatabaseSchemaName(DUMMY_NOTARY_NAME));
|
||||||
|
|
||||||
private List<String> perms = Arrays.asList(
|
private List<String> perms = Arrays.asList(
|
||||||
startFlow(CashPaymentFlow.class),
|
startFlow(CashPaymentFlow.class),
|
||||||
@ -65,7 +65,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
|
|||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
super.setUp();
|
super.setUp();
|
||||||
node = startNode(getALICE_NAME(), 1, singletonList(rpcUser));
|
node = startNode(ALICE_NAME, 1, singletonList(rpcUser));
|
||||||
client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress()));
|
client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.testing
|
package net.corda.client.rpc
|
||||||
|
|
||||||
import net.corda.core.internal.uncheckedCast
|
import net.corda.core.internal.uncheckedCast
|
||||||
import kotlin.reflect.KCallable
|
import kotlin.reflect.KCallable
|
@ -9,9 +9,9 @@ import net.corda.core.serialization.CordaSerializable
|
|||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.core.utilities.millis
|
import net.corda.core.utilities.millis
|
||||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||||
|
import net.corda.testing.internal.testThreadFactory
|
||||||
import net.corda.testing.node.internal.RPCDriverDSL
|
import net.corda.testing.node.internal.RPCDriverDSL
|
||||||
import net.corda.testing.node.internal.rpcDriver
|
import net.corda.testing.node.internal.rpcDriver
|
||||||
import net.corda.testing.internal.testThreadFactory
|
|
||||||
import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet
|
import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -12,7 +12,6 @@ import net.corda.testing.node.internal.performance.startPublishingFixedRateInjec
|
|||||||
import net.corda.testing.node.internal.performance.startReporter
|
import net.corda.testing.node.internal.performance.startReporter
|
||||||
import net.corda.testing.node.internal.performance.startTightLoopInjector
|
import net.corda.testing.node.internal.performance.startTightLoopInjector
|
||||||
import net.corda.testing.node.internal.rpcDriver
|
import net.corda.testing.node.internal.rpcDriver
|
||||||
import net.corda.testing.measure
|
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package net.corda.client.rpc
|
package net.corda.client.rpc
|
||||||
|
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
import net.corda.core.messaging.RPCOps
|
import net.corda.core.messaging.RPCOps
|
||||||
|
import net.corda.node.services.Permissions
|
||||||
import net.corda.node.services.messaging.rpcContext
|
import net.corda.node.services.messaging.rpcContext
|
||||||
import net.corda.nodeapi.internal.config.User
|
import net.corda.nodeapi.internal.config.User
|
||||||
import net.corda.testing.node.internal.RPCDriverDSL
|
import net.corda.testing.node.internal.RPCDriverDSL
|
||||||
|
@ -15,7 +15,7 @@ class SwapIdentitiesFlowTests {
|
|||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||||
mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true)
|
mockNet = MockNetwork(emptyList(), networkSendManuallyPumped = false, threadPerNode = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
gradlePluginsVersion=3.0.1
|
gradlePluginsVersion=3.0.2
|
||||||
kotlinVersion=1.1.60
|
kotlinVersion=1.1.60
|
||||||
platformVersion=1
|
platformVersion=1
|
||||||
guavaVersion=21.0
|
guavaVersion=21.0
|
||||||
|
@ -63,7 +63,7 @@ import javax.crypto.spec.SecretKeySpec
|
|||||||
*/
|
*/
|
||||||
object Crypto {
|
object Crypto {
|
||||||
/**
|
/**
|
||||||
* RSA_SHA256 signature scheme using SHA256 as hash algorithm.
|
* RSA signature scheme using SHA256 for message hashing.
|
||||||
* Note: Recommended key size >= 3072 bits.
|
* Note: Recommended key size >= 3072 bits.
|
||||||
*/
|
*/
|
||||||
@JvmField
|
@JvmField
|
||||||
@ -80,7 +80,7 @@ object Crypto {
|
|||||||
"RSA_SHA256 signature scheme using SHA256 as hash algorithm."
|
"RSA_SHA256 signature scheme using SHA256 as hash algorithm."
|
||||||
)
|
)
|
||||||
|
|
||||||
/** ECDSA signature scheme using the secp256k1 Koblitz curve. */
|
/** ECDSA signature scheme using the secp256k1 Koblitz curve and SHA256 for message hashing. */
|
||||||
@JvmField
|
@JvmField
|
||||||
val ECDSA_SECP256K1_SHA256 = SignatureScheme(
|
val ECDSA_SECP256K1_SHA256 = SignatureScheme(
|
||||||
2,
|
2,
|
||||||
@ -95,7 +95,7 @@ object Crypto {
|
|||||||
"ECDSA signature scheme using the secp256k1 Koblitz curve."
|
"ECDSA signature scheme using the secp256k1 Koblitz curve."
|
||||||
)
|
)
|
||||||
|
|
||||||
/** ECDSA signature scheme using the secp256r1 (NIST P-256) curve. */
|
/** ECDSA signature scheme using the secp256r1 (NIST P-256) curve and SHA256 for message hashing. */
|
||||||
@JvmField
|
@JvmField
|
||||||
val ECDSA_SECP256R1_SHA256 = SignatureScheme(
|
val ECDSA_SECP256R1_SHA256 = SignatureScheme(
|
||||||
3,
|
3,
|
||||||
@ -110,7 +110,7 @@ object Crypto {
|
|||||||
"ECDSA signature scheme using the secp256r1 (NIST P-256) curve."
|
"ECDSA signature scheme using the secp256r1 (NIST P-256) curve."
|
||||||
)
|
)
|
||||||
|
|
||||||
/** EdDSA signature scheme using the ed255519 twisted Edwards curve. */
|
/** EdDSA signature scheme using the ed25519 twisted Edwards curve and SHA512 for message hashing. */
|
||||||
@JvmField
|
@JvmField
|
||||||
val EDDSA_ED25519_SHA512 = SignatureScheme(
|
val EDDSA_ED25519_SHA512 = SignatureScheme(
|
||||||
4,
|
4,
|
||||||
@ -127,13 +127,15 @@ object Crypto {
|
|||||||
"EdDSA signature scheme using the ed25519 twisted Edwards curve."
|
"EdDSA signature scheme using the ed25519 twisted Edwards curve."
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/** DLSequence (ASN1Sequence) for SHA512 truncated to 256 bits, used in SPHINCS-256 signature scheme. */
|
||||||
* SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers
|
|
||||||
* at the cost of larger key sizes and loss of compatibility.
|
|
||||||
*/
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val SHA512_256 = DLSequence(arrayOf(NISTObjectIdentifiers.id_sha512_256))
|
val SHA512_256 = DLSequence(arrayOf(NISTObjectIdentifiers.id_sha512_256))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SPHINCS-256 hash-based signature scheme using SHA512 for message hashing. It provides 128bit security against
|
||||||
|
* post-quantum attackers at the cost of larger key nd signature sizes and loss of compatibility.
|
||||||
|
*/
|
||||||
|
// TODO: change val name to SPHINCS256_SHA512. This will break backwards compatibility.
|
||||||
@JvmField
|
@JvmField
|
||||||
val SPHINCS256_SHA256 = SignatureScheme(
|
val SPHINCS256_SHA256 = SignatureScheme(
|
||||||
5,
|
5,
|
||||||
@ -149,7 +151,8 @@ object Crypto {
|
|||||||
"at the cost of larger key sizes and loss of compatibility."
|
"at the cost of larger key sizes and loss of compatibility."
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Corda composite key type. */
|
/** Corda [CompositeKey] signature type. */
|
||||||
|
// TODO: change the val name to a more descriptive one as it's now confusing and looks like a Key type.
|
||||||
@JvmField
|
@JvmField
|
||||||
val COMPOSITE_KEY = SignatureScheme(
|
val COMPOSITE_KEY = SignatureScheme(
|
||||||
6,
|
6,
|
||||||
|
@ -22,7 +22,7 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) {
|
|||||||
* @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect).
|
* @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect).
|
||||||
*/
|
*/
|
||||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||||
fun verify(content: ByteArray) = by.verify(content, this)
|
fun verify(content: ByteArray): Boolean = by.verify(content, this)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility to simplify the act of verifying a signature.
|
* Utility to simplify the act of verifying a signature.
|
||||||
@ -32,7 +32,7 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) {
|
|||||||
* @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect).
|
* @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect).
|
||||||
*/
|
*/
|
||||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||||
fun verify(content: OpaqueBytes) = by.verify(content.bytes, this)
|
fun verify(content: OpaqueBytes): Boolean = by.verify(content.bytes, this)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility to simplify the act of verifying a signature. In comparison to [verify] doesn't throw an
|
* Utility to simplify the act of verifying a signature. In comparison to [verify] doesn't throw an
|
||||||
@ -45,7 +45,8 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) {
|
|||||||
* @return whether the signature is correct for this key.
|
* @return whether the signature is correct for this key.
|
||||||
*/
|
*/
|
||||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||||
fun isValid(content: ByteArray) = by.isValid(content, this)
|
fun isValid(content: ByteArray): Boolean = by.isValid(content, this)
|
||||||
fun withoutKey() : DigitalSignature = DigitalSignature(this.bytes)
|
|
||||||
|
fun withoutKey(): DigitalSignature = DigitalSignature(this.bytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package net.corda.core.identity
|
|||||||
|
|
||||||
import com.google.common.collect.ImmutableSet
|
import com.google.common.collect.ImmutableSet
|
||||||
import net.corda.core.internal.LegalNameValidator
|
import net.corda.core.internal.LegalNameValidator
|
||||||
|
import net.corda.core.internal.VisibleForTesting
|
||||||
import net.corda.core.internal.x500Name
|
import net.corda.core.internal.x500Name
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import org.bouncycastle.asn1.ASN1Encodable
|
import org.bouncycastle.asn1.ASN1Encodable
|
||||||
@ -78,8 +79,9 @@ data class CordaX500Name(val commonName: String?,
|
|||||||
const val MAX_LENGTH_ORGANISATION_UNIT = 64
|
const val MAX_LENGTH_ORGANISATION_UNIT = 64
|
||||||
const val MAX_LENGTH_COMMON_NAME = 64
|
const val MAX_LENGTH_COMMON_NAME = 64
|
||||||
private val supportedAttributes = setOf(BCStyle.O, BCStyle.C, BCStyle.L, BCStyle.CN, BCStyle.ST, BCStyle.OU)
|
private val supportedAttributes = setOf(BCStyle.O, BCStyle.C, BCStyle.L, BCStyle.CN, BCStyle.ST, BCStyle.OU)
|
||||||
private val countryCodes: Set<String> = ImmutableSet.copyOf(Locale.getISOCountries())
|
@VisibleForTesting
|
||||||
|
val unspecifiedCountry = "ZZ"
|
||||||
|
private val countryCodes: Set<String> = ImmutableSet.copyOf(Locale.getISOCountries() + unspecifiedCountry)
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun build(principal: X500Principal): CordaX500Name {
|
fun build(principal: X500Principal): CordaX500Name {
|
||||||
val x500Name = X500Name.getInstance(principal.encoded)
|
val x500Name = X500Name.getInstance(principal.encoded)
|
||||||
|
@ -12,4 +12,5 @@ import net.corda.core.serialization.CordaSerializable
|
|||||||
*/
|
*/
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
class MissingContractAttachments(val states: List<TransactionState<ContractState>>)
|
class MissingContractAttachments(val states: List<TransactionState<ContractState>>)
|
||||||
: FlowException("Cannot find contract attachments for ${states.map { it.contract }.distinct()}")
|
: FlowException("Cannot find contract attachments for ${states.map { it.contract }.distinct()}. " +
|
||||||
|
"See https://docs.corda.net/api-contract-constraints.html#debugging")
|
@ -13,21 +13,22 @@ import org.junit.Test;
|
|||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
import static net.corda.testing.CoreTestUtils.singleIdentity;
|
import static net.corda.testing.CoreTestUtils.singleIdentity;
|
||||||
import static net.corda.testing.node.NodeTestUtils.startFlow;
|
import static net.corda.testing.node.NodeTestUtils.startFlow;
|
||||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
public class FlowsInJavaTest {
|
public class FlowsInJavaTest {
|
||||||
private final MockNetwork mockNet = new MockNetwork();
|
private final MockNetwork mockNet = new MockNetwork(emptyList());
|
||||||
private StartedNode<MockNetwork.MockNode> aliceNode;
|
private StartedNode<MockNetwork.MockNode> aliceNode;
|
||||||
private StartedNode<MockNetwork.MockNode> bobNode;
|
private StartedNode<MockNetwork.MockNode> bobNode;
|
||||||
private Party bob;
|
private Party bob;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
aliceNode = mockNet.createPartyNode(TestConstants.getALICE_NAME());
|
aliceNode = mockNet.createPartyNode(TestConstants.ALICE_NAME);
|
||||||
bobNode = mockNet.createPartyNode(TestConstants.getBOB_NAME());
|
bobNode = mockNet.createPartyNode(TestConstants.BOB_NAME);
|
||||||
bob = singleIdentity(bobNode.getInfo());
|
bob = singleIdentity(bobNode.getInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ package net.corda.core.concurrent
|
|||||||
import com.nhaarman.mockito_kotlin.*
|
import com.nhaarman.mockito_kotlin.*
|
||||||
import net.corda.core.internal.concurrent.openFuture
|
import net.corda.core.internal.concurrent.openFuture
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.testing.rigorousMock
|
import net.corda.testing.internal.rigorousMock
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
|
@ -9,6 +9,7 @@ import net.corda.core.node.ServicesForResolution
|
|||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
import net.corda.testing.contracts.DummyContractV2
|
import net.corda.testing.contracts.DummyContractV2
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
@ -9,7 +9,7 @@ import net.corda.core.serialization.serialize
|
|||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.toBase58String
|
import net.corda.core.utilities.toBase58String
|
||||||
import net.corda.nodeapi.internal.crypto.*
|
import net.corda.nodeapi.internal.crypto.*
|
||||||
import net.corda.testing.kryoSpecific
|
import net.corda.testing.internal.kryoSpecific
|
||||||
import net.corda.testing.SerializationEnvironmentRule
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -14,6 +14,10 @@ import net.corda.finance.`issued by`
|
|||||||
import net.corda.finance.contracts.asset.Cash
|
import net.corda.finance.contracts.asset.Cash
|
||||||
import net.corda.node.services.api.IdentityServiceInternal
|
import net.corda.node.services.api.IdentityServiceInternal
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
|
import net.corda.testing.dsl.LedgerDSL
|
||||||
|
import net.corda.testing.dsl.TestLedgerDSLInterpreter
|
||||||
|
import net.corda.testing.dsl.TestTransactionDSLInterpreter
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.ledger
|
import net.corda.testing.node.ledger
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@ -31,9 +35,9 @@ class PartialMerkleTreeTest {
|
|||||||
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
||||||
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
||||||
val MEGA_CORP get() = megaCorp.party
|
val MEGA_CORP get() = megaCorp.party
|
||||||
val MEGA_CORP_PUBKEY get() = megaCorp.pubkey
|
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
|
||||||
val MINI_CORP get() = miniCorp.party
|
val MINI_CORP get() = miniCorp.party
|
||||||
val MINI_CORP_PUBKEY get() = miniCorp.pubkey
|
val MINI_CORP_PUBKEY get() = miniCorp.publicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@ -51,7 +55,7 @@ class PartialMerkleTreeTest {
|
|||||||
hashed = nodes.map { it.serialize().sha256() }
|
hashed = nodes.map { it.serialize().sha256() }
|
||||||
expectedRoot = MerkleTree.getMerkleTree(hashed.toMutableList() + listOf(zeroHash, zeroHash)).hash
|
expectedRoot = MerkleTree.getMerkleTree(hashed.toMutableList() + listOf(zeroHash, zeroHash)).hash
|
||||||
merkleTree = MerkleTree.getMerkleTree(hashed)
|
merkleTree = MerkleTree.getMerkleTree(hashed)
|
||||||
testLedger = MockServices(rigorousMock<IdentityServiceInternal>().also {
|
testLedger = MockServices(emptyList(), rigorousMock<IdentityServiceInternal>().also {
|
||||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||||
}, MEGA_CORP.name).ledger(DUMMY_NOTARY) {
|
}, MEGA_CORP.name).ledger(DUMMY_NOTARY) {
|
||||||
unverifiedTransaction {
|
unverifiedTransaction {
|
||||||
|
@ -29,7 +29,7 @@ class AttachmentTests {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
mockNet = MockNetwork()
|
mockNet = MockNetwork(emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
@ -14,6 +14,7 @@ import net.corda.core.utilities.getOrThrow
|
|||||||
import net.corda.node.internal.StartedNode
|
import net.corda.node.internal.StartedNode
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.startFlow
|
import net.corda.testing.node.startFlow
|
||||||
|
@ -13,7 +13,7 @@ import org.junit.After
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class ReceiveMultipleFlowTests {
|
class ReceiveMultipleFlowTests {
|
||||||
private val mockNet = MockNetwork()
|
private val mockNet = MockNetwork(emptyList())
|
||||||
private val nodes = (0..2).map { mockNet.createPartyNode() }
|
private val nodes = (0..2).map { mockNet.createPartyNode() }
|
||||||
@After
|
@After
|
||||||
fun stopNodes() {
|
fun stopNodes() {
|
||||||
|
@ -4,7 +4,7 @@ import com.nhaarman.mockito_kotlin.*
|
|||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.internal.join
|
import net.corda.core.internal.join
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.testing.rigorousMock
|
import net.corda.testing.internal.rigorousMock
|
||||||
import org.assertj.core.api.Assertions
|
import org.assertj.core.api.Assertions
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
|
@ -70,7 +70,7 @@ class AttachmentSerializationTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
mockNet = MockNetwork()
|
mockNet = MockNetwork(emptyList())
|
||||||
server = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME))
|
server = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME))
|
||||||
client = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME))
|
client = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME))
|
||||||
client.internals.disableDBCloseOnStop() // Otherwise the in-memory database may disappear (taking the checkpoint with it) while we reboot the client.
|
client.internals.disableDBCloseOnStop() // Otherwise the in-memory database may disappear (taking the checkpoint with it) while we reboot the client.
|
||||||
|
@ -9,6 +9,7 @@ import net.corda.core.transactions.TransactionBuilder
|
|||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.finance.POUNDS
|
import net.corda.finance.POUNDS
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
@ -25,9 +26,9 @@ class TransactionSerializationTests {
|
|||||||
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
||||||
val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party
|
val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party
|
||||||
val DUMMY_NOTARY get() = dummyNotary.party
|
val DUMMY_NOTARY get() = dummyNotary.party
|
||||||
val DUMMY_NOTARY_KEY get() = dummyNotary.key
|
val DUMMY_NOTARY_KEY get() = dummyNotary.keyPair
|
||||||
val MEGA_CORP get() = megaCorp.party
|
val MEGA_CORP get() = megaCorp.party
|
||||||
val MEGA_CORP_KEY get() = megaCorp.key
|
val MEGA_CORP_KEY get() = megaCorp.keyPair
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@ -106,7 +107,7 @@ class TransactionSerializationTests {
|
|||||||
Command(TestCash.Commands.Move(), DUMMY_KEY_2.public))
|
Command(TestCash.Commands.Move(), DUMMY_KEY_2.public))
|
||||||
|
|
||||||
val ptx2 = notaryServices.signInitialTransaction(tx2)
|
val ptx2 = notaryServices.signInitialTransaction(tx2)
|
||||||
val dummyServices = MockServices(rigorousMock(), MEGA_CORP.name, DUMMY_KEY_2)
|
val dummyServices = MockServices(emptyList(), rigorousMock(), MEGA_CORP.name, DUMMY_KEY_2)
|
||||||
val stx2 = dummyServices.addSignature(ptx2)
|
val stx2 = dummyServices.addSignature(ptx2)
|
||||||
|
|
||||||
stx.copy(sigs = stx2.sigs).verifyRequiredSignatures()
|
stx.copy(sigs = stx2.sigs).verifyRequiredSignatures()
|
||||||
@ -124,8 +125,7 @@ class TransactionSerializationTests {
|
|||||||
@Test
|
@Test
|
||||||
fun storeAndLoadWhenSigning() {
|
fun storeAndLoadWhenSigning() {
|
||||||
val ptx = megaCorpServices.signInitialTransaction(tx)
|
val ptx = megaCorpServices.signInitialTransaction(tx)
|
||||||
ptx.verifySignaturesExcept(notaryServices.key.public)
|
ptx.verifySignaturesExcept(DUMMY_NOTARY_KEY.public)
|
||||||
|
|
||||||
val stored = ptx.serialize()
|
val stored = ptx.serialize()
|
||||||
val loaded = stored.deserialize()
|
val loaded = stored.deserialize()
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import net.corda.core.identity.Party
|
|||||||
import net.corda.node.services.api.IdentityServiceInternal
|
import net.corda.node.services.api.IdentityServiceInternal
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
@ -28,7 +29,7 @@ class LedgerTransactionQueryTests {
|
|||||||
@JvmField
|
@JvmField
|
||||||
val testSerialization = SerializationEnvironmentRule()
|
val testSerialization = SerializationEnvironmentRule()
|
||||||
private val keyPair = generateKeyPair()
|
private val keyPair = generateKeyPair()
|
||||||
private val services = MockServices(rigorousMock<IdentityServiceInternal>().also {
|
private val services = MockServices(emptyList(), rigorousMock<IdentityServiceInternal>().also {
|
||||||
doReturn(null).whenever(it).partyFromKey(keyPair.public)
|
doReturn(null).whenever(it).partyFromKey(keyPair.public)
|
||||||
}, CordaX500Name("MegaCorp", "London", "GB"), keyPair)
|
}, CordaX500Name("MegaCorp", "London", "GB"), keyPair)
|
||||||
private val identity: Party = services.myInfo.singleIdentity()
|
private val identity: Party = services.myInfo.singleIdentity()
|
||||||
|
@ -12,6 +12,7 @@ import net.corda.finance.`issued by`
|
|||||||
import net.corda.finance.contracts.asset.Cash
|
import net.corda.finance.contracts.asset.Cash
|
||||||
import net.corda.node.services.api.IdentityServiceInternal
|
import net.corda.node.services.api.IdentityServiceInternal
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.ledger
|
import net.corda.testing.node.ledger
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
@ -27,7 +28,7 @@ class TransactionEncumbranceTests {
|
|||||||
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
||||||
val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party
|
val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party
|
||||||
val MEGA_CORP get() = megaCorp.party
|
val MEGA_CORP get() = megaCorp.party
|
||||||
val MEGA_CORP_PUBKEY get() = megaCorp.pubkey
|
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@ -61,7 +62,7 @@ class TransactionEncumbranceTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val ledgerServices = MockServices(rigorousMock<IdentityServiceInternal>().also {
|
private val ledgerServices = MockServices(emptyList(), rigorousMock<IdentityServiceInternal>().also {
|
||||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||||
}, MEGA_CORP.name)
|
}, MEGA_CORP.name)
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import net.corda.core.crypto.CompositeKey
|
|||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
@ -23,7 +24,7 @@ class TransactionTests {
|
|||||||
val BOB = TestIdentity(BOB_NAME, 80).party
|
val BOB = TestIdentity(BOB_NAME, 80).party
|
||||||
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
|
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
|
||||||
val DUMMY_NOTARY get() = dummyNotary.party
|
val DUMMY_NOTARY get() = dummyNotary.party
|
||||||
val DUMMY_NOTARY_KEY get() = dummyNotary.key
|
val DUMMY_NOTARY_KEY get() = dummyNotary.keyPair
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
|
@ -7,9 +7,10 @@ import com.google.common.collect.testing.features.CollectionSize
|
|||||||
import junit.framework.TestSuite
|
import junit.framework.TestSuite
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.testing.withTestSerialization
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.Suite
|
import org.junit.runners.Suite
|
||||||
@ -36,6 +37,10 @@ class NonEmptySetTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class General {
|
class General {
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val testSerialization = SerializationEnvironmentRule()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `copyOf - empty source`() {
|
fun `copyOf - empty source`() {
|
||||||
assertThatThrownBy { NonEmptySet.copyOf(HashSet<Int>()) }.isInstanceOf(IllegalArgumentException::class.java)
|
assertThatThrownBy { NonEmptySet.copyOf(HashSet<Int>()) }.isInstanceOf(IllegalArgumentException::class.java)
|
||||||
@ -48,11 +53,9 @@ class NonEmptySetTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `serialize deserialize`() {
|
fun `serialize deserialize`() {
|
||||||
withTestSerialization {
|
val original = NonEmptySet.of(-17, 22, 17)
|
||||||
val original = NonEmptySet.of(-17, 22, 17)
|
val copy = original.serialize().deserialize()
|
||||||
val copy = original.serialize().deserialize()
|
assertThat(copy).isEqualTo(original).isNotSameAs(original)
|
||||||
assertThat(copy).isEqualTo(original).isNotSameAs(original)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
API: Contract Constraints
|
API: Contract Constraints
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
A basic understanding of contract key concepts, which can be found :doc:`here </key-concepts-contracts>`,
|
.. note:: Before reading this page, you should be familiar with the key concepts of :doc:`key-concepts-contracts`.
|
||||||
is required reading for this page.
|
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
|
Contract constraints
|
||||||
|
--------------------
|
||||||
Transaction states specify a constraint over the contract that will be used to verify it. For a transaction to be
|
Transaction states specify a constraint over the contract that will be used to verify it. For a transaction to be
|
||||||
valid, the ``verify`` function associated with each state must run successfully. However, for this to be secure, it is
|
valid, the ``verify`` function associated with each state must run successfully. However, for this to be secure, it is
|
||||||
not sufficient to specify the ``verify`` function by name as there may exist multiple different implementations with
|
not sufficient to specify the ``verify`` function by name as there may exist multiple different implementations with
|
||||||
the same method signature and enclosing class. Contract constraints solve this problem by allowing a contract developer
|
the same method signature and enclosing class. Contract constraints solve this problem by allowing a contract developer
|
||||||
to constrain which ``verify`` functions out of the universe of implementations can be used (i.e. the universe is
|
to constrain which ``verify`` functions out of the universe of implementations can be used (i.e. the universe is
|
||||||
everything that matches the signature and contract constraints restricts this universe to a subset).
|
everything that matches the signature and contract constraints restrict this universe to a subset).
|
||||||
|
|
||||||
A typical constraint is the hash of the CorDapp JAR that contains the contract and states but will in future releases
|
A typical constraint is the hash of the CorDapp JAR that contains the contract and states but will in future releases
|
||||||
include constraints that require specific signers of the JAR, or both the signer and the hash. Constraints can be
|
include constraints that require specific signers of the JAR, or both the signer and the hash. Constraints can be
|
||||||
@ -60,10 +63,10 @@ that they were loaded from. This makes it possible to find the attachment for an
|
|||||||
automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying the constraints and
|
automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying the constraints and
|
||||||
contracts, attachments are associated with their respective contracts.
|
contracts, attachments are associated with their respective contracts.
|
||||||
|
|
||||||
Implementations
|
Implementations of AttachmentConstraint
|
||||||
---------------
|
---------------------------------------
|
||||||
|
|
||||||
There are three implementations of ``AttachmentConstraints`` with more planned in the future.
|
There are three implementations of ``AttachmentConstraint`` with more planned in the future.
|
||||||
|
|
||||||
``AlwaysAcceptAttachmentConstraint``: Any attachment (except a missing one) will satisfy this constraint.
|
``AlwaysAcceptAttachmentConstraint``: Any attachment (except a missing one) will satisfy this constraint.
|
||||||
|
|
||||||
@ -137,3 +140,18 @@ Full Nodes
|
|||||||
**********
|
**********
|
||||||
|
|
||||||
When testing against full nodes simply place your CorDapp into the cordapps directory of the node.
|
When testing against full nodes simply place your CorDapp into the cordapps directory of the node.
|
||||||
|
|
||||||
|
Debugging
|
||||||
|
---------
|
||||||
|
If an attachment constraint cannot be resolved, a ``MissingContractAttachments`` exception is thrown. There are two
|
||||||
|
common sources of ``MissingContractAttachments`` exceptions:
|
||||||
|
|
||||||
|
Not setting CorDapp packages in tests
|
||||||
|
*************************************
|
||||||
|
You are running a test and have not specified the CorDapp packages to scan. See the instructions above.
|
||||||
|
|
||||||
|
Wrong fully-qualified contract name
|
||||||
|
***********************************
|
||||||
|
You are specifying the fully-qualified name of the contract incorrectly. For example, you've defined ``MyContract`` in
|
||||||
|
the package ``com.mycompany.myapp.contracts``, but the fully-qualified contract name you pass to the
|
||||||
|
``TransactionBuilder`` is ``com.mycompany.myapp.MyContract`` (instead of ``com.mycompany.myapp.contracts.MyContract``).
|
@ -55,8 +55,8 @@ Helper methods are also provided with default values for arguments:
|
|||||||
The API provides both static (snapshot) and dynamic (snapshot with streaming updates) methods for a defined set of
|
The API provides both static (snapshot) and dynamic (snapshot with streaming updates) methods for a defined set of
|
||||||
filter criteria:
|
filter criteria:
|
||||||
|
|
||||||
- Use ``queryBy`` to obtain a only current snapshot of data (for a given ``QueryCriteria``)
|
- Use ``queryBy`` to obtain a current snapshot of data (for a given ``QueryCriteria``)
|
||||||
- Use ``trackBy`` to obtain a both a current snapshot and a future stream of updates (for a given ``QueryCriteria``)
|
- Use ``trackBy`` to obtain both a current snapshot and a future stream of updates (for a given ``QueryCriteria``)
|
||||||
|
|
||||||
.. note:: Streaming updates are only filtered based on contract type and state status (UNCONSUMED, CONSUMED, ALL)
|
.. note:: Streaming updates are only filtered based on contract type and state status (UNCONSUMED, CONSUMED, ALL)
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ CorDapps
|
|||||||
|
|
||||||
cordapp-overview
|
cordapp-overview
|
||||||
writing-a-cordapp
|
writing-a-cordapp
|
||||||
|
upgrade-notes
|
||||||
cordapp-build-systems
|
cordapp-build-systems
|
||||||
building-against-master
|
building-against-master
|
||||||
corda-api
|
corda-api
|
||||||
|
@ -6,6 +6,50 @@ from the previous milestone release.
|
|||||||
|
|
||||||
UNRELEASED
|
UNRELEASED
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
* The network map service concept has been re-designed. More information can be found in :doc:`network-map`.
|
||||||
|
|
||||||
|
* The previous design was never intended to be final but was rather a quick implementation in the earliest days of the
|
||||||
|
Corda project to unblock higher priority items. It suffers from numerous disadvantages including lack of scalability,
|
||||||
|
as one node is expected to hold open and manage connections to every node on the network; not reliable; hard to defend
|
||||||
|
against DoS attacks; etc.
|
||||||
|
|
||||||
|
* There is no longer a special network map node for distributing the network map to the other nodes. Instead the network
|
||||||
|
map is now a collection of signed ``NodeInfo`` files distributed via HTTP.
|
||||||
|
|
||||||
|
* The ``certificateSigningService`` config has been replaced by ``compatibilityZoneURL`` which is the base URL for the
|
||||||
|
doorman registration and for downloading the network map. There is also an end-point for the node to publish its node-info
|
||||||
|
object, which the node does each time it changes. ``networkMapService`` config has been removed.
|
||||||
|
|
||||||
|
* To support local and test deployments, the node polls the ``additional-node-infos`` directory for these signed ``NodeInfo``
|
||||||
|
objects which are stored in its local cache. On startup the node generates its own signed file with the filename format
|
||||||
|
"nodeInfo-*". This can be copied to every node's ``additional-node-infos`` directory that is part of the network.
|
||||||
|
|
||||||
|
* Cordform (which is the ``deployNodes`` gradle task) does this copying automatically for the demos. The ``NetworkMap``
|
||||||
|
parameter is no longer needed.
|
||||||
|
|
||||||
|
* For test deployments we've introduced a bootstrapping tool (see :doc:`setting-up-a-corda-network`).
|
||||||
|
|
||||||
|
* ``extraAdvertisedServiceIds``, ``notaryNodeAddress``, ``notaryClusterAddresses`` and ``bftSMaRt`` configs have been
|
||||||
|
removed. The configuration of notaries has been simplified into a single ``notary`` config object. See
|
||||||
|
:doc:`corda-configuration-file` for more details.
|
||||||
|
|
||||||
|
* Introducing the concept of network parameters which are a set of constants which all nodes on a network must agree on
|
||||||
|
to correctly interop.
|
||||||
|
|
||||||
|
* The set of valid notaries has been moved to the network parameters. Notaries are no longer identified by the CN in
|
||||||
|
their X500 name.
|
||||||
|
|
||||||
|
* Single node notaries no longer have a second separate notary identity. Their main identity *is* their notary identity.
|
||||||
|
Use ``NetworkMapCache.notaryIdentities`` to get the list of available notaries.
|
||||||
|
|
||||||
|
* The common name in the node's X500 legal name is no longer reserved and can be used as part of the node's name.
|
||||||
|
|
||||||
|
* Moved ``NodeInfoSchema`` to internal package as the node info's database schema is not part of the public API. This
|
||||||
|
was needed to allow changes to the schema.
|
||||||
|
|
||||||
|
* Support for external user credentials data source and password encryption [CORDA-827].
|
||||||
|
|
||||||
* Integrate database migration tool: http://www.liquibase.org/ :
|
* Integrate database migration tool: http://www.liquibase.org/ :
|
||||||
* The migration files are split per ``MappedSchemas``. (added new property: migrationResource used to point to the resource file containing the db changes corresponding to the JPA entities)
|
* The migration files are split per ``MappedSchemas``. (added new property: migrationResource used to point to the resource file containing the db changes corresponding to the JPA entities)
|
||||||
* config flag ``database.initialiseSchema`` was renamed to: ``database.runMigration`` (if true then the migration is run during startup just before hibernate is initialised.)
|
* config flag ``database.initialiseSchema`` was renamed to: ``database.runMigration`` (if true then the migration is run during startup just before hibernate is initialised.)
|
||||||
@ -45,24 +89,12 @@ UNRELEASED
|
|||||||
deploys nodes for development and testing, the latter turns a project into a cordapp project that generates JARs in
|
deploys nodes for development and testing, the latter turns a project into a cordapp project that generates JARs in
|
||||||
the standard CorDapp format.
|
the standard CorDapp format.
|
||||||
|
|
||||||
* ``Cordform`` and node identity generation:
|
|
||||||
* Removed the parameter ``NetworkMap`` from Cordform. Now at the end of the deployment the following happens:
|
|
||||||
1. Each node is started and its signed serialized NodeInfo is written to disk in the node base directory.
|
|
||||||
2. Every serialized ``NodeInfo`` above is copied in every other node "additional-node-info" folder under the NodeInfo folder.
|
|
||||||
|
|
||||||
* Nodes read and poll the filesystem for serialized ``NodeInfo`` in the ``additional-node-info`` directory.
|
|
||||||
|
|
||||||
* ``Cordapp`` now has a name field for identifying CorDapps and all CorDapp names are printed to console at startup.
|
* ``Cordapp`` now has a name field for identifying CorDapps and all CorDapp names are printed to console at startup.
|
||||||
|
|
||||||
* Enums now respect the whitelist applied to the Serializer factory serializing / deserializing them. If the enum isn't
|
* Enums now respect the whitelist applied to the Serializer factory serializing / deserializing them. If the enum isn't
|
||||||
either annotated with the @CordaSerializable annotation or explicitly whitelisted then a NotSerializableException is
|
either annotated with the @CordaSerializable annotation or explicitly whitelisted then a NotSerializableException is
|
||||||
thrown.
|
thrown.
|
||||||
|
|
||||||
* ``extraAdvertisedServiceIds`` config has been removed as part of the previous work to retire the concept of advertised
|
|
||||||
services. The remaining use of this config was for notaries, the configuring of which has been cleaned up and simplified.
|
|
||||||
``notaryNodeAddress``, ``notaryClusterAddresses`` and ``bftSMaRt`` have also been removed and replaced by a single
|
|
||||||
``notary`` config object. See :doc:`corda-configuration-file` for more details.
|
|
||||||
|
|
||||||
* Gradle task ``deployNodes`` can have an additional parameter ``configFile`` with the path to a properties file
|
* Gradle task ``deployNodes`` can have an additional parameter ``configFile`` with the path to a properties file
|
||||||
to be appended to node.conf.
|
to be appended to node.conf.
|
||||||
|
|
||||||
@ -79,8 +111,8 @@ UNRELEASED
|
|||||||
allows services to start flows marked with the ``StartableByService`` annotation. For backwards compatability
|
allows services to start flows marked with the ``StartableByService`` annotation. For backwards compatability
|
||||||
service classes with only ``ServiceHub`` constructors will still work.
|
service classes with only ``ServiceHub`` constructors will still work.
|
||||||
|
|
||||||
* ``TimeWindow`` now has a ``length`` property that returns the length of the time-window, or ``null`` if the
|
* ``TimeWindow`` now has a ``length`` property that returns the length of the time-window as a ``java.time.Duration`` object,
|
||||||
time-window is open-ended.
|
or ``null`` if the time-window isn't closed.
|
||||||
|
|
||||||
* A new ``SIGNERS_GROUP`` with ordinal 6 has been added to ``ComponentGroupEnum`` that corresponds to the ``Command``
|
* A new ``SIGNERS_GROUP`` with ordinal 6 has been added to ``ComponentGroupEnum`` that corresponds to the ``Command``
|
||||||
signers.
|
signers.
|
||||||
@ -96,18 +128,16 @@ UNRELEASED
|
|||||||
* The ``ReceiveTransactionFlow`` can now be told to record the transaction at the same time as receiving it. Using this
|
* The ``ReceiveTransactionFlow`` can now be told to record the transaction at the same time as receiving it. Using this
|
||||||
feature, better support for observer/regulator nodes has been added. See :doc:`tutorial-observer-nodes`.
|
feature, better support for observer/regulator nodes has been added. See :doc:`tutorial-observer-nodes`.
|
||||||
|
|
||||||
* Moved ``NodeInfoSchema`` to internal package as the node info's database schema is not part of the public API. This is
|
|
||||||
needed to allow new ``node_info_hash`` column to be added for the network map redesign work.
|
|
||||||
|
|
||||||
* Added an overload of ``TransactionWithSignatures.verifySignaturesExcept`` which takes in a collection of ``PublicKey``s.
|
* Added an overload of ``TransactionWithSignatures.verifySignaturesExcept`` which takes in a collection of ``PublicKey``s.
|
||||||
|
|
||||||
* Replaced node configuration parameter ``certificateSigningService`` with ``compatibilityZoneURL``, which is Corda
|
* ``DriverDSLExposedInterface`` has been renamed to ``DriverDSL`` and the ``waitForAllNodesToFinish()`` method has instead
|
||||||
compatibility zone network management service's address.
|
become a parameter on driver creation.
|
||||||
|
|
||||||
* ``waitForAllNodesToFinish()`` method in ``DriverDSLExposedInterface`` has instead become a parameter on driver creation.
|
* Values for the ``database.transactionIsolationLevel`` config now follow the ``java.sql.Connection`` int constants but
|
||||||
|
without the "TRANSACTION_" prefix, i.e. "NONE", "READ_UNCOMMITTED", etc.
|
||||||
|
|
||||||
* ``database.transactionIsolationLevel`` values now follow the ``java.sql.Connection`` int constants but without the
|
* Peer-to-peer communications is now via AMQP 1.0 as default.
|
||||||
"TRANSACTION_" prefix, i.e. "NONE", "READ_UNCOMMITTED", etc.
|
Although the legacy Artemis CORE bridging can still be used by setting the ``useAMQPBridges`` configuration property to false.
|
||||||
|
|
||||||
* Enterprise Corda only: Compatibility with SQL Server 2017 and SQL Azure databases.
|
* Enterprise Corda only: Compatibility with SQL Server 2017 and SQL Azure databases.
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ permissions that RPC can use for fine-grain access control.
|
|||||||
|
|
||||||
These users are added to the node's ``node.conf`` file.
|
These users are added to the node's ``node.conf`` file.
|
||||||
|
|
||||||
The syntax for adding an RPC user is:
|
The simplest way of adding an RPC user is to include it in the ``rpcUsers`` list:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
@ -59,9 +59,6 @@ Users need permissions to invoke any RPC call. By default, nothing is allowed. T
|
|||||||
...
|
...
|
||||||
]
|
]
|
||||||
|
|
||||||
.. note:: Currently, the node's web server has super-user access, meaning that it can run any RPC operation without
|
|
||||||
logging in. This will be changed in a future release.
|
|
||||||
|
|
||||||
Permissions Syntax
|
Permissions Syntax
|
||||||
^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
@ -71,6 +68,134 @@ Fine grained permissions allow a user to invoke a specific RPC operation, or to
|
|||||||
- to invoke a RPC operation: ``InvokeRpc.<rpc method name>`` e.g., ``InvokeRpc.nodeInfo``.
|
- to invoke a RPC operation: ``InvokeRpc.<rpc method name>`` e.g., ``InvokeRpc.nodeInfo``.
|
||||||
.. note:: Permission ``InvokeRpc.startFlow`` allows a user to initiate all flows.
|
.. note:: Permission ``InvokeRpc.startFlow`` allows a user to initiate all flows.
|
||||||
|
|
||||||
|
RPC security management
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Setting ``rpcUsers`` provides a simple way of granting RPC permissions to a fixed set of users, but has some
|
||||||
|
obvious shortcomings. To support use cases aiming for higher security and flexibility, Corda offers additional security
|
||||||
|
features such as:
|
||||||
|
|
||||||
|
* Fetching users credentials and permissions from an external data source (e.g.: a remote RDBMS), with optional in-memory
|
||||||
|
caching. In particular, this allows credentials and permissions to be updated externally without requiring nodes to be
|
||||||
|
restarted.
|
||||||
|
* Password stored in hash-encrypted form. This is regarded as must-have when security is a concern. Corda currently supports
|
||||||
|
a flexible password hash format conforming to the Modular Crypt Format provided by the `Apache Shiro framework <https://shiro.apache.org/static/1.2.5/apidocs/org/apache/shiro/crypto/hash/format/Shiro1CryptFormat.html>`_
|
||||||
|
|
||||||
|
These features are controlled by a set of options nested in the ``security`` field of ``node.conf``.
|
||||||
|
The following example shows how to configure retrieval of users credentials and permissions from a remote database with
|
||||||
|
passwords in hash-encrypted format and enable in-memory caching of users data:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. sourcecode:: groovy
|
||||||
|
|
||||||
|
security = {
|
||||||
|
authService = {
|
||||||
|
dataSource = {
|
||||||
|
type = "DB",
|
||||||
|
passwordEncryption = "SHIRO_1_CRYPT",
|
||||||
|
connection = {
|
||||||
|
jdbcUrl = "<jdbc connection string>"
|
||||||
|
username = "<db username>"
|
||||||
|
password = "<db user password>"
|
||||||
|
driverClassName = "<JDBC driver>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
options = {
|
||||||
|
cache = {
|
||||||
|
expireAfterSecs = 120
|
||||||
|
maxEntries = 10000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
It is also possible to have a static list of users embedded in the ``security`` structure by specifying a ``dataSource``
|
||||||
|
of ``INMEMORY`` type:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. sourcecode:: groovy
|
||||||
|
|
||||||
|
security = {
|
||||||
|
authService = {
|
||||||
|
dataSource = {
|
||||||
|
type = "INMEMORY",
|
||||||
|
users = [
|
||||||
|
{
|
||||||
|
username = "<username>",
|
||||||
|
password = "<password>",
|
||||||
|
permissions = ["<permission 1>", "<permission 2>", ...]
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.. warning:: A valid configuration cannot specify both the ``rpcUsers`` and ``security`` fields. Doing so will trigger
|
||||||
|
an exception at node startup.
|
||||||
|
|
||||||
|
Authentication/authorisation data
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The ``dataSource`` structure defines the data provider supplying credentials and permissions for users. There exist two
|
||||||
|
supported types of such data source, identified by the ``dataSource.type`` field:
|
||||||
|
|
||||||
|
:INMEMORY: A static list of user credentials and permissions specified by the ``users`` field.
|
||||||
|
|
||||||
|
:DB: An external RDBMS accessed via the JDBC connection described by ``connection``. Note that, unlike the ``INMEMORY``
|
||||||
|
case, in a user database permissions are assigned to *roles* rather than individual users. The current implementation
|
||||||
|
expects the database to store data according to the following schema:
|
||||||
|
|
||||||
|
- Table ``users`` containing columns ``username`` and ``password``. The ``username`` column *must have unique values*.
|
||||||
|
- Table ``user_roles`` containing columns ``username`` and ``role_name`` associating a user to a set of *roles*.
|
||||||
|
- Table ``roles_permissions`` containing columns ``role_name`` and ``permission`` associating a role to a set of
|
||||||
|
permission strings.
|
||||||
|
|
||||||
|
.. note:: There is no prescription on the SQL type of each column (although our tests were conducted on ``username`` and
|
||||||
|
``role_name`` declared of SQL type ``VARCHAR`` and ``password`` of ``TEXT`` type). It is also possible to have extra columns
|
||||||
|
in each table alongside the expected ones.
|
||||||
|
|
||||||
|
Password encryption
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Storing passwords in plain text is discouraged in applications where security is critical. Passwords are assumed
|
||||||
|
to be in plain format by default, unless a different format is specified by the ``passwordEncryption`` field, like:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. sourcecode:: groovy
|
||||||
|
|
||||||
|
passwordEncryption = SHIRO_1_CRYPT
|
||||||
|
|
||||||
|
``SHIRO_1_CRYPT`` identifies the `Apache Shiro fully reversible
|
||||||
|
Modular Crypt Format <https://shiro.apache.org/static/1.2.5/apidocs/org/apache/shiro/crypto/hash/format/Shiro1CryptFormat.html>`_,
|
||||||
|
it is currently the only non-plain password hash-encryption format supported. Hash-encrypted passwords in this
|
||||||
|
format can be produced by using the `Apache Shiro Hasher command line tool <https://shiro.apache.org/command-line-hasher.html>`_.
|
||||||
|
|
||||||
|
Caching user accounts data
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
A cache layer on top of the external data source of users credentials and permissions can significantly improve
|
||||||
|
performances in some cases, with the disadvantage of causing a (controllable) delay in picking up updates to the underlying data.
|
||||||
|
Caching is disabled by default, it can be enabled by defining the ``options.cache`` field in ``security.authService``,
|
||||||
|
for example:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. sourcecode:: groovy
|
||||||
|
|
||||||
|
options = {
|
||||||
|
cache = {
|
||||||
|
expireAfterSecs = 120
|
||||||
|
maxEntries = 10000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
This will enable a non-persistent cache contained in the node's memory with maximum number of entries set to ``maxEntries``
|
||||||
|
where entries are expired and refreshed after ``expireAfterSecs`` seconds.
|
||||||
|
|
||||||
Observables
|
Observables
|
||||||
-----------
|
-----------
|
||||||
The RPC system handles observables in a special way. When a method returns an observable, whether directly or
|
The RPC system handles observables in a special way. When a method returns an observable, whether directly or
|
||||||
|
@ -101,6 +101,9 @@ path to the node's base directory.
|
|||||||
|
|
||||||
:rpcAddress: The address of the RPC system on which RPC requests can be made to the node. If not provided then the node will run without RPC.
|
:rpcAddress: The address of the RPC system on which RPC requests can be made to the node. If not provided then the node will run without RPC.
|
||||||
|
|
||||||
|
:security: Contains various nested fields controlling user authentication/authorization, in particular for RPC accesses. See
|
||||||
|
:doc:`clientrpc` for details.
|
||||||
|
|
||||||
:webAddress: The host and port on which the webserver will listen if it is started. This is not used by the node itself.
|
:webAddress: The host and port on which the webserver will listen if it is started. This is not used by the node itself.
|
||||||
|
|
||||||
.. note:: If HTTPS is enabled then the browser security checks will require that the accessing url host name is one
|
.. note:: If HTTPS is enabled then the browser security checks will require that the accessing url host name is one
|
||||||
@ -191,4 +194,7 @@ path to the node's base directory.
|
|||||||
:sshPort: Port to be used for SSH connection, default ``22``.
|
:sshPort: Port to be used for SSH connection, default ``22``.
|
||||||
|
|
||||||
:exportJMXTo: If set to ``http``, will enable JMX metrics reporting via the Jolokia HTTP/JSON agent.
|
:exportJMXTo: If set to ``http``, will enable JMX metrics reporting via the Jolokia HTTP/JSON agent.
|
||||||
Default Jolokia access url is http://127.0.0.1:7005/jolokia/
|
Default Jolokia access url is http://127.0.0.1:7005/jolokia/
|
||||||
|
|
||||||
|
:useAMQPBridges: Optionally can be set to ``false`` to use Artemis CORE Bridges for peer-to-peer communications.
|
||||||
|
Otherwise, defaults to ``true`` and the AMQP 1.0 protocol will be used for message transfer between nodes.
|
@ -10,7 +10,6 @@ Corda nodes
|
|||||||
corda-configuration-file
|
corda-configuration-file
|
||||||
clientrpc
|
clientrpc
|
||||||
shell
|
shell
|
||||||
node-auth-config
|
|
||||||
node-database
|
node-database
|
||||||
node-administration
|
node-administration
|
||||||
out-of-process-verification
|
out-of-process-verification
|
@ -36,6 +36,7 @@ dependencies {
|
|||||||
compile project(':core')
|
compile project(':core')
|
||||||
compile project(':client:jfx')
|
compile project(':client:jfx')
|
||||||
compile project(':node-driver')
|
compile project(':node-driver')
|
||||||
|
compile project(':webserver')
|
||||||
testCompile project(':verifier')
|
testCompile project(':verifier')
|
||||||
testCompile project(':test-utils')
|
testCompile project(':test-utils')
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.docs.java.tutorial.helloworld;
|
package com.template;
|
||||||
|
|
||||||
import net.corda.core.contracts.CommandData;
|
import net.corda.core.contracts.CommandData;
|
||||||
import net.corda.core.contracts.Contract;
|
import net.corda.core.contracts.Contract;
|
@ -1,17 +1,21 @@
|
|||||||
package net.corda.docs.java.tutorial.helloworld;
|
package net.corda.docs.java.tutorial.helloworld;
|
||||||
|
|
||||||
// DOCSTART 01
|
|
||||||
import co.paralleluniverse.fibers.Suspendable;
|
import co.paralleluniverse.fibers.Suspendable;
|
||||||
|
import com.template.TemplateContract;
|
||||||
|
import net.corda.core.flows.*;
|
||||||
|
|
||||||
|
// DOCSTART 01
|
||||||
|
// Add these imports:
|
||||||
import net.corda.core.contracts.Command;
|
import net.corda.core.contracts.Command;
|
||||||
import net.corda.core.contracts.CommandData;
|
import net.corda.core.contracts.CommandData;
|
||||||
import net.corda.core.flows.*;
|
|
||||||
import net.corda.core.identity.Party;
|
import net.corda.core.identity.Party;
|
||||||
import net.corda.core.transactions.SignedTransaction;
|
import net.corda.core.transactions.SignedTransaction;
|
||||||
import net.corda.core.transactions.TransactionBuilder;
|
import net.corda.core.transactions.TransactionBuilder;
|
||||||
import net.corda.core.utilities.ProgressTracker;
|
import net.corda.core.utilities.ProgressTracker;
|
||||||
|
|
||||||
import static net.corda.docs.java.tutorial.helloworld.TemplateContract.TEMPLATE_CONTRACT_ID;
|
import static com.template.TemplateContract.TEMPLATE_CONTRACT_ID;
|
||||||
|
|
||||||
|
// Replace TemplateFlow's definition with:
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
public class IOUFlow extends FlowLogic<Void> {
|
public class IOUFlow extends FlowLogic<Void> {
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
package net.corda.docs.java.tutorial.helloworld;
|
package net.corda.docs.java.tutorial.helloworld;
|
||||||
|
|
||||||
// DOCSTART 01
|
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import net.corda.core.contracts.ContractState;
|
import net.corda.core.contracts.ContractState;
|
||||||
import net.corda.core.identity.AbstractParty;
|
import net.corda.core.identity.AbstractParty;
|
||||||
import net.corda.core.identity.Party;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
// DOCSTART 01
|
||||||
|
// Add these imports:
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import net.corda.core.identity.Party;
|
||||||
|
|
||||||
|
// Replace TemplateState's definition with:
|
||||||
public class IOUState implements ContractState {
|
public class IOUState implements ContractState {
|
||||||
private final int value;
|
private final int value;
|
||||||
private final Party lender;
|
private final Party lender;
|
||||||
|
@ -3,48 +3,33 @@ package net.corda.docs.java.tutorial.testdsl;
|
|||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
import net.corda.core.contracts.PartyAndReference;
|
import net.corda.core.contracts.PartyAndReference;
|
||||||
import net.corda.core.identity.CordaX500Name;
|
import net.corda.core.identity.CordaX500Name;
|
||||||
import net.corda.core.identity.Party;
|
|
||||||
import net.corda.finance.contracts.ICommercialPaperState;
|
import net.corda.finance.contracts.ICommercialPaperState;
|
||||||
import net.corda.finance.contracts.JavaCommercialPaper;
|
import net.corda.finance.contracts.JavaCommercialPaper;
|
||||||
import net.corda.finance.contracts.asset.Cash;
|
import net.corda.finance.contracts.asset.Cash;
|
||||||
import net.corda.node.services.api.IdentityServiceInternal;
|
|
||||||
import net.corda.testing.SerializationEnvironmentRule;
|
|
||||||
import net.corda.testing.node.MockServices;
|
import net.corda.testing.node.MockServices;
|
||||||
import net.corda.testing.TestIdentity;
|
import net.corda.testing.TestIdentity;
|
||||||
import org.junit.Rule;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
import static net.corda.core.crypto.Crypto.generateKeyPair;
|
import static net.corda.core.crypto.Crypto.generateKeyPair;
|
||||||
import static net.corda.finance.Currencies.DOLLARS;
|
import static net.corda.finance.Currencies.DOLLARS;
|
||||||
import static net.corda.finance.Currencies.issuedBy;
|
import static net.corda.finance.Currencies.issuedBy;
|
||||||
import static net.corda.finance.contracts.JavaCommercialPaper.JCP_PROGRAM_ID;
|
import static net.corda.finance.contracts.JavaCommercialPaper.JCP_PROGRAM_ID;
|
||||||
|
import static net.corda.testing.node.MockServicesKt.makeTestIdentityService;
|
||||||
import static net.corda.testing.node.NodeTestUtils.ledger;
|
import static net.corda.testing.node.NodeTestUtils.ledger;
|
||||||
import static net.corda.testing.node.NodeTestUtils.transaction;
|
import static net.corda.testing.node.NodeTestUtils.transaction;
|
||||||
import static net.corda.testing.CoreTestUtils.rigorousMock;
|
|
||||||
import static net.corda.testing.TestConstants.*;
|
import static net.corda.testing.TestConstants.*;
|
||||||
import static org.mockito.Mockito.doReturn;
|
|
||||||
|
|
||||||
public class CommercialPaperTest {
|
public class CommercialPaperTest {
|
||||||
private static final TestIdentity ALICE = new TestIdentity(getALICE_NAME(), 70L);
|
private static final TestIdentity ALICE = new TestIdentity(ALICE_NAME, 70L);
|
||||||
private static final PublicKey BIG_CORP_PUBKEY = generateKeyPair().getPublic();
|
private static final PublicKey BIG_CORP_PUBKEY = generateKeyPair().getPublic();
|
||||||
private static final TestIdentity BOB = new TestIdentity(getBOB_NAME(), 80L);
|
private static final TestIdentity BOB = new TestIdentity(BOB_NAME, 80L);
|
||||||
private static final TestIdentity MEGA_CORP = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB"));
|
private static final TestIdentity MEGA_CORP = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB"));
|
||||||
private static final Party DUMMY_NOTARY = new TestIdentity(getDUMMY_NOTARY_NAME(), 20L).getParty();
|
|
||||||
@Rule
|
|
||||||
public final SerializationEnvironmentRule testSerialization = new SerializationEnvironmentRule();
|
|
||||||
private final byte[] defaultRef = {123};
|
private final byte[] defaultRef = {123};
|
||||||
private final MockServices ledgerServices;
|
private final MockServices ledgerServices = new MockServices(emptyList(), makeTestIdentityService(MEGA_CORP.getIdentity()));
|
||||||
|
|
||||||
{
|
|
||||||
IdentityServiceInternal identityService = rigorousMock(IdentityServiceInternal.class);
|
|
||||||
doReturn(MEGA_CORP.getParty()).when(identityService).partyFromKey(MEGA_CORP.getPubkey());
|
|
||||||
doReturn(null).when(identityService).partyFromKey(BIG_CORP_PUBKEY);
|
|
||||||
doReturn(null).when(identityService).partyFromKey(ALICE.getPubkey());
|
|
||||||
ledgerServices = new MockServices(identityService, MEGA_CORP.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
// DOCSTART 1
|
// DOCSTART 1
|
||||||
private ICommercialPaperState getPaper() {
|
private ICommercialPaperState getPaper() {
|
||||||
@ -52,7 +37,7 @@ public class CommercialPaperTest {
|
|||||||
MEGA_CORP.ref(defaultRef),
|
MEGA_CORP.ref(defaultRef),
|
||||||
MEGA_CORP.getParty(),
|
MEGA_CORP.getParty(),
|
||||||
issuedBy(DOLLARS(1000), MEGA_CORP.ref(defaultRef)),
|
issuedBy(DOLLARS(1000), MEGA_CORP.ref(defaultRef)),
|
||||||
getTEST_TX_TIME().plus(7, ChronoUnit.DAYS)
|
TEST_TX_TIME.plus(7, ChronoUnit.DAYS)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// DOCEND 1
|
// DOCEND 1
|
||||||
@ -61,7 +46,7 @@ public class CommercialPaperTest {
|
|||||||
@Test
|
@Test
|
||||||
public void simpleCP() {
|
public void simpleCP() {
|
||||||
ICommercialPaperState inState = getPaper();
|
ICommercialPaperState inState = getPaper();
|
||||||
ledger(ledgerServices, DUMMY_NOTARY, l -> {
|
ledger(ledgerServices, l -> {
|
||||||
l.transaction(tx -> {
|
l.transaction(tx -> {
|
||||||
tx.attachments(JCP_PROGRAM_ID);
|
tx.attachments(JCP_PROGRAM_ID);
|
||||||
tx.input(JCP_PROGRAM_ID, inState);
|
tx.input(JCP_PROGRAM_ID, inState);
|
||||||
@ -76,10 +61,10 @@ public class CommercialPaperTest {
|
|||||||
@Test
|
@Test
|
||||||
public void simpleCPMove() {
|
public void simpleCPMove() {
|
||||||
ICommercialPaperState inState = getPaper();
|
ICommercialPaperState inState = getPaper();
|
||||||
ledger(ledgerServices, DUMMY_NOTARY, l -> {
|
ledger(ledgerServices, l -> {
|
||||||
l.transaction(tx -> {
|
l.transaction(tx -> {
|
||||||
tx.input(JCP_PROGRAM_ID, inState);
|
tx.input(JCP_PROGRAM_ID, inState);
|
||||||
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move());
|
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||||
tx.attachments(JCP_PROGRAM_ID);
|
tx.attachments(JCP_PROGRAM_ID);
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
@ -92,10 +77,10 @@ public class CommercialPaperTest {
|
|||||||
@Test
|
@Test
|
||||||
public void simpleCPMoveFails() {
|
public void simpleCPMoveFails() {
|
||||||
ICommercialPaperState inState = getPaper();
|
ICommercialPaperState inState = getPaper();
|
||||||
ledger(ledgerServices, DUMMY_NOTARY, l -> {
|
ledger(ledgerServices, l -> {
|
||||||
l.transaction(tx -> {
|
l.transaction(tx -> {
|
||||||
tx.input(JCP_PROGRAM_ID, inState);
|
tx.input(JCP_PROGRAM_ID, inState);
|
||||||
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move());
|
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||||
tx.attachments(JCP_PROGRAM_ID);
|
tx.attachments(JCP_PROGRAM_ID);
|
||||||
return tx.failsWith("the state is propagated");
|
return tx.failsWith("the state is propagated");
|
||||||
});
|
});
|
||||||
@ -108,10 +93,10 @@ public class CommercialPaperTest {
|
|||||||
@Test
|
@Test
|
||||||
public void simpleCPMoveSuccess() {
|
public void simpleCPMoveSuccess() {
|
||||||
ICommercialPaperState inState = getPaper();
|
ICommercialPaperState inState = getPaper();
|
||||||
ledger(ledgerServices, DUMMY_NOTARY, l -> {
|
ledger(ledgerServices, l -> {
|
||||||
l.transaction(tx -> {
|
l.transaction(tx -> {
|
||||||
tx.input(JCP_PROGRAM_ID, inState);
|
tx.input(JCP_PROGRAM_ID, inState);
|
||||||
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move());
|
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||||
tx.attachments(JCP_PROGRAM_ID);
|
tx.attachments(JCP_PROGRAM_ID);
|
||||||
tx.failsWith("the state is propagated");
|
tx.failsWith("the state is propagated");
|
||||||
tx.output(JCP_PROGRAM_ID, "alice's paper", inState.withOwner(ALICE.getParty()));
|
tx.output(JCP_PROGRAM_ID, "alice's paper", inState.withOwner(ALICE.getParty()));
|
||||||
@ -125,17 +110,17 @@ public class CommercialPaperTest {
|
|||||||
// DOCSTART 6
|
// DOCSTART 6
|
||||||
@Test
|
@Test
|
||||||
public void simpleIssuanceWithTweak() {
|
public void simpleIssuanceWithTweak() {
|
||||||
ledger(ledgerServices, DUMMY_NOTARY, l -> {
|
ledger(ledgerServices, l -> {
|
||||||
l.transaction(tx -> {
|
l.transaction(tx -> {
|
||||||
tx.output(JCP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp.
|
tx.output(JCP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp.
|
||||||
tx.attachments(JCP_PROGRAM_ID);
|
tx.attachments(JCP_PROGRAM_ID);
|
||||||
tx.tweak(tw -> {
|
tx.tweak(tw -> {
|
||||||
tw.command(BIG_CORP_PUBKEY, new JavaCommercialPaper.Commands.Issue());
|
tw.command(BIG_CORP_PUBKEY, new JavaCommercialPaper.Commands.Issue());
|
||||||
tw.timeWindow(getTEST_TX_TIME());
|
tw.timeWindow(TEST_TX_TIME);
|
||||||
return tw.failsWith("output states are issued by a command signer");
|
return tw.failsWith("output states are issued by a command signer");
|
||||||
});
|
});
|
||||||
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Issue());
|
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue());
|
||||||
tx.timeWindow(getTEST_TX_TIME());
|
tx.timeWindow(TEST_TX_TIME);
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
@ -146,16 +131,16 @@ public class CommercialPaperTest {
|
|||||||
// DOCSTART 7
|
// DOCSTART 7
|
||||||
@Test
|
@Test
|
||||||
public void simpleIssuanceWithTweakTopLevelTx() {
|
public void simpleIssuanceWithTweakTopLevelTx() {
|
||||||
transaction(ledgerServices, DUMMY_NOTARY, tx -> {
|
transaction(ledgerServices, tx -> {
|
||||||
tx.output(JCP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp.
|
tx.output(JCP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp.
|
||||||
tx.attachments(JCP_PROGRAM_ID);
|
tx.attachments(JCP_PROGRAM_ID);
|
||||||
tx.tweak(tw -> {
|
tx.tweak(tw -> {
|
||||||
tw.command(BIG_CORP_PUBKEY, new JavaCommercialPaper.Commands.Issue());
|
tw.command(BIG_CORP_PUBKEY, new JavaCommercialPaper.Commands.Issue());
|
||||||
tw.timeWindow(getTEST_TX_TIME());
|
tw.timeWindow(TEST_TX_TIME);
|
||||||
return tw.failsWith("output states are issued by a command signer");
|
return tw.failsWith("output states are issued by a command signer");
|
||||||
});
|
});
|
||||||
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Issue());
|
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue());
|
||||||
tx.timeWindow(getTEST_TX_TIME());
|
tx.timeWindow(TEST_TX_TIME);
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -165,7 +150,7 @@ public class CommercialPaperTest {
|
|||||||
@Test
|
@Test
|
||||||
public void chainCommercialPaper() {
|
public void chainCommercialPaper() {
|
||||||
PartyAndReference issuer = MEGA_CORP.ref(defaultRef);
|
PartyAndReference issuer = MEGA_CORP.ref(defaultRef);
|
||||||
ledger(ledgerServices, DUMMY_NOTARY, l -> {
|
ledger(ledgerServices, l -> {
|
||||||
l.unverifiedTransaction(tx -> {
|
l.unverifiedTransaction(tx -> {
|
||||||
tx.output(Cash.PROGRAM_ID, "alice's $900",
|
tx.output(Cash.PROGRAM_ID, "alice's $900",
|
||||||
new Cash.State(issuedBy(DOLLARS(900), issuer), ALICE.getParty()));
|
new Cash.State(issuedBy(DOLLARS(900), issuer), ALICE.getParty()));
|
||||||
@ -176,9 +161,9 @@ public class CommercialPaperTest {
|
|||||||
// Some CP is issued onto the ledger by MegaCorp.
|
// Some CP is issued onto the ledger by MegaCorp.
|
||||||
l.transaction("Issuance", tx -> {
|
l.transaction("Issuance", tx -> {
|
||||||
tx.output(JCP_PROGRAM_ID, "paper", getPaper());
|
tx.output(JCP_PROGRAM_ID, "paper", getPaper());
|
||||||
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Issue());
|
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue());
|
||||||
tx.attachments(JCP_PROGRAM_ID);
|
tx.attachments(JCP_PROGRAM_ID);
|
||||||
tx.timeWindow(getTEST_TX_TIME());
|
tx.timeWindow(TEST_TX_TIME);
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -188,8 +173,8 @@ public class CommercialPaperTest {
|
|||||||
tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), MEGA_CORP.getParty()));
|
tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), MEGA_CORP.getParty()));
|
||||||
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
||||||
tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(ALICE.getParty()));
|
tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(ALICE.getParty()));
|
||||||
tx.command(ALICE.getPubkey(), new Cash.Commands.Move());
|
tx.command(ALICE.getPublicKey(), new Cash.Commands.Move());
|
||||||
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move());
|
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
@ -201,7 +186,7 @@ public class CommercialPaperTest {
|
|||||||
@Test
|
@Test
|
||||||
public void chainCommercialPaperDoubleSpend() {
|
public void chainCommercialPaperDoubleSpend() {
|
||||||
PartyAndReference issuer = MEGA_CORP.ref(defaultRef);
|
PartyAndReference issuer = MEGA_CORP.ref(defaultRef);
|
||||||
ledger(ledgerServices, DUMMY_NOTARY, l -> {
|
ledger(ledgerServices, l -> {
|
||||||
l.unverifiedTransaction(tx -> {
|
l.unverifiedTransaction(tx -> {
|
||||||
tx.output(Cash.PROGRAM_ID, "alice's $900",
|
tx.output(Cash.PROGRAM_ID, "alice's $900",
|
||||||
new Cash.State(issuedBy(DOLLARS(900), issuer), ALICE.getParty()));
|
new Cash.State(issuedBy(DOLLARS(900), issuer), ALICE.getParty()));
|
||||||
@ -212,9 +197,9 @@ public class CommercialPaperTest {
|
|||||||
// Some CP is issued onto the ledger by MegaCorp.
|
// Some CP is issued onto the ledger by MegaCorp.
|
||||||
l.transaction("Issuance", tx -> {
|
l.transaction("Issuance", tx -> {
|
||||||
tx.output(Cash.PROGRAM_ID, "paper", getPaper());
|
tx.output(Cash.PROGRAM_ID, "paper", getPaper());
|
||||||
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Issue());
|
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue());
|
||||||
tx.attachments(JCP_PROGRAM_ID);
|
tx.attachments(JCP_PROGRAM_ID);
|
||||||
tx.timeWindow(getTEST_TX_TIME());
|
tx.timeWindow(TEST_TX_TIME);
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -224,8 +209,8 @@ public class CommercialPaperTest {
|
|||||||
tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), MEGA_CORP.getParty()));
|
tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), MEGA_CORP.getParty()));
|
||||||
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
||||||
tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(ALICE.getParty()));
|
tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(ALICE.getParty()));
|
||||||
tx.command(ALICE.getPubkey(), new Cash.Commands.Move());
|
tx.command(ALICE.getPublicKey(), new Cash.Commands.Move());
|
||||||
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move());
|
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -234,7 +219,7 @@ public class CommercialPaperTest {
|
|||||||
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
||||||
// We moved a paper to other pubkey.
|
// We moved a paper to other pubkey.
|
||||||
tx.output(JCP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(BOB.getParty()));
|
tx.output(JCP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(BOB.getParty()));
|
||||||
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move());
|
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
l.fails();
|
l.fails();
|
||||||
@ -247,7 +232,7 @@ public class CommercialPaperTest {
|
|||||||
@Test
|
@Test
|
||||||
public void chainCommercialPaperTweak() {
|
public void chainCommercialPaperTweak() {
|
||||||
PartyAndReference issuer = MEGA_CORP.ref(defaultRef);
|
PartyAndReference issuer = MEGA_CORP.ref(defaultRef);
|
||||||
ledger(ledgerServices, DUMMY_NOTARY, l -> {
|
ledger(ledgerServices, l -> {
|
||||||
l.unverifiedTransaction(tx -> {
|
l.unverifiedTransaction(tx -> {
|
||||||
tx.output(Cash.PROGRAM_ID, "alice's $900",
|
tx.output(Cash.PROGRAM_ID, "alice's $900",
|
||||||
new Cash.State(issuedBy(DOLLARS(900), issuer), ALICE.getParty()));
|
new Cash.State(issuedBy(DOLLARS(900), issuer), ALICE.getParty()));
|
||||||
@ -258,9 +243,9 @@ public class CommercialPaperTest {
|
|||||||
// Some CP is issued onto the ledger by MegaCorp.
|
// Some CP is issued onto the ledger by MegaCorp.
|
||||||
l.transaction("Issuance", tx -> {
|
l.transaction("Issuance", tx -> {
|
||||||
tx.output(Cash.PROGRAM_ID, "paper", getPaper());
|
tx.output(Cash.PROGRAM_ID, "paper", getPaper());
|
||||||
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Issue());
|
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue());
|
||||||
tx.attachments(JCP_PROGRAM_ID);
|
tx.attachments(JCP_PROGRAM_ID);
|
||||||
tx.timeWindow(getTEST_TX_TIME());
|
tx.timeWindow(TEST_TX_TIME);
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -270,8 +255,8 @@ public class CommercialPaperTest {
|
|||||||
tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), MEGA_CORP.getParty()));
|
tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), MEGA_CORP.getParty()));
|
||||||
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
||||||
tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(ALICE.getParty()));
|
tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(ALICE.getParty()));
|
||||||
tx.command(ALICE.getPubkey(), new Cash.Commands.Move());
|
tx.command(ALICE.getPublicKey(), new Cash.Commands.Move());
|
||||||
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move());
|
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -281,7 +266,7 @@ public class CommercialPaperTest {
|
|||||||
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
||||||
// We moved a paper to another pubkey.
|
// We moved a paper to another pubkey.
|
||||||
tx.output(JCP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(BOB.getParty()));
|
tx.output(JCP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(BOB.getParty()));
|
||||||
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move());
|
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
lw.fails();
|
lw.fails();
|
||||||
|
@ -1,20 +1,25 @@
|
|||||||
package net.corda.docs.java.tutorial.twoparty;
|
package net.corda.docs.java.tutorial.twoparty;
|
||||||
|
|
||||||
// DOCSTART 01
|
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import net.corda.core.contracts.CommandData;
|
import net.corda.core.contracts.CommandData;
|
||||||
import net.corda.core.contracts.CommandWithParties;
|
|
||||||
import net.corda.core.contracts.Contract;
|
import net.corda.core.contracts.Contract;
|
||||||
import net.corda.core.identity.Party;
|
|
||||||
import net.corda.core.transactions.LedgerTransaction;
|
import net.corda.core.transactions.LedgerTransaction;
|
||||||
|
|
||||||
|
// DOCSTART 01
|
||||||
|
// Add these imports:
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import net.corda.core.contracts.CommandWithParties;
|
||||||
|
import net.corda.core.identity.Party;
|
||||||
|
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
||||||
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
||||||
|
|
||||||
|
// Replace TemplateContract's definition with:
|
||||||
public class IOUContract implements Contract {
|
public class IOUContract implements Contract {
|
||||||
|
public static final String IOU_CONTRACT_ID = "com.template.IOUContract";
|
||||||
|
|
||||||
// Our Create command.
|
// Our Create command.
|
||||||
public static class Create implements CommandData {
|
public static class Create implements CommandData {
|
||||||
}
|
}
|
||||||
|
@ -45,15 +45,14 @@ public class IOUFlow extends FlowLogic<Void> {
|
|||||||
// We retrieve the notary identity from the network map.
|
// We retrieve the notary identity from the network map.
|
||||||
final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
|
final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
|
||||||
|
|
||||||
|
// DOCSTART 02
|
||||||
// We create a transaction builder.
|
// We create a transaction builder.
|
||||||
final TransactionBuilder txBuilder = new TransactionBuilder();
|
final TransactionBuilder txBuilder = new TransactionBuilder();
|
||||||
txBuilder.setNotary(notary);
|
txBuilder.setNotary(notary);
|
||||||
|
|
||||||
// DOCSTART 02
|
|
||||||
// We create the transaction components.
|
// We create the transaction components.
|
||||||
IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty);
|
IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty);
|
||||||
String outputContract = IOUContract.class.getName();
|
StateAndContract outputContractAndState = new StateAndContract(outputState, IOUContract.IOU_CONTRACT_ID);
|
||||||
StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract);
|
|
||||||
List<PublicKey> requiredSigners = ImmutableList.of(getOurIdentity().getOwningKey(), otherParty.getOwningKey());
|
List<PublicKey> requiredSigners = ImmutableList.of(getOurIdentity().getOwningKey(), otherParty.getOwningKey());
|
||||||
Command cmd = new Command<>(new IOUContract.Create(), requiredSigners);
|
Command cmd = new Command<>(new IOUContract.Create(), requiredSigners);
|
||||||
|
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
package net.corda.docs.java.tutorial.twoparty;
|
package net.corda.docs.java.tutorial.twoparty;
|
||||||
|
|
||||||
// DOCSTART 01
|
// DOCSTART 01
|
||||||
|
// Add these imports:
|
||||||
import co.paralleluniverse.fibers.Suspendable;
|
import co.paralleluniverse.fibers.Suspendable;
|
||||||
import net.corda.core.contracts.ContractState;
|
import net.corda.core.contracts.ContractState;
|
||||||
import net.corda.core.flows.*;
|
import net.corda.core.flows.*;
|
||||||
import net.corda.core.transactions.SignedTransaction;
|
import net.corda.core.transactions.SignedTransaction;
|
||||||
import net.corda.core.utilities.ProgressTracker;
|
import net.corda.core.utilities.ProgressTracker;
|
||||||
import net.corda.docs.java.tutorial.helloworld.IOUFlow;
|
|
||||||
import net.corda.docs.java.tutorial.helloworld.IOUState;
|
|
||||||
|
|
||||||
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
||||||
|
|
||||||
|
// Define IOUFlowResponder:
|
||||||
@InitiatedBy(IOUFlow.class)
|
@InitiatedBy(IOUFlow.class)
|
||||||
public class IOUFlowResponder extends FlowLogic<Void> {
|
public class IOUFlowResponder extends FlowLogic<Void> {
|
||||||
private final FlowSession otherPartySession;
|
private final FlowSession otherPartySession;
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
package net.corda.docs.tutorial.helloworld
|
package net.corda.docs.tutorial.helloworld
|
||||||
|
|
||||||
// DOCSTART 01
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.Command
|
|
||||||
import net.corda.core.flows.FinalityFlow
|
import net.corda.core.flows.FinalityFlow
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.flows.InitiatingFlow
|
import net.corda.core.flows.InitiatingFlow
|
||||||
import net.corda.core.flows.StartableByRPC
|
import net.corda.core.flows.StartableByRPC
|
||||||
|
|
||||||
|
// DOCSTART 01
|
||||||
|
// Add these imports:
|
||||||
|
import net.corda.core.contracts.Command
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
|
|
||||||
|
// Replace TemplateFlow's definition with:
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class IOUFlow(val iouValue: Int,
|
class IOUFlow(val iouValue: Int,
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package net.corda.docs.tutorial.helloworld
|
package net.corda.docs.tutorial.helloworld
|
||||||
|
|
||||||
// DOCSTART 01
|
|
||||||
import net.corda.core.contracts.ContractState
|
import net.corda.core.contracts.ContractState
|
||||||
|
|
||||||
|
// DOCSTART 01
|
||||||
|
// Add these imports:
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
|
||||||
|
// Replace TemplateState's definition with:
|
||||||
class IOUState(val value: Int,
|
class IOUState(val value: Int,
|
||||||
val lender: Party,
|
val lender: Party,
|
||||||
val borrower: Party) : ContractState {
|
val borrower: Party) : ContractState {
|
||||||
|
@ -67,7 +67,7 @@ class TutorialMockNetwork {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
mockNet = MockNetwork()
|
mockNet = MockNetwork(emptyList())
|
||||||
nodeA = mockNet.createPartyNode()
|
nodeA = mockNet.createPartyNode()
|
||||||
nodeB = mockNet.createPartyNode()
|
nodeB = mockNet.createPartyNode()
|
||||||
nodeB.registerInitiatedFlow(FlowB::class.java)
|
nodeB.registerInitiatedFlow(FlowB::class.java)
|
||||||
|
@ -14,6 +14,7 @@ import net.corda.finance.contracts.asset.CASH
|
|||||||
import net.corda.finance.contracts.asset.Cash
|
import net.corda.finance.contracts.asset.Cash
|
||||||
import net.corda.node.services.api.IdentityServiceInternal
|
import net.corda.node.services.api.IdentityServiceInternal
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.ledger
|
import net.corda.testing.node.ledger
|
||||||
import net.corda.testing.node.transaction
|
import net.corda.testing.node.transaction
|
||||||
@ -28,15 +29,15 @@ class CommercialPaperTest {
|
|||||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||||
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
||||||
val ALICE get() = alice.party
|
val ALICE get() = alice.party
|
||||||
val ALICE_PUBKEY get() = alice.pubkey
|
val ALICE_PUBKEY get() = alice.publicKey
|
||||||
val MEGA_CORP get() = megaCorp.party
|
val MEGA_CORP get() = megaCorp.party
|
||||||
val MEGA_CORP_PUBKEY get() = megaCorp.pubkey
|
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
val testSerialization = SerializationEnvironmentRule()
|
val testSerialization = SerializationEnvironmentRule()
|
||||||
private val ledgerServices = MockServices(rigorousMock<IdentityServiceInternal>().also {
|
private val ledgerServices = MockServices(emptyList(), rigorousMock<IdentityServiceInternal>().also {
|
||||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||||
doReturn(null).whenever(it).partyFromKey(BIG_CORP_PUBKEY)
|
doReturn(null).whenever(it).partyFromKey(BIG_CORP_PUBKEY)
|
||||||
doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY)
|
doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY)
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
package net.corda.docs.tutorial.twoparty
|
package net.corda.docs.tutorial.twoparty
|
||||||
|
|
||||||
// DOCSTART 01
|
|
||||||
import net.corda.core.contracts.CommandData
|
import net.corda.core.contracts.CommandData
|
||||||
import net.corda.core.contracts.Contract
|
import net.corda.core.contracts.Contract
|
||||||
import net.corda.core.contracts.requireSingleCommand
|
|
||||||
import net.corda.core.contracts.requireThat
|
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
|
|
||||||
|
// DOCSTART 01
|
||||||
|
// Add these imports:
|
||||||
|
import net.corda.core.contracts.*
|
||||||
|
|
||||||
|
// Replace IOUContract's contract ID and definition with:
|
||||||
|
val IOU_CONTRACT_ID = "com.template.IOUContract"
|
||||||
|
|
||||||
class IOUContract : Contract {
|
class IOUContract : Contract {
|
||||||
// Our Create command.
|
// Our Create command.
|
||||||
class Create : CommandData
|
class Create : CommandData
|
||||||
@ -20,7 +24,7 @@ class IOUContract : Contract {
|
|||||||
"There should be one output state of type IOUState." using (tx.outputs.size == 1)
|
"There should be one output state of type IOUState." using (tx.outputs.size == 1)
|
||||||
|
|
||||||
// IOU-specific constraints.
|
// IOU-specific constraints.
|
||||||
val out = tx.outputsOfType<net.corda.docs.tutorial.helloworld.IOUState>().single()
|
val out = tx.outputsOfType<IOUState>().single()
|
||||||
"The IOU's value must be non-negative." using (out.value > 0)
|
"The IOU's value must be non-negative." using (out.value > 0)
|
||||||
"The lender and the borrower cannot be the same entity." using (out.lender != out.borrower)
|
"The lender and the borrower cannot be the same entity." using (out.lender != out.borrower)
|
||||||
|
|
||||||
|
@ -6,9 +6,17 @@ import net.corda.core.contracts.Command
|
|||||||
import net.corda.core.contracts.StateAndContract
|
import net.corda.core.contracts.StateAndContract
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
|
import net.corda.core.serialization.SerializationWhitelist
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import kotlin.reflect.jvm.jvmName
|
import net.corda.webserver.services.WebServerPluginRegistry
|
||||||
|
import java.util.function.Function
|
||||||
|
import javax.ws.rs.GET
|
||||||
|
import javax.ws.rs.Path
|
||||||
|
import javax.ws.rs.Produces
|
||||||
|
import javax.ws.rs.core.MediaType
|
||||||
|
import javax.ws.rs.core.Response
|
||||||
// DOCEND 01
|
// DOCEND 01
|
||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
@ -25,14 +33,13 @@ class IOUFlow(val iouValue: Int,
|
|||||||
// We retrieve the notary identity from the network map.
|
// We retrieve the notary identity from the network map.
|
||||||
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
||||||
|
|
||||||
// We create a transaction builder
|
// DOCSTART 02
|
||||||
|
// We create a transaction builder.
|
||||||
val txBuilder = TransactionBuilder(notary = notary)
|
val txBuilder = TransactionBuilder(notary = notary)
|
||||||
|
|
||||||
// DOCSTART 02
|
|
||||||
// We create the transaction components.
|
// We create the transaction components.
|
||||||
val outputState = IOUState(iouValue, ourIdentity, otherParty)
|
val outputState = IOUState(iouValue, ourIdentity, otherParty)
|
||||||
val outputContract = IOUContract::class.jvmName
|
val outputContractAndState = StateAndContract(outputState, IOU_CONTRACT_ID)
|
||||||
val outputContractAndState = StateAndContract(outputState, outputContract)
|
|
||||||
val cmd = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey))
|
val cmd = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey))
|
||||||
|
|
||||||
// We add the items to the builder.
|
// We add the items to the builder.
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
package net.corda.docs.tutorial.twoparty
|
package net.corda.docs.tutorial.twoparty
|
||||||
|
|
||||||
// DOCSTART 01
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.requireThat
|
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.flows.FlowSession
|
import net.corda.core.flows.FlowSession
|
||||||
import net.corda.core.flows.InitiatedBy
|
import net.corda.core.flows.InitiatedBy
|
||||||
import net.corda.core.flows.SignTransactionFlow
|
import net.corda.core.flows.SignTransactionFlow
|
||||||
import net.corda.core.transactions.SignedTransaction
|
|
||||||
import net.corda.docs.tutorial.helloworld.IOUFlow
|
import net.corda.docs.tutorial.helloworld.IOUFlow
|
||||||
import net.corda.docs.tutorial.helloworld.IOUState
|
import net.corda.docs.tutorial.helloworld.IOUState
|
||||||
|
|
||||||
|
// DOCSTART 01
|
||||||
|
// Add these imports:
|
||||||
|
import net.corda.core.contracts.requireThat
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
|
||||||
|
// Define IOUFlowResponder:
|
||||||
@InitiatedBy(IOUFlow::class)
|
@InitiatedBy(IOUFlow::class)
|
||||||
class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
Writing the flow
|
Writing the flow
|
||||||
================
|
================
|
||||||
A flow encodes a sequence of steps that a node can run to achieve a specific ledger update. By installing new flows on
|
A flow encodes a sequence of steps that a node can perform to achieve a specific ledger update. By installing new flows
|
||||||
a node, we allow the node to handle new business processes. The flow we define will allow a node to issue an
|
on a node, we allow the node to handle new business processes. The flow we define will allow a node to issue an
|
||||||
``IOUState`` onto the ledger.
|
``IOUState`` onto the ledger.
|
||||||
|
|
||||||
Flow outline
|
Flow outline
|
||||||
@ -40,8 +40,8 @@ FlowLogic
|
|||||||
---------
|
---------
|
||||||
All flows must subclass ``FlowLogic``. You then define the steps taken by the flow by overriding ``FlowLogic.call``.
|
All flows must subclass ``FlowLogic``. You then define the steps taken by the flow by overriding ``FlowLogic.call``.
|
||||||
|
|
||||||
Let's define our ``IOUFlow`` in either ``TemplateFlow.java`` or ``App.kt``. Delete both the existing flows in the
|
Let's define our ``IOUFlow`` in either ``TemplateFlow.java`` or ``App.kt``. Delete the two existing flows in the
|
||||||
template, and replace them with the following:
|
template (``Initiator`` and ``Responder``), and replace them with the following:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
|
@ -107,9 +107,15 @@ commands.
|
|||||||
|
|
||||||
We want to create an IOU of 100 with PartyB. We start the ``IOUFlow`` by typing:
|
We want to create an IOU of 100 with PartyB. We start the ``IOUFlow`` by typing:
|
||||||
|
|
||||||
.. code:: bash
|
.. container:: codeset
|
||||||
|
|
||||||
start IOUFlow iouValue: 99, otherParty: "O=PartyB,L=New York,C=US"
|
.. code-block:: java
|
||||||
|
|
||||||
|
start IOUFlow arg0: 99, arg1: "O=PartyB,L=New York,C=US"
|
||||||
|
|
||||||
|
.. code-block:: kotlin
|
||||||
|
|
||||||
|
start IOUFlow iouValue: 99, otherParty: "O=PartyB,L=New York,C=US"
|
||||||
|
|
||||||
This single command will cause PartyA and PartyB to automatically agree an IOU. This is one of the great advantages of
|
This single command will cause PartyA and PartyB to automatically agree an IOU. This is one of the great advantages of
|
||||||
the flow framework - it allows you to reduce complex negotiation and update processes into a single function call.
|
the flow framework - it allows you to reduce complex negotiation and update processes into a single function call.
|
||||||
@ -118,13 +124,7 @@ If the flow worked, it should have recorded a new IOU in the vaults of both Part
|
|||||||
|
|
||||||
We can check the contents of each node's vault by running:
|
We can check the contents of each node's vault by running:
|
||||||
|
|
||||||
.. container:: codeset
|
.. code-block:: base
|
||||||
|
|
||||||
.. code-block:: java
|
|
||||||
|
|
||||||
run vaultQuery contractStateType: com.template.state.IOUState
|
|
||||||
|
|
||||||
.. code-block:: kotlin
|
|
||||||
|
|
||||||
run vaultQuery contractStateType: com.template.IOUState
|
run vaultQuery contractStateType: com.template.IOUState
|
||||||
|
|
||||||
@ -174,6 +174,11 @@ parts:
|
|||||||
* The ``IOUState``, representing IOUs on the ledger
|
* The ``IOUState``, representing IOUs on the ledger
|
||||||
* The ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger
|
* The ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger
|
||||||
|
|
||||||
|
After completing this tutorial, your CorDapp should look like this:
|
||||||
|
|
||||||
|
* Java: https://github.com/corda/corda-tut1-solution-java
|
||||||
|
* Kotlin: https://github.com/corda/corda-tut1-solution-kotlin
|
||||||
|
|
||||||
Next steps
|
Next steps
|
||||||
----------
|
----------
|
||||||
There are a number of improvements we could make to this CorDapp:
|
There are a number of improvements we could make to this CorDapp:
|
||||||
@ -183,4 +188,4 @@ There are a number of improvements we could make to this CorDapp:
|
|||||||
* We could add an API, to make it easier to interact with the CorDapp
|
* We could add an API, to make it easier to interact with the CorDapp
|
||||||
|
|
||||||
But for now, the biggest priority is to add an ``IOUContract`` imposing constraints on the evolution of each
|
But for now, the biggest priority is to add an ``IOUContract`` imposing constraints on the evolution of each
|
||||||
``IOUState`` over time. This will be the focus of our next tutorial.
|
``IOUState`` over time. This will be the focus of our next tutorial.
|
@ -24,7 +24,6 @@ interface is defined as follows:
|
|||||||
val participants: List<AbstractParty>
|
val participants: List<AbstractParty>
|
||||||
}
|
}
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
The first thing you'll probably notice about this interface declaration is that its not written in Java or another
|
The first thing you'll probably notice about this interface declaration is that its not written in Java or another
|
||||||
common language. The core Corda platform, including the interface declaration above, is entirely written in Kotlin.
|
common language. The core Corda platform, including the interface declaration above, is entirely written in Kotlin.
|
||||||
|
|
||||||
@ -70,7 +69,7 @@ later is often as simple as adding an additional property to your class definiti
|
|||||||
|
|
||||||
Defining IOUState
|
Defining IOUState
|
||||||
-----------------
|
-----------------
|
||||||
Let's get started by opening ``TemplateState.java`` (for Java) or ``App.kt`` (for Kotlin) and updating
|
Let's get started by opening ``TemplateState.java`` (for Java) or ``StatesAndContracts.kt`` (for Kotlin) and updating
|
||||||
``TemplateState`` to define an ``IOUState``:
|
``TemplateState`` to define an ``IOUState``:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
@ -41,34 +41,33 @@ https://docs.corda.net/tutorial-cordapp.html#opening-the-example-cordapp-in-inte
|
|||||||
|
|
||||||
Template structure
|
Template structure
|
||||||
------------------
|
------------------
|
||||||
The template has a number of files, but we can ignore most of them. To implement our IOU CorDapp in Java, we'll only
|
The template has a number of files, but we can ignore most of them. We will only be modifying the following files:
|
||||||
need to modify two files. For Kotlin, we'll simply be modifying the ``App.kt`` file:
|
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. code-block:: java
|
.. code-block:: java
|
||||||
|
|
||||||
// 1. The state
|
// 1. The state
|
||||||
src/main/java/com/template/TemplateState.java
|
cordapp-contracts-states/src/main/java/com/template/TemplateState.java
|
||||||
|
|
||||||
// 2. The flow
|
// 2. The flow
|
||||||
src/main/java/com/template/TemplateFlow.java
|
cordapp/src/main/java/com/template/TemplateFlow.java
|
||||||
|
|
||||||
.. code-block:: kotlin
|
.. code-block:: kotlin
|
||||||
|
|
||||||
src/main/kotlin/com/template/App.kt
|
// 1. The state
|
||||||
|
cordapp-contracts-states/src/main/kotlin/com/template/StatesAndContracts.kt
|
||||||
|
|
||||||
|
// 2. The flow
|
||||||
|
cordapp/src/main/kotlin/com/template/App.kt
|
||||||
|
|
||||||
Clean up
|
Clean up
|
||||||
--------
|
--------
|
||||||
To prevent build errors later on, we should delete the following files before we begin:
|
To prevent build errors later on, we should delete the following files before we begin:
|
||||||
|
|
||||||
* Java:
|
* Java: ``cordapp/src/main/java/com/template/TemplateClient.java``
|
||||||
* ``src/main/java/com/template/TemplateClient.java``
|
|
||||||
* ``src/test/java/com/template/FlowTests.java``
|
|
||||||
|
|
||||||
* Kotlin:
|
* Kotlin: ``cordapp/src/main/kotlin/com/template/TemplateClient.kt``
|
||||||
* ``src/main/kotlin/com/template/TemplateClient.kt``
|
|
||||||
* ``src/test/kotlin/com/template/FlowTests.kt``
|
|
||||||
|
|
||||||
Progress so far
|
Progress so far
|
||||||
---------------
|
---------------
|
||||||
|
@ -2,7 +2,7 @@ Network Map
|
|||||||
===========
|
===========
|
||||||
|
|
||||||
The network map stores a collection of ``NodeInfo`` objects, each representing another node with which the node can interact.
|
The network map stores a collection of ``NodeInfo`` objects, each representing another node with which the node can interact.
|
||||||
There two sources from which a Corda node can retrieve ``NodeInfo`` objects:
|
There are two sources from which a Corda node can retrieve ``NodeInfo`` objects:
|
||||||
|
|
||||||
1. the REST protocol with the network map service, which also provides a publishing API,
|
1. the REST protocol with the network map service, which also provides a publishing API,
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ Protocol Design
|
|||||||
---------------
|
---------------
|
||||||
The node info publishing protocol:
|
The node info publishing protocol:
|
||||||
|
|
||||||
* Create a ``NodeInfo`` object, and sign it to create a ``SignedData<NodeInfo>`` object. TODO: We will need list of signatures in ``SignedData`` to support multiple node identities in the future.
|
* Create a ``NodeInfo`` object, and sign it to create a ``SignedNodeInfo`` object.
|
||||||
|
|
||||||
* Serialise the signed data and POST the data to the network map server.
|
* Serialise the signed data and POST the data to the network map server.
|
||||||
|
|
||||||
@ -25,7 +25,18 @@ Node side network map update protocol:
|
|||||||
|
|
||||||
* The Corda node will query the network map service periodically according to the ``Expires`` attribute in the HTTP header.
|
* The Corda node will query the network map service periodically according to the ``Expires`` attribute in the HTTP header.
|
||||||
|
|
||||||
* The network map service returns a signed ``NetworkMap`` object, containing list of node info hashes and the network parameters hashes.
|
* The network map service returns a signed ``NetworkMap`` object which looks as follows:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
|
data class NetworkMap {
|
||||||
|
val nodeInfoHashes: List<SecureHash>,
|
||||||
|
val networkParametersHash: SecureHash
|
||||||
|
}
|
||||||
|
|
||||||
|
The object contains list of node info hashes and hash of the network parameters data structure (without the signatures).
|
||||||
|
|
||||||
* The node updates its local copy of ``NodeInfos`` if it is different from the newly downloaded ``NetworkMap``.
|
* The node updates its local copy of ``NodeInfos`` if it is different from the newly downloaded ``NetworkMap``.
|
||||||
|
|
||||||
@ -34,13 +45,13 @@ Network Map service REST API:
|
|||||||
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
|
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||||
| Request method | Path | Description |
|
| Request method | Path | Description |
|
||||||
+================+===================================+========================================================================================================================================================+
|
+================+===================================+========================================================================================================================================================+
|
||||||
| POST | /api/network-map/publish | Publish new ``NodeInfo`` to the network map service, the legal identity in ``NodeInfo`` must match with the identity registered with the doorman. |
|
| POST | /network-map/publish | Publish new ``NodeInfo`` to the network map service, the legal identity in ``NodeInfo`` must match with the identity registered with the doorman. |
|
||||||
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
|
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||||
| GET | /api/network-map | Retrieve ``NetworkMap`` from the server, the ``NetworkMap`` object contains list of node info hashes and NetworkParameters hash. |
|
| GET | /network-map | Retrieve ``NetworkMap`` from the server, the ``NetworkMap`` object contains list of node info hashes and ``NetworkParameters`` hash. |
|
||||||
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
|
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||||
| GET | /api/network-map/node-info/{hash} | Retrieve ``NodeInfo`` object with the same hash. |
|
| GET | /network-map/node-info/{hash} | Retrieve ``NodeInfo`` object with the same hash. |
|
||||||
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
|
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||||
| GET | /api/network-map/parameters/{hash}| Retrieve ``NetworkParameters`` object with the same hash. |
|
| GET | /network-map/parameters/{hash} | Retrieve ``NetworkParameters`` object with the same hash. |
|
||||||
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
|
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||||
|
|
||||||
TODO: Access control of the network map will be added in the future.
|
TODO: Access control of the network map will be added in the future.
|
||||||
@ -50,8 +61,24 @@ The ``additional-node-infos`` directory
|
|||||||
---------------------------------------
|
---------------------------------------
|
||||||
Each Corda node reads, and continuously polls, the files contained in a directory named ``additional-node-infos`` inside the node base directory.
|
Each Corda node reads, and continuously polls, the files contained in a directory named ``additional-node-infos`` inside the node base directory.
|
||||||
|
|
||||||
Nodes expect to find a serialized ``SignedData<NodeInfo>`` object, the same object which is sent to network map server.
|
Nodes expect to find a serialized ``SignedNodeInfo`` object, the same object which is sent to network map server.
|
||||||
|
|
||||||
Whenever a node starts it writes on disk a file containing its own ``NodeInfo``, this file is called ``nodeInfo-XXX`` where ``XXX`` is a long string.
|
Whenever a node starts it writes on disk a file containing its own ``NodeInfo``, this file is called ``nodeInfo-XXX`` where ``XXX`` is a long string.
|
||||||
|
|
||||||
Hence if an operator wants node A to see node B they can pick B's ``NodeInfo`` file from B base directory and drop it into A's ``additional-node-infos`` directory.
|
Hence if an operator wants node A to see node B they can pick B's ``NodeInfo`` file from B base directory and drop it into A's ``additional-node-infos`` directory.
|
||||||
|
|
||||||
|
|
||||||
|
Network parameters
|
||||||
|
------------------
|
||||||
|
Network parameters are constants that every node participating in the network needs to agree on and use for interop purposes.
|
||||||
|
The structure is distributed as a file containing serialized ``SignedData<NetworkParameters>`` with a signature from
|
||||||
|
a sub-key of the compatibility zone root cert. Network map advertises the hash of currently used network parameters.
|
||||||
|
The ``NetworkParameters`` structure contains:
|
||||||
|
* ``minimumPlatformVersion`` - minimum version of Corda platform that is required for nodes in the network.
|
||||||
|
* ``notaries`` - list of well known and trusted notary identities with information on validation type.
|
||||||
|
* ``maxMessageSize`` - maximum P2P message size sent over the wire in bytes.
|
||||||
|
* ``maxTransactionSize`` - maximum permitted transaction size in bytes.
|
||||||
|
* ``modifiedTime`` - the time the network parameters were created by the CZ operator.
|
||||||
|
* ``epoch`` - version number of the network parameters. Starting from 1, this will always increment on each new set of parameters.
|
||||||
|
|
||||||
|
The set of parameters is still under development and we may find the need to add additional fields.
|
||||||
|
@ -1,136 +0,0 @@
|
|||||||
Access security settings
|
|
||||||
========================
|
|
||||||
|
|
||||||
Access to node functionalities via SSH or RPC is protected by an authentication and authorisation policy.
|
|
||||||
|
|
||||||
The field ``security`` in ``node.conf`` exposes various sub-fields related to authentication/authorisation specifying:
|
|
||||||
|
|
||||||
* The data source providing credentials and permissions for users (e.g.: a remote RDBMS)
|
|
||||||
* An optional password encryption method.
|
|
||||||
* An optional caching of users data from Node side.
|
|
||||||
|
|
||||||
.. warning:: Specifying both ``rpcUsers`` and ``security`` fields in ``node.conf`` is considered an illegal setting and
|
|
||||||
rejected by the node at startup since ``rpcUsers`` is effectively deprecated in favour of ``security.authService``.
|
|
||||||
|
|
||||||
**Example 1:** connect to remote RDBMS for credentials/permissions, with encrypted user passwords and
|
|
||||||
caching on node-side:
|
|
||||||
|
|
||||||
.. container:: codeset
|
|
||||||
|
|
||||||
.. sourcecode:: groovy
|
|
||||||
|
|
||||||
security = {
|
|
||||||
authService = {
|
|
||||||
dataSource = {
|
|
||||||
type = "DB",
|
|
||||||
passwordEncryption = "SHIRO_1_CRYPT",
|
|
||||||
connection = {
|
|
||||||
jdbcUrl = "<jdbc connection string>"
|
|
||||||
username = "<db username>"
|
|
||||||
password = "<db user password>"
|
|
||||||
driverClassName = "<JDBC driver>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
options = {
|
|
||||||
cache = {
|
|
||||||
expiryTimeSecs = 120
|
|
||||||
capacity = 10000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
**Example 2:** list of user credentials and permissions hard-coded in ``node.conf``
|
|
||||||
|
|
||||||
.. container:: codeset
|
|
||||||
|
|
||||||
.. sourcecode:: groovy
|
|
||||||
|
|
||||||
security = {
|
|
||||||
authService = {
|
|
||||||
dataSource = {
|
|
||||||
type = "INMEMORY",
|
|
||||||
users =[
|
|
||||||
{
|
|
||||||
username = "user1"
|
|
||||||
password = "password"
|
|
||||||
permissions = [
|
|
||||||
"StartFlow.net.corda.flows.ExampleFlow1",
|
|
||||||
"StartFlow.net.corda.flows.ExampleFlow2",
|
|
||||||
...
|
|
||||||
]
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Let us look in more details at the structure of ``security.authService``:
|
|
||||||
|
|
||||||
Authentication/authorisation data
|
|
||||||
---------------------------------
|
|
||||||
|
|
||||||
The ``dataSource`` field defines the data provider supplying credentials and permissions for users. The ``type``
|
|
||||||
subfield identify the type of data provider, currently supported one are:
|
|
||||||
|
|
||||||
* **INMEMORY:** a list of user credentials and permissions hard-coded in configuration in the ``users`` field
|
|
||||||
(see example 2 above)
|
|
||||||
|
|
||||||
* **DB:** An external RDBMS accessed via the JDBC connection described by ``connection``. The current implementation
|
|
||||||
expect the database to store data according to the following schema:
|
|
||||||
|
|
||||||
- Table ``users`` containing columns ``username`` and ``password``.
|
|
||||||
The ``username`` column *must have unique values*.
|
|
||||||
- Table ``user_roles`` containing columns ``username`` and ``role_name`` associating a user to a set of *roles*
|
|
||||||
- Table ``roles_permissions`` containing columns ``role_name`` and ``permission`` associating a role to a set of
|
|
||||||
permission strings
|
|
||||||
|
|
||||||
Note in particular how in the DB case permissions are assigned to _roles_ rather than individual users.
|
|
||||||
Also, there is no prescription on the SQL type of the columns (although in our tests we defined ``username`` and
|
|
||||||
``role_name`` of SQL type ``VARCHAR`` and ``password`` of ``TEXT`` type) and it is allowed to put additional columns
|
|
||||||
besides the one expected by the implementation.
|
|
||||||
|
|
||||||
Password encryption
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
Storing passwords in plain text is discouraged in production systems aiming for high security requirements. We support
|
|
||||||
reading passwords stored using the Apache Shiro fully reversible Modular Crypt Format, specified in the documentation
|
|
||||||
of ``org.apache.shiro.crypto.hash.format.Shiro1CryptFormat``.
|
|
||||||
|
|
||||||
Password are assumed in plain format by default. To specify an encryption it is necessary to use the field:
|
|
||||||
|
|
||||||
.. container:: codeset
|
|
||||||
|
|
||||||
.. sourcecode:: groovy
|
|
||||||
|
|
||||||
passwordEncryption = SHIRO_1_CRYPT
|
|
||||||
|
|
||||||
Hash encrypted password based on the Shiro1CryptFormat can be produced with the `Apache Shiro Hasher tool <https://shiro.apache.org/command-line-hasher.html>`_
|
|
||||||
|
|
||||||
Cache
|
|
||||||
-----
|
|
||||||
|
|
||||||
Adding a cache layer on top of an external provider of users credentials and permissions can significantly benefit
|
|
||||||
performances in some cases, with the disadvantage of introducing a latency in the propagation of changes to the data.
|
|
||||||
|
|
||||||
Caching of users data is disabled by default, it can be enabled by defining the ``options.cache`` field, like seen in
|
|
||||||
the examples above:
|
|
||||||
|
|
||||||
.. container:: codeset
|
|
||||||
|
|
||||||
.. sourcecode:: groovy
|
|
||||||
|
|
||||||
options = {
|
|
||||||
cache = {
|
|
||||||
expiryTimeSecs = 120
|
|
||||||
capacity = 10000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
This will enable an in-memory cache with maximum capacity (number of entries) and maximum life time of entries given by
|
|
||||||
respectively the values set by the ``capacity`` and ``expiryTimeSecs`` fields.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -32,7 +32,13 @@ A Corda network has three types of certificate authorities (CAs):
|
|||||||
* The **node CAs**
|
* The **node CAs**
|
||||||
|
|
||||||
* Each node serves as its own CA in issuing the child certificates that it uses to sign its identity
|
* Each node serves as its own CA in issuing the child certificates that it uses to sign its identity
|
||||||
keys, anonymous keys and TLS certificates
|
keys and TLS certificates
|
||||||
|
|
||||||
|
We can visualise the permissioning structure as follows:
|
||||||
|
|
||||||
|
.. image:: resources/certificate_structure.png
|
||||||
|
:scale: 55%
|
||||||
|
:align: center
|
||||||
|
|
||||||
Keypair and certificate formats
|
Keypair and certificate formats
|
||||||
-------------------------------
|
-------------------------------
|
||||||
@ -45,6 +51,13 @@ public/private keypairs and certificates. The keypairs and certificates should o
|
|||||||
|
|
||||||
* The TLS certificates must follow the `TLS v1.2 standard <https://tools.ietf.org/html/rfc5246>`_
|
* The TLS certificates must follow the `TLS v1.2 standard <https://tools.ietf.org/html/rfc5246>`_
|
||||||
|
|
||||||
|
* The root network CA, intermediate network CA and node CA keys, as well as the node TLS
|
||||||
|
keys, must follow one of the following schemes:
|
||||||
|
|
||||||
|
* ECDSA using the NIST P-256 curve (secp256r1)
|
||||||
|
|
||||||
|
* RSA with 3072-bit key size
|
||||||
|
|
||||||
Creating the root and intermediate network CAs
|
Creating the root and intermediate network CAs
|
||||||
----------------------------------------------
|
----------------------------------------------
|
||||||
|
|
||||||
|
@ -6,6 +6,5 @@ Release process
|
|||||||
|
|
||||||
release-notes
|
release-notes
|
||||||
changelog
|
changelog
|
||||||
upgrade-notes
|
|
||||||
codestyle
|
codestyle
|
||||||
testing
|
testing
|
BIN
docs/source/resources/certificate_structure.png
Normal file
BIN
docs/source/resources/certificate_structure.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 108 KiB |
@ -6,11 +6,9 @@ Creating a Corda network
|
|||||||
A Corda network consists of a number of machines running nodes. These nodes communicate using persistent protocols in
|
A Corda network consists of a number of machines running nodes. These nodes communicate using persistent protocols in
|
||||||
order to create and validate transactions.
|
order to create and validate transactions.
|
||||||
|
|
||||||
There are four broader categories of functionality one such node may have. These pieces of functionality are provided
|
There are three broader categories of functionality one such node may have. These pieces of functionality are provided
|
||||||
as services, and one node may run several of them.
|
as services, and one node may run several of them.
|
||||||
|
|
||||||
* Network map: The node running the network map provides a way to resolve identities to physical node addresses and
|
|
||||||
associated public keys
|
|
||||||
* Notary: Nodes running a notary service witness state spends and have the final say in whether a transaction is a
|
* Notary: Nodes running a notary service witness state spends and have the final say in whether a transaction is a
|
||||||
double-spend or not
|
double-spend or not
|
||||||
* Oracle: Network services that link the ledger to the outside world by providing facts that affect the validity of
|
* Oracle: Network services that link the ledger to the outside world by providing facts that affect the validity of
|
||||||
@ -46,12 +44,38 @@ The most important fields regarding network configuration are:
|
|||||||
* ``rpcAddress``: The address to which Artemis will bind for RPC calls.
|
* ``rpcAddress``: The address to which Artemis will bind for RPC calls.
|
||||||
* ``webAddress``: The address the webserver should bind. Note that the port must be distinct from that of ``p2pAddress`` and ``rpcAddress`` if they are on the same machine.
|
* ``webAddress``: The address the webserver should bind. Note that the port must be distinct from that of ``p2pAddress`` and ``rpcAddress`` if they are on the same machine.
|
||||||
|
|
||||||
|
Bootstrapping the network
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The nodes see each other using the network map. This is a collection of statically signed node-info files, one for each
|
||||||
|
node in the network. Most production deployments will use a highly available, secure distribution of the network map via HTTP.
|
||||||
|
|
||||||
|
For test deployments where the nodes (at least initially) reside on the same filesystem, these node-info files can be
|
||||||
|
placed directly in the node's ``additional-node-infos`` directory from where the node will pick them up and store them
|
||||||
|
in its local network map cache. The node generates its own node-info file on startup.
|
||||||
|
|
||||||
|
In addition to the network map, all the nodes on a network must use the same set of network parameters. These are a set
|
||||||
|
of constants which guarantee interoperability between nodes. The HTTP network map distributes the network parameters
|
||||||
|
which the node downloads automatically. In the absence of this the network parameters must be generated locally. This can
|
||||||
|
be done with the network bootstrapper. This a tool that scans all the node configurations from a common directory to
|
||||||
|
generate the network parameters file which is copied to the nodes' directories. It also copies each node's node-info file
|
||||||
|
to every other node.
|
||||||
|
|
||||||
|
The bootstrapper tool can be built with the command:
|
||||||
|
|
||||||
|
``gradlew buildBootstrapperJar``
|
||||||
|
|
||||||
|
The resulting jar can be found in ``tools/bootstrapper/build/libs/``.
|
||||||
|
|
||||||
|
To use it, run the following command, specifying the root directory which hosts all the node directories as the argument:
|
||||||
|
|
||||||
|
``java -jar network-bootstrapper.jar <nodes-root-dir>``
|
||||||
|
|
||||||
Starting the nodes
|
Starting the nodes
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
You may now start the nodes in any order. Note that the node is not fully started until it has successfully registered with the network map!
|
You may now start the nodes in any order. You should see a banner, some log lines and eventually ``Node started up and registered``,
|
||||||
|
indicating that the node is fully started.
|
||||||
You should see a banner, some log lines and eventually ``Node started up and registered``, indicating that the node is fully started.
|
|
||||||
|
|
||||||
.. TODO: Add a better way of polling for startup. A programmatic way of determining whether a node is up is to check whether it's ``webAddress`` is bound.
|
.. TODO: Add a better way of polling for startup. A programmatic way of determining whether a node is up is to check whether it's ``webAddress`` is bound.
|
||||||
|
|
||||||
@ -66,7 +90,6 @@ details/diagnosing problems check the logs.
|
|||||||
Logging is standard log4j2_ and may be configured accordingly. Logs
|
Logging is standard log4j2_ and may be configured accordingly. Logs
|
||||||
are by default redirected to files in ``NODE_DIRECTORY/logs/``.
|
are by default redirected to files in ``NODE_DIRECTORY/logs/``.
|
||||||
|
|
||||||
|
|
||||||
Connecting to the nodes
|
Connecting to the nodes
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ We can picture this transaction as follows:
|
|||||||
Defining IOUContract
|
Defining IOUContract
|
||||||
--------------------
|
--------------------
|
||||||
Let's write a contract that enforces these constraints. We'll do this by modifying either ``TemplateContract.java`` or
|
Let's write a contract that enforces these constraints. We'll do this by modifying either ``TemplateContract.java`` or
|
||||||
``App.kt`` and updating ``TemplateContract`` to define an ``IOUContract``:
|
``StatesAndContracts.kt`` and updating ``TemplateContract`` to define an ``IOUContract``:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ We'll do this by modifying the flow we wrote in the previous tutorial.
|
|||||||
|
|
||||||
Verifying the transaction
|
Verifying the transaction
|
||||||
-------------------------
|
-------------------------
|
||||||
In ``IOUFlow.java``/``IOUFlow.kt``, change the imports block to the following:
|
In ``IOUFlow.java``/``App.kt``, change the imports block to the following:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
@ -31,7 +31,8 @@ In ``IOUFlow.java``/``IOUFlow.kt``, change the imports block to the following:
|
|||||||
:start-after: DOCSTART 01
|
:start-after: DOCSTART 01
|
||||||
:end-before: DOCEND 01
|
:end-before: DOCEND 01
|
||||||
|
|
||||||
And update ``IOUFlow.call`` by changing the code following the creation of the ``TransactionBuilder`` as follows:
|
And update ``IOUFlow.call`` by changing the code following the retrieval of the notary's identity from the network as
|
||||||
|
follows:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
@ -138,6 +139,11 @@ Our CorDapp now imposes restrictions on the issuance of IOUs. Most importantly,
|
|||||||
from both the lender and the borrower before an IOU can be created on the ledger. This prevents either the lender or
|
from both the lender and the borrower before an IOU can be created on the ledger. This prevents either the lender or
|
||||||
the borrower from unilaterally updating the ledger in a way that only benefits themselves.
|
the borrower from unilaterally updating the ledger in a way that only benefits themselves.
|
||||||
|
|
||||||
|
After completing this tutorial, your CorDapp should look like this:
|
||||||
|
|
||||||
|
* Java: https://github.com/corda/corda-tut2-solution-java
|
||||||
|
* Kotlin: https://github.com/corda/corda-tut2-solution-kotlin
|
||||||
|
|
||||||
You should now be ready to develop your own CorDapps. You can also find a list of sample CorDapps
|
You should now be ready to develop your own CorDapps. You can also find a list of sample CorDapps
|
||||||
`here <https://www.corda.net/samples/>`_. As you write CorDapps, you'll also want to learn more about the
|
`here <https://www.corda.net/samples/>`_. As you write CorDapps, you'll also want to learn more about the
|
||||||
:doc:`Corda API <corda-api>`.
|
:doc:`Corda API <corda-api>`.
|
||||||
|
@ -1,11 +1,23 @@
|
|||||||
Tutorials
|
Tutorials
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
This section is split into two parts.
|
||||||
|
|
||||||
|
The Hello, World tutorials should be followed in sequence and show how to extend the Java or Kotlin CorDapp Template
|
||||||
|
into a full CorDapp.
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
hello-world-introduction
|
hello-world-introduction
|
||||||
tut-two-party-introduction
|
tut-two-party-introduction
|
||||||
|
|
||||||
|
The remaining tutorials cover individual platform features in isolation. They don't depend on the code from the Hello,
|
||||||
|
World tutorials, and can be read in any order.
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
tutorial-contract
|
tutorial-contract
|
||||||
tutorial-test-dsl
|
tutorial-test-dsl
|
||||||
contract-upgrade
|
contract-upgrade
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
Upgrade notes
|
Upgrading a CorDapp to a new version
|
||||||
=============
|
====================================
|
||||||
|
|
||||||
These notes provide instructions for upgrading your CorDapps from previous versions, starting with the upgrade from our
|
These notes provide instructions for upgrading your CorDapps from previous versions, starting with the upgrade from our
|
||||||
first public Beta (:ref:`Milestone 12 <changelog_m12>`), to :ref:`V1.0 <changelog_v1>`.
|
first public Beta (:ref:`Milestone 12 <changelog_m12>`), to :ref:`V1.0 <changelog_v1>`.
|
||||||
|
@ -32,8 +32,8 @@ Flow versioning
|
|||||||
---------------
|
---------------
|
||||||
|
|
||||||
In addition to the evolution of the platform, flows that run on top of the platform can also evolve. It may be that the
|
In addition to the evolution of the platform, flows that run on top of the platform can also evolve. It may be that the
|
||||||
flow protocol between an initiating flow and it's intiated flow changes from one CorDapp release to the next in such as
|
flow protocol between an initiating flow and its initiated flow changes from one CorDapp release to the next in such a
|
||||||
way to be backwards incompatible with existing flows. For example, if a sequence of sends and receives needs to change
|
way to be backward incompatible with existing flows. For example, if a sequence of sends and receives needs to change
|
||||||
or if the semantics of a particular receive changes.
|
or if the semantics of a particular receive changes.
|
||||||
|
|
||||||
The ``InitiatingFlow`` annotation (see :doc:`flow-state-machine` for more information on the flow annotations) has a ``version``
|
The ``InitiatingFlow`` annotation (see :doc:`flow-state-machine` for more information on the flow annotations) has a ``version``
|
||||||
|
@ -9,6 +9,10 @@ import net.corda.finance.contracts.Frequency
|
|||||||
import net.corda.finance.contracts.Tenor
|
import net.corda.finance.contracts.Tenor
|
||||||
import net.corda.node.services.api.IdentityServiceInternal
|
import net.corda.node.services.api.IdentityServiceInternal
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
|
import net.corda.testing.dsl.EnforceVerifyOrFail
|
||||||
|
import net.corda.testing.dsl.TransactionDSL
|
||||||
|
import net.corda.testing.dsl.TransactionDSLInterpreter
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.transaction
|
import net.corda.testing.node.transaction
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
|
@ -12,51 +12,52 @@ import net.corda.testing.node.MockServices;
|
|||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
import static net.corda.finance.Currencies.DOLLARS;
|
import static net.corda.finance.Currencies.DOLLARS;
|
||||||
import static net.corda.finance.Currencies.issuedBy;
|
import static net.corda.finance.Currencies.issuedBy;
|
||||||
import static net.corda.testing.node.NodeTestUtils.transaction;
|
import static net.corda.testing.node.NodeTestUtils.transaction;
|
||||||
import static net.corda.testing.CoreTestUtils.rigorousMock;
|
import static net.corda.testing.internal.InternalTestUtilsKt.rigorousMock;
|
||||||
import static net.corda.testing.TestConstants.getDUMMY_NOTARY_NAME;
|
import static net.corda.testing.TestConstants.DUMMY_NOTARY_NAME;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is an incomplete Java replica of CashTests.kt to show how to use the Java test DSL
|
* This is an incomplete Java replica of CashTests.kt to show how to use the Java test DSL
|
||||||
*/
|
*/
|
||||||
public class CashTestsJava {
|
public class CashTestsJava {
|
||||||
private static final Party DUMMY_NOTARY = new TestIdentity(getDUMMY_NOTARY_NAME(), 20L).getParty();
|
private static final Party DUMMY_NOTARY = new TestIdentity(DUMMY_NOTARY_NAME, 20L).getParty();
|
||||||
private static final TestIdentity MEGA_CORP = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB"));
|
private static final TestIdentity MEGA_CORP = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB"));
|
||||||
private static final TestIdentity MINI_CORP = new TestIdentity(new CordaX500Name("MiniCorp", "London", "GB"));
|
private static final TestIdentity MINI_CORP = new TestIdentity(new CordaX500Name("MiniCorp", "London", "GB"));
|
||||||
private final PartyAndReference defaultIssuer = MEGA_CORP.ref((byte) 1);
|
private final PartyAndReference defaultIssuer = MEGA_CORP.ref((byte) 1);
|
||||||
private final Cash.State inState = new Cash.State(issuedBy(DOLLARS(1000), defaultIssuer), new AnonymousParty(MEGA_CORP.getPubkey()));
|
private final Cash.State inState = new Cash.State(issuedBy(DOLLARS(1000), defaultIssuer), new AnonymousParty(MEGA_CORP.getPublicKey()));
|
||||||
private final Cash.State outState = new Cash.State(inState.getAmount(), new AnonymousParty(MINI_CORP.getPubkey()));
|
private final Cash.State outState = new Cash.State(inState.getAmount(), new AnonymousParty(MINI_CORP.getPublicKey()));
|
||||||
@Rule
|
@Rule
|
||||||
public final SerializationEnvironmentRule testSerialization = new SerializationEnvironmentRule();
|
public final SerializationEnvironmentRule testSerialization = new SerializationEnvironmentRule();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void trivial() {
|
public void trivial() {
|
||||||
IdentityServiceInternal identityService = rigorousMock(IdentityServiceInternal.class);
|
IdentityServiceInternal identityService = rigorousMock(IdentityServiceInternal.class);
|
||||||
doReturn(MEGA_CORP.getParty()).when(identityService).partyFromKey(MEGA_CORP.getPubkey());
|
doReturn(MEGA_CORP.getParty()).when(identityService).partyFromKey(MEGA_CORP.getPublicKey());
|
||||||
doReturn(MINI_CORP.getParty()).when(identityService).partyFromKey(MINI_CORP.getPubkey());
|
doReturn(MINI_CORP.getParty()).when(identityService).partyFromKey(MINI_CORP.getPublicKey());
|
||||||
transaction(new MockServices(identityService, MEGA_CORP.getName()), DUMMY_NOTARY, tx -> {
|
transaction(new MockServices(emptyList(), identityService, MEGA_CORP.getName()), DUMMY_NOTARY, tx -> {
|
||||||
tx.attachment(Cash.PROGRAM_ID);
|
tx.attachment(Cash.PROGRAM_ID);
|
||||||
|
|
||||||
tx.input(Cash.PROGRAM_ID, inState);
|
tx.input(Cash.PROGRAM_ID, inState);
|
||||||
|
|
||||||
tx.tweak(tw -> {
|
tx.tweak(tw -> {
|
||||||
tw.output(Cash.PROGRAM_ID, new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), new AnonymousParty(MINI_CORP.getPubkey())));
|
tw.output(Cash.PROGRAM_ID, new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), new AnonymousParty(MINI_CORP.getPublicKey())));
|
||||||
tw.command(MEGA_CORP.getPubkey(), new Cash.Commands.Move());
|
tw.command(MEGA_CORP.getPublicKey(), new Cash.Commands.Move());
|
||||||
return tw.failsWith("the amounts balance");
|
return tw.failsWith("the amounts balance");
|
||||||
});
|
});
|
||||||
|
|
||||||
tx.tweak(tw -> {
|
tx.tweak(tw -> {
|
||||||
tw.output(Cash.PROGRAM_ID, outState);
|
tw.output(Cash.PROGRAM_ID, outState);
|
||||||
tw.command(MEGA_CORP.getPubkey(), DummyCommandData.INSTANCE);
|
tw.command(MEGA_CORP.getPublicKey(), DummyCommandData.INSTANCE);
|
||||||
// Invalid command
|
// Invalid command
|
||||||
return tw.failsWith("required net.corda.finance.contracts.asset.Cash.Commands.Move command");
|
return tw.failsWith("required net.corda.finance.contracts.asset.Cash.Commands.Move command");
|
||||||
});
|
});
|
||||||
tx.tweak(tw -> {
|
tx.tweak(tw -> {
|
||||||
tw.output(Cash.PROGRAM_ID, outState);
|
tw.output(Cash.PROGRAM_ID, outState);
|
||||||
tw.command(MINI_CORP.getPubkey(), new Cash.Commands.Move());
|
tw.command(MINI_CORP.getPublicKey(), new Cash.Commands.Move());
|
||||||
return tw.failsWith("the owning keys are a subset of the signing keys");
|
return tw.failsWith("the owning keys are a subset of the signing keys");
|
||||||
});
|
});
|
||||||
tx.tweak(tw -> {
|
tx.tweak(tw -> {
|
||||||
@ -64,14 +65,14 @@ public class CashTestsJava {
|
|||||||
// issuedBy() can't be directly imported because it conflicts with other identically named functions
|
// issuedBy() can't be directly imported because it conflicts with other identically named functions
|
||||||
// with different overloads (for some reason).
|
// with different overloads (for some reason).
|
||||||
tw.output(Cash.PROGRAM_ID, outState.issuedBy(MINI_CORP.getParty()));
|
tw.output(Cash.PROGRAM_ID, outState.issuedBy(MINI_CORP.getParty()));
|
||||||
tw.command(MEGA_CORP.getPubkey(), new Cash.Commands.Move());
|
tw.command(MEGA_CORP.getPublicKey(), new Cash.Commands.Move());
|
||||||
return tw.failsWith("at least one cash input");
|
return tw.failsWith("at least one cash input");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simple reallocation works.
|
// Simple reallocation works.
|
||||||
return tx.tweak(tw -> {
|
return tx.tweak(tw -> {
|
||||||
tw.output(Cash.PROGRAM_ID, outState);
|
tw.output(Cash.PROGRAM_ID, outState);
|
||||||
tw.command(MEGA_CORP.getPubkey(), new Cash.Commands.Move());
|
tw.command(MEGA_CORP.getPublicKey(), new Cash.Commands.Move());
|
||||||
return tw.verifies();
|
return tw.verifies();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -18,7 +18,11 @@ import net.corda.finance.`issued by`
|
|||||||
import net.corda.finance.contracts.asset.*
|
import net.corda.finance.contracts.asset.*
|
||||||
import net.corda.node.services.api.IdentityServiceInternal
|
import net.corda.node.services.api.IdentityServiceInternal
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import net.corda.testing.contracts.VaultFiller
|
import net.corda.testing.dsl.EnforceVerifyOrFail
|
||||||
|
import net.corda.testing.dsl.TransactionDSL
|
||||||
|
import net.corda.testing.dsl.TransactionDSLInterpreter
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
|
import net.corda.testing.internal.vault.VaultFiller
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
|
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
|
||||||
import net.corda.testing.node.ledger
|
import net.corda.testing.node.ledger
|
||||||
@ -46,7 +50,7 @@ interface ICommercialPaperTestTemplate {
|
|||||||
private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
||||||
private val MEGA_CORP get() = megaCorp.party
|
private val MEGA_CORP get() = megaCorp.party
|
||||||
private val MEGA_CORP_IDENTITY get() = megaCorp.identity
|
private val MEGA_CORP_IDENTITY get() = megaCorp.identity
|
||||||
private val MEGA_CORP_PUBKEY get() = megaCorp.pubkey
|
private val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
|
||||||
|
|
||||||
class JavaCommercialPaperTest : ICommercialPaperTestTemplate {
|
class JavaCommercialPaperTest : ICommercialPaperTestTemplate {
|
||||||
override fun getPaper(): ICommercialPaperState = JavaCommercialPaper.State(
|
override fun getPaper(): ICommercialPaperState = JavaCommercialPaper.State(
|
||||||
@ -105,13 +109,13 @@ class CommercialPaperTestsGeneric {
|
|||||||
private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
|
private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
|
||||||
private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
||||||
private val ALICE get() = alice.party
|
private val ALICE get() = alice.party
|
||||||
private val ALICE_KEY get() = alice.key
|
private val ALICE_KEY get() = alice.keyPair
|
||||||
private val ALICE_PUBKEY get() = alice.pubkey
|
private val ALICE_PUBKEY get() = alice.publicKey
|
||||||
private val DUMMY_NOTARY get() = dummyNotary.party
|
private val DUMMY_NOTARY get() = dummyNotary.party
|
||||||
private val DUMMY_NOTARY_IDENTITY get() = dummyNotary.identity
|
private val DUMMY_NOTARY_IDENTITY get() = dummyNotary.identity
|
||||||
private val MINI_CORP get() = miniCorp.party
|
private val MINI_CORP get() = miniCorp.party
|
||||||
private val MINI_CORP_IDENTITY get() = miniCorp.identity
|
private val MINI_CORP_IDENTITY get() = miniCorp.identity
|
||||||
private val MINI_CORP_PUBKEY get() = miniCorp.pubkey
|
private val MINI_CORP_PUBKEY get() = miniCorp.publicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parameterized.Parameter
|
@Parameterized.Parameter
|
||||||
@ -120,7 +124,7 @@ class CommercialPaperTestsGeneric {
|
|||||||
@JvmField
|
@JvmField
|
||||||
val testSerialization = SerializationEnvironmentRule()
|
val testSerialization = SerializationEnvironmentRule()
|
||||||
val issuer = MEGA_CORP.ref(123)
|
val issuer = MEGA_CORP.ref(123)
|
||||||
private val ledgerServices = MockServices(rigorousMock<IdentityServiceInternal>().also {
|
private val ledgerServices = MockServices(emptyList(), rigorousMock<IdentityServiceInternal>().also {
|
||||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||||
doReturn(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY)
|
doReturn(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY)
|
||||||
doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY)
|
doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY)
|
||||||
@ -260,16 +264,15 @@ class CommercialPaperTestsGeneric {
|
|||||||
private lateinit var aliceServices: MockServices
|
private lateinit var aliceServices: MockServices
|
||||||
private lateinit var aliceVaultService: VaultService
|
private lateinit var aliceVaultService: VaultService
|
||||||
private lateinit var alicesVault: Vault<ContractState>
|
private lateinit var alicesVault: Vault<ContractState>
|
||||||
private val notaryServices = MockServices(rigorousMock(), MEGA_CORP.name, dummyNotary.key)
|
private val notaryServices = MockServices(emptyList(), rigorousMock(), MEGA_CORP.name, dummyNotary.keyPair)
|
||||||
private val issuerServices = MockServices(listOf("net.corda.finance.contracts", "net.corda.finance.schemas"), rigorousMock(), MEGA_CORP.name, dummyCashIssuer.key)
|
private val issuerServices = MockServices(listOf("net.corda.finance.contracts", "net.corda.finance.schemas"), rigorousMock(), MEGA_CORP.name, dummyCashIssuer.keyPair)
|
||||||
private lateinit var moveTX: SignedTransaction
|
private lateinit var moveTX: SignedTransaction
|
||||||
@Test
|
@Test
|
||||||
fun `issue move and then redeem`() {
|
fun `issue move and then redeem`() {
|
||||||
val aliceDatabaseAndServices = makeTestDatabaseAndMockServices(
|
val aliceDatabaseAndServices = makeTestDatabaseAndMockServices(
|
||||||
listOf(ALICE_KEY),
|
listOf("net.corda.finance.contracts"),
|
||||||
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)),
|
makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY),
|
||||||
listOf("net.corda.finance.contracts", "net.corda.finance.schemas"),
|
TestIdentity(MEGA_CORP.name, ALICE_KEY))
|
||||||
MEGA_CORP.name)
|
|
||||||
val databaseAlice = aliceDatabaseAndServices.first
|
val databaseAlice = aliceDatabaseAndServices.first
|
||||||
aliceServices = aliceDatabaseAndServices.second
|
aliceServices = aliceDatabaseAndServices.second
|
||||||
aliceVaultService = aliceServices.vaultService
|
aliceVaultService = aliceServices.vaultService
|
||||||
@ -279,10 +282,9 @@ class CommercialPaperTestsGeneric {
|
|||||||
aliceVaultService = aliceServices.vaultService
|
aliceVaultService = aliceServices.vaultService
|
||||||
}
|
}
|
||||||
val bigCorpDatabaseAndServices = makeTestDatabaseAndMockServices(
|
val bigCorpDatabaseAndServices = makeTestDatabaseAndMockServices(
|
||||||
listOf(BIG_CORP_KEY),
|
listOf("net.corda.finance.contracts"),
|
||||||
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)),
|
makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY),
|
||||||
listOf("net.corda.finance.contracts", "net.corda.finance.schemas"),
|
TestIdentity(MEGA_CORP.name, BIG_CORP_KEY))
|
||||||
MEGA_CORP.name)
|
|
||||||
val databaseBigCorp = bigCorpDatabaseAndServices.first
|
val databaseBigCorp = bigCorpDatabaseAndServices.first
|
||||||
bigCorpServices = bigCorpDatabaseAndServices.second
|
bigCorpServices = bigCorpDatabaseAndServices.second
|
||||||
bigCorpVaultService = bigCorpServices.vaultService
|
bigCorpVaultService = bigCorpServices.vaultService
|
||||||
@ -308,8 +310,8 @@ class CommercialPaperTestsGeneric {
|
|||||||
// Alice pays $9000 to BigCorp to own some of their debt.
|
// Alice pays $9000 to BigCorp to own some of their debt.
|
||||||
moveTX = run {
|
moveTX = run {
|
||||||
val builder = TransactionBuilder(DUMMY_NOTARY)
|
val builder = TransactionBuilder(DUMMY_NOTARY)
|
||||||
Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public))
|
Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(BIG_CORP_KEY.public))
|
||||||
CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(aliceServices.key.public))
|
CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(ALICE_KEY.public))
|
||||||
val ptx = aliceServices.signInitialTransaction(builder)
|
val ptx = aliceServices.signInitialTransaction(builder)
|
||||||
val ptx2 = bigCorpServices.addSignature(ptx)
|
val ptx2 = bigCorpServices.addSignature(ptx)
|
||||||
val stx = notaryServices.addSignature(ptx2)
|
val stx = notaryServices.addSignature(ptx2)
|
||||||
|
@ -3,7 +3,6 @@ package net.corda.finance.contracts.asset
|
|||||||
import com.nhaarman.mockito_kotlin.*
|
import com.nhaarman.mockito_kotlin.*
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.generateKeyPair
|
|
||||||
import net.corda.core.identity.*
|
import net.corda.core.identity.*
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.services.VaultService
|
import net.corda.core.node.services.VaultService
|
||||||
@ -21,7 +20,12 @@ import net.corda.node.services.vault.NodeVaultService
|
|||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import net.corda.testing.contracts.DummyState
|
import net.corda.testing.contracts.DummyState
|
||||||
import net.corda.testing.contracts.VaultFiller
|
import net.corda.testing.internal.LogHelper
|
||||||
|
import net.corda.testing.dsl.EnforceVerifyOrFail
|
||||||
|
import net.corda.testing.dsl.TransactionDSL
|
||||||
|
import net.corda.testing.dsl.TransactionDSLInterpreter
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
|
import net.corda.testing.internal.vault.VaultFiller
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
|
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
|
||||||
import net.corda.testing.node.ledger
|
import net.corda.testing.node.ledger
|
||||||
@ -37,27 +41,27 @@ import kotlin.test.*
|
|||||||
class CashTests {
|
class CashTests {
|
||||||
private companion object {
|
private companion object {
|
||||||
val alice = TestIdentity(ALICE_NAME, 70)
|
val alice = TestIdentity(ALICE_NAME, 70)
|
||||||
val BOB_PUBKEY = TestIdentity(BOB_NAME, 80).pubkey
|
val BOB_PUBKEY = TestIdentity(BOB_NAME, 80).publicKey
|
||||||
val charlie = TestIdentity(CHARLIE_NAME, 90)
|
val charlie = TestIdentity(CHARLIE_NAME, 90)
|
||||||
val DUMMY_CASH_ISSUER_IDENTITY = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10).identity
|
val DUMMY_CASH_ISSUER_IDENTITY = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10).identity
|
||||||
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
|
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
|
||||||
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
||||||
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
||||||
val ALICE get() = alice.party
|
val ALICE get() = alice.party
|
||||||
val ALICE_PUBKEY get() = alice.pubkey
|
val ALICE_PUBKEY get() = alice.publicKey
|
||||||
val CHARLIE get() = charlie.party
|
val CHARLIE get() = charlie.party
|
||||||
val CHARLIE_IDENTITY get() = charlie.identity
|
val CHARLIE_IDENTITY get() = charlie.identity
|
||||||
val DUMMY_NOTARY get() = dummyNotary.party
|
val DUMMY_NOTARY get() = dummyNotary.party
|
||||||
val DUMMY_NOTARY_IDENTITY get() = dummyNotary.identity
|
val DUMMY_NOTARY_IDENTITY get() = dummyNotary.identity
|
||||||
val DUMMY_NOTARY_KEY get() = dummyNotary.key
|
val DUMMY_NOTARY_KEY get() = dummyNotary.keyPair
|
||||||
val MEGA_CORP get() = megaCorp.party
|
val MEGA_CORP get() = megaCorp.party
|
||||||
val MEGA_CORP_IDENTITY get() = megaCorp.identity
|
val MEGA_CORP_IDENTITY get() = megaCorp.identity
|
||||||
val MEGA_CORP_KEY get() = megaCorp.key
|
val MEGA_CORP_KEY get() = megaCorp.keyPair
|
||||||
val MEGA_CORP_PUBKEY get() = megaCorp.pubkey
|
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
|
||||||
val MINI_CORP get() = miniCorp.party
|
val MINI_CORP get() = miniCorp.party
|
||||||
val MINI_CORP_IDENTITY get() = miniCorp.identity
|
val MINI_CORP_IDENTITY get() = miniCorp.identity
|
||||||
val MINI_CORP_KEY get() = miniCorp.key
|
val MINI_CORP_KEY get() = miniCorp.keyPair
|
||||||
val MINI_CORP_PUBKEY get() = miniCorp.pubkey
|
val MINI_CORP_PUBKEY get() = miniCorp.publicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@ -99,10 +103,9 @@ class CashTests {
|
|||||||
}, MINI_CORP.name, MINI_CORP_KEY)
|
}, MINI_CORP.name, MINI_CORP_KEY)
|
||||||
val notaryServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
val notaryServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||||
val databaseAndServices = makeTestDatabaseAndMockServices(
|
val databaseAndServices = makeTestDatabaseAndMockServices(
|
||||||
listOf(generateKeyPair()),
|
listOf("net.corda.finance.contracts.asset"),
|
||||||
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)),
|
makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY),
|
||||||
listOf("net.corda.finance.contracts.asset", "net.corda.finance.schemas"),
|
TestIdentity(CordaX500Name("Me", "London", "GB")))
|
||||||
CordaX500Name("Me", "London", "GB"))
|
|
||||||
database = databaseAndServices.first
|
database = databaseAndServices.first
|
||||||
ourServices = databaseAndServices.second
|
ourServices = databaseAndServices.second
|
||||||
|
|
||||||
@ -138,7 +141,7 @@ class CashTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun transaction(script: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail) = run {
|
private fun transaction(script: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail) = run {
|
||||||
MockServices(rigorousMock<IdentityServiceInternal>().also {
|
MockServices(emptyList(), rigorousMock<IdentityServiceInternal>().also {
|
||||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||||
doReturn(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY)
|
doReturn(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY)
|
||||||
doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY)
|
doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY)
|
||||||
|
@ -22,6 +22,8 @@ import net.corda.node.services.api.IdentityServiceInternal
|
|||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
import net.corda.testing.contracts.DummyState
|
import net.corda.testing.contracts.DummyState
|
||||||
|
import net.corda.testing.dsl.*
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.ledger
|
import net.corda.testing.node.ledger
|
||||||
import net.corda.testing.node.transaction
|
import net.corda.testing.node.transaction
|
||||||
@ -45,14 +47,14 @@ class ObligationTests {
|
|||||||
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
||||||
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
||||||
val ALICE get() = alice.party
|
val ALICE get() = alice.party
|
||||||
val ALICE_PUBKEY get() = alice.pubkey
|
val ALICE_PUBKEY get() = alice.publicKey
|
||||||
val BOB get() = bob.party
|
val BOB get() = bob.party
|
||||||
val BOB_PUBKEY get() = bob.pubkey
|
val BOB_PUBKEY get() = bob.publicKey
|
||||||
val DUMMY_NOTARY get() = dummyNotary.party
|
val DUMMY_NOTARY get() = dummyNotary.party
|
||||||
val MEGA_CORP get() = megaCorp.party
|
val MEGA_CORP get() = megaCorp.party
|
||||||
val MEGA_CORP_PUBKEY get() = megaCorp.pubkey
|
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
|
||||||
val MINI_CORP get() = miniCorp.party
|
val MINI_CORP get() = miniCorp.party
|
||||||
val MINI_CORP_PUBKEY get() = miniCorp.pubkey
|
val MINI_CORP_PUBKEY get() = miniCorp.publicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@ -77,7 +79,7 @@ class ObligationTests {
|
|||||||
)
|
)
|
||||||
private val outState = inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY))
|
private val outState = inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY))
|
||||||
private val miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), miniCorp)
|
private val miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), miniCorp)
|
||||||
private val notaryServices = MockServices(rigorousMock(), MEGA_CORP.name, dummyNotary.key)
|
private val notaryServices = MockServices(emptyList(), rigorousMock(), MEGA_CORP.name, dummyNotary.keyPair)
|
||||||
private val identityService = rigorousMock<IdentityServiceInternal>().also {
|
private val identityService = rigorousMock<IdentityServiceInternal>().also {
|
||||||
doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY)
|
doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY)
|
||||||
doReturn(null).whenever(it).partyFromKey(BOB_PUBKEY)
|
doReturn(null).whenever(it).partyFromKey(BOB_PUBKEY)
|
||||||
@ -86,7 +88,7 @@ class ObligationTests {
|
|||||||
doReturn(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY)
|
doReturn(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY)
|
||||||
}
|
}
|
||||||
private val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), identityService, MEGA_CORP.name)
|
private val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), identityService, MEGA_CORP.name)
|
||||||
private val ledgerServices get() = MockServices(identityService, MEGA_CORP.name)
|
private val ledgerServices get() = MockServices(emptyList(), identityService, MEGA_CORP.name)
|
||||||
private fun cashObligationTestRoots(
|
private fun cashObligationTestRoots(
|
||||||
group: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>
|
group: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>
|
||||||
) = group.apply {
|
) = group.apply {
|
||||||
|
@ -2,18 +2,16 @@ package net.corda.plugins
|
|||||||
|
|
||||||
import groovy.lang.Closure
|
import groovy.lang.Closure
|
||||||
import net.corda.cordform.CordformDefinition
|
import net.corda.cordform.CordformDefinition
|
||||||
import net.corda.cordform.CordformNode
|
|
||||||
import org.apache.tools.ant.filters.FixCrLfFilter
|
import org.apache.tools.ant.filters.FixCrLfFilter
|
||||||
import org.gradle.api.DefaultTask
|
import org.gradle.api.DefaultTask
|
||||||
import org.gradle.api.GradleException
|
|
||||||
import org.gradle.api.plugins.JavaPluginConvention
|
import org.gradle.api.plugins.JavaPluginConvention
|
||||||
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
|
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
|
||||||
import org.gradle.api.tasks.TaskAction
|
import org.gradle.api.tasks.TaskAction
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import java.util.jar.JarInputStream
|
import java.util.jar.JarInputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -118,13 +116,13 @@ open class Cordform : DefaultTask() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The parametersGenerator needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath.
|
* The NetworkBootstrapper needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath.
|
||||||
*/
|
*/
|
||||||
private fun loadNetworkParamsGenClass(): Class<*> {
|
private fun loadNetworkBootstrapperClass(): Class<*> {
|
||||||
val plugin = project.convention.getPlugin(JavaPluginConvention::class.java)
|
val plugin = project.convention.getPlugin(JavaPluginConvention::class.java)
|
||||||
val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath
|
val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath
|
||||||
val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray()
|
val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray()
|
||||||
return URLClassLoader(urls, javaClass.classLoader).loadClass("net.corda.nodeapi.internal.NetworkParametersGenerator")
|
return URLClassLoader(urls, javaClass.classLoader).loadClass("net.corda.nodeapi.internal.network.NetworkBootstrapper")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -136,8 +134,7 @@ open class Cordform : DefaultTask() {
|
|||||||
initializeConfiguration()
|
initializeConfiguration()
|
||||||
installRunScript()
|
installRunScript()
|
||||||
nodes.forEach(Node::build)
|
nodes.forEach(Node::build)
|
||||||
generateAndInstallNodeInfos()
|
bootstrapNetwork()
|
||||||
generateAndInstallNetworkParameters()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeConfiguration() {
|
private fun initializeConfiguration() {
|
||||||
@ -164,14 +161,17 @@ open class Cordform : DefaultTask() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateAndInstallNetworkParameters() {
|
private fun bootstrapNetwork() {
|
||||||
project.logger.info("Generating and installing network parameters")
|
val networkBootstrapperClass = loadNetworkBootstrapperClass()
|
||||||
val networkParamsGenClass = loadNetworkParamsGenClass()
|
val networkBootstrapper = networkBootstrapperClass.newInstance()
|
||||||
val nodeDirs = nodes.map(Node::fullPath)
|
val bootstrapMethod = networkBootstrapperClass.getMethod("bootstrap", Path::class.java).apply { isAccessible = true }
|
||||||
val networkParamsGenObject = networkParamsGenClass.newInstance()
|
// Call NetworkBootstrapper.bootstrap
|
||||||
val runMethod = networkParamsGenClass.getMethod("run", List::class.java).apply { isAccessible = true }
|
try {
|
||||||
// Call NetworkParametersGenerator.run
|
val rootDir = project.projectDir.toPath().resolve(directory).toAbsolutePath().normalize()
|
||||||
runMethod.invoke(networkParamsGenObject, nodeDirs)
|
bootstrapMethod.invoke(networkBootstrapper, rootDir)
|
||||||
|
} catch (e: InvocationTargetException) {
|
||||||
|
throw e.cause!!
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CordformDefinition.getMatchingCordapps(): List<File> {
|
private fun CordformDefinition.getMatchingCordapps(): List<File> {
|
||||||
@ -197,90 +197,4 @@ open class Cordform : DefaultTask() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateAndInstallNodeInfos() {
|
|
||||||
generateNodeInfos()
|
|
||||||
installNodeInfos()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateNodeInfos() {
|
|
||||||
project.logger.info("Generating node infos")
|
|
||||||
val nodeProcesses = buildNodeProcesses()
|
|
||||||
try {
|
|
||||||
validateNodeProcessess(nodeProcesses)
|
|
||||||
} finally {
|
|
||||||
destroyNodeProcesses(nodeProcesses)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildNodeProcesses(): Map<Node, Process> {
|
|
||||||
val command = generateNodeInfoCommand()
|
|
||||||
return nodes.map {
|
|
||||||
it.makeLogDirectory()
|
|
||||||
buildProcess(it, command, "generate-info.log") }.toMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateNodeProcessess(nodeProcesses: Map<Node, Process>) {
|
|
||||||
nodeProcesses.forEach { (node, process) ->
|
|
||||||
validateNodeProcess(node, process)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun destroyNodeProcesses(nodeProcesses: Map<Node, Process>) {
|
|
||||||
nodeProcesses.forEach { (_, process) ->
|
|
||||||
process.destroyForcibly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildProcess(node: Node, command: List<String>, logFile: String): Pair<Node, Process> {
|
|
||||||
val process = ProcessBuilder(command)
|
|
||||||
.directory(node.fullPath().toFile())
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
// InheritIO causes hangs on windows due the gradle buffer also not being flushed.
|
|
||||||
// Must redirect to output or logger (node log is still written, this is just startup banner)
|
|
||||||
.redirectOutput(node.logFile(logFile).toFile())
|
|
||||||
.addEnvironment("CAPSULE_CACHE_DIR", Node.capsuleCacheDir)
|
|
||||||
.start()
|
|
||||||
return Pair(node, process)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateNodeInfoCommand(): List<String> = listOf(
|
|
||||||
"java",
|
|
||||||
"-Dcapsule.log=verbose",
|
|
||||||
"-Dcapsule.dir=${Node.capsuleCacheDir}",
|
|
||||||
"-jar",
|
|
||||||
Node.nodeJarName,
|
|
||||||
"--just-generate-node-info"
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun validateNodeProcess(node: Node, process: Process) {
|
|
||||||
val generateTimeoutSeconds = 60L
|
|
||||||
if (!process.waitFor(generateTimeoutSeconds, TimeUnit.SECONDS)) {
|
|
||||||
throw GradleException("Node took longer $generateTimeoutSeconds seconds than too to generate node info - see node log at ${node.fullPath()}/logs")
|
|
||||||
}
|
|
||||||
if (process.exitValue() != 0) {
|
|
||||||
throw GradleException("Node exited with ${process.exitValue()} when generating node infos - see node log at ${node.fullPath()}/logs")
|
|
||||||
}
|
|
||||||
project.logger.info("Generated node info for ${node.fullPath()}")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun installNodeInfos() {
|
|
||||||
project.logger.info("Installing node infos")
|
|
||||||
for (source in nodes) {
|
|
||||||
for (destination in nodes) {
|
|
||||||
if (source.nodeDir != destination.nodeDir) {
|
|
||||||
project.copy {
|
|
||||||
it.apply {
|
|
||||||
from(source.fullPath().toString())
|
|
||||||
include("nodeInfo-*")
|
|
||||||
into(destination.fullPath().resolve(CordformNode.NODE_INFO_DIRECTORY).toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Node.logFile(name: String): Path = this.logDirectory().resolve(name)
|
|
||||||
private fun ProcessBuilder.addEnvironment(key: String, value: String) = this.apply { environment().put(key, value) }
|
|
||||||
}
|
}
|
||||||
|
@ -20,13 +20,8 @@ class Node(private val project: Project) : CordformNode() {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
val webJarName = "corda-webserver.jar"
|
val webJarName = "corda-webserver.jar"
|
||||||
private val configFileProperty = "configFile"
|
private val configFileProperty = "configFile"
|
||||||
val capsuleCacheDir: String = "./cache"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fullPath(): Path = project.projectDir.toPath().resolve(nodeDir.toPath())
|
|
||||||
fun logDirectory(): Path = fullPath().resolve("logs")
|
|
||||||
fun makeLogDirectory() = Files.createDirectories(logDirectory())
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the list of CorDapps to install to the plugins directory. Each cordapp is a fully qualified Maven
|
* Set the list of CorDapps to install to the plugins directory. Each cordapp is a fully qualified Maven
|
||||||
* dependency name, eg: com.example:product-name:0.1
|
* dependency name, eg: com.example:product-name:0.1
|
||||||
|
@ -122,7 +122,7 @@ private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: I
|
|||||||
end tell""")
|
end tell""")
|
||||||
}
|
}
|
||||||
OS.WINDOWS -> {
|
OS.WINDOWS -> {
|
||||||
listOf("cmd", "/C", "start ${command.joinToString(" ")}")
|
listOf("cmd", "/C", "start ${command.joinToString(" ") { windowsSpaceEscape(it) }}")
|
||||||
}
|
}
|
||||||
OS.LINUX -> {
|
OS.LINUX -> {
|
||||||
// Start shell to keep window open unless java terminated normally or due to SIGTERM:
|
// Start shell to keep window open unless java terminated normally or due to SIGTERM:
|
||||||
@ -136,12 +136,11 @@ end tell""")
|
|||||||
})
|
})
|
||||||
|
|
||||||
private fun unixCommand() = command.map(::quotedFormOf).joinToString(" ")
|
private fun unixCommand() = command.map(::quotedFormOf).joinToString(" ")
|
||||||
override fun getJavaPath(): String {
|
override fun getJavaPath(): String = File(File(System.getProperty("java.home"), "bin"), "java").path
|
||||||
val path = File(File(System.getProperty("java.home"), "bin"), "java").path
|
|
||||||
// Replace below is to fix an issue with spaces in paths on Windows.
|
// Replace below is to fix an issue with spaces in paths on Windows.
|
||||||
// Quoting the entire path does not work, only the space or directory within the path.
|
// Quoting the entire path does not work, only the space or directory within the path.
|
||||||
return if (os == OS.WINDOWS) path.replace(" ", "\" \"") else path
|
private fun windowsSpaceEscape(s:String) = s.replace(" ", "\" \"")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun quotedFormOf(text: String) = "'${text.replace("'", "'\\''")}'" // Suitable for UNIX shells.
|
private fun quotedFormOf(text: String) = "'${text.replace("'", "'\\''")}'" // Suitable for UNIX shells.
|
||||||
|
@ -29,12 +29,14 @@ class ArtemisTcpTransport {
|
|||||||
// but we allow classical RSA certificates to work in case:
|
// but we allow classical RSA certificates to work in case:
|
||||||
// a) we need to use keytool certificates in some demos,
|
// a) we need to use keytool certificates in some demos,
|
||||||
// b) we use cloud providers or HSMs that do not support ECC.
|
// b) we use cloud providers or HSMs that do not support ECC.
|
||||||
private val CIPHER_SUITES = listOf(
|
val CIPHER_SUITES = listOf(
|
||||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||||
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"
|
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val TLS_VERSIONS = listOf("TLSv1.2")
|
||||||
|
|
||||||
fun tcpTransport(
|
fun tcpTransport(
|
||||||
direction: ConnectionDirection,
|
direction: ConnectionDirection,
|
||||||
hostAndPort: NetworkHostAndPort,
|
hostAndPort: NetworkHostAndPort,
|
||||||
@ -68,7 +70,7 @@ class ArtemisTcpTransport {
|
|||||||
TransportConstants.TRUSTSTORE_PATH_PROP_NAME to config.trustStoreFile,
|
TransportConstants.TRUSTSTORE_PATH_PROP_NAME to config.trustStoreFile,
|
||||||
TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to config.trustStorePassword,
|
TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to config.trustStorePassword,
|
||||||
TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","),
|
TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","),
|
||||||
TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to "TLSv1.2",
|
TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to TLS_VERSIONS.joinToString(","),
|
||||||
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true,
|
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true,
|
||||||
VERIFY_PEER_LEGAL_NAME to (direction as? ConnectionDirection.Outbound)?.expectedCommonNames
|
VERIFY_PEER_LEGAL_NAME to (direction as? ConnectionDirection.Outbound)?.expectedCommonNames
|
||||||
)
|
)
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
package net.corda.nodeapi.internal
|
|
||||||
|
|
||||||
import net.corda.core.crypto.SignedData
|
|
||||||
import net.corda.core.crypto.entropyToKeyPair
|
|
||||||
import net.corda.core.crypto.sign
|
|
||||||
import net.corda.core.internal.copyTo
|
|
||||||
import net.corda.core.internal.div
|
|
||||||
import net.corda.core.serialization.serialize
|
|
||||||
import net.corda.nodeapi.internal.NetworkParameters
|
|
||||||
import java.math.BigInteger
|
|
||||||
import java.nio.file.FileAlreadyExistsException
|
|
||||||
import java.nio.file.Path
|
|
||||||
|
|
||||||
class NetworkParametersCopier(networkParameters: NetworkParameters) {
|
|
||||||
private companion object {
|
|
||||||
val DUMMY_MAP_KEY = entropyToKeyPair(BigInteger.valueOf(123))
|
|
||||||
}
|
|
||||||
|
|
||||||
private val serializedNetworkParameters = networkParameters.let {
|
|
||||||
val serialize = it.serialize()
|
|
||||||
val signature = DUMMY_MAP_KEY.sign(serialize)
|
|
||||||
SignedData(serialize, signature).serialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun install(dir: Path) {
|
|
||||||
try {
|
|
||||||
serializedNetworkParameters.open().copyTo(dir / "network-parameters")
|
|
||||||
} catch (e: FileAlreadyExistsException) {
|
|
||||||
// Leave the file untouched if it already exists
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,111 +0,0 @@
|
|||||||
package net.corda.nodeapi.internal
|
|
||||||
|
|
||||||
import com.typesafe.config.ConfigFactory
|
|
||||||
import net.corda.core.crypto.SignedData
|
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.internal.div
|
|
||||||
import net.corda.core.internal.list
|
|
||||||
import net.corda.core.internal.readAll
|
|
||||||
import net.corda.core.node.NodeInfo
|
|
||||||
import net.corda.core.serialization.SerializationContext
|
|
||||||
import net.corda.core.serialization.deserialize
|
|
||||||
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
|
||||||
import net.corda.core.serialization.internal._contextSerializationEnv
|
|
||||||
import net.corda.core.utilities.ByteSequence
|
|
||||||
import net.corda.core.utilities.contextLogger
|
|
||||||
import net.corda.core.utilities.days
|
|
||||||
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
|
|
||||||
import net.corda.nodeapi.internal.serialization.KRYO_P2P_CONTEXT
|
|
||||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
|
||||||
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
|
|
||||||
import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme
|
|
||||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is loaded by Cordform using reflection to generate the network parameters. It is assumed that Cordform has
|
|
||||||
* already asked each node to generate its node info file.
|
|
||||||
*/
|
|
||||||
@Suppress("UNUSED")
|
|
||||||
class NetworkParametersGenerator {
|
|
||||||
companion object {
|
|
||||||
private val logger = contextLogger()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun run(nodesDirs: List<Path>) {
|
|
||||||
logger.info("NetworkParameters generation using node directories: $nodesDirs")
|
|
||||||
try {
|
|
||||||
initialiseSerialization()
|
|
||||||
val notaryInfos = gatherNotaryIdentities(nodesDirs)
|
|
||||||
val copier = NetworkParametersCopier(NetworkParameters(
|
|
||||||
minimumPlatformVersion = 1,
|
|
||||||
notaries = notaryInfos,
|
|
||||||
modifiedTime = Instant.now(),
|
|
||||||
eventHorizon = 10000.days,
|
|
||||||
maxMessageSize = 40000,
|
|
||||||
maxTransactionSize = 40000,
|
|
||||||
epoch = 1
|
|
||||||
))
|
|
||||||
nodesDirs.forEach(copier::install)
|
|
||||||
} finally {
|
|
||||||
_contextSerializationEnv.set(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun gatherNotaryIdentities(nodesDirs: List<Path>): List<NotaryInfo> {
|
|
||||||
return nodesDirs.mapNotNull { nodeDir ->
|
|
||||||
val nodeConfig = ConfigFactory.parseFile((nodeDir / "node.conf").toFile())
|
|
||||||
if (nodeConfig.hasPath("notary")) {
|
|
||||||
val validating = nodeConfig.getConfig("notary").getBoolean("validating")
|
|
||||||
val nodeInfoFile = nodeDir.list { paths -> paths.filter { it.fileName.toString().startsWith("nodeInfo-") }.findFirst().get() }
|
|
||||||
processFile(nodeInfoFile)?.let { NotaryInfo(it.notaryIdentity(), validating) }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.distinct() // We need distinct as nodes part of a distributed notary share the same notary identity
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun NodeInfo.notaryIdentity(): Party {
|
|
||||||
return when (legalIdentities.size) {
|
|
||||||
// Single node notaries have just one identity like all other nodes. This identity is the notary identity
|
|
||||||
1 -> legalIdentities[0]
|
|
||||||
// Nodes which are part of a distributed notary have a second identity which is the composite identity of the
|
|
||||||
// cluster and is shared by all the other members. This is the notary identity.
|
|
||||||
2 -> legalIdentities[1]
|
|
||||||
else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun processFile(file: Path): NodeInfo? {
|
|
||||||
return try {
|
|
||||||
logger.info("Reading NodeInfo from file: $file")
|
|
||||||
val signedData = file.readAll().deserialize<SignedData<NodeInfo>>()
|
|
||||||
signedData.verified()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.warn("Exception parsing NodeInfo from file. $file", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to to set serialization env, because generation of parameters is run from Cordform.
|
|
||||||
// KryoServerSerializationScheme is not accessible from nodeapi.
|
|
||||||
private fun initialiseSerialization() {
|
|
||||||
val context = if (java.lang.Boolean.getBoolean("net.corda.testing.amqp.enable")) AMQP_P2P_CONTEXT else KRYO_P2P_CONTEXT
|
|
||||||
_contextSerializationEnv.set(SerializationEnvironmentImpl(
|
|
||||||
SerializationFactoryImpl().apply {
|
|
||||||
registerScheme(KryoParametersSerializationScheme)
|
|
||||||
registerScheme(AMQPServerSerializationScheme())
|
|
||||||
},
|
|
||||||
context)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private object KryoParametersSerializationScheme : AbstractKryoSerializationScheme() {
|
|
||||||
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
|
|
||||||
return byteSequence == KryoHeaderV0_1 && target == SerializationContext.UseCase.P2P
|
|
||||||
}
|
|
||||||
override fun rpcClientKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
|
|
||||||
override fun rpcServerKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,44 @@
|
|||||||
|
package net.corda.nodeapi.internal
|
||||||
|
|
||||||
|
import net.corda.core.crypto.CompositeKey
|
||||||
|
import net.corda.core.crypto.DigitalSignature
|
||||||
|
import net.corda.core.crypto.verify
|
||||||
|
import net.corda.core.node.NodeInfo
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
import net.corda.core.serialization.SerializedBytes
|
||||||
|
import net.corda.core.serialization.deserialize
|
||||||
|
import java.security.SignatureException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A signed [NodeInfo] object containing a signature for each identity. The list of signatures is expected
|
||||||
|
* to be in the same order as the identities.
|
||||||
|
*/
|
||||||
|
// TODO Add signatures for composite keys. The current thinking is to make sure there is a signature for each leaf key
|
||||||
|
// that the node owns. This check can only be done by the network map server as it can check with the doorman if a node
|
||||||
|
// is part of a composite identity. This of course further requires the doorman being able to issue CSRs for composite
|
||||||
|
// public keys.
|
||||||
|
@CordaSerializable
|
||||||
|
class SignedNodeInfo(val raw: SerializedBytes<NodeInfo>, val signatures: List<DigitalSignature>) {
|
||||||
|
fun verified(): NodeInfo {
|
||||||
|
val nodeInfo = raw.deserialize()
|
||||||
|
val identities = nodeInfo.legalIdentities.filterNot { it.owningKey is CompositeKey }
|
||||||
|
|
||||||
|
if (identities.size < signatures.size) {
|
||||||
|
throw SignatureException("Extra signatures. Found ${signatures.size} expected ${identities.size}")
|
||||||
|
}
|
||||||
|
if (identities.size > signatures.size) {
|
||||||
|
throw SignatureException("Missing signatures. Found ${signatures.size} expected ${identities.size}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val rawBytes = raw.bytes // To avoid cloning the byte array multiple times
|
||||||
|
identities.zip(signatures).forEach { (identity, signature) ->
|
||||||
|
try {
|
||||||
|
identity.owningKey.verify(rawBytes, signature)
|
||||||
|
} catch (e: SignatureException) {
|
||||||
|
throw SignatureException("$identity: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeInfo
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,167 @@
|
|||||||
|
package net.corda.nodeapi.internal.network
|
||||||
|
|
||||||
|
import com.typesafe.config.ConfigFactory
|
||||||
|
import net.corda.cordform.CordformNode
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.*
|
||||||
|
import net.corda.core.node.NodeInfo
|
||||||
|
import net.corda.core.serialization.SerializationContext
|
||||||
|
import net.corda.core.serialization.deserialize
|
||||||
|
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||||
|
import net.corda.core.serialization.internal._contextSerializationEnv
|
||||||
|
import net.corda.core.utilities.ByteSequence
|
||||||
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
|
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
|
||||||
|
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||||
|
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
|
||||||
|
import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme
|
||||||
|
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.concurrent.TimeUnit.SECONDS
|
||||||
|
import kotlin.streams.toList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to bootstrap a local network of Corda nodes on the same filesystem.
|
||||||
|
*/
|
||||||
|
class NetworkBootstrapper {
|
||||||
|
companion object {
|
||||||
|
// TODO This will probably need to change once we start using a bundled JVM
|
||||||
|
private val nodeInfoGenCmd = listOf(
|
||||||
|
"java",
|
||||||
|
"-jar",
|
||||||
|
"corda.jar",
|
||||||
|
"--just-generate-node-info"
|
||||||
|
)
|
||||||
|
|
||||||
|
private const val LOGS_DIR_NAME = "logs"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
val arg = args.singleOrNull() ?: throw IllegalArgumentException("Expecting single argument which is the nodes' parent directory")
|
||||||
|
NetworkBootstrapper().bootstrap(Paths.get(arg).toAbsolutePath().normalize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bootstrap(directory: Path) {
|
||||||
|
directory.createDirectories()
|
||||||
|
println("Bootstrapping local network in $directory")
|
||||||
|
val nodeDirs = directory.list { paths -> paths.filter { (it / "corda.jar").exists() }.toList() }
|
||||||
|
require(nodeDirs.isNotEmpty()) { "No nodes found" }
|
||||||
|
println("Nodes found in the following sub-directories: ${nodeDirs.map { it.fileName }}")
|
||||||
|
val processes = startNodeInfoGeneration(nodeDirs)
|
||||||
|
initialiseSerialization()
|
||||||
|
try {
|
||||||
|
println("Waiting for all nodes to generate their node-info files")
|
||||||
|
val nodeInfoFiles = gatherNodeInfoFiles(processes, nodeDirs)
|
||||||
|
println("Distributing all node info-files to all nodes")
|
||||||
|
distributeNodeInfos(nodeDirs, nodeInfoFiles)
|
||||||
|
println("Gathering notary identities")
|
||||||
|
val notaryInfos = gatherNotaryInfos(nodeInfoFiles)
|
||||||
|
println("Notary identities to be used in network-parameters file: ${notaryInfos.joinToString("; ") { it.prettyPrint() }}")
|
||||||
|
installNetworkParameters(notaryInfos, nodeDirs)
|
||||||
|
println("Bootstrapping complete!")
|
||||||
|
} finally {
|
||||||
|
_contextSerializationEnv.set(null)
|
||||||
|
processes.forEach { if (it.isAlive) it.destroyForcibly() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startNodeInfoGeneration(nodeDirs: List<Path>): List<Process> {
|
||||||
|
return nodeDirs.map { nodeDir ->
|
||||||
|
val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories()
|
||||||
|
ProcessBuilder(nodeInfoGenCmd)
|
||||||
|
.directory(nodeDir.toFile())
|
||||||
|
.redirectErrorStream(true)
|
||||||
|
.redirectOutput((logsDir / "node-info-gen.log").toFile())
|
||||||
|
.apply { environment()["CAPSULE_CACHE_DIR"] = "../.cache" }
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun gatherNodeInfoFiles(processes: List<Process>, nodeDirs: List<Path>): List<Path> {
|
||||||
|
val timeOutInSeconds = 60L
|
||||||
|
return processes.zip(nodeDirs).map { (process, nodeDir) ->
|
||||||
|
check(process.waitFor(timeOutInSeconds, SECONDS)) {
|
||||||
|
"Node in ${nodeDir.fileName} took longer than ${timeOutInSeconds}s to generate its node-info - see logs in ${nodeDir / LOGS_DIR_NAME}"
|
||||||
|
}
|
||||||
|
check(process.exitValue() == 0) {
|
||||||
|
"Node in ${nodeDir.fileName} exited with ${process.exitValue()} when generating its node-info - see logs in ${nodeDir / LOGS_DIR_NAME}"
|
||||||
|
}
|
||||||
|
nodeDir.list { paths -> paths.filter { it.fileName.toString().startsWith("nodeInfo-") }.findFirst().get() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun distributeNodeInfos(nodeDirs: List<Path>, nodeInfoFiles: List<Path>) {
|
||||||
|
for (nodeDir in nodeDirs) {
|
||||||
|
val additionalNodeInfosDir = (nodeDir / CordformNode.NODE_INFO_DIRECTORY).createDirectories()
|
||||||
|
for (nodeInfoFile in nodeInfoFiles) {
|
||||||
|
nodeInfoFile.copyToDirectory(additionalNodeInfosDir, StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun gatherNotaryInfos(nodeInfoFiles: List<Path>): List<NotaryInfo> {
|
||||||
|
return nodeInfoFiles.mapNotNull { nodeInfoFile ->
|
||||||
|
// The config contains the notary type
|
||||||
|
val nodeConfig = ConfigFactory.parseFile((nodeInfoFile.parent / "node.conf").toFile())
|
||||||
|
if (nodeConfig.hasPath("notary")) {
|
||||||
|
val validating = nodeConfig.getConfig("notary").getBoolean("validating")
|
||||||
|
// And the node-info file contains the notary's identity
|
||||||
|
val nodeInfo = nodeInfoFile.readAll().deserialize<SignedNodeInfo>().verified()
|
||||||
|
NotaryInfo(nodeInfo.notaryIdentity(), validating)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.distinct() // We need distinct as nodes part of a distributed notary share the same notary identity
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun installNetworkParameters(notaryInfos: List<NotaryInfo>, nodeDirs: List<Path>) {
|
||||||
|
// TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize
|
||||||
|
val copier = NetworkParametersCopier(NetworkParameters(
|
||||||
|
minimumPlatformVersion = 1,
|
||||||
|
notaries = notaryInfos,
|
||||||
|
modifiedTime = Instant.now(),
|
||||||
|
maxMessageSize = 10485760,
|
||||||
|
maxTransactionSize = 40000,
|
||||||
|
epoch = 1
|
||||||
|
), overwriteFile = true)
|
||||||
|
|
||||||
|
nodeDirs.forEach(copier::install)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NotaryInfo.prettyPrint(): String = "${identity.name} (${if (validating) "" else "non-"}validating)"
|
||||||
|
|
||||||
|
private fun NodeInfo.notaryIdentity(): Party {
|
||||||
|
return when (legalIdentities.size) {
|
||||||
|
// Single node notaries have just one identity like all other nodes. This identity is the notary identity
|
||||||
|
1 -> legalIdentities[0]
|
||||||
|
// Nodes which are part of a distributed notary have a second identity which is the composite identity of the
|
||||||
|
// cluster and is shared by all the other members. This is the notary identity.
|
||||||
|
2 -> legalIdentities[1]
|
||||||
|
else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to to set serialization env, because generation of parameters is run from Cordform.
|
||||||
|
// KryoServerSerializationScheme is not accessible from nodeapi.
|
||||||
|
private fun initialiseSerialization() {
|
||||||
|
_contextSerializationEnv.set(SerializationEnvironmentImpl(
|
||||||
|
SerializationFactoryImpl().apply {
|
||||||
|
registerScheme(KryoParametersSerializationScheme)
|
||||||
|
registerScheme(AMQPServerSerializationScheme())
|
||||||
|
},
|
||||||
|
AMQP_P2P_CONTEXT)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object KryoParametersSerializationScheme : AbstractKryoSerializationScheme() {
|
||||||
|
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
|
||||||
|
return byteSequence == KryoHeaderV0_1 && target == SerializationContext.UseCase.P2P
|
||||||
|
}
|
||||||
|
override fun rpcClientKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
|
||||||
|
override fun rpcServerKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
}
|
@ -1,19 +1,21 @@
|
|||||||
package net.corda.nodeapi.internal
|
package net.corda.nodeapi.internal.network
|
||||||
|
|
||||||
import net.corda.core.crypto.DigitalSignature
|
import net.corda.core.crypto.DigitalSignature
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.verify
|
import net.corda.core.crypto.verify
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.SerializedBytes
|
import net.corda.core.serialization.SerializedBytes
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
|
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||||
import java.security.SignatureException
|
import java.security.SignatureException
|
||||||
import java.security.cert.CertPathValidatorException
|
import java.security.cert.CertPathValidatorException
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.time.Duration
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
// TODO: Need more discussion on rather we should move this class out of internal.
|
const val NETWORK_PARAMS_FILE_NAME = "network-parameters"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data class containing hash of [NetworkParameters] and network participant's [NodeInfo] hashes.
|
* Data class containing hash of [NetworkParameters] and network participant's [NodeInfo] hashes.
|
||||||
*/
|
*/
|
||||||
@ -21,21 +23,21 @@ import java.time.Instant
|
|||||||
data class NetworkMap(val nodeInfoHashes: List<SecureHash>, val networkParameterHash: SecureHash)
|
data class NetworkMap(val nodeInfoHashes: List<SecureHash>, val networkParameterHash: SecureHash)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property minimumPlatformVersion
|
* @property minimumPlatformVersion Minimum version of Corda platform that is required for nodes in the network.
|
||||||
* @property notaries
|
* @property notaries List of well known and trusted notary identities with information on validation type.
|
||||||
* @property eventHorizon
|
|
||||||
* @property maxMessageSize Maximum P2P message sent over the wire in bytes.
|
* @property maxMessageSize Maximum P2P message sent over the wire in bytes.
|
||||||
* @property maxTransactionSize Maximum permitted transaction size in bytes.
|
* @property maxTransactionSize Maximum permitted transaction size in bytes.
|
||||||
* @property modifiedTime
|
* @property modifiedTime
|
||||||
* @property epoch Version number of the network parameters. Starting from 1, this will always increment on each new set
|
* @property epoch Version number of the network parameters. Starting from 1, this will always increment on each new set
|
||||||
* of parameters.
|
* of parameters.
|
||||||
*/
|
*/
|
||||||
// TODO Wire up the parameters
|
// TODO Add eventHorizon - how many days a node can be offline before being automatically ejected from the network.
|
||||||
|
// It needs separate design.
|
||||||
|
// TODO Currently maxTransactionSize is not wired.
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
data class NetworkParameters(
|
data class NetworkParameters(
|
||||||
val minimumPlatformVersion: Int,
|
val minimumPlatformVersion: Int,
|
||||||
val notaries: List<NotaryInfo>,
|
val notaries: List<NotaryInfo>,
|
||||||
val eventHorizon: Duration,
|
|
||||||
val maxMessageSize: Int,
|
val maxMessageSize: Int,
|
||||||
val maxTransactionSize: Int,
|
val maxTransactionSize: Int,
|
||||||
val modifiedTime: Instant,
|
val modifiedTime: Instant,
|
||||||
@ -45,6 +47,8 @@ data class NetworkParameters(
|
|||||||
require(minimumPlatformVersion > 0) { "minimumPlatformVersion must be at least 1" }
|
require(minimumPlatformVersion > 0) { "minimumPlatformVersion must be at least 1" }
|
||||||
require(notaries.distinctBy { it.identity } == notaries) { "Duplicate notary identities" }
|
require(notaries.distinctBy { it.identity } == notaries) { "Duplicate notary identities" }
|
||||||
require(epoch > 0) { "epoch must be at least 1" }
|
require(epoch > 0) { "epoch must be at least 1" }
|
||||||
|
require(maxMessageSize > 0) { "maxMessageSize must be at least 1" }
|
||||||
|
require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,20 +60,23 @@ data class NotaryInfo(val identity: Party, val validating: Boolean)
|
|||||||
* contained within.
|
* contained within.
|
||||||
*/
|
*/
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
class SignedNetworkMap(val raw: SerializedBytes<NetworkMap>, val sig: DigitalSignatureWithCert) {
|
class SignedNetworkMap(val raw: SerializedBytes<NetworkMap>, val signature: DigitalSignatureWithCert) {
|
||||||
/**
|
/**
|
||||||
* Return the deserialized NetworkMap if the signature and certificate can be verified.
|
* Return the deserialized NetworkMap if the signature and certificate can be verified.
|
||||||
*
|
*
|
||||||
* @throws CertPathValidatorException if the certificate path is invalid.
|
* @throws CertPathValidatorException if the certificate path is invalid.
|
||||||
* @throws SignatureException if the signature is invalid.
|
* @throws SignatureException if the signature is invalid.
|
||||||
*/
|
*/
|
||||||
@Throws(SignatureException::class)
|
@Throws(SignatureException::class, CertPathValidatorException::class)
|
||||||
fun verified(): NetworkMap {
|
fun verified(trustedRoot: X509Certificate): NetworkMap {
|
||||||
sig.by.publicKey.verify(raw.bytes, sig)
|
signature.by.publicKey.verify(raw.bytes, signature)
|
||||||
|
// Assume network map cert is under the default trust root.
|
||||||
|
X509Utilities.validateCertificateChain(trustedRoot, signature.by, trustedRoot)
|
||||||
return raw.deserialize()
|
return raw.deserialize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This class should reside in the [DigitalSignature] class.
|
// TODO: This class should reside in the [DigitalSignature] class.
|
||||||
|
// TODO: Removing the val from signatureBytes causes serialisation issues
|
||||||
/** A digital signature that identifies who the public key is owned by, and the certificate which provides prove of the identity */
|
/** A digital signature that identifies who the public key is owned by, and the certificate which provides prove of the identity */
|
||||||
class DigitalSignatureWithCert(val by: X509Certificate, val signatureBytes: ByteArray) : DigitalSignature(signatureBytes)
|
class DigitalSignatureWithCert(val by: X509Certificate, val signatureBytes: ByteArray) : DigitalSignature(signatureBytes)
|
@ -0,0 +1,35 @@
|
|||||||
|
package net.corda.nodeapi.internal.network
|
||||||
|
|
||||||
|
import net.corda.core.crypto.Crypto
|
||||||
|
import net.corda.core.crypto.SignedData
|
||||||
|
import net.corda.core.crypto.sign
|
||||||
|
import net.corda.core.internal.copyTo
|
||||||
|
import net.corda.core.internal.div
|
||||||
|
import net.corda.core.serialization.serialize
|
||||||
|
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||||
|
import java.nio.file.FileAlreadyExistsException
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
|
import java.security.KeyPair
|
||||||
|
|
||||||
|
class NetworkParametersCopier(
|
||||||
|
networkParameters: NetworkParameters,
|
||||||
|
signingKeyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME),
|
||||||
|
overwriteFile: Boolean = false
|
||||||
|
) {
|
||||||
|
private val copyOptions = if (overwriteFile) arrayOf(StandardCopyOption.REPLACE_EXISTING) else emptyArray()
|
||||||
|
private val serializedNetworkParameters = networkParameters.let {
|
||||||
|
val serialize = it.serialize()
|
||||||
|
val signature = signingKeyPair.sign(serialize)
|
||||||
|
SignedData(serialize, signature).serialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun install(nodeDir: Path) {
|
||||||
|
try {
|
||||||
|
serializedNetworkParameters.open().copyTo(nodeDir / NETWORK_PARAMS_FILE_NAME, *copyOptions)
|
||||||
|
} catch (e: FileAlreadyExistsException) {
|
||||||
|
// This is only thrown if the file already exists and we didn't specify to overwrite it. In that case we
|
||||||
|
// ignore this exception as we're happy with the existing file.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.nodeapi.internal
|
package net.corda.nodeapi.internal.network
|
||||||
|
|
||||||
import net.corda.cordform.CordformNode
|
import net.corda.cordform.CordformNode
|
||||||
import net.corda.core.internal.ThreadBox
|
import net.corda.core.internal.ThreadBox
|
||||||
@ -65,7 +65,6 @@ class NodeInfoFilesCopier(scheduler: Scheduler = Schedulers.io()) : AutoCloseabl
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param nodeConfig the configuration to be removed.
|
|
||||||
* Remove the configuration of a node which is about to be stopped or already stopped.
|
* Remove the configuration of a node which is about to be stopped or already stopped.
|
||||||
* No files written by that node will be copied to other nodes, nor files from other nodes will be copied to this
|
* No files written by that node will be copied to other nodes, nor files from other nodes will be copied to this
|
||||||
* one.
|
* one.
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.testing
|
package net.corda.nodeapi
|
||||||
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
@ -14,6 +14,7 @@ import net.corda.core.transactions.TransactionBuilder
|
|||||||
import net.corda.node.internal.cordapp.CordappLoader
|
import net.corda.node.internal.cordapp.CordappLoader
|
||||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.services.MockAttachmentStorage
|
import net.corda.testing.services.MockAttachmentStorage
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
@ -19,6 +19,8 @@ import net.corda.nodeapi.internal.serialization.SerializeAsTokenContextImpl
|
|||||||
import net.corda.nodeapi.internal.serialization.attachmentsClassLoaderEnabledPropertyName
|
import net.corda.nodeapi.internal.serialization.attachmentsClassLoaderEnabledPropertyName
|
||||||
import net.corda.nodeapi.internal.serialization.withTokenContext
|
import net.corda.nodeapi.internal.serialization.withTokenContext
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
|
import net.corda.testing.internal.kryoSpecific
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.services.MockAttachmentStorage
|
import net.corda.testing.services.MockAttachmentStorage
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
package net.corda.nodeapi.internal
|
||||||
|
|
||||||
|
import net.corda.core.crypto.Crypto
|
||||||
|
import net.corda.testing.ALICE_NAME
|
||||||
|
import net.corda.testing.BOB_NAME
|
||||||
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
|
import net.corda.testing.internal.TestNodeInfoBuilder
|
||||||
|
import net.corda.testing.internal.signWith
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import java.security.SignatureException
|
||||||
|
|
||||||
|
class SignedNodeInfoTest {
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val testSerialization = SerializationEnvironmentRule()
|
||||||
|
|
||||||
|
private val nodeInfoBuilder = TestNodeInfoBuilder()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `verifying single identity`() {
|
||||||
|
nodeInfoBuilder.addIdentity(ALICE_NAME)
|
||||||
|
val (nodeInfo, signedNodeInfo) = nodeInfoBuilder.buildWithSigned()
|
||||||
|
assertThat(signedNodeInfo.verified()).isEqualTo(nodeInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `verifying multiple identities`() {
|
||||||
|
nodeInfoBuilder.addIdentity(ALICE_NAME)
|
||||||
|
nodeInfoBuilder.addIdentity(BOB_NAME)
|
||||||
|
val (nodeInfo, signedNodeInfo) = nodeInfoBuilder.buildWithSigned()
|
||||||
|
assertThat(signedNodeInfo.verified()).isEqualTo(nodeInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `verifying missing signature`() {
|
||||||
|
val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME)
|
||||||
|
nodeInfoBuilder.addIdentity(BOB_NAME)
|
||||||
|
val nodeInfo = nodeInfoBuilder.build()
|
||||||
|
val signedNodeInfo = nodeInfo.signWith(listOf(aliceKey))
|
||||||
|
assertThatThrownBy { signedNodeInfo.verified() }
|
||||||
|
.isInstanceOf(SignatureException::class.java)
|
||||||
|
.hasMessageContaining("Missing signatures")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `verifying extra signature`() {
|
||||||
|
val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME)
|
||||||
|
val nodeInfo = nodeInfoBuilder.build()
|
||||||
|
val signedNodeInfo = nodeInfo.signWith(listOf(aliceKey, generateKeyPair().private))
|
||||||
|
assertThatThrownBy { signedNodeInfo.verified() }
|
||||||
|
.isInstanceOf(SignatureException::class.java)
|
||||||
|
.hasMessageContaining("Extra signatures")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `verifying incorrect signature`() {
|
||||||
|
nodeInfoBuilder.addIdentity(ALICE_NAME)
|
||||||
|
val nodeInfo = nodeInfoBuilder.build()
|
||||||
|
val signedNodeInfo = nodeInfo.signWith(listOf(generateKeyPair().private))
|
||||||
|
assertThatThrownBy { signedNodeInfo.verified() }
|
||||||
|
.isInstanceOf(SignatureException::class.java)
|
||||||
|
.hasMessageContaining(ALICE_NAME.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `verifying with signatures in wrong order`() {
|
||||||
|
val (_, aliceKey) = nodeInfoBuilder.addIdentity(ALICE_NAME)
|
||||||
|
val (_, bobKey) = nodeInfoBuilder.addIdentity(BOB_NAME)
|
||||||
|
val nodeInfo = nodeInfoBuilder.build()
|
||||||
|
val signedNodeInfo = nodeInfo.signWith(listOf(bobKey, aliceKey))
|
||||||
|
assertThatThrownBy { signedNodeInfo.verified() }
|
||||||
|
.isInstanceOf(SignatureException::class.java)
|
||||||
|
.hasMessageContaining(ALICE_NAME.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateKeyPair() = Crypto.generateKeyPair()
|
||||||
|
}
|
@ -51,7 +51,7 @@ class X509UtilitiesTest {
|
|||||||
val bob = TestIdentity(BOB_NAME, 80)
|
val bob = TestIdentity(BOB_NAME, 80)
|
||||||
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
|
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
|
||||||
val BOB get() = bob.party
|
val BOB get() = bob.party
|
||||||
val BOB_PUBKEY get() = bob.pubkey
|
val BOB_PUBKEY get() = bob.publicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
package net.corda.nodeapi
|
package net.corda.nodeapi.internal.network
|
||||||
|
|
||||||
import net.corda.cordform.CordformNode
|
import net.corda.cordform.CordformNode
|
||||||
import net.corda.nodeapi.internal.NodeInfoFilesCopier
|
import net.corda.nodeapi.eventually
|
||||||
import net.corda.testing.eventually
|
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -15,14 +14,7 @@ import java.util.concurrent.TimeUnit
|
|||||||
import kotlin.streams.toList
|
import kotlin.streams.toList
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
/**
|
|
||||||
* tests for [NodeInfoFilesCopier]
|
|
||||||
*/
|
|
||||||
class NodeInfoFilesCopierTest {
|
class NodeInfoFilesCopierTest {
|
||||||
|
|
||||||
@Rule @JvmField var folder = TemporaryFolder()
|
|
||||||
private val rootPath get() = folder.root.toPath()
|
|
||||||
private val scheduler = TestScheduler()
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ORGANIZATION = "Organization"
|
private const val ORGANIZATION = "Organization"
|
||||||
private const val NODE_1_PATH = "node1"
|
private const val NODE_1_PATH = "node1"
|
||||||
@ -34,6 +26,13 @@ class NodeInfoFilesCopierTest {
|
|||||||
private val BAD_NODE_INFO_NAME = "something"
|
private val BAD_NODE_INFO_NAME = "something"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val folder = TemporaryFolder()
|
||||||
|
|
||||||
|
private val rootPath get() = folder.root.toPath()
|
||||||
|
private val scheduler = TestScheduler()
|
||||||
|
|
||||||
private fun nodeDir(nodeBaseDir : String) = rootPath.resolve(nodeBaseDir).resolve(ORGANIZATION.toLowerCase())
|
private fun nodeDir(nodeBaseDir : String) = rootPath.resolve(nodeBaseDir).resolve(ORGANIZATION.toLowerCase())
|
||||||
|
|
||||||
private val node1RootPath by lazy { nodeDir(NODE_1_PATH) }
|
private val node1RootPath by lazy { nodeDir(NODE_1_PATH) }
|
||||||
@ -41,7 +40,7 @@ class NodeInfoFilesCopierTest {
|
|||||||
private val node1AdditionalNodeInfoPath by lazy { node1RootPath.resolve(CordformNode.NODE_INFO_DIRECTORY) }
|
private val node1AdditionalNodeInfoPath by lazy { node1RootPath.resolve(CordformNode.NODE_INFO_DIRECTORY) }
|
||||||
private val node2AdditionalNodeInfoPath by lazy { node2RootPath.resolve(CordformNode.NODE_INFO_DIRECTORY) }
|
private val node2AdditionalNodeInfoPath by lazy { node2RootPath.resolve(CordformNode.NODE_INFO_DIRECTORY) }
|
||||||
|
|
||||||
lateinit var nodeInfoFilesCopier: NodeInfoFilesCopier
|
private lateinit var nodeInfoFilesCopier: NodeInfoFilesCopier
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
@ -5,6 +5,7 @@ import net.corda.core.identity.CordaX500Name
|
|||||||
import net.corda.core.serialization.*
|
import net.corda.core.serialization.*
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
@ -23,7 +24,7 @@ class ContractAttachmentSerializerTest {
|
|||||||
private lateinit var factory: SerializationFactory
|
private lateinit var factory: SerializationFactory
|
||||||
private lateinit var context: SerializationContext
|
private lateinit var context: SerializationContext
|
||||||
private lateinit var contextWithToken: SerializationContext
|
private lateinit var contextWithToken: SerializationContext
|
||||||
private val mockServices = MockServices(rigorousMock(), CordaX500Name("MegaCorp", "London", "GB"))
|
private val mockServices = MockServices(emptyList(), rigorousMock(), CordaX500Name("MegaCorp", "London", "GB"))
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
factory = testSerialization.env.serializationFactory
|
factory = testSerialization.env.serializationFactory
|
||||||
|
@ -13,7 +13,7 @@ import net.corda.nodeapi.internal.AttachmentsClassLoaderTests
|
|||||||
import net.corda.nodeapi.internal.serialization.kryo.CordaKryo
|
import net.corda.nodeapi.internal.serialization.kryo.CordaKryo
|
||||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||||
import net.corda.testing.services.MockAttachmentStorage
|
import net.corda.testing.services.MockAttachmentStorage
|
||||||
import net.corda.testing.rigorousMock
|
import net.corda.testing.internal.rigorousMock
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.rules.ExpectedException
|
import org.junit.rules.ExpectedException
|
||||||
|
@ -33,7 +33,7 @@ import kotlin.test.assertTrue
|
|||||||
|
|
||||||
class KryoTests {
|
class KryoTests {
|
||||||
companion object {
|
companion object {
|
||||||
private val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).pubkey
|
private val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).publicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
|
@ -8,8 +8,8 @@ import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput
|
|||||||
import net.corda.nodeapi.internal.serialization.amqp.Envelope
|
import net.corda.nodeapi.internal.serialization.amqp.Envelope
|
||||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||||
import net.corda.testing.amqpSpecific
|
import net.corda.testing.internal.amqpSpecific
|
||||||
import net.corda.testing.kryoSpecific
|
import net.corda.testing.internal.kryoSpecific
|
||||||
import net.corda.testing.SerializationEnvironmentRule
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
import org.assertj.core.api.Assertions
|
import org.assertj.core.api.Assertions
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
@ -9,8 +9,8 @@ import net.corda.core.serialization.serialize
|
|||||||
import net.corda.node.services.statemachine.DataSessionMessage
|
import net.corda.node.services.statemachine.DataSessionMessage
|
||||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||||
import net.corda.testing.SerializationEnvironmentRule
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
import net.corda.testing.amqpSpecific
|
import net.corda.testing.internal.amqpSpecific
|
||||||
import net.corda.testing.kryoSpecific
|
import net.corda.testing.internal.kryoSpecific
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
@ -8,7 +8,7 @@ import net.corda.core.utilities.OpaqueBytes
|
|||||||
import net.corda.nodeapi.internal.serialization.kryo.CordaKryo
|
import net.corda.nodeapi.internal.serialization.kryo.CordaKryo
|
||||||
import net.corda.nodeapi.internal.serialization.kryo.DefaultKryoCustomizer
|
import net.corda.nodeapi.internal.serialization.kryo.DefaultKryoCustomizer
|
||||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||||
import net.corda.testing.rigorousMock
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.SerializationEnvironmentRule
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
@ -6,8 +6,8 @@ import net.corda.core.serialization.deserialize
|
|||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.node.services.statemachine.DataSessionMessage
|
import net.corda.node.services.statemachine.DataSessionMessage
|
||||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||||
|
import net.corda.testing.internal.kryoSpecific
|
||||||
import net.corda.testing.SerializationEnvironmentRule
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
import net.corda.testing.kryoSpecific
|
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
@ -29,6 +29,7 @@ import org.apache.qpid.proton.codec.EncoderImpl
|
|||||||
import org.assertj.core.api.Assertions.*
|
import org.assertj.core.api.Assertions.*
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -49,11 +50,15 @@ class SerializationOutputTests {
|
|||||||
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
||||||
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
||||||
val MEGA_CORP get() = megaCorp.party
|
val MEGA_CORP get() = megaCorp.party
|
||||||
val MEGA_CORP_PUBKEY get() = megaCorp.pubkey
|
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
|
||||||
val MINI_CORP get() = miniCorp.party
|
val MINI_CORP get() = miniCorp.party
|
||||||
val MINI_CORP_PUBKEY get() = miniCorp.pubkey
|
val MINI_CORP_PUBKEY get() = miniCorp.publicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val testSerialization = SerializationEnvironmentRule()
|
||||||
|
|
||||||
data class Foo(val bar: String, val pub: Int)
|
data class Foo(val bar: String, val pub: Int)
|
||||||
|
|
||||||
data class testFloat(val f: Float)
|
data class testFloat(val f: Float)
|
||||||
@ -473,9 +478,9 @@ class SerializationOutputTests {
|
|||||||
assertSerializedThrowableEquivalent(t, desThrowable)
|
assertSerializedThrowableEquivalent(t, desThrowable)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun serdesThrowableWithInternalInfo(t: Throwable, factory: SerializerFactory, factory2: SerializerFactory, expectedEqual: Boolean = true): Throwable = withTestSerialization {
|
private fun serdesThrowableWithInternalInfo(t: Throwable, factory: SerializerFactory, factory2: SerializerFactory, expectedEqual: Boolean = true): Throwable {
|
||||||
val newContext = SerializationFactory.defaultFactory.defaultContext.withProperty(CommonPropertyNames.IncludeInternalInfo, true)
|
val newContext = SerializationFactory.defaultFactory.defaultContext.withProperty(CommonPropertyNames.IncludeInternalInfo, true)
|
||||||
SerializationFactory.defaultFactory.asCurrent { withCurrentContext(newContext) { serdes(t, factory, factory2, expectedEqual) } }
|
return SerializationFactory.defaultFactory.asCurrent { withCurrentContext(newContext) { serdes(t, factory, factory2, expectedEqual) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -0,0 +1,242 @@
|
|||||||
|
package net.corda.node.amqp
|
||||||
|
|
||||||
|
import com.nhaarman.mockito_kotlin.any
|
||||||
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
|
import net.corda.core.internal.div
|
||||||
|
import net.corda.core.node.NodeInfo
|
||||||
|
import net.corda.core.node.services.NetworkMapCache
|
||||||
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
|
import net.corda.core.utilities.toBase58String
|
||||||
|
import net.corda.node.internal.protonwrapper.netty.AMQPServer
|
||||||
|
import net.corda.node.internal.security.RPCSecurityManager
|
||||||
|
import net.corda.node.services.config.*
|
||||||
|
import net.corda.node.services.messaging.ArtemisMessagingClient
|
||||||
|
import net.corda.node.services.messaging.ArtemisMessagingServer
|
||||||
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
||||||
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
|
||||||
|
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||||
|
import net.corda.testing.*
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
|
import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID
|
||||||
|
import org.apache.activemq.artemis.api.core.RoutingType
|
||||||
|
import org.apache.activemq.artemis.api.core.SimpleString
|
||||||
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import rx.Observable
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotEquals
|
||||||
|
|
||||||
|
class AMQPBridgeTest {
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val temporaryFolder = TemporaryFolder()
|
||||||
|
|
||||||
|
private val ALICE = TestIdentity(ALICE_NAME)
|
||||||
|
private val BOB = TestIdentity(BOB_NAME)
|
||||||
|
|
||||||
|
private val artemisPort = freePort()
|
||||||
|
private val artemisPort2 = freePort()
|
||||||
|
private val amqpPort = freePort()
|
||||||
|
private val artemisAddress = NetworkHostAndPort("localhost", artemisPort)
|
||||||
|
private val artemisAddress2 = NetworkHostAndPort("localhost", artemisPort2)
|
||||||
|
private val amqpAddress = NetworkHostAndPort("localhost", amqpPort)
|
||||||
|
|
||||||
|
private abstract class AbstractNodeConfiguration : NodeConfiguration
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test acked and nacked messages`() {
|
||||||
|
// Create local queue
|
||||||
|
val sourceQueueName = "internal.peers." + BOB.publicKey.toBase58String()
|
||||||
|
val (artemisServer, artemisClient) = createArtemis(sourceQueueName)
|
||||||
|
|
||||||
|
// Pre-populate local queue with 3 messages
|
||||||
|
val artemis = artemisClient.started!!
|
||||||
|
for (i in 0 until 3) {
|
||||||
|
val artemisMessage = artemis.session.createMessage(true).apply {
|
||||||
|
putIntProperty("CountProp", i)
|
||||||
|
writeBodyBufferBytes("Test$i".toByteArray())
|
||||||
|
// Use the magic deduplication property built into Artemis as our message identity too
|
||||||
|
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
|
||||||
|
}
|
||||||
|
artemis.producer.send(sourceQueueName, artemisMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Create target server
|
||||||
|
val amqpServer = createAMQPServer()
|
||||||
|
|
||||||
|
val receive = amqpServer.onReceive.toBlocking().iterator
|
||||||
|
amqpServer.start()
|
||||||
|
|
||||||
|
val received1 = receive.next()
|
||||||
|
val messageID1 = received1.applicationProperties["CountProp"] as Int
|
||||||
|
assertArrayEquals("Test$messageID1".toByteArray(), received1.payload)
|
||||||
|
assertEquals(0, messageID1)
|
||||||
|
received1.complete(true) // Accept first message
|
||||||
|
|
||||||
|
val received2 = receive.next()
|
||||||
|
val messageID2 = received2.applicationProperties["CountProp"] as Int
|
||||||
|
assertArrayEquals("Test$messageID2".toByteArray(), received2.payload)
|
||||||
|
assertEquals(1, messageID2)
|
||||||
|
received2.complete(false) // Reject message
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val received3 = receive.next()
|
||||||
|
val messageID3 = received3.applicationProperties["CountProp"] as Int
|
||||||
|
assertArrayEquals("Test$messageID3".toByteArray(), received3.payload)
|
||||||
|
assertNotEquals(0, messageID3)
|
||||||
|
if (messageID3 != 1) { // keep rejecting any batched items following rejection
|
||||||
|
received3.complete(false)
|
||||||
|
} else { // beginnings of replay so accept again
|
||||||
|
received3.complete(true)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val received4 = receive.next()
|
||||||
|
val messageID4 = received4.applicationProperties["CountProp"] as Int
|
||||||
|
assertArrayEquals("Test$messageID4".toByteArray(), received4.payload)
|
||||||
|
if (messageID4 != 1) { // we may get a duplicate of the rejected message, in which case skip
|
||||||
|
assertEquals(2, messageID4) // next message should be in order though
|
||||||
|
break
|
||||||
|
}
|
||||||
|
received4.complete(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a fresh item and check receive
|
||||||
|
val artemisMessage = artemis.session.createMessage(true).apply {
|
||||||
|
putIntProperty("CountProp", -1)
|
||||||
|
writeBodyBufferBytes("Test_end".toByteArray())
|
||||||
|
// Use the magic deduplication property built into Artemis as our message identity too
|
||||||
|
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
|
||||||
|
}
|
||||||
|
artemis.producer.send(sourceQueueName, artemisMessage)
|
||||||
|
|
||||||
|
val received5 = receive.next()
|
||||||
|
val messageID5 = received5.applicationProperties["CountProp"] as Int
|
||||||
|
assertArrayEquals("Test_end".toByteArray(), received5.payload)
|
||||||
|
assertEquals(-1, messageID5) // next message should be in order
|
||||||
|
received5.complete(true)
|
||||||
|
|
||||||
|
amqpServer.stop()
|
||||||
|
artemisClient.stop()
|
||||||
|
artemisServer.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test legacy bridge still works`() {
|
||||||
|
// Create local queue
|
||||||
|
val sourceQueueName = "internal.peers." + ALICE.publicKey.toBase58String()
|
||||||
|
val (artemisLegacyServer, artemisLegacyClient) = createLegacyArtemis(sourceQueueName)
|
||||||
|
|
||||||
|
|
||||||
|
val (artemisServer, artemisClient) = createArtemis(null)
|
||||||
|
|
||||||
|
val artemis = artemisLegacyClient.started!!
|
||||||
|
for (i in 0 until 3) {
|
||||||
|
val artemisMessage = artemis.session.createMessage(true).apply {
|
||||||
|
putIntProperty("CountProp", i)
|
||||||
|
writeBodyBufferBytes("Test$i".toByteArray())
|
||||||
|
// Use the magic deduplication property built into Artemis as our message identity too
|
||||||
|
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
|
||||||
|
}
|
||||||
|
artemis.producer.send(sourceQueueName, artemisMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val subs = artemisClient.started!!.session.createConsumer(P2P_QUEUE)
|
||||||
|
for (i in 0 until 3) {
|
||||||
|
val msg = subs.receive()
|
||||||
|
val messageBody = ByteArray(msg.bodySize).apply { msg.bodyBuffer.readBytes(this) }
|
||||||
|
assertArrayEquals("Test$i".toByteArray(), messageBody)
|
||||||
|
assertEquals(i, msg.getIntProperty("CountProp"))
|
||||||
|
}
|
||||||
|
|
||||||
|
artemisClient.stop()
|
||||||
|
artemisServer.stop()
|
||||||
|
artemisLegacyClient.stop()
|
||||||
|
artemisLegacyServer.stop()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createArtemis(sourceQueueName: String?): Pair<ArtemisMessagingServer, ArtemisMessagingClient> {
|
||||||
|
val artemisConfig = rigorousMock<AbstractNodeConfiguration>().also {
|
||||||
|
doReturn(temporaryFolder.root.toPath() / "artemis").whenever(it).baseDirectory
|
||||||
|
doReturn(ALICE_NAME).whenever(it).myLegalName
|
||||||
|
doReturn("trustpass").whenever(it).trustStorePassword
|
||||||
|
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||||
|
doReturn("").whenever(it).exportJMXto
|
||||||
|
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
|
||||||
|
doReturn(true).whenever(it).useAMQPBridges
|
||||||
|
}
|
||||||
|
artemisConfig.configureWithDevSSLCertificate()
|
||||||
|
val networkMap = rigorousMock<NetworkMapCache>().also {
|
||||||
|
doReturn(Observable.never<NetworkMapCache.MapChange>()).whenever(it).changed
|
||||||
|
doReturn(listOf(NodeInfo(listOf(amqpAddress), listOf(BOB.identity), 1, 1L))).whenever(it).getNodesByLegalIdentityKey(any())
|
||||||
|
}
|
||||||
|
val userService = rigorousMock<RPCSecurityManager>()
|
||||||
|
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort, null, networkMap, userService, MAX_MESSAGE_SIZE)
|
||||||
|
val artemisClient = ArtemisMessagingClient(artemisConfig, artemisAddress, MAX_MESSAGE_SIZE)
|
||||||
|
artemisServer.start()
|
||||||
|
artemisClient.start()
|
||||||
|
val artemis = artemisClient.started!!
|
||||||
|
if (sourceQueueName != null) {
|
||||||
|
// Local queue for outgoing messages
|
||||||
|
artemis.session.createQueue(sourceQueueName, RoutingType.MULTICAST, sourceQueueName, true)
|
||||||
|
}
|
||||||
|
return Pair(artemisServer, artemisClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createLegacyArtemis(sourceQueueName: String): Pair<ArtemisMessagingServer, ArtemisMessagingClient> {
|
||||||
|
val artemisConfig = rigorousMock<AbstractNodeConfiguration>().also {
|
||||||
|
doReturn(temporaryFolder.root.toPath() / "artemis2").whenever(it).baseDirectory
|
||||||
|
doReturn(BOB_NAME).whenever(it).myLegalName
|
||||||
|
doReturn("trustpass").whenever(it).trustStorePassword
|
||||||
|
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||||
|
doReturn("").whenever(it).exportJMXto
|
||||||
|
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
|
||||||
|
doReturn(false).whenever(it).useAMQPBridges
|
||||||
|
doReturn(ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0))).whenever(it).activeMQServer
|
||||||
|
}
|
||||||
|
artemisConfig.configureWithDevSSLCertificate()
|
||||||
|
val networkMap = rigorousMock<NetworkMapCache>().also {
|
||||||
|
doReturn(Observable.never<NetworkMapCache.MapChange>()).whenever(it).changed
|
||||||
|
doReturn(listOf(NodeInfo(listOf(artemisAddress), listOf(ALICE.identity), 1, 1L))).whenever(it).getNodesByLegalIdentityKey(any())
|
||||||
|
}
|
||||||
|
val userService = rigorousMock<RPCSecurityManager>()
|
||||||
|
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort2, null, networkMap, userService, MAX_MESSAGE_SIZE)
|
||||||
|
val artemisClient = ArtemisMessagingClient(artemisConfig, artemisAddress2, MAX_MESSAGE_SIZE)
|
||||||
|
artemisServer.start()
|
||||||
|
artemisClient.start()
|
||||||
|
val artemis = artemisClient.started!!
|
||||||
|
// Local queue for outgoing messages
|
||||||
|
artemis.session.createQueue(sourceQueueName, RoutingType.MULTICAST, sourceQueueName, true)
|
||||||
|
return Pair(artemisServer, artemisClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAMQPServer(): AMQPServer {
|
||||||
|
val serverConfig = rigorousMock<AbstractNodeConfiguration>().also {
|
||||||
|
doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory
|
||||||
|
doReturn(BOB_NAME).whenever(it).myLegalName
|
||||||
|
doReturn("trustpass").whenever(it).trustStorePassword
|
||||||
|
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||||
|
}
|
||||||
|
serverConfig.configureWithDevSSLCertificate()
|
||||||
|
|
||||||
|
val serverTruststore = loadKeyStore(serverConfig.trustStoreFile, serverConfig.trustStorePassword)
|
||||||
|
val serverKeystore = loadKeyStore(serverConfig.sslKeystore, serverConfig.keyStorePassword)
|
||||||
|
val amqpServer = AMQPServer("0.0.0.0",
|
||||||
|
amqpPort,
|
||||||
|
ArtemisMessagingComponent.PEER_USER,
|
||||||
|
ArtemisMessagingComponent.PEER_USER,
|
||||||
|
serverKeystore,
|
||||||
|
serverConfig.keyStorePassword,
|
||||||
|
serverTruststore,
|
||||||
|
trace = true)
|
||||||
|
return amqpServer
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,309 @@
|
|||||||
|
package net.corda.node.amqp
|
||||||
|
|
||||||
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
|
import io.netty.channel.EventLoopGroup
|
||||||
|
import io.netty.channel.nio.NioEventLoopGroup
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.internal.div
|
||||||
|
import net.corda.core.node.services.NetworkMapCache
|
||||||
|
import net.corda.core.toFuture
|
||||||
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
|
import net.corda.node.internal.protonwrapper.messages.MessageStatus
|
||||||
|
import net.corda.node.internal.protonwrapper.netty.AMQPClient
|
||||||
|
import net.corda.node.internal.protonwrapper.netty.AMQPServer
|
||||||
|
import net.corda.node.internal.security.RPCSecurityManager
|
||||||
|
import net.corda.node.services.config.CertChainPolicyConfig
|
||||||
|
import net.corda.node.services.config.NodeConfiguration
|
||||||
|
import net.corda.node.services.config.configureWithDevSSLCertificate
|
||||||
|
import net.corda.node.services.messaging.ArtemisMessagingClient
|
||||||
|
import net.corda.node.services.messaging.ArtemisMessagingServer
|
||||||
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
|
||||||
|
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||||
|
import net.corda.testing.*
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
|
import org.apache.activemq.artemis.api.core.RoutingType
|
||||||
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import rx.Observable.never
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class ProtonWrapperTests {
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val temporaryFolder = TemporaryFolder()
|
||||||
|
|
||||||
|
private val serverPort = freePort()
|
||||||
|
private val serverPort2 = freePort()
|
||||||
|
private val artemisPort = freePort()
|
||||||
|
|
||||||
|
private abstract class AbstractNodeConfiguration : NodeConfiguration
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Simple AMPQ Client to Server`() {
|
||||||
|
val amqpServer = createServer(serverPort)
|
||||||
|
amqpServer.use {
|
||||||
|
amqpServer.start()
|
||||||
|
val receiveSubs = amqpServer.onReceive.subscribe {
|
||||||
|
assertEquals(BOB_NAME.toString(), it.sourceLegalName)
|
||||||
|
assertEquals("p2p.inbound", it.topic)
|
||||||
|
assertEquals("Test", String(it.payload))
|
||||||
|
it.complete(true)
|
||||||
|
}
|
||||||
|
val amqpClient = createClient()
|
||||||
|
amqpClient.use {
|
||||||
|
val serverConnected = amqpServer.onConnection.toFuture()
|
||||||
|
val clientConnected = amqpClient.onConnection.toFuture()
|
||||||
|
amqpClient.start()
|
||||||
|
val serverConnect = serverConnected.get()
|
||||||
|
assertEquals(true, serverConnect.connected)
|
||||||
|
assertEquals(BOB_NAME, CordaX500Name.parse(serverConnect.remoteCert!!.subject.toString()))
|
||||||
|
val clientConnect = clientConnected.get()
|
||||||
|
assertEquals(true, clientConnect.connected)
|
||||||
|
assertEquals(ALICE_NAME, CordaX500Name.parse(clientConnect.remoteCert!!.subject.toString()))
|
||||||
|
val msg = amqpClient.createMessage("Test".toByteArray(),
|
||||||
|
"p2p.inbound",
|
||||||
|
ALICE_NAME.toString(),
|
||||||
|
emptyMap())
|
||||||
|
amqpClient.write(msg)
|
||||||
|
assertEquals(MessageStatus.Acknowledged, msg.onComplete.get())
|
||||||
|
receiveSubs.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `AMPQ Client refuses to connect to unexpected server`() {
|
||||||
|
val amqpServer = createServer(serverPort, CordaX500Name("Rogue 1", "London", "GB"))
|
||||||
|
amqpServer.use {
|
||||||
|
amqpServer.start()
|
||||||
|
val amqpClient = createClient()
|
||||||
|
amqpClient.use {
|
||||||
|
val clientConnected = amqpClient.onConnection.toFuture()
|
||||||
|
amqpClient.start()
|
||||||
|
val clientConnect = clientConnected.get()
|
||||||
|
assertEquals(false, clientConnect.connected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Client Failover for multiple IP`() {
|
||||||
|
val amqpServer = createServer(serverPort)
|
||||||
|
val amqpServer2 = createServer(serverPort2)
|
||||||
|
val amqpClient = createClient()
|
||||||
|
try {
|
||||||
|
val serverConnected = amqpServer.onConnection.toFuture()
|
||||||
|
val serverConnected2 = amqpServer2.onConnection.toFuture()
|
||||||
|
val clientConnected = amqpClient.onConnection.toBlocking().iterator
|
||||||
|
amqpServer.start()
|
||||||
|
amqpClient.start()
|
||||||
|
val serverConn1 = serverConnected.get()
|
||||||
|
assertEquals(true, serverConn1.connected)
|
||||||
|
assertEquals(BOB_NAME, CordaX500Name.parse(serverConn1.remoteCert!!.subject.toString()))
|
||||||
|
val connState1 = clientConnected.next()
|
||||||
|
assertEquals(true, connState1.connected)
|
||||||
|
assertEquals(ALICE_NAME, CordaX500Name.parse(connState1.remoteCert!!.subject.toString()))
|
||||||
|
assertEquals(serverPort, connState1.remoteAddress.port)
|
||||||
|
|
||||||
|
// Fail over
|
||||||
|
amqpServer2.start()
|
||||||
|
amqpServer.stop()
|
||||||
|
val connState2 = clientConnected.next()
|
||||||
|
assertEquals(false, connState2.connected)
|
||||||
|
assertEquals(serverPort, connState2.remoteAddress.port)
|
||||||
|
val serverConn2 = serverConnected2.get()
|
||||||
|
assertEquals(true, serverConn2.connected)
|
||||||
|
assertEquals(BOB_NAME, CordaX500Name.parse(serverConn2.remoteCert!!.subject.toString()))
|
||||||
|
val connState3 = clientConnected.next()
|
||||||
|
assertEquals(true, connState3.connected)
|
||||||
|
assertEquals(ALICE_NAME, CordaX500Name.parse(connState3.remoteCert!!.subject.toString()))
|
||||||
|
assertEquals(serverPort2, connState3.remoteAddress.port)
|
||||||
|
|
||||||
|
// Fail back
|
||||||
|
amqpServer.start()
|
||||||
|
amqpServer2.stop()
|
||||||
|
val connState4 = clientConnected.next()
|
||||||
|
assertEquals(false, connState4.connected)
|
||||||
|
assertEquals(serverPort2, connState4.remoteAddress.port)
|
||||||
|
val serverConn3 = serverConnected.get()
|
||||||
|
assertEquals(true, serverConn3.connected)
|
||||||
|
assertEquals(BOB_NAME, CordaX500Name.parse(serverConn3.remoteCert!!.subject.toString()))
|
||||||
|
val connState5 = clientConnected.next()
|
||||||
|
assertEquals(true, connState5.connected)
|
||||||
|
assertEquals(ALICE_NAME, CordaX500Name.parse(connState5.remoteCert!!.subject.toString()))
|
||||||
|
assertEquals(serverPort, connState5.remoteAddress.port)
|
||||||
|
} finally {
|
||||||
|
amqpClient.close()
|
||||||
|
amqpServer.close()
|
||||||
|
amqpServer2.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Send a message from AMQP to Artemis inbox`() {
|
||||||
|
val (server, artemisClient) = createArtemisServerAndClient()
|
||||||
|
val amqpClient = createClient()
|
||||||
|
val clientConnected = amqpClient.onConnection.toFuture()
|
||||||
|
amqpClient.start()
|
||||||
|
assertEquals(true, clientConnected.get().connected)
|
||||||
|
assertEquals(CHARLIE_NAME, CordaX500Name.parse(clientConnected.get().remoteCert!!.subject.toString()))
|
||||||
|
val artemis = artemisClient.started!!
|
||||||
|
val sendAddress = "p2p.inbound"
|
||||||
|
artemis.session.createQueue(sendAddress, RoutingType.MULTICAST, "queue", true)
|
||||||
|
val consumer = artemis.session.createConsumer("queue")
|
||||||
|
val testData = "Test".toByteArray()
|
||||||
|
val testProperty = mutableMapOf<Any?, Any?>()
|
||||||
|
testProperty["TestProp"] = "1"
|
||||||
|
val message = amqpClient.createMessage(testData, sendAddress, CHARLIE_NAME.toString(), testProperty)
|
||||||
|
amqpClient.write(message)
|
||||||
|
assertEquals(MessageStatus.Acknowledged, message.onComplete.get())
|
||||||
|
val received = consumer.receive()
|
||||||
|
assertEquals("1", received.getStringProperty("TestProp"))
|
||||||
|
assertArrayEquals(testData, ByteArray(received.bodySize).apply { received.bodyBuffer.readBytes(this) })
|
||||||
|
amqpClient.stop()
|
||||||
|
artemisClient.stop()
|
||||||
|
server.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `shared AMQPClient threadpool tests`() {
|
||||||
|
val amqpServer = createServer(serverPort)
|
||||||
|
amqpServer.use {
|
||||||
|
val connectionEvents = amqpServer.onConnection.toBlocking().iterator
|
||||||
|
amqpServer.start()
|
||||||
|
val sharedThreads = NioEventLoopGroup()
|
||||||
|
val amqpClient1 = createSharedThreadsClient(sharedThreads, 0)
|
||||||
|
val amqpClient2 = createSharedThreadsClient(sharedThreads, 1)
|
||||||
|
amqpClient1.start()
|
||||||
|
val connection1 = connectionEvents.next()
|
||||||
|
assertEquals(true, connection1.connected)
|
||||||
|
val connection1ID = CordaX500Name.parse(connection1.remoteCert!!.subject.toString())
|
||||||
|
assertEquals("client 0", connection1ID.organisationUnit)
|
||||||
|
val source1 = connection1.remoteAddress
|
||||||
|
amqpClient2.start()
|
||||||
|
val connection2 = connectionEvents.next()
|
||||||
|
assertEquals(true, connection2.connected)
|
||||||
|
val connection2ID = CordaX500Name.parse(connection2.remoteCert!!.subject.toString())
|
||||||
|
assertEquals("client 1", connection2ID.organisationUnit)
|
||||||
|
val source2 = connection2.remoteAddress
|
||||||
|
// Stopping one shouldn't disconnect the other
|
||||||
|
amqpClient1.stop()
|
||||||
|
val connection3 = connectionEvents.next()
|
||||||
|
assertEquals(false, connection3.connected)
|
||||||
|
assertEquals(source1, connection3.remoteAddress)
|
||||||
|
assertEquals(false, amqpClient1.connected)
|
||||||
|
assertEquals(true, amqpClient2.connected)
|
||||||
|
// Now shutdown both
|
||||||
|
amqpClient2.stop()
|
||||||
|
val connection4 = connectionEvents.next()
|
||||||
|
assertEquals(false, connection4.connected)
|
||||||
|
assertEquals(source2, connection4.remoteAddress)
|
||||||
|
assertEquals(false, amqpClient1.connected)
|
||||||
|
assertEquals(false, amqpClient2.connected)
|
||||||
|
// Now restarting one should work
|
||||||
|
amqpClient1.start()
|
||||||
|
val connection5 = connectionEvents.next()
|
||||||
|
assertEquals(true, connection5.connected)
|
||||||
|
val connection5ID = CordaX500Name.parse(connection5.remoteCert!!.subject.toString())
|
||||||
|
assertEquals("client 0", connection5ID.organisationUnit)
|
||||||
|
assertEquals(true, amqpClient1.connected)
|
||||||
|
assertEquals(false, amqpClient2.connected)
|
||||||
|
// Cleanup
|
||||||
|
amqpClient1.stop()
|
||||||
|
sharedThreads.shutdownGracefully()
|
||||||
|
sharedThreads.terminationFuture().sync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createArtemisServerAndClient(): Pair<ArtemisMessagingServer, ArtemisMessagingClient> {
|
||||||
|
val artemisConfig = rigorousMock<AbstractNodeConfiguration>().also {
|
||||||
|
doReturn(temporaryFolder.root.toPath() / "artemis").whenever(it).baseDirectory
|
||||||
|
doReturn(CHARLIE_NAME).whenever(it).myLegalName
|
||||||
|
doReturn("trustpass").whenever(it).trustStorePassword
|
||||||
|
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||||
|
doReturn("").whenever(it).exportJMXto
|
||||||
|
doReturn(emptyList<CertChainPolicyConfig>()).whenever(it).certificateChainCheckPolicies
|
||||||
|
doReturn(true).whenever(it).useAMQPBridges
|
||||||
|
}
|
||||||
|
artemisConfig.configureWithDevSSLCertificate()
|
||||||
|
|
||||||
|
val networkMap = rigorousMock<NetworkMapCache>().also {
|
||||||
|
doReturn(never<NetworkMapCache.MapChange>()).whenever(it).changed
|
||||||
|
}
|
||||||
|
val userService = rigorousMock<RPCSecurityManager>()
|
||||||
|
val server = ArtemisMessagingServer(artemisConfig, artemisPort, null, networkMap, userService, MAX_MESSAGE_SIZE)
|
||||||
|
val client = ArtemisMessagingClient(artemisConfig, NetworkHostAndPort("localhost", artemisPort), MAX_MESSAGE_SIZE)
|
||||||
|
server.start()
|
||||||
|
client.start()
|
||||||
|
return Pair(server, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createClient(): AMQPClient {
|
||||||
|
val clientConfig = rigorousMock<AbstractNodeConfiguration>().also {
|
||||||
|
doReturn(temporaryFolder.root.toPath() / "client").whenever(it).baseDirectory
|
||||||
|
doReturn(BOB_NAME).whenever(it).myLegalName
|
||||||
|
doReturn("trustpass").whenever(it).trustStorePassword
|
||||||
|
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||||
|
}
|
||||||
|
clientConfig.configureWithDevSSLCertificate()
|
||||||
|
|
||||||
|
val clientTruststore = loadKeyStore(clientConfig.trustStoreFile, clientConfig.trustStorePassword)
|
||||||
|
val clientKeystore = loadKeyStore(clientConfig.sslKeystore, clientConfig.keyStorePassword)
|
||||||
|
val amqpClient = AMQPClient(listOf(NetworkHostAndPort("localhost", serverPort),
|
||||||
|
NetworkHostAndPort("localhost", serverPort2),
|
||||||
|
NetworkHostAndPort("localhost", artemisPort)),
|
||||||
|
setOf(ALICE_NAME, CHARLIE_NAME),
|
||||||
|
PEER_USER,
|
||||||
|
PEER_USER,
|
||||||
|
clientKeystore,
|
||||||
|
clientConfig.keyStorePassword,
|
||||||
|
clientTruststore, true)
|
||||||
|
return amqpClient
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSharedThreadsClient(sharedEventGroup: EventLoopGroup, id: Int): AMQPClient {
|
||||||
|
val clientConfig = rigorousMock<AbstractNodeConfiguration>().also {
|
||||||
|
doReturn(temporaryFolder.root.toPath() / "client_%$id").whenever(it).baseDirectory
|
||||||
|
doReturn(CordaX500Name(null, "client $id", "Corda", "London", null, "GB")).whenever(it).myLegalName
|
||||||
|
doReturn("trustpass").whenever(it).trustStorePassword
|
||||||
|
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||||
|
}
|
||||||
|
clientConfig.configureWithDevSSLCertificate()
|
||||||
|
|
||||||
|
val clientTruststore = loadKeyStore(clientConfig.trustStoreFile, clientConfig.trustStorePassword)
|
||||||
|
val clientKeystore = loadKeyStore(clientConfig.sslKeystore, clientConfig.keyStorePassword)
|
||||||
|
val amqpClient = AMQPClient(listOf(NetworkHostAndPort("localhost", serverPort)),
|
||||||
|
setOf(ALICE_NAME),
|
||||||
|
PEER_USER,
|
||||||
|
PEER_USER,
|
||||||
|
clientKeystore,
|
||||||
|
clientConfig.keyStorePassword,
|
||||||
|
clientTruststore, true, sharedEventGroup)
|
||||||
|
return amqpClient
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createServer(port: Int, name: CordaX500Name = ALICE_NAME): AMQPServer {
|
||||||
|
val serverConfig = rigorousMock<AbstractNodeConfiguration>().also {
|
||||||
|
doReturn(temporaryFolder.root.toPath() / "server").whenever(it).baseDirectory
|
||||||
|
doReturn(name).whenever(it).myLegalName
|
||||||
|
doReturn("trustpass").whenever(it).trustStorePassword
|
||||||
|
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||||
|
}
|
||||||
|
serverConfig.configureWithDevSSLCertificate()
|
||||||
|
|
||||||
|
val serverTruststore = loadKeyStore(serverConfig.trustStoreFile, serverConfig.trustStorePassword)
|
||||||
|
val serverKeystore = loadKeyStore(serverConfig.sslKeystore, serverConfig.keyStorePassword)
|
||||||
|
val amqpServer = AMQPServer("0.0.0.0",
|
||||||
|
port,
|
||||||
|
PEER_USER,
|
||||||
|
PEER_USER,
|
||||||
|
serverKeystore,
|
||||||
|
serverConfig.keyStorePassword,
|
||||||
|
serverTruststore)
|
||||||
|
return amqpServer
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -24,15 +24,21 @@ import net.corda.testing.*
|
|||||||
import net.corda.testing.driver.DriverDSL
|
import net.corda.testing.driver.DriverDSL
|
||||||
import net.corda.testing.driver.NodeHandle
|
import net.corda.testing.driver.NodeHandle
|
||||||
import net.corda.testing.driver.driver
|
import net.corda.testing.driver.driver
|
||||||
|
import net.corda.testing.internal.withoutTestSerialization
|
||||||
import net.corda.testing.services.MockAttachmentStorage
|
import net.corda.testing.services.MockAttachmentStorage
|
||||||
|
import net.corda.testing.internal.rigorousMock
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.ClassRule
|
import org.junit.ClassRule
|
||||||
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
class AttachmentLoadingTests : IntegrationTest() {
|
class AttachmentLoadingTests : IntegrationTest() {
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val testSerialization = SerializationEnvironmentRule()
|
||||||
private val attachments = MockAttachmentStorage()
|
private val attachments = MockAttachmentStorage()
|
||||||
private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), attachments)
|
private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), attachments)
|
||||||
private val cordapp get() = provider.cordapps.first()
|
private val cordapp get() = provider.cordapps.first()
|
||||||
@ -83,7 +89,7 @@ class AttachmentLoadingTests : IntegrationTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test a wire transaction has loaded the correct attachment`() = withTestSerialization {
|
fun `test a wire transaction has loaded the correct attachment`() {
|
||||||
val appClassLoader = appContext.classLoader
|
val appClassLoader = appContext.classLoader
|
||||||
val contractClass = appClassLoader.loadClass(ISOLATED_CONTRACT_ID).asSubclass(Contract::class.java)
|
val contractClass = appClassLoader.loadClass(ISOLATED_CONTRACT_ID).asSubclass(Contract::class.java)
|
||||||
val generateInitialMethod = contractClass.getDeclaredMethod("generateInitial", PartyAndReference::class.java, Integer.TYPE, Party::class.java)
|
val generateInitialMethod = contractClass.getDeclaredMethod("generateInitial", PartyAndReference::class.java, Integer.TYPE, Party::class.java)
|
||||||
@ -99,7 +105,7 @@ class AttachmentLoadingTests : IntegrationTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test that attachments retrieved over the network are not used for code`() {
|
fun `test that attachments retrieved over the network are not used for code`() = withoutTestSerialization {
|
||||||
driver {
|
driver {
|
||||||
installIsolatedCordappTo(bankAName)
|
installIsolatedCordappTo(bankAName)
|
||||||
val (bankA, bankB) = createTwoNodes()
|
val (bankA, bankB) = createTwoNodes()
|
||||||
@ -107,15 +113,17 @@ class AttachmentLoadingTests : IntegrationTest() {
|
|||||||
bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow()
|
bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `tests that if the attachment is loaded on both sides already that a flow can run`() {
|
fun `tests that if the attachment is loaded on both sides already that a flow can run`() = withoutTestSerialization {
|
||||||
driver {
|
driver {
|
||||||
installIsolatedCordappTo(bankAName)
|
installIsolatedCordappTo(bankAName)
|
||||||
installIsolatedCordappTo(bankBName)
|
installIsolatedCordappTo(bankBName)
|
||||||
val (bankA, bankB) = createTwoNodes()
|
val (bankA, bankB) = createTwoNodes()
|
||||||
bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow()
|
bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow()
|
||||||
}
|
}
|
||||||
|
Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,10 +24,12 @@ import net.corda.node.services.config.BFTSMaRtConfiguration
|
|||||||
import net.corda.node.services.config.NotaryConfig
|
import net.corda.node.services.config.NotaryConfig
|
||||||
import net.corda.node.services.transactions.minClusterSize
|
import net.corda.node.services.transactions.minClusterSize
|
||||||
import net.corda.node.services.transactions.minCorrectReplicas
|
import net.corda.node.services.transactions.minCorrectReplicas
|
||||||
import net.corda.nodeapi.internal.NetworkParametersCopier
|
|
||||||
import net.corda.nodeapi.internal.NotaryInfo
|
|
||||||
import net.corda.nodeapi.internal.ServiceIdentityGenerator
|
import net.corda.nodeapi.internal.ServiceIdentityGenerator
|
||||||
import net.corda.testing.*
|
import net.corda.nodeapi.internal.network.NotaryInfo
|
||||||
|
import net.corda.testing.chooseIdentity
|
||||||
|
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||||
|
import net.corda.testing.IntegrationTest
|
||||||
|
import net.corda.testing.IntegrationTestSchemas
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
||||||
@ -55,7 +57,7 @@ class BFTNotaryServiceTests : IntegrationTest() {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun before() {
|
fun before() {
|
||||||
mockNet = MockNetwork()
|
mockNet = MockNetwork(emptyList())
|
||||||
node = mockNet.createNode()
|
node = mockNet.createNode()
|
||||||
}
|
}
|
||||||
@After
|
@After
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user