Merge remote-tracking branch 'open/master' into colljos-os-merge-rc01

This commit is contained in:
josecoll 2017-12-18 10:24:38 +00:00
commit e9fc6f7c8d
215 changed files with 4624 additions and 1717 deletions

2
.idea/compiler.xml generated
View File

@ -10,6 +10,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_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_test" target="1.8" />
<module name="business-network-demo_integrationTest" target="1.8" />

View File

@ -4,7 +4,7 @@ buildscript {
file("$projectDir/constants.properties").withInputStream { constants.load(it) }
// 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
ext.corda_platform_version = constants.getProperty("platformVersion")
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
@ -20,7 +20,21 @@ buildscript {
ext.capsule_version = '1.0.1'
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.jetty_version = '9.4.7.v20170914'
ext.jersey_version = '2.25'

View File

@ -12,6 +12,7 @@ import net.corda.core.transactions.SignedTransaction
import net.corda.finance.USD
import net.corda.testing.*
import net.corda.testing.contracts.DummyContract
import net.corda.testing.internal.rigorousMock
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -24,7 +25,7 @@ class JacksonSupportTest {
private companion object {
val SEED = BigInteger.valueOf(20170922L)!!
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 MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party
}

View File

@ -31,8 +31,8 @@ import static net.corda.finance.Currencies.DOLLARS;
import static net.corda.finance.contracts.GetBalances.getCashBalance;
import static net.corda.node.services.Permissions.invokeRpc;
import static net.corda.node.services.Permissions.startFlow;
import static net.corda.testing.TestConstants.getALICE_NAME;
import static net.corda.testing.TestConstants.getDUMMY_NOTARY_NAME;
import static net.corda.testing.TestConstants.ALICE_NAME;
import static net.corda.testing.TestConstants.DUMMY_NOTARY_NAME;
public class CordaRPCJavaClientTest extends NodeBasedTest {
public CordaRPCJavaClientTest() {
@ -41,7 +41,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
@ClassRule
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(
startFlow(CashPaymentFlow.class),
@ -65,7 +65,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
@Before
public void setUp() throws Exception {
super.setUp();
node = startNode(getALICE_NAME(), 1, singletonList(rpcUser));
node = startNode(ALICE_NAME, 1, singletonList(rpcUser));
client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress()));
}

View File

@ -1,4 +1,4 @@
package net.corda.testing
package net.corda.client.rpc
import net.corda.core.internal.uncheckedCast
import kotlin.reflect.KCallable

View File

@ -9,9 +9,9 @@ import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.millis
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.rpcDriver
import net.corda.testing.internal.testThreadFactory
import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet
import org.junit.After
import org.junit.Test

View File

@ -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.startTightLoopInjector
import net.corda.testing.node.internal.rpcDriver
import net.corda.testing.measure
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith

View File

@ -1,6 +1,9 @@
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.node.services.Permissions
import net.corda.node.services.messaging.rpcContext
import net.corda.nodeapi.internal.config.User
import net.corda.testing.node.internal.RPCDriverDSL

View File

@ -15,7 +15,7 @@ class SwapIdentitiesFlowTests {
@Before
fun setup() {
// 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

View File

@ -1,4 +1,4 @@
gradlePluginsVersion=3.0.1
gradlePluginsVersion=3.0.2
kotlinVersion=1.1.60
platformVersion=1
guavaVersion=21.0

View File

@ -63,7 +63,7 @@ import javax.crypto.spec.SecretKeySpec
*/
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.
*/
@JvmField
@ -80,7 +80,7 @@ object Crypto {
"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
val ECDSA_SECP256K1_SHA256 = SignatureScheme(
2,
@ -95,7 +95,7 @@ object Crypto {
"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
val ECDSA_SECP256R1_SHA256 = SignatureScheme(
3,
@ -110,7 +110,7 @@ object Crypto {
"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
val EDDSA_ED25519_SHA512 = SignatureScheme(
4,
@ -127,13 +127,15 @@ object Crypto {
"EdDSA signature scheme using the ed25519 twisted Edwards curve."
)
/**
* 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.
*/
/** DLSequence (ASN1Sequence) for SHA512 truncated to 256 bits, used in SPHINCS-256 signature scheme. */
@JvmField
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
val SPHINCS256_SHA256 = SignatureScheme(
5,
@ -149,7 +151,8 @@ object Crypto {
"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
val COMPOSITE_KEY = SignatureScheme(
6,

View File

@ -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(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.
@ -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(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
@ -45,7 +45,8 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) {
* @return whether the signature is correct for this key.
*/
@Throws(InvalidKeyException::class, SignatureException::class)
fun isValid(content: ByteArray) = by.isValid(content, this)
fun withoutKey() : DigitalSignature = DigitalSignature(this.bytes)
fun isValid(content: ByteArray): Boolean = by.isValid(content, this)
fun withoutKey(): DigitalSignature = DigitalSignature(this.bytes)
}
}

View File

@ -2,6 +2,7 @@ package net.corda.core.identity
import com.google.common.collect.ImmutableSet
import net.corda.core.internal.LegalNameValidator
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.x500Name
import net.corda.core.serialization.CordaSerializable
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_COMMON_NAME = 64
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
fun build(principal: X500Principal): CordaX500Name {
val x500Name = X500Name.getInstance(principal.encoded)

View File

@ -12,4 +12,5 @@ import net.corda.core.serialization.CordaSerializable
*/
@CordaSerializable
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")

View File

@ -13,21 +13,22 @@ import org.junit.Test;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import static java.util.Collections.emptyList;
import static net.corda.testing.CoreTestUtils.singleIdentity;
import static net.corda.testing.node.NodeTestUtils.startFlow;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.Assert.fail;
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> bobNode;
private Party bob;
@Before
public void setUp() throws Exception {
aliceNode = mockNet.createPartyNode(TestConstants.getALICE_NAME());
bobNode = mockNet.createPartyNode(TestConstants.getBOB_NAME());
aliceNode = mockNet.createPartyNode(TestConstants.ALICE_NAME);
bobNode = mockNet.createPartyNode(TestConstants.BOB_NAME);
bob = singleIdentity(bobNode.getInfo());
}

View File

@ -3,7 +3,7 @@ package net.corda.core.concurrent
import com.nhaarman.mockito_kotlin.*
import net.corda.core.internal.concurrent.openFuture
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.junit.Test
import org.slf4j.Logger

View File

@ -9,6 +9,7 @@ import net.corda.core.node.ServicesForResolution
import net.corda.testing.*
import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyContractV2
import net.corda.testing.internal.rigorousMock
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals

View File

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

View File

@ -14,6 +14,10 @@ import net.corda.finance.`issued by`
import net.corda.finance.contracts.asset.Cash
import net.corda.node.services.api.IdentityServiceInternal
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.ledger
import org.junit.Before
@ -31,9 +35,9 @@ class PartialMerkleTreeTest {
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
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_PUBKEY get() = miniCorp.pubkey
val MINI_CORP_PUBKEY get() = miniCorp.publicKey
}
@Rule
@ -51,7 +55,7 @@ class PartialMerkleTreeTest {
hashed = nodes.map { it.serialize().sha256() }
expectedRoot = MerkleTree.getMerkleTree(hashed.toMutableList() + listOf(zeroHash, zeroHash)).hash
merkleTree = MerkleTree.getMerkleTree(hashed)
testLedger = MockServices(rigorousMock<IdentityServiceInternal>().also {
testLedger = MockServices(emptyList(), rigorousMock<IdentityServiceInternal>().also {
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
}, MEGA_CORP.name).ledger(DUMMY_NOTARY) {
unverifiedTransaction {

View File

@ -29,7 +29,7 @@ class AttachmentTests {
@Before
fun setUp() {
mockNet = MockNetwork()
mockNet = MockNetwork(emptyList())
}
@After

View File

@ -14,6 +14,7 @@ import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.StartedNode
import net.corda.testing.*
import net.corda.testing.contracts.DummyContract
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockServices
import net.corda.testing.node.startFlow

View File

@ -13,7 +13,7 @@ import org.junit.After
import org.junit.Test
class ReceiveMultipleFlowTests {
private val mockNet = MockNetwork()
private val mockNet = MockNetwork(emptyList())
private val nodes = (0..2).map { mockNet.createPartyNode() }
@After
fun stopNodes() {

View File

@ -4,7 +4,7 @@ import com.nhaarman.mockito_kotlin.*
import net.corda.core.concurrent.CordaFuture
import net.corda.core.internal.join
import net.corda.core.utilities.getOrThrow
import net.corda.testing.rigorousMock
import net.corda.testing.internal.rigorousMock
import org.assertj.core.api.Assertions
import org.junit.Test
import org.slf4j.Logger

View File

@ -70,7 +70,7 @@ class AttachmentSerializationTest {
@Before
fun setUp() {
mockNet = MockNetwork()
mockNet = MockNetwork(emptyList())
server = mockNet.createNode(MockNodeParameters(legalName = ALICE_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.

View File

@ -9,6 +9,7 @@ import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.seconds
import net.corda.finance.POUNDS
import net.corda.testing.*
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices
import org.junit.Before
import org.junit.Rule
@ -25,9 +26,9 @@ class TransactionSerializationTests {
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).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_KEY get() = megaCorp.key
val MEGA_CORP_KEY get() = megaCorp.keyPair
}
@Rule
@ -106,7 +107,7 @@ class TransactionSerializationTests {
Command(TestCash.Commands.Move(), DUMMY_KEY_2.public))
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)
stx.copy(sigs = stx2.sigs).verifyRequiredSignatures()
@ -124,8 +125,7 @@ class TransactionSerializationTests {
@Test
fun storeAndLoadWhenSigning() {
val ptx = megaCorpServices.signInitialTransaction(tx)
ptx.verifySignaturesExcept(notaryServices.key.public)
ptx.verifySignaturesExcept(DUMMY_NOTARY_KEY.public)
val stored = ptx.serialize()
val loaded = stored.deserialize()

View File

@ -10,6 +10,7 @@ import net.corda.core.identity.Party
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.testing.*
import net.corda.testing.contracts.DummyContract
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices
import org.junit.Before
import org.junit.Rule
@ -28,7 +29,7 @@ class LedgerTransactionQueryTests {
@JvmField
val testSerialization = SerializationEnvironmentRule()
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)
}, CordaX500Name("MegaCorp", "London", "GB"), keyPair)
private val identity: Party = services.myInfo.singleIdentity()

View File

@ -12,6 +12,7 @@ import net.corda.finance.`issued by`
import net.corda.finance.contracts.asset.Cash
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.testing.*
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices
import net.corda.testing.node.ledger
import org.junit.Rule
@ -27,7 +28,7 @@ class TransactionEncumbranceTests {
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party
val MEGA_CORP get() = megaCorp.party
val MEGA_CORP_PUBKEY get() = megaCorp.pubkey
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
}
@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)
}, MEGA_CORP.name)

View File

@ -6,6 +6,7 @@ import net.corda.core.crypto.CompositeKey
import net.corda.core.identity.Party
import net.corda.testing.*
import net.corda.testing.contracts.DummyContract
import net.corda.testing.internal.rigorousMock
import org.junit.Rule
import org.junit.Test
import java.math.BigInteger
@ -23,7 +24,7 @@ class TransactionTests {
val BOB = TestIdentity(BOB_NAME, 80).party
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
val DUMMY_NOTARY get() = dummyNotary.party
val DUMMY_NOTARY_KEY get() = dummyNotary.key
val DUMMY_NOTARY_KEY get() = dummyNotary.keyPair
}
@Rule

View File

@ -7,9 +7,10 @@ import com.google.common.collect.testing.features.CollectionSize
import junit.framework.TestSuite
import net.corda.core.serialization.deserialize
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.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Suite
@ -36,6 +37,10 @@ class NonEmptySetTest {
}
class General {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
@Test
fun `copyOf - empty source`() {
assertThatThrownBy { NonEmptySet.copyOf(HashSet<Int>()) }.isInstanceOf(IllegalArgumentException::class.java)
@ -48,11 +53,9 @@ class NonEmptySetTest {
@Test
fun `serialize deserialize`() {
withTestSerialization {
val original = NonEmptySet.of(-17, 22, 17)
val copy = original.serialize().deserialize()
assertThat(copy).isEqualTo(original).isNotSameAs(original)
}
val original = NonEmptySet.of(-17, 22, 17)
val copy = original.serialize().deserialize()
assertThat(copy).isEqualTo(original).isNotSameAs(original)
}
}

View File

@ -1,15 +1,18 @@
API: Contract Constraints
=========================
A basic understanding of contract key concepts, which can be found :doc:`here </key-concepts-contracts>`,
is required reading for this page.
.. note:: Before reading this page, you should be familiar with the key concepts of :doc:`key-concepts-contracts`.
.. contents::
Contract constraints
--------------------
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
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
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
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
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.
@ -137,3 +140,18 @@ Full Nodes
**********
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``).

View File

@ -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
filter criteria:
- Use ``queryBy`` to obtain a only 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 ``queryBy`` to obtain a current snapshot of data (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)

View File

@ -6,6 +6,7 @@ CorDapps
cordapp-overview
writing-a-cordapp
upgrade-notes
cordapp-build-systems
building-against-master
corda-api

View File

@ -6,6 +6,50 @@ from the previous milestone release.
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/ :
* 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.)
@ -45,24 +89,12 @@ UNRELEASED
deploys nodes for development and testing, the latter turns a project into a cordapp project that generates JARs in
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.
* 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
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
to be appended to node.conf.
@ -79,8 +111,8 @@ UNRELEASED
allows services to start flows marked with the ``StartableByService`` annotation. For backwards compatability
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
time-window is open-ended.
* ``TimeWindow`` now has a ``length`` property that returns the length of the time-window as a ``java.time.Duration`` object,
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``
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
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.
* Replaced node configuration parameter ``certificateSigningService`` with ``compatibilityZoneURL``, which is Corda
compatibility zone network management service's address.
* ``DriverDSLExposedInterface`` has been renamed to ``DriverDSL`` and the ``waitForAllNodesToFinish()`` method has instead
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
"TRANSACTION_" prefix, i.e. "NONE", "READ_UNCOMMITTED", etc.
* Peer-to-peer communications is now via AMQP 1.0 as default.
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.

View File

@ -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.
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
@ -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
^^^^^^^^^^^^^^^^^^
@ -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``.
.. 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
-----------
The RPC system handles observables in a special way. When a method returns an observable, whether directly or

View File

@ -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.
: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.
.. 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``.
: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.

View File

@ -10,7 +10,6 @@ Corda nodes
corda-configuration-file
clientrpc
shell
node-auth-config
node-database
node-administration
out-of-process-verification

View File

@ -36,6 +36,7 @@ dependencies {
compile project(':core')
compile project(':client:jfx')
compile project(':node-driver')
compile project(':webserver')
testCompile project(':verifier')
testCompile project(':test-utils')

View File

@ -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.Contract;

View File

@ -1,17 +1,21 @@
package net.corda.docs.java.tutorial.helloworld;
// DOCSTART 01
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.CommandData;
import net.corda.core.flows.*;
import net.corda.core.identity.Party;
import net.corda.core.transactions.SignedTransaction;
import net.corda.core.transactions.TransactionBuilder;
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
@StartableByRPC
public class IOUFlow extends FlowLogic<Void> {

View File

@ -1,13 +1,15 @@
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.identity.AbstractParty;
import net.corda.core.identity.Party;
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 {
private final int value;
private final Party lender;

View File

@ -3,48 +3,33 @@ package net.corda.docs.java.tutorial.testdsl;
import kotlin.Unit;
import net.corda.core.contracts.PartyAndReference;
import net.corda.core.identity.CordaX500Name;
import net.corda.core.identity.Party;
import net.corda.finance.contracts.ICommercialPaperState;
import net.corda.finance.contracts.JavaCommercialPaper;
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.TestIdentity;
import org.junit.Rule;
import org.junit.Test;
import java.security.PublicKey;
import java.time.temporal.ChronoUnit;
import static java.util.Collections.emptyList;
import static net.corda.core.crypto.Crypto.generateKeyPair;
import static net.corda.finance.Currencies.DOLLARS;
import static net.corda.finance.Currencies.issuedBy;
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.transaction;
import static net.corda.testing.CoreTestUtils.rigorousMock;
import static net.corda.testing.TestConstants.*;
import static org.mockito.Mockito.doReturn;
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 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 Party DUMMY_NOTARY = new TestIdentity(getDUMMY_NOTARY_NAME(), 20L).getParty();
@Rule
public final SerializationEnvironmentRule testSerialization = new SerializationEnvironmentRule();
private final byte[] defaultRef = {123};
private final MockServices ledgerServices;
{
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());
}
private final MockServices ledgerServices = new MockServices(emptyList(), makeTestIdentityService(MEGA_CORP.getIdentity()));
// DOCSTART 1
private ICommercialPaperState getPaper() {
@ -52,7 +37,7 @@ public class CommercialPaperTest {
MEGA_CORP.ref(defaultRef),
MEGA_CORP.getParty(),
issuedBy(DOLLARS(1000), MEGA_CORP.ref(defaultRef)),
getTEST_TX_TIME().plus(7, ChronoUnit.DAYS)
TEST_TX_TIME.plus(7, ChronoUnit.DAYS)
);
}
// DOCEND 1
@ -61,7 +46,7 @@ public class CommercialPaperTest {
@Test
public void simpleCP() {
ICommercialPaperState inState = getPaper();
ledger(ledgerServices, DUMMY_NOTARY, l -> {
ledger(ledgerServices, l -> {
l.transaction(tx -> {
tx.attachments(JCP_PROGRAM_ID);
tx.input(JCP_PROGRAM_ID, inState);
@ -76,10 +61,10 @@ public class CommercialPaperTest {
@Test
public void simpleCPMove() {
ICommercialPaperState inState = getPaper();
ledger(ledgerServices, DUMMY_NOTARY, l -> {
ledger(ledgerServices, l -> {
l.transaction(tx -> {
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);
return tx.verifies();
});
@ -92,10 +77,10 @@ public class CommercialPaperTest {
@Test
public void simpleCPMoveFails() {
ICommercialPaperState inState = getPaper();
ledger(ledgerServices, DUMMY_NOTARY, l -> {
ledger(ledgerServices, l -> {
l.transaction(tx -> {
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);
return tx.failsWith("the state is propagated");
});
@ -108,10 +93,10 @@ public class CommercialPaperTest {
@Test
public void simpleCPMoveSuccess() {
ICommercialPaperState inState = getPaper();
ledger(ledgerServices, DUMMY_NOTARY, l -> {
ledger(ledgerServices, l -> {
l.transaction(tx -> {
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.failsWith("the state is propagated");
tx.output(JCP_PROGRAM_ID, "alice's paper", inState.withOwner(ALICE.getParty()));
@ -125,17 +110,17 @@ public class CommercialPaperTest {
// DOCSTART 6
@Test
public void simpleIssuanceWithTweak() {
ledger(ledgerServices, DUMMY_NOTARY, l -> {
ledger(ledgerServices, l -> {
l.transaction(tx -> {
tx.output(JCP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp.
tx.attachments(JCP_PROGRAM_ID);
tx.tweak(tw -> {
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");
});
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Issue());
tx.timeWindow(getTEST_TX_TIME());
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue());
tx.timeWindow(TEST_TX_TIME);
return tx.verifies();
});
return Unit.INSTANCE;
@ -146,16 +131,16 @@ public class CommercialPaperTest {
// DOCSTART 7
@Test
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.attachments(JCP_PROGRAM_ID);
tx.tweak(tw -> {
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");
});
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Issue());
tx.timeWindow(getTEST_TX_TIME());
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Issue());
tx.timeWindow(TEST_TX_TIME);
return tx.verifies();
});
}
@ -165,7 +150,7 @@ public class CommercialPaperTest {
@Test
public void chainCommercialPaper() {
PartyAndReference issuer = MEGA_CORP.ref(defaultRef);
ledger(ledgerServices, DUMMY_NOTARY, l -> {
ledger(ledgerServices, l -> {
l.unverifiedTransaction(tx -> {
tx.output(Cash.PROGRAM_ID, "alice's $900",
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.
l.transaction("Issuance", tx -> {
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.timeWindow(getTEST_TX_TIME());
tx.timeWindow(TEST_TX_TIME);
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()));
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(ALICE.getParty()));
tx.command(ALICE.getPubkey(), new Cash.Commands.Move());
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move());
tx.command(ALICE.getPublicKey(), new Cash.Commands.Move());
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move());
return tx.verifies();
});
return Unit.INSTANCE;
@ -201,7 +186,7 @@ public class CommercialPaperTest {
@Test
public void chainCommercialPaperDoubleSpend() {
PartyAndReference issuer = MEGA_CORP.ref(defaultRef);
ledger(ledgerServices, DUMMY_NOTARY, l -> {
ledger(ledgerServices, l -> {
l.unverifiedTransaction(tx -> {
tx.output(Cash.PROGRAM_ID, "alice's $900",
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.
l.transaction("Issuance", tx -> {
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.timeWindow(getTEST_TX_TIME());
tx.timeWindow(TEST_TX_TIME);
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()));
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(ALICE.getParty()));
tx.command(ALICE.getPubkey(), new Cash.Commands.Move());
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move());
tx.command(ALICE.getPublicKey(), new Cash.Commands.Move());
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move());
return tx.verifies();
});
@ -234,7 +219,7 @@ public class CommercialPaperTest {
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
// We moved a paper to other pubkey.
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();
});
l.fails();
@ -247,7 +232,7 @@ public class CommercialPaperTest {
@Test
public void chainCommercialPaperTweak() {
PartyAndReference issuer = MEGA_CORP.ref(defaultRef);
ledger(ledgerServices, DUMMY_NOTARY, l -> {
ledger(ledgerServices, l -> {
l.unverifiedTransaction(tx -> {
tx.output(Cash.PROGRAM_ID, "alice's $900",
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.
l.transaction("Issuance", tx -> {
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.timeWindow(getTEST_TX_TIME());
tx.timeWindow(TEST_TX_TIME);
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()));
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(ALICE.getParty()));
tx.command(ALICE.getPubkey(), new Cash.Commands.Move());
tx.command(MEGA_CORP.getPubkey(), new JavaCommercialPaper.Commands.Move());
tx.command(ALICE.getPublicKey(), new Cash.Commands.Move());
tx.command(MEGA_CORP.getPublicKey(), new JavaCommercialPaper.Commands.Move());
return tx.verifies();
});
@ -281,7 +266,7 @@ public class CommercialPaperTest {
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
// We moved a paper to another pubkey.
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();
});
lw.fails();

View File

@ -1,20 +1,25 @@
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.CommandWithParties;
import net.corda.core.contracts.Contract;
import net.corda.core.identity.Party;
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.util.List;
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
import static net.corda.core.contracts.ContractsDSL.requireThat;
// Replace TemplateContract's definition with:
public class IOUContract implements Contract {
public static final String IOU_CONTRACT_ID = "com.template.IOUContract";
// Our Create command.
public static class Create implements CommandData {
}

View File

@ -45,15 +45,14 @@ public class IOUFlow extends FlowLogic<Void> {
// We retrieve the notary identity from the network map.
final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
// DOCSTART 02
// We create a transaction builder.
final TransactionBuilder txBuilder = new TransactionBuilder();
txBuilder.setNotary(notary);
// DOCSTART 02
// We create the transaction components.
IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty);
String outputContract = IOUContract.class.getName();
StateAndContract outputContractAndState = new StateAndContract(outputState, outputContract);
StateAndContract outputContractAndState = new StateAndContract(outputState, IOUContract.IOU_CONTRACT_ID);
List<PublicKey> requiredSigners = ImmutableList.of(getOurIdentity().getOwningKey(), otherParty.getOwningKey());
Command cmd = new Command<>(new IOUContract.Create(), requiredSigners);

View File

@ -1,16 +1,16 @@
package net.corda.docs.java.tutorial.twoparty;
// DOCSTART 01
// Add these imports:
import co.paralleluniverse.fibers.Suspendable;
import net.corda.core.contracts.ContractState;
import net.corda.core.flows.*;
import net.corda.core.transactions.SignedTransaction;
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;
// Define IOUFlowResponder:
@InitiatedBy(IOUFlow.class)
public class IOUFlowResponder extends FlowLogic<Void> {
private final FlowSession otherPartySession;

View File

@ -1,16 +1,19 @@
package net.corda.docs.tutorial.helloworld
// DOCSTART 01
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Command
import net.corda.core.flows.FinalityFlow
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
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.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker
// Replace TemplateFlow's definition with:
@InitiatingFlow
@StartableByRPC
class IOUFlow(val iouValue: Int,

View File

@ -1,9 +1,12 @@
package net.corda.docs.tutorial.helloworld
// DOCSTART 01
import net.corda.core.contracts.ContractState
// DOCSTART 01
// Add these imports:
import net.corda.core.identity.Party
// Replace TemplateState's definition with:
class IOUState(val value: Int,
val lender: Party,
val borrower: Party) : ContractState {

View File

@ -67,7 +67,7 @@ class TutorialMockNetwork {
@Before
fun setUp() {
mockNet = MockNetwork()
mockNet = MockNetwork(emptyList())
nodeA = mockNet.createPartyNode()
nodeB = mockNet.createPartyNode()
nodeB.registerInitiatedFlow(FlowB::class.java)

View File

@ -14,6 +14,7 @@ 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.*
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices
import net.corda.testing.node.ledger
import net.corda.testing.node.transaction
@ -28,15 +29,15 @@ class CommercialPaperTest {
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
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_PUBKEY get() = megaCorp.pubkey
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
}
@Rule
@JvmField
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(null).whenever(it).partyFromKey(BIG_CORP_PUBKEY)
doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY)

View File

@ -1,12 +1,16 @@
package net.corda.docs.tutorial.twoparty
// DOCSTART 01
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.Contract
import net.corda.core.contracts.requireSingleCommand
import net.corda.core.contracts.requireThat
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 {
// Our Create command.
class Create : CommandData
@ -20,7 +24,7 @@ class IOUContract : Contract {
"There should be one output state of type IOUState." using (tx.outputs.size == 1)
// 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 lender and the borrower cannot be the same entity." using (out.lender != out.borrower)

View File

@ -6,9 +6,17 @@ import net.corda.core.contracts.Command
import net.corda.core.contracts.StateAndContract
import net.corda.core.flows.*
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.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
@InitiatingFlow
@ -25,14 +33,13 @@ class IOUFlow(val iouValue: Int,
// We retrieve the notary identity from the network map.
val notary = serviceHub.networkMapCache.notaryIdentities[0]
// We create a transaction builder
// DOCSTART 02
// We create a transaction builder.
val txBuilder = TransactionBuilder(notary = notary)
// DOCSTART 02
// We create the transaction components.
val outputState = IOUState(iouValue, ourIdentity, otherParty)
val outputContract = IOUContract::class.jvmName
val outputContractAndState = StateAndContract(outputState, outputContract)
val outputContractAndState = StateAndContract(outputState, IOU_CONTRACT_ID)
val cmd = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey))
// We add the items to the builder.

View File

@ -1,16 +1,19 @@
package net.corda.docs.tutorial.twoparty
// DOCSTART 01
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.requireThat
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy
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.IOUState
// DOCSTART 01
// Add these imports:
import net.corda.core.contracts.requireThat
import net.corda.core.transactions.SignedTransaction
// Define IOUFlowResponder:
@InitiatedBy(IOUFlow::class)
class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
@Suspendable

View File

@ -6,8 +6,8 @@
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 node, we allow the node to handle new business processes. The flow we define will allow a node to issue an
A flow encodes a sequence of steps that a node can perform to achieve a specific ledger update. By installing new flows
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.
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``.
Let's define our ``IOUFlow`` in either ``TemplateFlow.java`` or ``App.kt``. Delete both the existing flows in the
template, and replace them with the following:
Let's define our ``IOUFlow`` in either ``TemplateFlow.java`` or ``App.kt``. Delete the two existing flows in the
template (``Initiator`` and ``Responder``), and replace them with the following:
.. container:: codeset

View File

@ -107,9 +107,15 @@ commands.
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
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:
.. container:: codeset
.. code-block:: java
run vaultQuery contractStateType: com.template.state.IOUState
.. code-block:: kotlin
.. code-block:: base
run vaultQuery contractStateType: com.template.IOUState
@ -174,6 +174,11 @@ parts:
* The ``IOUState``, representing IOUs on the 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
----------
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
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.

View File

@ -24,7 +24,6 @@ interface is defined as follows:
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
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
-----------------
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``:
.. container:: codeset

View File

@ -41,34 +41,33 @@ https://docs.corda.net/tutorial-cordapp.html#opening-the-example-cordapp-in-inte
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
need to modify two files. For Kotlin, we'll simply be modifying the ``App.kt`` file:
The template has a number of files, but we can ignore most of them. We will only be modifying the following files:
.. container:: codeset
.. code-block:: java
// 1. The state
src/main/java/com/template/TemplateState.java
cordapp-contracts-states/src/main/java/com/template/TemplateState.java
// 2. The flow
src/main/java/com/template/TemplateFlow.java
cordapp/src/main/java/com/template/TemplateFlow.java
.. 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
--------
To prevent build errors later on, we should delete the following files before we begin:
* Java:
* ``src/main/java/com/template/TemplateClient.java``
* ``src/test/java/com/template/FlowTests.java``
* Java: ``cordapp/src/main/java/com/template/TemplateClient.java``
* Kotlin:
* ``src/main/kotlin/com/template/TemplateClient.kt``
* ``src/test/kotlin/com/template/FlowTests.kt``
* Kotlin: ``cordapp/src/main/kotlin/com/template/TemplateClient.kt``
Progress so far
---------------

View File

@ -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.
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,
@ -13,7 +13,7 @@ Protocol Design
---------------
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.
@ -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 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``.
@ -34,13 +45,13 @@ Network Map service REST API:
+----------------+-----------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
| 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.
@ -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.
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.
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.

View File

@ -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.

View File

@ -32,7 +32,13 @@ A Corda network has three types of certificate authorities (CAs):
* The **node CAs**
* 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
-------------------------------
@ -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 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
----------------------------------------------

View File

@ -6,6 +6,5 @@ Release process
release-notes
changelog
upgrade-notes
codestyle
testing

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -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
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.
* 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
double-spend or not
* 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.
* ``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
~~~~~~~~~~~~~~~~~~
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 should see a banner, some log lines and eventually ``Node started up and registered``, indicating that the node is fully started.
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.
.. 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
are by default redirected to files in ``NODE_DIRECTORY/logs/``.
Connecting to the nodes
~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -77,7 +77,7 @@ We can picture this transaction as follows:
Defining IOUContract
--------------------
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

View File

@ -17,7 +17,7 @@ We'll do this by modifying the flow we wrote in the previous tutorial.
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
@ -31,7 +31,8 @@ In ``IOUFlow.java``/``IOUFlow.kt``, change the imports block to the following:
:start-after: DOCSTART 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
@ -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
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
`here <https://www.corda.net/samples/>`_. As you write CorDapps, you'll also want to learn more about the
:doc:`Corda API <corda-api>`.

View File

@ -1,11 +1,23 @@
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::
:maxdepth: 1
hello-world-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-test-dsl
contract-upgrade

View File

@ -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
first public Beta (:ref:`Milestone 12 <changelog_m12>`), to :ref:`V1.0 <changelog_v1>`.

View File

@ -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
flow protocol between an initiating flow and it's intiated flow changes from one CorDapp release to the next in such as
way to be backwards incompatible with existing flows. For example, if a sequence of sends and receives needs to change
flow protocol between an initiating flow and its initiated flow changes from one CorDapp release to the next in such a
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.
The ``InitiatingFlow`` annotation (see :doc:`flow-state-machine` for more information on the flow annotations) has a ``version``

View File

@ -9,6 +9,10 @@ import net.corda.finance.contracts.Frequency
import net.corda.finance.contracts.Tenor
import net.corda.node.services.api.IdentityServiceInternal
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.transaction
import org.junit.Ignore

View File

@ -12,51 +12,52 @@ import net.corda.testing.node.MockServices;
import org.junit.Rule;
import org.junit.Test;
import static java.util.Collections.emptyList;
import static net.corda.finance.Currencies.DOLLARS;
import static net.corda.finance.Currencies.issuedBy;
import static net.corda.testing.node.NodeTestUtils.transaction;
import static net.corda.testing.CoreTestUtils.rigorousMock;
import static net.corda.testing.TestConstants.getDUMMY_NOTARY_NAME;
import static net.corda.testing.internal.InternalTestUtilsKt.rigorousMock;
import static net.corda.testing.TestConstants.DUMMY_NOTARY_NAME;
import static org.mockito.Mockito.doReturn;
/**
* This is an incomplete Java replica of CashTests.kt to show how to use the Java test DSL
*/
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 MINI_CORP = new TestIdentity(new CordaX500Name("MiniCorp", "London", "GB"));
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 outState = new Cash.State(inState.getAmount(), new AnonymousParty(MINI_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.getPublicKey()));
@Rule
public final SerializationEnvironmentRule testSerialization = new SerializationEnvironmentRule();
@Test
public void trivial() {
IdentityServiceInternal identityService = rigorousMock(IdentityServiceInternal.class);
doReturn(MEGA_CORP.getParty()).when(identityService).partyFromKey(MEGA_CORP.getPubkey());
doReturn(MINI_CORP.getParty()).when(identityService).partyFromKey(MINI_CORP.getPubkey());
transaction(new MockServices(identityService, MEGA_CORP.getName()), DUMMY_NOTARY, tx -> {
doReturn(MEGA_CORP.getParty()).when(identityService).partyFromKey(MEGA_CORP.getPublicKey());
doReturn(MINI_CORP.getParty()).when(identityService).partyFromKey(MINI_CORP.getPublicKey());
transaction(new MockServices(emptyList(), identityService, MEGA_CORP.getName()), DUMMY_NOTARY, tx -> {
tx.attachment(Cash.PROGRAM_ID);
tx.input(Cash.PROGRAM_ID, inState);
tx.tweak(tw -> {
tw.output(Cash.PROGRAM_ID, new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), new AnonymousParty(MINI_CORP.getPubkey())));
tw.command(MEGA_CORP.getPubkey(), new Cash.Commands.Move());
tw.output(Cash.PROGRAM_ID, new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), new AnonymousParty(MINI_CORP.getPublicKey())));
tw.command(MEGA_CORP.getPublicKey(), new Cash.Commands.Move());
return tw.failsWith("the amounts balance");
});
tx.tweak(tw -> {
tw.output(Cash.PROGRAM_ID, outState);
tw.command(MEGA_CORP.getPubkey(), DummyCommandData.INSTANCE);
tw.command(MEGA_CORP.getPublicKey(), DummyCommandData.INSTANCE);
// Invalid command
return tw.failsWith("required net.corda.finance.contracts.asset.Cash.Commands.Move command");
});
tx.tweak(tw -> {
tw.output(Cash.PROGRAM_ID, outState);
tw.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");
});
tx.tweak(tw -> {
@ -64,14 +65,14 @@ public class CashTestsJava {
// issuedBy() can't be directly imported because it conflicts with other identically named functions
// with different overloads (for some reason).
tw.output(Cash.PROGRAM_ID, outState.issuedBy(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");
});
// Simple reallocation works.
return tx.tweak(tw -> {
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();
});
});

View File

@ -18,7 +18,11 @@ import net.corda.finance.`issued by`
import net.corda.finance.contracts.asset.*
import net.corda.node.services.api.IdentityServiceInternal
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.Companion.makeTestDatabaseAndMockServices
import net.corda.testing.node.ledger
@ -46,7 +50,7 @@ interface ICommercialPaperTestTemplate {
private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
private val MEGA_CORP get() = megaCorp.party
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 {
override fun getPaper(): ICommercialPaperState = JavaCommercialPaper.State(
@ -105,13 +109,13 @@ class CommercialPaperTestsGeneric {
private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
private val ALICE get() = alice.party
private val ALICE_KEY get() = alice.key
private val ALICE_PUBKEY get() = alice.pubkey
private val ALICE_KEY get() = alice.keyPair
private val ALICE_PUBKEY get() = alice.publicKey
private val DUMMY_NOTARY get() = dummyNotary.party
private val DUMMY_NOTARY_IDENTITY get() = dummyNotary.identity
private val MINI_CORP get() = miniCorp.party
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
@ -120,7 +124,7 @@ class CommercialPaperTestsGeneric {
@JvmField
val testSerialization = SerializationEnvironmentRule()
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(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY)
doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY)
@ -260,16 +264,15 @@ class CommercialPaperTestsGeneric {
private lateinit var aliceServices: MockServices
private lateinit var aliceVaultService: VaultService
private lateinit var alicesVault: Vault<ContractState>
private val notaryServices = MockServices(rigorousMock(), MEGA_CORP.name, dummyNotary.key)
private val issuerServices = MockServices(listOf("net.corda.finance.contracts", "net.corda.finance.schemas"), rigorousMock(), MEGA_CORP.name, dummyCashIssuer.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.keyPair)
private lateinit var moveTX: SignedTransaction
@Test
fun `issue move and then redeem`() {
val aliceDatabaseAndServices = makeTestDatabaseAndMockServices(
listOf(ALICE_KEY),
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)),
listOf("net.corda.finance.contracts", "net.corda.finance.schemas"),
MEGA_CORP.name)
listOf("net.corda.finance.contracts"),
makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY),
TestIdentity(MEGA_CORP.name, ALICE_KEY))
val databaseAlice = aliceDatabaseAndServices.first
aliceServices = aliceDatabaseAndServices.second
aliceVaultService = aliceServices.vaultService
@ -279,10 +282,9 @@ class CommercialPaperTestsGeneric {
aliceVaultService = aliceServices.vaultService
}
val bigCorpDatabaseAndServices = makeTestDatabaseAndMockServices(
listOf(BIG_CORP_KEY),
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)),
listOf("net.corda.finance.contracts", "net.corda.finance.schemas"),
MEGA_CORP.name)
listOf("net.corda.finance.contracts"),
makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY),
TestIdentity(MEGA_CORP.name, BIG_CORP_KEY))
val databaseBigCorp = bigCorpDatabaseAndServices.first
bigCorpServices = bigCorpDatabaseAndServices.second
bigCorpVaultService = bigCorpServices.vaultService
@ -308,8 +310,8 @@ class CommercialPaperTestsGeneric {
// Alice pays $9000 to BigCorp to own some of their debt.
moveTX = run {
val builder = TransactionBuilder(DUMMY_NOTARY)
Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public))
CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(aliceServices.key.public))
Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(BIG_CORP_KEY.public))
CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(ALICE_KEY.public))
val ptx = aliceServices.signInitialTransaction(builder)
val ptx2 = bigCorpServices.addSignature(ptx)
val stx = notaryServices.addSignature(ptx2)

View File

@ -3,7 +3,6 @@ package net.corda.finance.contracts.asset
import com.nhaarman.mockito_kotlin.*
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.*
import net.corda.core.node.ServiceHub
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.testing.*
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.Companion.makeTestDatabaseAndMockServices
import net.corda.testing.node.ledger
@ -37,27 +41,27 @@ import kotlin.test.*
class CashTests {
private companion object {
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 DUMMY_CASH_ISSUER_IDENTITY = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10).identity
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
val ALICE get() = alice.party
val ALICE_PUBKEY get() = alice.pubkey
val ALICE_PUBKEY get() = alice.publicKey
val CHARLIE get() = charlie.party
val CHARLIE_IDENTITY get() = charlie.identity
val DUMMY_NOTARY get() = dummyNotary.party
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_IDENTITY get() = megaCorp.identity
val MEGA_CORP_KEY get() = megaCorp.key
val MEGA_CORP_PUBKEY get() = megaCorp.pubkey
val MEGA_CORP_KEY get() = megaCorp.keyPair
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
val MINI_CORP get() = miniCorp.party
val MINI_CORP_IDENTITY get() = miniCorp.identity
val MINI_CORP_KEY get() = miniCorp.key
val MINI_CORP_PUBKEY get() = miniCorp.pubkey
val MINI_CORP_KEY get() = miniCorp.keyPair
val MINI_CORP_PUBKEY get() = miniCorp.publicKey
}
@Rule
@ -99,10 +103,9 @@ class CashTests {
}, MINI_CORP.name, MINI_CORP_KEY)
val notaryServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
val databaseAndServices = makeTestDatabaseAndMockServices(
listOf(generateKeyPair()),
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)),
listOf("net.corda.finance.contracts.asset", "net.corda.finance.schemas"),
CordaX500Name("Me", "London", "GB"))
listOf("net.corda.finance.contracts.asset"),
makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY),
TestIdentity(CordaX500Name("Me", "London", "GB")))
database = databaseAndServices.first
ourServices = databaseAndServices.second
@ -138,7 +141,7 @@ class CashTests {
}
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(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY)
doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY)

View File

@ -22,6 +22,8 @@ import net.corda.node.services.api.IdentityServiceInternal
import net.corda.testing.*
import net.corda.testing.contracts.DummyContract
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.ledger
import net.corda.testing.node.transaction
@ -45,14 +47,14 @@ class ObligationTests {
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
val ALICE get() = alice.party
val ALICE_PUBKEY get() = alice.pubkey
val ALICE_PUBKEY get() = alice.publicKey
val BOB get() = bob.party
val BOB_PUBKEY get() = bob.pubkey
val BOB_PUBKEY get() = bob.publicKey
val DUMMY_NOTARY get() = dummyNotary.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_PUBKEY get() = miniCorp.pubkey
val MINI_CORP_PUBKEY get() = miniCorp.publicKey
}
@Rule
@ -77,7 +79,7 @@ class ObligationTests {
)
private val outState = inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY))
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 {
doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY)
doReturn(null).whenever(it).partyFromKey(BOB_PUBKEY)
@ -86,7 +88,7 @@ class ObligationTests {
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 ledgerServices get() = MockServices(identityService, MEGA_CORP.name)
private val ledgerServices get() = MockServices(emptyList(), identityService, MEGA_CORP.name)
private fun cashObligationTestRoots(
group: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>
) = group.apply {

View File

@ -2,18 +2,16 @@ package net.corda.plugins
import groovy.lang.Closure
import net.corda.cordform.CordformDefinition
import net.corda.cordform.CordformNode
import org.apache.tools.ant.filters.FixCrLfFilter
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.lang.reflect.InvocationTargetException
import java.net.URLClassLoader
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.TimeUnit
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 classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath
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()
installRunScript()
nodes.forEach(Node::build)
generateAndInstallNodeInfos()
generateAndInstallNetworkParameters()
bootstrapNetwork()
}
private fun initializeConfiguration() {
@ -164,14 +161,17 @@ open class Cordform : DefaultTask() {
}
}
private fun generateAndInstallNetworkParameters() {
project.logger.info("Generating and installing network parameters")
val networkParamsGenClass = loadNetworkParamsGenClass()
val nodeDirs = nodes.map(Node::fullPath)
val networkParamsGenObject = networkParamsGenClass.newInstance()
val runMethod = networkParamsGenClass.getMethod("run", List::class.java).apply { isAccessible = true }
// Call NetworkParametersGenerator.run
runMethod.invoke(networkParamsGenObject, nodeDirs)
private fun bootstrapNetwork() {
val networkBootstrapperClass = loadNetworkBootstrapperClass()
val networkBootstrapper = networkBootstrapperClass.newInstance()
val bootstrapMethod = networkBootstrapperClass.getMethod("bootstrap", Path::class.java).apply { isAccessible = true }
// Call NetworkBootstrapper.bootstrap
try {
val rootDir = project.projectDir.toPath().resolve(directory).toAbsolutePath().normalize()
bootstrapMethod.invoke(networkBootstrapper, rootDir)
} catch (e: InvocationTargetException) {
throw e.cause!!
}
}
private fun CordformDefinition.getMatchingCordapps(): List<File> {
@ -197,90 +197,4 @@ open class Cordform : DefaultTask() {
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) }
}

View File

@ -20,13 +20,8 @@ class Node(private val project: Project) : CordformNode() {
@JvmStatic
val webJarName = "corda-webserver.jar"
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
* dependency name, eg: com.example:product-name:0.1

View File

@ -122,7 +122,7 @@ private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: I
end tell""")
}
OS.WINDOWS -> {
listOf("cmd", "/C", "start ${command.joinToString(" ")}")
listOf("cmd", "/C", "start ${command.joinToString(" ") { windowsSpaceEscape(it) }}")
}
OS.LINUX -> {
// 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(" ")
override fun getJavaPath(): String {
val path = File(File(System.getProperty("java.home"), "bin"), "java").path
// 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.
return if (os == OS.WINDOWS) path.replace(" ", "\" \"") else path
}
override fun getJavaPath(): String = File(File(System.getProperty("java.home"), "bin"), "java").path
// 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.
private fun windowsSpaceEscape(s:String) = s.replace(" ", "\" \"")
}
private fun quotedFormOf(text: String) = "'${text.replace("'", "'\\''")}'" // Suitable for UNIX shells.

View File

@ -29,12 +29,14 @@ class ArtemisTcpTransport {
// but we allow classical RSA certificates to work in case:
// a) we need to use keytool certificates in some demos,
// 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_RSA_WITH_AES_128_GCM_SHA256",
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"
)
val TLS_VERSIONS = listOf("TLSv1.2")
fun tcpTransport(
direction: ConnectionDirection,
hostAndPort: NetworkHostAndPort,
@ -68,7 +70,7 @@ class ArtemisTcpTransport {
TransportConstants.TRUSTSTORE_PATH_PROP_NAME to config.trustStoreFile,
TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to config.trustStorePassword,
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,
VERIFY_PEER_LEGAL_NAME to (direction as? ConnectionDirection.Outbound)?.expectedCommonNames
)

View File

@ -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
}
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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.SecureHash
import net.corda.core.crypto.verify
import net.corda.core.identity.Party
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 net.corda.nodeapi.internal.crypto.X509Utilities
import java.security.SignatureException
import java.security.cert.CertPathValidatorException
import java.security.cert.X509Certificate
import java.time.Duration
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.
*/
@ -21,21 +23,21 @@ import java.time.Instant
data class NetworkMap(val nodeInfoHashes: List<SecureHash>, val networkParameterHash: SecureHash)
/**
* @property minimumPlatformVersion
* @property notaries
* @property eventHorizon
* @property minimumPlatformVersion Minimum version of Corda platform that is required for nodes in the network.
* @property notaries List of well known and trusted notary identities with information on validation type.
* @property maxMessageSize Maximum P2P message sent over the wire in bytes.
* @property maxTransactionSize Maximum permitted transaction size in bytes.
* @property modifiedTime
* @property epoch Version number of the network parameters. Starting from 1, this will always increment on each new set
* 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
data class NetworkParameters(
val minimumPlatformVersion: Int,
val notaries: List<NotaryInfo>,
val eventHorizon: Duration,
val maxMessageSize: Int,
val maxTransactionSize: Int,
val modifiedTime: Instant,
@ -45,6 +47,8 @@ data class NetworkParameters(
require(minimumPlatformVersion > 0) { "minimumPlatformVersion must be at least 1" }
require(notaries.distinctBy { it.identity } == notaries) { "Duplicate notary identities" }
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.
*/
@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.
*
* @throws CertPathValidatorException if the certificate path is invalid.
* @throws SignatureException if the signature is invalid.
*/
@Throws(SignatureException::class)
fun verified(): NetworkMap {
sig.by.publicKey.verify(raw.bytes, sig)
@Throws(SignatureException::class, CertPathValidatorException::class)
fun verified(trustedRoot: X509Certificate): NetworkMap {
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()
}
}
// 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 */
class DigitalSignatureWithCert(val by: X509Certificate, val signatureBytes: ByteArray) : DigitalSignature(signatureBytes)
class DigitalSignatureWithCert(val by: X509Certificate, val signatureBytes: ByteArray) : DigitalSignature(signatureBytes)

View File

@ -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.
}
}
}

View File

@ -1,4 +1,4 @@
package net.corda.nodeapi.internal
package net.corda.nodeapi.internal.network
import net.corda.cordform.CordformNode
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.
* No files written by that node will be copied to other nodes, nor files from other nodes will be copied to this
* one.

View File

@ -1,4 +1,4 @@
package net.corda.testing
package net.corda.nodeapi
import java.time.Duration

View File

@ -14,6 +14,7 @@ import net.corda.core.transactions.TransactionBuilder
import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.testing.*
import net.corda.testing.internal.rigorousMock
import net.corda.testing.services.MockAttachmentStorage
import org.junit.Assert.*
import org.junit.Rule

View File

@ -19,6 +19,8 @@ import net.corda.nodeapi.internal.serialization.SerializeAsTokenContextImpl
import net.corda.nodeapi.internal.serialization.attachmentsClassLoaderEnabledPropertyName
import net.corda.nodeapi.internal.serialization.withTokenContext
import net.corda.testing.*
import net.corda.testing.internal.kryoSpecific
import net.corda.testing.internal.rigorousMock
import net.corda.testing.services.MockAttachmentStorage
import org.apache.commons.io.IOUtils
import org.junit.Assert.*

View File

@ -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()
}

View File

@ -51,7 +51,7 @@ class X509UtilitiesTest {
val bob = TestIdentity(BOB_NAME, 80)
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
val BOB get() = bob.party
val BOB_PUBKEY get() = bob.pubkey
val BOB_PUBKEY get() = bob.publicKey
}
@Rule

View File

@ -1,8 +1,7 @@
package net.corda.nodeapi
package net.corda.nodeapi.internal.network
import net.corda.cordform.CordformNode
import net.corda.nodeapi.internal.NodeInfoFilesCopier
import net.corda.testing.eventually
import net.corda.nodeapi.eventually
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -15,14 +14,7 @@ import java.util.concurrent.TimeUnit
import kotlin.streams.toList
import kotlin.test.assertEquals
/**
* tests for [NodeInfoFilesCopier]
*/
class NodeInfoFilesCopierTest {
@Rule @JvmField var folder = TemporaryFolder()
private val rootPath get() = folder.root.toPath()
private val scheduler = TestScheduler()
companion object {
private const val ORGANIZATION = "Organization"
private const val NODE_1_PATH = "node1"
@ -34,6 +26,13 @@ class NodeInfoFilesCopierTest {
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 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 node2AdditionalNodeInfoPath by lazy { node2RootPath.resolve(CordformNode.NODE_INFO_DIRECTORY) }
lateinit var nodeInfoFilesCopier: NodeInfoFilesCopier
private lateinit var nodeInfoFilesCopier: NodeInfoFilesCopier
@Before
fun setUp() {

View File

@ -5,6 +5,7 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.serialization.*
import net.corda.testing.*
import net.corda.testing.contracts.DummyContract
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
@ -23,7 +24,7 @@ class ContractAttachmentSerializerTest {
private lateinit var factory: SerializationFactory
private lateinit var context: 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
fun setup() {
factory = testSerialization.env.serializationFactory

View File

@ -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.KryoHeaderV0_1
import net.corda.testing.services.MockAttachmentStorage
import net.corda.testing.rigorousMock
import net.corda.testing.internal.rigorousMock
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException

View File

@ -33,7 +33,7 @@ import kotlin.test.assertTrue
class KryoTests {
companion object {
private val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).pubkey
private val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).publicKey
}
@Rule

View File

@ -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.SerializerFactory
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
import net.corda.testing.amqpSpecific
import net.corda.testing.kryoSpecific
import net.corda.testing.internal.amqpSpecific
import net.corda.testing.internal.kryoSpecific
import net.corda.testing.SerializationEnvironmentRule
import org.assertj.core.api.Assertions
import org.junit.Assert.assertArrayEquals

View File

@ -9,8 +9,8 @@ import net.corda.core.serialization.serialize
import net.corda.node.services.statemachine.DataSessionMessage
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.amqpSpecific
import net.corda.testing.kryoSpecific
import net.corda.testing.internal.amqpSpecific
import net.corda.testing.internal.kryoSpecific
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Assert.assertArrayEquals
import org.junit.Rule

View File

@ -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.DefaultKryoCustomizer
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 org.assertj.core.api.Assertions.assertThat
import org.junit.Before

View File

@ -6,8 +6,8 @@ import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.node.services.statemachine.DataSessionMessage
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
import net.corda.testing.internal.kryoSpecific
import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.kryoSpecific
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Rule

View File

@ -29,6 +29,7 @@ import org.apache.qpid.proton.codec.EncoderImpl
import org.assertj.core.api.Assertions.*
import org.junit.Assert.*
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.IOException
@ -49,11 +50,15 @@ class SerializationOutputTests {
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
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_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 testFloat(val f: Float)
@ -473,9 +478,9 @@ class SerializationOutputTests {
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)
SerializationFactory.defaultFactory.asCurrent { withCurrentContext(newContext) { serdes(t, factory, factory2, expectedEqual) } }
return SerializationFactory.defaultFactory.asCurrent { withCurrentContext(newContext) { serdes(t, factory, factory2, expectedEqual) } }
}
@Test

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -24,15 +24,21 @@ import net.corda.testing.*
import net.corda.testing.driver.DriverDSL
import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.driver
import net.corda.testing.internal.withoutTestSerialization
import net.corda.testing.services.MockAttachmentStorage
import net.corda.testing.internal.rigorousMock
import org.junit.Assert.assertEquals
import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import java.net.URLClassLoader
import java.nio.file.Files
import kotlin.test.assertFailsWith
class AttachmentLoadingTests : IntegrationTest() {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val attachments = MockAttachmentStorage()
private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), attachments)
private val cordapp get() = provider.cordapps.first()
@ -83,7 +89,7 @@ class AttachmentLoadingTests : IntegrationTest() {
}
@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 contractClass = appClassLoader.loadClass(ISOLATED_CONTRACT_ID).asSubclass(Contract::class.java)
val generateInitialMethod = contractClass.getDeclaredMethod("generateInitial", PartyAndReference::class.java, Integer.TYPE, Party::class.java)
@ -99,7 +105,7 @@ class AttachmentLoadingTests : IntegrationTest() {
}
@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 {
installIsolatedCordappTo(bankAName)
val (bankA, bankB) = createTwoNodes()
@ -107,15 +113,17 @@ class AttachmentLoadingTests : IntegrationTest() {
bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow()
}
}
Unit
}
@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 {
installIsolatedCordappTo(bankAName)
installIsolatedCordappTo(bankBName)
val (bankA, bankB) = createTwoNodes()
bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow()
}
Unit
}
}

View File

@ -24,10 +24,12 @@ import net.corda.node.services.config.BFTSMaRtConfiguration
import net.corda.node.services.config.NotaryConfig
import net.corda.node.services.transactions.minClusterSize
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.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.contracts.DummyContract
import net.corda.testing.node.MockNetwork
@ -55,7 +57,7 @@ class BFTNotaryServiceTests : IntegrationTest() {
@Before
fun before() {
mockNet = MockNetwork()
mockNet = MockNetwork(emptyList())
node = mockNet.createNode()
}
@After

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